From a6c5f484b92f8ac2c2b2d1361f6e1648b38328aa Mon Sep 17 00:00:00 2001 From: richard-dds Date: Fri, 10 May 2019 16:01:25 -0400 Subject: [PATCH 01/11] Add NotificationSender, one test --- ...04bb5bb3a0e_add_notification_recipients.py | 34 +++++++++++++++++++ atst/models/__init__.py | 1 + atst/models/notification_recipient.py | 10 ++++++ atst/utils/notification_sender.py | 20 +++++++++++ tests/factories.py | 7 ++++ tests/utils/test_notification_sender.py | 27 +++++++++++++++ 6 files changed, 99 insertions(+) create mode 100644 alembic/versions/404bb5bb3a0e_add_notification_recipients.py create mode 100644 atst/models/notification_recipient.py create mode 100644 atst/utils/notification_sender.py create mode 100644 tests/utils/test_notification_sender.py diff --git a/alembic/versions/404bb5bb3a0e_add_notification_recipients.py b/alembic/versions/404bb5bb3a0e_add_notification_recipients.py new file mode 100644 index 00000000..b8127705 --- /dev/null +++ b/alembic/versions/404bb5bb3a0e_add_notification_recipients.py @@ -0,0 +1,34 @@ +"""add_notification_recipients + +Revision ID: 404bb5bb3a0e +Revises: 432c5287256d +Create Date: 2019-05-10 15:25:22.627996 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '404bb5bb3a0e' +down_revision = '432c5287256d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notification_recipients', + sa.Column('time_created', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('time_updated', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('notification_recipients') + # ### end Alembic commands ### diff --git a/atst/models/__init__.py b/atst/models/__init__.py index 8a3cdbfa..e3fc7e1c 100644 --- a/atst/models/__init__.py +++ b/atst/models/__init__.py @@ -17,5 +17,6 @@ from .portfolio_invitation import PortfolioInvitation from .application_invitation import ApplicationInvitation from .task_order import TaskOrder from .dd_254 import DD254 +from .notification_recipient import NotificationRecipient from .mixins.invites import Status as InvitationStatus diff --git a/atst/models/notification_recipient.py b/atst/models/notification_recipient.py new file mode 100644 index 00000000..72b55cbe --- /dev/null +++ b/atst/models/notification_recipient.py @@ -0,0 +1,10 @@ +from sqlalchemy import String, Column + +from atst.models import Base, types, mixins + + +class NotificationRecipient(Base, mixins.TimestampsMixin): + __tablename__ = "notification_recipients" + + id = types.Id() + email = Column(String, nullable=False) diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py new file mode 100644 index 00000000..0260993f --- /dev/null +++ b/atst/utils/notification_sender.py @@ -0,0 +1,20 @@ +from atst.queue import ATSTQueue +from atst.database import db +from atst.models import NotificationRecipient + + +class NotificationSender(object): + EMAIL_SUBJECT = "ATST notification" + + def __init__(self, queue: ATSTQueue): + self.queue = queue + + def send(self, body, type_=None): + recipients = self._get_recipients(type_) + self.queue.send_mail(recipients, self.EMAIL_SUBJECT, body) + + def _get_recipients(self, type_): + return [ + recipient.email + for recipient in db.session.query(NotificationRecipient).all() + ] diff --git a/tests/factories.py b/tests/factories.py index 8bdf9310..d9de9c97 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -318,3 +318,10 @@ class DD254Factory(Base): required_distribution = factory.LazyFunction( lambda: [random_choice(data.REQUIRED_DISTRIBUTIONS)] ) + + +class NotificationRecipientFactory(Base): + class Meta: + model = NotificationRecipient + + email = factory.Faker("email") diff --git a/tests/utils/test_notification_sender.py b/tests/utils/test_notification_sender.py new file mode 100644 index 00000000..d0f7150a --- /dev/null +++ b/tests/utils/test_notification_sender.py @@ -0,0 +1,27 @@ +import pytest +from unittest.mock import Mock + +from tests.factories import NotificationRecipientFactory +from atst.utils.notification_sender import NotificationSender + + +@pytest.fixture +def mock_queue(queue): + return Mock(spec=queue) + + +@pytest.fixture +def notification_sender(mock_queue): + return NotificationSender(mock_queue) + + +def test_can_send_notification(mock_queue, notification_sender): + recipient_email = "test@example.com" + email_body = "This is a test" + + NotificationRecipientFactory.create(email=recipient_email) + notification_sender.send(email_body) + + mock_queue.send_mail.assert_called_once_with( + ["test@example.com"], notification_sender.EMAIL_SUBJECT, email_body + ) From aaa9d47ccfdf0fa785018add943b25d89c52c124 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 13 May 2019 11:40:54 -0400 Subject: [PATCH 02/11] Wire up NotificationSender to the app's error handler --- atst/routes/errors.py | 2 ++ atst/utils/notification_sender.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/atst/routes/errors.py b/atst/routes/errors.py index c129e08d..d6edc566 100644 --- a/atst/routes/errors.py +++ b/atst/routes/errors.py @@ -11,6 +11,7 @@ from atst.domain.invitations import ( from atst.domain.authnid.crl import CRLInvalidException from atst.domain.portfolios import PortfolioError from atst.utils.flash import formatted_flash as flash +from atst.utils.notification_sender import notification_sender def log_error(e): @@ -20,6 +21,7 @@ def log_error(e): def handle_error(e, message="Not Found", code=404): log_error(e) + notification_sender.send(message) return render_template("error.html", message=message), code diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py index 0260993f..4b47f472 100644 --- a/atst/utils/notification_sender.py +++ b/atst/utils/notification_sender.py @@ -1,4 +1,4 @@ -from atst.queue import ATSTQueue +from atst.queue import ATSTQueue, queue from atst.database import db from atst.models import NotificationRecipient @@ -18,3 +18,6 @@ class NotificationSender(object): recipient.email for recipient in db.session.query(NotificationRecipient).all() ] + + +notification_sender = NotificationSender(queue) From c03b69b351f528287b06de6e8358a88e2f430c22 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 13 May 2019 12:06:33 -0400 Subject: [PATCH 03/11] More idiomatic initialization of notification_sender --- atst/app.py | 6 ++++++ atst/utils/notification_sender.py | 12 +++++++----- tests/utils/test_notification_sender.py | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/atst/app.py b/atst/app.py index ef5ff09c..f4c5be4d 100644 --- a/atst/app.py +++ b/atst/app.py @@ -29,6 +29,7 @@ from atst.utils import mailer from atst.utils.form_cache import FormCache from atst.utils.json import CustomJSONEncoder from atst.queue import queue +from atst.utils.notification_sender import NotificationSender from logging.config import dictConfig from atst.utils.logging import JsonFormatter, RequestContextFilter @@ -63,6 +64,7 @@ def make_app(config): make_csp_provider(app) make_crl_validator(app) make_mailer(app) + make_notification_sender(app) queue.init_app(app) db.init_app(app) @@ -247,6 +249,10 @@ def make_mailer(app): app.mailer = mailer.Mailer(mailer_connection, sender) +def make_notification_sender(app): + app.notification_sender = NotificationSender(queue, app.logger) + + def apply_json_logger(): dictConfig( { diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py index 4b47f472..881106fa 100644 --- a/atst/utils/notification_sender.py +++ b/atst/utils/notification_sender.py @@ -1,4 +1,6 @@ -from atst.queue import ATSTQueue, queue +from logging import Logger + +from atst.queue import ATSTQueue from atst.database import db from atst.models import NotificationRecipient @@ -6,11 +8,14 @@ from atst.models import NotificationRecipient class NotificationSender(object): EMAIL_SUBJECT = "ATST notification" - def __init__(self, queue: ATSTQueue): + def __init__(self, queue: ATSTQueue, logger: Logger): self.queue = queue + self.logger = logger def send(self, body, type_=None): recipients = self._get_recipients(type_) + self.logger.info( + "Sending a notification to these recipients: {}\n\n{}".format(recipients, body)) self.queue.send_mail(recipients, self.EMAIL_SUBJECT, body) def _get_recipients(self, type_): @@ -18,6 +23,3 @@ class NotificationSender(object): recipient.email for recipient in db.session.query(NotificationRecipient).all() ] - - -notification_sender = NotificationSender(queue) diff --git a/tests/utils/test_notification_sender.py b/tests/utils/test_notification_sender.py index d0f7150a..0e8f3c23 100644 --- a/tests/utils/test_notification_sender.py +++ b/tests/utils/test_notification_sender.py @@ -11,8 +11,8 @@ def mock_queue(queue): @pytest.fixture -def notification_sender(mock_queue): - return NotificationSender(mock_queue) +def notification_sender(mock_queue, mock_logger): + return NotificationSender(mock_queue, mock_logger) def test_can_send_notification(mock_queue, notification_sender): From df7f72245a438a4d985079e9c3a923c63ba503f7 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Mon, 13 May 2019 12:07:37 -0400 Subject: [PATCH 04/11] Send notification when we encounter an error --- atst/routes/errors.py | 3 +-- tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/atst/routes/errors.py b/atst/routes/errors.py index d6edc566..7d03d29d 100644 --- a/atst/routes/errors.py +++ b/atst/routes/errors.py @@ -11,7 +11,6 @@ from atst.domain.invitations import ( from atst.domain.authnid.crl import CRLInvalidException from atst.domain.portfolios import PortfolioError from atst.utils.flash import formatted_flash as flash -from atst.utils.notification_sender import notification_sender def log_error(e): @@ -21,7 +20,7 @@ def log_error(e): def handle_error(e, message="Not Found", code=404): log_error(e) - notification_sender.send(message) + current_app.notification_sender.send(message) return render_template("error.html", message=message), code diff --git a/tests/conftest.py b/tests/conftest.py index 6faaf715..dbff3193 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ import tests.factories as factories from tests.mocks import PDF_FILENAME, PDF_FILENAME2 from tests.utils import FakeLogger -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta from cryptography.hazmat.primitives.asymmetric import rsa from cryptography import x509 from cryptography.hazmat.backends import default_backend From d3b42d5bfc53a7aa3d73ae953afb637858d517d6 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 10:04:31 -0400 Subject: [PATCH 05/11] Formatting --- atst/utils/notification_sender.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py index 881106fa..5567d000 100644 --- a/atst/utils/notification_sender.py +++ b/atst/utils/notification_sender.py @@ -15,7 +15,10 @@ class NotificationSender(object): def send(self, body, type_=None): recipients = self._get_recipients(type_) self.logger.info( - "Sending a notification to these recipients: {}\n\n{}".format(recipients, body)) + "Sending a notification to these recipients: {}\n\n{}".format( + recipients, body + ) + ) self.queue.send_mail(recipients, self.EMAIL_SUBJECT, body) def _get_recipients(self, type_): From 97b9d84c38e79db87c7528a5f9b73e963a4b2faa Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 10:17:03 -0400 Subject: [PATCH 06/11] Select only recipient emails from the db --- atst/utils/notification_sender.py | 7 +++---- tests/utils/test_notification_sender.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py index 5567d000..0fdc31b0 100644 --- a/atst/utils/notification_sender.py +++ b/atst/utils/notification_sender.py @@ -1,4 +1,5 @@ from logging import Logger +from sqlalchemy import select from atst.queue import ATSTQueue from atst.database import db @@ -22,7 +23,5 @@ class NotificationSender(object): self.queue.send_mail(recipients, self.EMAIL_SUBJECT, body) def _get_recipients(self, type_): - return [ - recipient.email - for recipient in db.session.query(NotificationRecipient).all() - ] + query = select([NotificationRecipient.email]) + return db.session.execute(query).fetchone() diff --git a/tests/utils/test_notification_sender.py b/tests/utils/test_notification_sender.py index 0e8f3c23..458433c7 100644 --- a/tests/utils/test_notification_sender.py +++ b/tests/utils/test_notification_sender.py @@ -23,5 +23,5 @@ def test_can_send_notification(mock_queue, notification_sender): notification_sender.send(email_body) mock_queue.send_mail.assert_called_once_with( - ["test@example.com"], notification_sender.EMAIL_SUBJECT, email_body + ("test@example.com",), notification_sender.EMAIL_SUBJECT, email_body ) From 9e97df79f5c52385ab7c7ca5bd9fb5e6d16c53ba Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 10:45:42 -0400 Subject: [PATCH 07/11] Mock out app NotificationSender during testing --- tests/conftest.py | 12 +++++++++++- tests/utils.py | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index dbff3193..31f836fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from atst.database import db as _db from atst.queue import queue as atst_queue import tests.factories as factories from tests.mocks import PDF_FILENAME, PDF_FILENAME2 -from tests.utils import FakeLogger +from tests.utils import FakeLogger, FakeNotificationSender from datetime import datetime, timedelta from cryptography.hazmat.primitives.asymmetric import rsa @@ -307,3 +307,13 @@ def mock_logger(app): yield app.logger app.logger = real_logger + + +@pytest.fixture +def mock_notification_sender(app): + real_notification_sender = app.notification_sender + app.notification_sender = FakeNotificationSender() + + yield app.notification_sender + + app.notification_sender = real_notification_sender diff --git a/tests/utils.py b/tests/utils.py index a8d7522a..cd7c1bba 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,8 @@ from flask import template_rendered from contextlib import contextmanager +from unittest.mock import Mock + +from atst.utils.notification_sender import NotificationSender @contextmanager @@ -37,3 +40,6 @@ class FakeLogger: self.messages.append(msg) if "extra" in kwargs: self.extras.append(kwargs["extra"]) + + +FakeNotificationSender = Mock(spec=NotificationSender) From 9ba1def91c0c7ed3719732201e48e4a0b1e63d96 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 11:16:48 -0400 Subject: [PATCH 08/11] Use separate queue method for notifications --- atst/queue.py | 3 +++ atst/utils/notification_sender.py | 2 +- tests/conftest.py | 2 +- tests/utils/test_notification_sender.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/atst/queue.py b/atst/queue.py index befe45b0..9793ee33 100644 --- a/atst/queue.py +++ b/atst/queue.py @@ -30,6 +30,9 @@ class ATSTQueue(RQ): def send_mail(self, recipients, subject, body): self._queue_job(ATSTQueue._send_mail, recipients, subject, body) + def send_notification_mail(self, recipients, subject, body): + self._queue_job(ATSTQueue._send_mail, recipients, subject, body) + # pylint: disable=pointless-string-statement """Class methods to actually perform the work. diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py index 0fdc31b0..dbf11a96 100644 --- a/atst/utils/notification_sender.py +++ b/atst/utils/notification_sender.py @@ -20,7 +20,7 @@ class NotificationSender(object): recipients, body ) ) - self.queue.send_mail(recipients, self.EMAIL_SUBJECT, body) + self.queue.send_notification_mail(recipients, self.EMAIL_SUBJECT, body) def _get_recipients(self, type_): query = select([NotificationRecipient.email]) diff --git a/tests/conftest.py b/tests/conftest.py index 31f836fa..c46c4436 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -310,7 +310,7 @@ def mock_logger(app): @pytest.fixture -def mock_notification_sender(app): +def notification_sender(app): real_notification_sender = app.notification_sender app.notification_sender = FakeNotificationSender() diff --git a/tests/utils/test_notification_sender.py b/tests/utils/test_notification_sender.py index 458433c7..df5efaa7 100644 --- a/tests/utils/test_notification_sender.py +++ b/tests/utils/test_notification_sender.py @@ -22,6 +22,6 @@ def test_can_send_notification(mock_queue, notification_sender): NotificationRecipientFactory.create(email=recipient_email) notification_sender.send(email_body) - mock_queue.send_mail.assert_called_once_with( + mock_queue.send_notification_mail.assert_called_once_with( ("test@example.com",), notification_sender.EMAIL_SUBJECT, email_body ) From 9399b34a56d83fe4b320036df88610c0d3288863 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 11:26:39 -0400 Subject: [PATCH 09/11] Autouse notification_sender fixture --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index c46c4436..0b6d4f52 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -309,7 +309,7 @@ def mock_logger(app): app.logger = real_logger -@pytest.fixture +@pytest.fixture(scope="session", autouse=True) def notification_sender(app): real_notification_sender = app.notification_sender app.notification_sender = FakeNotificationSender() From d8e75668b0f0c8d6541ab4d8c7b49d8a3f75e10e Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 11:40:28 -0400 Subject: [PATCH 10/11] Log notifications at the time of sending --- atst/app.py | 2 +- atst/queue.py | 11 ++++++++++- atst/utils/notification_sender.py | 9 +-------- tests/utils/test_notification_sender.py | 4 ++-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/atst/app.py b/atst/app.py index f4c5be4d..c729c65a 100644 --- a/atst/app.py +++ b/atst/app.py @@ -250,7 +250,7 @@ def make_mailer(app): def make_notification_sender(app): - app.notification_sender = NotificationSender(queue, app.logger) + app.notification_sender = NotificationSender(queue) def apply_json_logger(): diff --git a/atst/queue.py b/atst/queue.py index 9793ee33..3f2d3042 100644 --- a/atst/queue.py +++ b/atst/queue.py @@ -31,7 +31,7 @@ class ATSTQueue(RQ): self._queue_job(ATSTQueue._send_mail, recipients, subject, body) def send_notification_mail(self, recipients, subject, body): - self._queue_job(ATSTQueue._send_mail, recipients, subject, body) + self._queue_job(ATSTQueue._send_notification_mail, recipients, subject, body) # pylint: disable=pointless-string-statement """Class methods to actually perform the work. @@ -44,5 +44,14 @@ class ATSTQueue(RQ): def _send_mail(self, recipients, subject, body): app.mailer.send(recipients, subject, body) + @classmethod + def _send_notification_mail(self, recipients, subject, body): + app.logger.info( + "Sending a notification to these recipients: {}\n\n{}".format( + recipients, body + ) + ) + app.mailer.send(recipients, subject, body) + queue = ATSTQueue() diff --git a/atst/utils/notification_sender.py b/atst/utils/notification_sender.py index dbf11a96..cfd8c8d1 100644 --- a/atst/utils/notification_sender.py +++ b/atst/utils/notification_sender.py @@ -1,4 +1,3 @@ -from logging import Logger from sqlalchemy import select from atst.queue import ATSTQueue @@ -9,17 +8,11 @@ from atst.models import NotificationRecipient class NotificationSender(object): EMAIL_SUBJECT = "ATST notification" - def __init__(self, queue: ATSTQueue, logger: Logger): + def __init__(self, queue: ATSTQueue): self.queue = queue - self.logger = logger def send(self, body, type_=None): recipients = self._get_recipients(type_) - self.logger.info( - "Sending a notification to these recipients: {}\n\n{}".format( - recipients, body - ) - ) self.queue.send_notification_mail(recipients, self.EMAIL_SUBJECT, body) def _get_recipients(self, type_): diff --git a/tests/utils/test_notification_sender.py b/tests/utils/test_notification_sender.py index df5efaa7..5aec0081 100644 --- a/tests/utils/test_notification_sender.py +++ b/tests/utils/test_notification_sender.py @@ -11,8 +11,8 @@ def mock_queue(queue): @pytest.fixture -def notification_sender(mock_queue, mock_logger): - return NotificationSender(mock_queue, mock_logger) +def notification_sender(mock_queue): + return NotificationSender(mock_queue) def test_can_send_notification(mock_queue, notification_sender): From fe5def3a92395055573ffebb30ae35545b0f1054 Mon Sep 17 00:00:00 2001 From: richard-dds Date: Wed, 15 May 2019 14:31:16 -0400 Subject: [PATCH 11/11] Send notification emails on some http errors --- atst/routes/errors.py | 15 +++++++++------ tests/conftest.py | 2 +- tests/routes/test_errors.py | 15 +++++++++++++++ tests/utils.py | 2 +- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/atst/routes/errors.py b/atst/routes/errors.py index 7d03d29d..cc3f5f21 100644 --- a/atst/routes/errors.py +++ b/atst/routes/errors.py @@ -12,15 +12,22 @@ from atst.domain.authnid.crl import CRLInvalidException from atst.domain.portfolios import PortfolioError from atst.utils.flash import formatted_flash as flash +NO_NOTIFY_STATUS_CODES = set([404, 401]) + def log_error(e): error_message = e.message if hasattr(e, "message") else str(e) current_app.logger.exception(error_message) +def notify(e, message, code): + if code not in NO_NOTIFY_STATUS_CODES: + current_app.notification_sender.send(message) + + def handle_error(e, message="Not Found", code=404): log_error(e) - current_app.notification_sender.send(message) + notify(e, message, code) return render_template("error.html", message=message), code @@ -57,13 +64,9 @@ def make_error_pages(app): @app.errorhandler(Exception) # pylint: disable=unused-variable def exception(e): - log_error(e) if current_app.debug: raise e - return ( - render_template("error.html", message="An Unexpected Error Occurred"), - 500, - ) + return handle_error(e, message="An Unexpected Error Occurred", code=500) @app.errorhandler(InvitationError) @app.errorhandler(InvitationWrongUserError) diff --git a/tests/conftest.py b/tests/conftest.py index 0b6d4f52..cede1f3c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -309,7 +309,7 @@ def mock_logger(app): app.logger = real_logger -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="function", autouse=True) def notification_sender(app): real_notification_sender = app.notification_sender app.notification_sender = FakeNotificationSender() diff --git a/tests/routes/test_errors.py b/tests/routes/test_errors.py index 0ffff430..f5b0289e 100644 --- a/tests/routes/test_errors.py +++ b/tests/routes/test_errors.py @@ -1,5 +1,7 @@ import pytest from flask import url_for +from copy import copy +from tests.factories import UserFactory @pytest.fixture @@ -20,3 +22,16 @@ def test_csrf_error(csrf_enabled_app, client): body = response.data.decode() assert "Session Expired" in body assert "Log in required" in body + + +def test_errors_generate_notifications(app, client, user_session, notification_sender): + user_session(UserFactory.create()) + new_app = copy(app) + + @new_app.route("/throw") + def throw(): + raise ValueError() + + new_app.test_client().get("/throw") + + notification_sender.send.assert_called_once() diff --git a/tests/utils.py b/tests/utils.py index cd7c1bba..913a80e7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -42,4 +42,4 @@ class FakeLogger: self.extras.append(kwargs["extra"]) -FakeNotificationSender = Mock(spec=NotificationSender) +FakeNotificationSender = lambda: Mock(spec=NotificationSender)