From 037798e5f269d388182a19d6589ce1d423760f24 Mon Sep 17 00:00:00 2001 From: Andreas Ellewsen <andreas@ellewsen.no> Date: Fri, 13 May 2022 14:25:04 +0200 Subject: [PATCH] Notify sponsors of bad emails in sent invites If the sponsor writes a valid but non-existent email in a guest invite we retried sending this email indefinitely. Instead of leaving django q in an endless loop, we now send an email to the sponsor warning about the problem instead. --- greg/tasks.py | 15 +- greg/tests/test_tasks.py | 15 +- gregui/api/views/{__init__,py => __init__.py} | 0 gregui/api/views/invitation.py | 28 ++-- gregui/mailutils.py | 129 ------------------ gregui/mailutils/__init__.py | 0 gregui/mailutils/confirm_guest.py | 48 +++++++ gregui/mailutils/invite_guest.py | 102 ++++++++++++++ gregui/mailutils/protocol.py | 20 +++ gregui/mailutils/role_ending.py | 20 +++ .../0004_add_email_template_type.py | 27 ++++ gregui/models.py | 3 + gregui/tests/api/views/test_invite_guest.py | 27 ++-- gregui/tests/conftest.py | 10 ++ gregui/tests/test_mailutils.py | 113 ++++++++++----- 15 files changed, 343 insertions(+), 214 deletions(-) rename gregui/api/views/{__init__,py => __init__.py} (100%) delete mode 100644 gregui/mailutils.py create mode 100644 gregui/mailutils/__init__.py create mode 100644 gregui/mailutils/confirm_guest.py create mode 100644 gregui/mailutils/invite_guest.py create mode 100644 gregui/mailutils/protocol.py create mode 100644 gregui/mailutils/role_ending.py create mode 100644 gregui/migrations/0004_add_email_template_type.py diff --git a/greg/tasks.py b/greg/tasks.py index 9e751c9f..59a5c03e 100644 --- a/greg/tasks.py +++ b/greg/tasks.py @@ -9,10 +9,10 @@ from django.conf import settings from greg.importers.orgreg import OrgregImporter from greg.models import Role, Sponsor -from gregui import mailutils +from gregui.mailutils.role_ending import RolesEnding -def notify_sponsors_roles_ending() -> None: +def notify_sponsors_roles_ending() -> list[str]: """ This task notifies sponsors of roles (that are accessible to them) that are about to expire. @@ -51,11 +51,16 @@ def notify_sponsors_roles_ending() -> None: sp2roles[sp].append(role) # Send emails to sponsors + remindermailer = RolesEnding() + task_ids = [] for sp, roles in sp2roles.items(): - mailutils.send_role_ending_mail( - mail_to=sponsors.get(id=sp).work_email, # type: ignore - num_roles=len(roles), + task_ids.append( + remindermailer.queue_mail( + mail_to=sponsors.get(id=sp).work_email, # type: ignore + num_roles=len(roles), + ) ) + return task_ids def import_from_orgreg(): diff --git a/greg/tests/test_tasks.py b/greg/tests/test_tasks.py index 31d975e2..13bad3ed 100644 --- a/greg/tests/test_tasks.py +++ b/greg/tests/test_tasks.py @@ -31,19 +31,8 @@ def test_notify_sponsors_roles_ending( role_end_reminder_template, role_person_foo2, sponsor_org_unit ): mail.outbox = [] - notify_sponsors_roles_ending() - assert len(mail.outbox) == 1 - assert mail.outbox[0].to == ["sponsor_guy@example.com"] - assert ( - mail.outbox[0].body - == """Dette er en automatisk generert melding fra gjesteregistreringstjenesten. -Du administrerer 1 gjesteroller som snart utløper. -Dersom det er ønskelig å forlenge rollene kan dette gjøres i webgrensesnittet. - -This message has been automatically generated by the guest registration system. -You can maintan 1 guest roles about to expire. -To extend the guest role please use the web interface.""" - ) + task_ids = notify_sponsors_roles_ending() + assert len(task_ids) == 1 @pytest.fixture diff --git a/gregui/api/views/__init__,py b/gregui/api/views/__init__.py similarity index 100% rename from gregui/api/views/__init__,py rename to gregui/api/views/__init__.py diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 1d4fb1ee..47343090 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -23,7 +23,8 @@ from greg.permissions import IsSponsor from greg.utils import get_default_invitation_expire_date_from_now from gregui.api.serializers.guest import GuestRegisterSerializer from gregui.api.serializers.invitation import InviteGuestSerializer -from gregui.mailutils import send_confirmation_mail_from_link, send_invite_mail +from gregui.mailutils.confirm_guest import ConfirmGuest +from gregui.mailutils.invite_guest import InviteGuest from gregui.models import GregUserProfile logger = structlog.getLogger(__name__) @@ -77,20 +78,8 @@ class InvitationView(CreateAPIView, DestroyAPIView): invitation__role__person_id=person.id, invitation__role__sponsor_id=sponsor_user.sponsor_id, ): - try: - send_invite_mail(invitationlink) - except Exception: - logger.exception("send_invite_mail_failed", exc_info=True) - # The invite has been created, so send 201, but include some data saying the - # e-mail was not sent properly - return Response( - status=status.HTTP_201_CREATED, - data={ - "code": "invite_email_failed", - "message": "Failed to send invite e-mail", - }, - ) - + invitemailer = InviteGuest() + invitemailer.queue_mail(invitationlink) # Empty json-body to avoid client complaining about non-json data in response return Response(status=status.HTTP_201_CREATED, data={}) @@ -353,7 +342,8 @@ class InvitedGuestView(GenericAPIView): invite_link.save() # Send an email to the sponsor - send_confirmation_mail_from_link(invite_link) + confirmmailer = ConfirmGuest() + confirmmailer.send_confirmation_mail_from_link(invite_link) return Response(status=status.HTTP_200_OK) def _verify_only_allowed_updates_in_request( @@ -477,7 +467,7 @@ class ResendInvitationView(UpdateModelMixin, APIView): return Response(status=status.HTTP_400_BAD_REQUEST) non_expired_links = invitation_links.filter(expire__gt=timezone.now()) - + invitemailer = InviteGuest() if non_expired_links.count() > 0: if non_expired_links.count() > 1: # Do not expect this to happen @@ -485,7 +475,7 @@ class ResendInvitationView(UpdateModelMixin, APIView): # Just resend all and do not create a new one for link in non_expired_links: - send_invite_mail(link) + invitemailer.queue_mail(link) else: # All the invitation links have expired, create a new one invitations_to_resend = set( @@ -503,6 +493,6 @@ class ResendInvitationView(UpdateModelMixin, APIView): invitation=invitation, expire=get_default_invitation_expire_date_from_now(), ) - send_invite_mail(invitation_link) + invitemailer.queue_mail(invitation_link) return Response(status=status.HTTP_200_OK) diff --git a/gregui/mailutils.py b/gregui/mailutils.py deleted file mode 100644 index 83c8dc65..00000000 --- a/gregui/mailutils.py +++ /dev/null @@ -1,129 +0,0 @@ -import logging - -from typing import Optional, Union -from django.conf import settings -from django.template.context import Context -from django_q.tasks import async_task - -from greg.models import InvitationLink -from gregui.models import EmailTemplate - -logger = logging.getLogger(__name__) - - -def prepare_arguments( - template: EmailTemplate, context: dict[str, str], mail_to: str -) -> dict[str, Union[str, list[str]]]: - """Combine input to a dict ready for use as arguments ti django's send_mail""" - return { - "subject": template.get_subject(context), - "message": template.get_body(context), - "from_email": template.from_email or None, - "recipient_list": [mail_to], - } - - -def make_registration_url(token: str) -> str: - return "{base}/invite#{token}".format( - base=settings.BASE_URL, - token=token, - ) - - -def make_guest_profile_url(person_id: Union[str, int]) -> str: - return "{base}/sponsor/guest/{person_id}".format( - base=settings.BASE_URL, - person_id=person_id, - ) - - -def registration_template( - institution: str, sponsor: str, mail_to: str, token: str -) -> dict[str, Union[str, list[str]]]: - """ - Prepare email for registration - - Produces a complete set of arguments ready for use with django.core.mail.send_mail - when sending a registration email to the guest. - """ - template = EmailTemplate.objects.get( - template_key=EmailTemplate.EmailType.GUEST_REGISTRATION - ) - registration_link = make_registration_url(token) - context = Context( - { - "institution": institution, - "sponsor": sponsor, - "registration_link": registration_link, - } - ) - return prepare_arguments(template, context, mail_to) - - -def confirmation_template( - guest_name: str, mail_to: str, guest_person_id: Union[str, int] -) -> dict[str, Union[str, list[str]]]: - """ - Prepare email for confirmation - - Produces a complete set of arguments ready for use with django.core.mail.send_mail - when sending a confirmation email to the sponsor. - """ - template = EmailTemplate.objects.get( - template_key=EmailTemplate.EmailType.SPONSOR_CONFIRMATION - ) - confirmation_link = make_guest_profile_url(guest_person_id) - context = Context({"guest": guest_name, "confirmation_link": confirmation_link}) - return prepare_arguments(template, context, mail_to) - - -def reminder_template(mail_to: str, num_roles: int) -> dict[str, Union[str, list[str]]]: - template = EmailTemplate.objects.get( - template_key=EmailTemplate.EmailType.ROLE_END_REMINDER - ) - context = Context({"num_roles": num_roles}) - return prepare_arguments(template, context, mail_to) - - -def send_registration_mail(mail_to: str, sponsor: str, token: str) -> str: - arguments = registration_template(settings.INSTANCE_NAME, sponsor, mail_to, token) - return async_task("django.core.mail.send_mail", **arguments) - - -def send_confirmation_mail( - mail_to: str, guest_name: str, guest_person_id: Union[int, str] -) -> str: - arguments = confirmation_template( - guest_name=guest_name, mail_to=mail_to, guest_person_id=guest_person_id - ) - return async_task("django.core.mail.send_mail", **arguments) - - -def send_role_ending_mail(mail_to: str, num_roles: int) -> str: - arguments = reminder_template(mail_to, num_roles) - return async_task("django.core.mail.send_mail", **arguments) - - -def send_invite_mail(link: InvitationLink) -> Optional[str]: - email_address = link.invitation.role.person.private_email - if not email_address: - logger.warning( - "No e-mail address found for invitation link with ID: {%s}", link.id - ) - return None - - sponsor = link.invitation.role.sponsor - return send_registration_mail( - mail_to=email_address.value, - sponsor=f"{sponsor.first_name} {sponsor.last_name}", - token=link.uuid, - ) - - -def send_confirmation_mail_from_link(link: InvitationLink) -> Optional[str]: - email_address = link.invitation.role.sponsor.work_email - guest = link.invitation.role.person - guest_name = f"{guest.first_name} {guest.last_name}" - return send_confirmation_mail( - mail_to=email_address, guest_name=guest_name, guest_person_id=guest.id - ) diff --git a/gregui/mailutils/__init__.py b/gregui/mailutils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gregui/mailutils/confirm_guest.py b/gregui/mailutils/confirm_guest.py new file mode 100644 index 00000000..e2ff17b0 --- /dev/null +++ b/gregui/mailutils/confirm_guest.py @@ -0,0 +1,48 @@ +from typing import Optional, Union + +from django.conf import settings +from django.template.context import Context +from django_q.tasks import async_task + +from greg.models import InvitationLink +from gregui.mailutils.protocol import prepare_arguments +from gregui.models import EmailTemplate + + +class ConfirmGuest: + def __init__(self): + pass + + def _make_guest_profile_url(self, person_id: Union[str, int]) -> str: + return "{base}/sponsor/guest/{person_id}".format( + base=settings.BASE_URL, + person_id=person_id, + ) + + def get_template(self): + return EmailTemplate.objects.get( + template_key=EmailTemplate.EmailType.SPONSOR_CONFIRMATION + ) + + def queue_mail( + self, mail_to: str, guest_name: str, guest_person_id: Union[int, str] + ) -> str: + arguments = prepare_arguments( + template=self.get_template(), + context=Context( + { + "guest": guest_name, + "confirmation_link": self._make_guest_profile_url(guest_person_id), + } + ), + mail_to=mail_to, + ) + return async_task("django.core.mail.send_mail", **arguments) + + def send_confirmation_mail_from_link(self, link: InvitationLink) -> Optional[str]: + guest = link.invitation.role.person + return self.queue_mail( + mail_to=link.invitation.role.sponsor.work_email, + guest_name=f"{guest.first_name} {guest.last_name}", + guest_person_id=guest.id, + ) diff --git a/gregui/mailutils/invite_guest.py b/gregui/mailutils/invite_guest.py new file mode 100644 index 00000000..c8796420 --- /dev/null +++ b/gregui/mailutils/invite_guest.py @@ -0,0 +1,102 @@ +from smtplib import SMTPRecipientsRefused +from typing import Optional + +import structlog +from django.conf import settings +from django.core import mail +from django.template.context import Context +from django_q.tasks import async_task + +from greg.models import InvitationLink, Sponsor +from gregui.models import EmailTemplate + +logger = structlog.getLogger(__name__) + + +class InviteGuest: + def make_registration_url(self, token: str) -> str: + return "{base}/invite#{token}".format( + base=settings.BASE_URL, + token=token, + ) + + def get_template(self): + return EmailTemplate.objects.get( + template_key=EmailTemplate.EmailType.GUEST_REGISTRATION + ) + + def get_notice_template(self): + return EmailTemplate.objects.get( + template_key=EmailTemplate.EmailType.INVALID_EMAIL + ) + + def send_bad_email_notice( + self, guest_name: str, guest_mail: str, sponsor_mail: str + ): + template = self.get_notice_template() + context = Context( + { + "guest_name": guest_name, + "guest_email": guest_mail, + } + ) + mail.send_mail( + subject=template.get_subject(context), + message=template.get_body(context), + from_email=template.from_email or None, + recipient_list=[sponsor_mail], + ) + + def queue_mail(self, link: InvitationLink) -> Optional[str]: + guest = link.invitation.role.person + email_address = guest.private_email + if not email_address: + logger.warning( + "No e-mail address found for invitation link with ID: {%s}", link.id + ) + return None + sponsor = link.invitation.role.sponsor + return async_task( + "gregui.mailutils.invite_guest.try_send_registration_mail", + **{ + "guest_name": f"{guest.first_name} {guest.last_name}", + "guest_mail": email_address.value, + "sponsor_id": sponsor.id, + "sponsor_name": f"{sponsor.first_name} {sponsor.last_name}", + "token": link.uuid, + }, + ) + + +def try_send_registration_mail( + guest_name: str, + guest_mail: str, + sponsor_id: int, + sponsor_name: str, + token: str, +): + ig = InviteGuest() + template = ig.get_template() + context = Context( + { + "institution": settings.INSTANCE_NAME, + "sponsor": sponsor_name, + "registration_link": ig.make_registration_url(token), + } + ) + try: + mail.send_mail( + subject=template.get_subject(context), + message=template.get_body(context), + from_email=template.from_email or None, + recipient_list=[guest_mail], + ) + except SMTPRecipientsRefused: + sp = Sponsor.objects.get(pk=sponsor_id) + # TODO: Mark the invite in the frontend as bad + if sp.work_email: + ig.send_bad_email_notice( + guest_name=guest_name, + guest_mail=guest_mail, + sponsor_mail=sp.work_email, + ) diff --git a/gregui/mailutils/protocol.py b/gregui/mailutils/protocol.py new file mode 100644 index 00000000..45dd3c2f --- /dev/null +++ b/gregui/mailutils/protocol.py @@ -0,0 +1,20 @@ +from typing import Protocol, Union + +from gregui.models import EmailTemplate + + +def prepare_arguments( + template: EmailTemplate, context: dict[str, str], mail_to: str +) -> dict[str, Union[str, list[str]]]: + """Combine input to a dict ready for use as arguments to django's send_mail""" + return { + "subject": template.get_subject(context), + "message": template.get_body(context), + "from_email": template.from_email or None, + "recipient_list": [mail_to], + } + + +class EmailSender(Protocol): + def queue_mail(self, *args): + pass diff --git a/gregui/mailutils/role_ending.py b/gregui/mailutils/role_ending.py new file mode 100644 index 00000000..2366cfa1 --- /dev/null +++ b/gregui/mailutils/role_ending.py @@ -0,0 +1,20 @@ +from django.template.context import Context +from django_q.tasks import async_task + +from gregui.mailutils.protocol import prepare_arguments +from gregui.models import EmailTemplate + + +class RolesEnding: + def __init__(self): + pass + + def queue_mail(self, mail_to: str, num_roles: int) -> str: + arguments = prepare_arguments( + template=EmailTemplate.objects.get( + template_key=EmailTemplate.EmailType.ROLE_END_REMINDER + ), + context=Context({"num_roles": num_roles}), + mail_to=mail_to, + ) + return async_task("django.core.mail.send_mail", **arguments) diff --git a/gregui/migrations/0004_add_email_template_type.py b/gregui/migrations/0004_add_email_template_type.py new file mode 100644 index 00000000..4c3c0fce --- /dev/null +++ b/gregui/migrations/0004_add_email_template_type.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.2 on 2022-05-13 12:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("gregui", "0003_add_email_template_type"), + ] + + operations = [ + migrations.AlterField( + model_name="emailtemplate", + name="template_key", + field=models.CharField( + choices=[ + ("guest_registration", "Guest Registration"), + ("sponsor_confirmation", "Sponsor Confirmation"), + ("role_end_reminder", "Role End Reminder"), + ("invalid_email", "Invalid Email"), + ], + max_length=64, + unique=True, + ), + ), + ] diff --git a/gregui/models.py b/gregui/models.py index 23de6d77..256f7ba6 100644 --- a/gregui/models.py +++ b/gregui/models.py @@ -37,6 +37,8 @@ class EmailTemplate(BaseModel): GUEST_REGISTRATION is for informing a guest that they have been invited SPONSOR_CONFIRMATION is for informing the sponsor they must verify the guest's information ROLE_END_REMINDER is used when reminding the sponsor if their ending roles in the near future + INVALID_EMAIL is used to notify sponsors when email sending fails + due to a problem with the address of an invited guest """ class EmailType(models.TextChoices): @@ -45,6 +47,7 @@ class EmailTemplate(BaseModel): GUEST_REGISTRATION = "guest_registration" SPONSOR_CONFIRMATION = "sponsor_confirmation" ROLE_END_REMINDER = "role_end_reminder" + INVALID_EMAIL = "invalid_email" template_key = models.CharField( max_length=64, choices=EmailType.choices, unique=True diff --git a/gregui/tests/api/views/test_invite_guest.py b/gregui/tests/api/views/test_invite_guest.py index 588529ed..51e9f545 100644 --- a/gregui/tests/api/views/test_invite_guest.py +++ b/gregui/tests/api/views/test_invite_guest.py @@ -8,12 +8,12 @@ from rest_framework.test import APIRequestFactory, force_authenticate from greg.models import Identity, Person, Role, Invitation, InvitationLink from gregui.api.views.invitation import InvitationView +from gregui.mailutils.invite_guest import InviteGuest @pytest.mark.django_db def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker): - # Mock function to avoid exception because there are no e-mail templates in the database - mocker.patch("gregui.api.views.invitation.send_invite_mail") + send_invite_mock_function = mocker.patch.object(InviteGuest, "queue_mail") test_comment = "This is a test comment" contact_person_unit = "This is a test contact person" @@ -33,11 +33,12 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker): "contact_person_unit": contact_person_unit, }, } - url = reverse("gregui-v1:invitation") assert len(Person.objects.all()) == 0 - request = APIRequestFactory().post(url, data, format="json") + request = APIRequestFactory().post( + path=reverse("gregui-v1:invitation"), data=data, format="json" + ) force_authenticate(request, user=user_sponsor) response = InvitationView.as_view()(request) @@ -62,6 +63,7 @@ def test_invite_guest(client, user_sponsor, unit_foo, role_type_foo, mocker): assert role.end_date == role_end_date.date() assert role.contact_person_unit == contact_person_unit assert role.comments == test_comment + send_invite_mock_function.assert_called() @pytest.mark.django_db @@ -117,9 +119,7 @@ def test_invite_resend_existing_invite_active( invitation_link_expired, mocker, ): - send_invite_mock_function = mocker.patch( - "gregui.api.views.invitation.send_invite_mail" - ) + send_invite_mock_function = mocker.patch.object(InviteGuest, "queue_mail") log_in(user_sponsor) invitation_links_for_person = InvitationLink.objects.filter( @@ -154,9 +154,8 @@ def test_invite_resend_existing_invite_active( def test_invite_resend_existing_invite_not_active( client, log_in, user_sponsor, person_invited, invitation_link_expired, mocker ): - send_invite_mock_function = mocker.patch( - "gregui.api.views.invitation.send_invite_mail", return_value=10 - ) + send_invite_mock_function = mocker.patch.object(InviteGuest, "queue_mail") + log_in(user_sponsor) invitation_links_for_person = InvitationLink.objects.filter( @@ -205,9 +204,7 @@ def test_invite_resend_person_multiple_links_send_all( client, log_in, user_sponsor, invited_person, registration_template, mocker ): """Resending an invite for a person with multiple working links should resend all of them.""" - send_invite_mock_function = mocker.patch( - "gregui.api.views.invitation.send_invite_mail" - ) + send_invite_mock_function = mocker.patch.object(InviteGuest, "queue_mail") log_in(user_sponsor) person, invitation = invited_person invitation = Invitation.objects.filter(role__person_id=person.id).first() @@ -236,9 +233,7 @@ def test_invite_resend_person_multiple_expired_links_send_all( Resending invites for a person with multiple invitations where all links are expired should resend all of them. """ - send_invite_mock_function = mocker.patch( - "gregui.api.views.invitation.send_invite_mail" - ) + send_invite_mock_function = mocker.patch.object(InviteGuest, "queue_mail") log_in(user_sponsor) # Expire invitation link diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index c7500fd5..b999205c 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -616,6 +616,16 @@ To complete the registration of your guest account, please follow this link: {{ return EmailTemplate.objects.get(id=et.id) +@pytest.fixture +def invalid_email_template(): + et = EmailTemplate.objects.create( + template_key=EmailTemplate.EmailType.INVALID_EMAIL, + subject="invalid email", + body="""Your guest {{guest_name}} has bad email {{guest_email}}""", + ) + return EmailTemplate.objects.get(pk=et.id) + + @pytest.fixture def consent_type_foo() -> ConsentType: type_foo = ConsentType.objects.create( diff --git a/gregui/tests/test_mailutils.py b/gregui/tests/test_mailutils.py index ce5f66cd..672f738c 100644 --- a/gregui/tests/test_mailutils.py +++ b/gregui/tests/test_mailutils.py @@ -1,8 +1,12 @@ +from smtplib import SMTPRecipientsRefused +from django.conf import settings from django.core import mail -from django_q.tasks import result +from django.template.context import Context import pytest -from gregui import mailutils +from gregui.mailutils.confirm_guest import ConfirmGuest + +from gregui.mailutils.invite_guest import InviteGuest, try_send_registration_mail @pytest.mark.django_db @@ -12,20 +16,25 @@ def test_registration_template(registration_template): "recipient_list": ["test@example.com"], "subject": "registration subject", "message": """Dette er en automatisk generert melding fra gjesteregistreringstjenesten. -Du har blitt registrert som gjest på InstanceName av Foo Bar. -For å fullføre registreringen av gjestekontoen følg denne lenken: https://example.org/invite#secret-key +Du har blitt registrert som gjest på local av Foo Bar. +For å fullføre registreringen av gjestekontoen følg denne lenken: Baz This message has been automatically generated by the guest registration system. -You have been registered as a guest at InstanceName by Foo Bar. -To complete the registration of your guest account, please follow this link: https://example.org/invite#secret-key""", +You have been registered as a guest at local by Foo Bar. +To complete the registration of your guest account, please follow this link: Baz""", } - rendered_template = mailutils.registration_template( - institution="InstanceName", - sponsor="Foo Bar", - mail_to="test@example.com", - token="secret-key", + ig = InviteGuest() + template = ig.get_template() + context = Context( + { + "institution": settings.INSTANCE_NAME, + "sponsor": "Foo Bar", + "registration_link": "Baz", + } ) - assert rendered_template == prefilled_template + assert template.get_subject(context) == prefilled_template["subject"] + assert template.get_body(context) == prefilled_template["message"] + assert template.from_email == prefilled_template["from_email"] @pytest.mark.django_db @@ -35,24 +44,34 @@ def test_confirmation_template(confirmation_template): "recipient_list": ["test@example.com"], "subject": "confirmation subject", "message": """Dette er en automatisk generert melding fra gjesteregistreringstjenesten. -Din gjest, Foo Bar, har fullført registrering, bekreft gjesten her: https://example.org/sponsor/guest/123 +Din gjest, Foo Bar, har fullført registrering, bekreft gjesten her: Baz This message has been automatically generated by the guest registration system. -Your guest, Foo Bar, has completed their registration, please confirm the guest here: https://example.org/sponsor/guest/123""", +Your guest, Foo Bar, has completed their registration, please confirm the guest here: Baz""", } - rendered_template = mailutils.confirmation_template( - guest_name="Foo Bar", mail_to="test@example.com", guest_person_id=123 + scg = ConfirmGuest() + template = scg.get_template() + context = Context( + { + "guest": "Foo Bar", + "confirmation_link": "Baz", + } ) - assert rendered_template == prefilled_template + assert template.get_subject(context) == prefilled_template["subject"] + assert template.get_body(context) == prefilled_template["message"] + assert template.from_email == prefilled_template["from_email"] @pytest.mark.django_db def test_registration_mail(registration_template): mail.outbox = [] - task_id = mailutils.send_registration_mail( - mail_to="test@example.no", sponsor="Foo", token="secret-key" + try_send_registration_mail( + guest_name="Guest Guesterson", + guest_mail="test@example.no", + sponsor_id=1, + sponsor_name="Foo", + token="secret-key", ) - assert result(task_id) == 1 assert len(mail.outbox) == 1 assert mail.outbox[0].to == ["test@example.no"] @@ -60,21 +79,20 @@ def test_registration_mail(registration_template): @pytest.mark.django_db def test_confirmation_mail(confirmation_template): mail.outbox = [] - task_id = mailutils.send_confirmation_mail( + scg = ConfirmGuest() + task_id = scg.queue_mail( mail_to="test@example.no", guest_name="Foo", guest_person_id=123 ) - assert result(task_id) == 1 - assert len(mail.outbox) == 1 - assert mail.outbox[0].to == ["test@example.no"] + assert isinstance(task_id, str) @pytest.mark.django_db def test_send_invite_mail(registration_template, invited_person): """Verify function queues an email when called""" _, link = invited_person - assert len(mail.outbox) == 0 - assert mailutils.send_invite_mail(link) - assert len(mail.outbox) == 1 + ig = InviteGuest() + task_id = ig.queue_mail(link) + assert isinstance(task_id, str) @pytest.mark.django_db @@ -82,9 +100,9 @@ def test_send_invite_mail_no_mail(invited_person): """Verify function returns None if an email is not present""" person, link = invited_person person.private_email.delete() - assert len(mail.outbox) == 0 - assert mailutils.send_invite_mail(link) is None - assert len(mail.outbox) == 0 + ig = InviteGuest() + task_id = ig.queue_mail(link) + assert task_id is None @pytest.mark.django_db @@ -92,5 +110,36 @@ def test_confirmation_mail_from_link(invited_person, confirmation_template): mail.outbox = [] _, link = invited_person assert len(mail.outbox) == 0 - assert mailutils.send_confirmation_mail_from_link(link) - assert len(mail.outbox) == 1 + scg = ConfirmGuest() + task_id = scg.send_confirmation_mail_from_link(link) + assert isinstance(task_id, str) + + +@pytest.mark.django_db +def test_try_send_registration_mail( + registration_template, invalid_email_template, mocker, sponsor_foo +): + send_invite_mock_function = mocker.patch("django.core.mail.send_mail") + + def side_effect(*args, **kwargs): + if "oladunk@example.com" in kwargs["recipient_list"]: + raise SMTPRecipientsRefused(recipients=kwargs["recipient_list"]) + + send_invite_mock_function.side_effect = side_effect + + try_send_registration_mail( + guest_name="Ola Dunk", + guest_mail="oladunk@example.com", + sponsor_id=sponsor_foo.id, + sponsor_name=sponsor_foo.first_name + " " + sponsor_foo.last_name, + token="secret-key", + ) + assert send_invite_mock_function.call_count == 2 + send_invite_mock_function.assert_called_with( + **{ + "subject": "invalid email", + "message": "Your guest Ola Dunk has bad email oladunk@example.com", + "from_email": None, + "recipient_list": ["foo@example.org"], + } + ) -- GitLab