From cd94dc50917ca5f426b41b9b0060a1e4fb7c8dbd Mon Sep 17 00:00:00 2001 From: Marie PUPO JEAMMET Date: Fri, 10 Jan 2025 18:25:07 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(dimail)=20send=20pending=20mailboxes?= =?UTF-8?q?=20upon=20domain=20activation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send creation requests to dimail for all pending mailboxes when domain goes from "pending" to "enabled". --- CHANGELOG.md | 5 ++ .../mailbox_manager/api/client/serializers.py | 14 ++-- .../migrations/0016_alter_mailbox_domain.py | 19 ++++++ src/backend/mailbox_manager/models.py | 2 +- .../tests/test_admin_actions.py | 62 +++++++++++++---- .../tests/test_utils_dimail_client.py | 67 +++++++++++++++++++ src/backend/mailbox_manager/utils/dimail.py | 32 +++++++-- 7 files changed, 172 insertions(+), 29 deletions(-) create mode 100644 src/backend/mailbox_manager/migrations/0016_alter_mailbox_domain.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 81aeed4..830b15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,15 @@ and this project adheres to ## [Unreleased] +### Added + +- ✨(dimail) send pending mailboxes upon domain activation + ### Fixed - 🚑️(plugins) fix name from SIRET specific case #674 + ## [1.10.1] - 2025-01-27 ### Added diff --git a/src/backend/mailbox_manager/api/client/serializers.py b/src/backend/mailbox_manager/api/client/serializers.py index 9de7620..4d298db 100644 --- a/src/backend/mailbox_manager/api/client/serializers.py +++ b/src/backend/mailbox_manager/api/client/serializers.py @@ -36,23 +36,19 @@ class MailboxSerializer(serializers.ModelSerializer): Override create function to fire a request on mailbox creation. """ mailbox = super().create(validated_data) - if validated_data["domain"].status == enums.MailDomainStatusChoices.ENABLED: + if mailbox.domain.status == enums.MailDomainStatusChoices.ENABLED: client = DimailAPIClient() # send new mailbox request to dimail - response = client.create_mailbox( - validated_data, self.context["request"].user.sub - ) + response = client.create_mailbox(mailbox, self.context["request"].user.sub) - # fix format to have actual json, and remove uuid - mailbox_data = json.loads( - response.content.decode("utf-8").replace("'", '"') - ) + # fix format to have actual json + dimail_data = json.loads(response.content.decode("utf-8").replace("'", '"')) mailbox.status = enums.MailDomainStatusChoices.ENABLED mailbox.save() # send confirmation email client.notify_mailbox_creation( - recipient=validated_data["secondary_email"], mailbox_data=mailbox_data + recipient=mailbox.secondary_email, mailbox_data=dimail_data ) # actually save mailbox on our database diff --git a/src/backend/mailbox_manager/migrations/0016_alter_mailbox_domain.py b/src/backend/mailbox_manager/migrations/0016_alter_mailbox_domain.py new file mode 100644 index 0000000..eda1f60 --- /dev/null +++ b/src/backend/mailbox_manager/migrations/0016_alter_mailbox_domain.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.5 on 2025-01-31 17:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailbox_manager', '0015_change_mailboxes_status_to_enabled'), + ] + + operations = [ + migrations.AlterField( + model_name='mailbox', + name='domain', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailboxes', to='mailbox_manager.maildomain'), + ), + ] diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index 77ba8e5..19cafa4 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -181,7 +181,7 @@ class Mailbox(BaseModel): domain = models.ForeignKey( MailDomain, on_delete=models.CASCADE, - related_name="mail_domain", + related_name="mailboxes", null=False, blank=False, ) diff --git a/src/backend/mailbox_manager/tests/test_admin_actions.py b/src/backend/mailbox_manager/tests/test_admin_actions.py index 73df6f7..c9f1465 100644 --- a/src/backend/mailbox_manager/tests/test_admin_actions.py +++ b/src/backend/mailbox_manager/tests/test_admin_actions.py @@ -9,16 +9,17 @@ from django.urls import reverse import pytest import responses +from rest_framework import status from core import factories as core_factories -from mailbox_manager import enums, factories +from mailbox_manager import enums, factories, models from .fixtures.dimail import CHECK_DOMAIN_BROKEN, CHECK_DOMAIN_OK @pytest.mark.django_db -def test_admin_action__fetch_domain_status_from_dimail(client): +def test_fetch_domain_status__should_switch_to_failed_when_domain_broken(client): """Test admin action to check health of some domains""" admin = core_factories.UserFactory(is_staff=True, is_superuser=True) client.force_login(admin) @@ -42,38 +43,73 @@ def test_admin_action__fetch_domain_status_from_dimail(client): rsps.GET, re.compile(rf".*/domains/{domain1.name}/check/"), body=json.dumps(body_content_domain1), - status=200, + status=status.HTTP_200_OK, content_type="application/json", ) rsps.add( rsps.GET, re.compile(rf".*/domains/{domain2.name}/check/"), body=json.dumps(body_content_domain2), - status=200, + status=status.HTTP_200_OK, content_type="application/json", ) response = client.post(url, data, follow=True) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK domain1.refresh_from_db() domain2.refresh_from_db() assert domain1.status == enums.MailDomainStatusChoices.FAILED assert domain2.status == enums.MailDomainStatusChoices.FAILED assert "Check domains done with success" in response.content.decode("utf-8") - # check with a valid domain info from dimail + +@pytest.mark.django_db +def test_fetch_domain_status__should_switch_to_enabled_when_domain_ok(client): + """Test admin action should switch domain state to ENABLED + when dimail's response is "ok". It should also activate any pending mailbox.""" + admin = core_factories.UserFactory(is_staff=True, is_superuser=True) + client.force_login(admin) + domain1 = factories.MailDomainFactory() + factories.MailboxFactory.create_batch(3, domain=domain1) + + domain2 = factories.MailDomainFactory() + data = { + "action": "fetch_domain_status_from_dimail", + "_selected_action": [domain1.id], + } + url = reverse("admin:mailbox_manager_maildomain_changelist") + + with responses.RequestsMock() as rsps: body_content_domain1 = CHECK_DOMAIN_OK.copy() body_content_domain1["name"] = domain1.name + rsps.add( rsps.GET, re.compile(rf".*/domains/{domain1.name}/check/"), body=json.dumps(body_content_domain1), - status=200, + status=status.HTTP_200_OK, content_type="application/json", ) + rsps.add( + rsps.GET, + re.compile(r".*/token/"), + body='{"access_token": "domain_owner_token"}', + status=status.HTTP_200_OK, + content_type="application/json", + ) + rsps.add( + rsps.POST, + re.compile(rf".*/domains/{domain1.name}/mailboxes/"), + body=response_mailbox_created(f"truc@{domain1.name}"), + status=status.HTTP_201_CREATED, + content_type="application/json", + ) + response = client.post(url, data, follow=True) - assert response.status_code == 200 - domain1.refresh_from_db() - domain2.refresh_from_db() - assert domain1.status == enums.MailDomainStatusChoices.ENABLED - assert domain2.status == enums.MailDomainStatusChoices.FAILED - assert "Check domains done with success" in response.content.decode("utf-8") + assert response.status_code == status.HTTP_200_OK + domain1.refresh_from_db() + domain2.refresh_from_db() + assert domain1.status == enums.MailDomainStatusChoices.ENABLED + assert domain2.status == enums.MailDomainStatusChoices.PENDING + assert "Check domains done with success" in response.content.decode("utf-8") + for mailbox in models.Mailbox.objects.filter(domain=domain1): + assert mailbox.status == enums.MailboxStatusChoices.ENABLED diff --git a/src/backend/mailbox_manager/tests/test_utils_dimail_client.py b/src/backend/mailbox_manager/tests/test_utils_dimail_client.py index 6493710..891c390 100644 --- a/src/backend/mailbox_manager/tests/test_utils_dimail_client.py +++ b/src/backend/mailbox_manager/tests/test_utils_dimail_client.py @@ -3,6 +3,7 @@ Unit tests for dimail client """ import json +import logging import re from email.errors import HeaderParseError, NonASCIILocalPartDefect from logging import Logger @@ -191,3 +192,69 @@ def test_dimail__fetch_domain_status_from_dimail(): response = dimail_client.fetch_domain_status(domain) assert response.status_code == status.HTTP_200_OK assert domain.status == enums.MailDomainStatusChoices.ENABLED + +def test_dimail___enable_pending_mailboxes(caplog): + """Status of pending mailboxes should switch to "enabled" when calling enable_pending_mailboxes.""" + caplog.set_level(logging.INFO) + + domain = factories.MailDomainFactory() + mailbox1 = factories.MailboxFactory( + domain=domain, status=enums.MailboxStatusChoices.PENDING + ) + mailbox2 = factories.MailboxFactory( + domain=domain, status=enums.MailboxStatusChoices.PENDING + ) + factories.MailboxFactory.create_batch( + 2, domain=domain, status=enums.MailboxStatusChoices.ENABLED + ) + + dimail_client = DimailAPIClient() + with responses.RequestsMock() as rsps: + rsps.add( + rsps.GET, + re.compile(r".*/token/"), + body='{"access_token": "domain_owner_token"}', + status=status.HTTP_200_OK, + content_type="application/json", + ) + rsps.add( + rsps.POST, + re.compile(rf".*/domains/{domain.name}/mailboxes/"), + body=str( + { + "email": f"mock@{domain.name}", + "password": "newpass", + "uuid": "uuid", + } + ), + status=status.HTTP_201_CREATED, + content_type="application/json", + ) + dimail_client.enable_pending_mailboxes(domain=domain) + + mailbox1.refresh_from_db() + mailbox2.refresh_from_db() + assert mailbox1.status == enums.MailboxStatusChoices.ENABLED + assert mailbox2.status == enums.MailboxStatusChoices.ENABLED + + assert len(caplog.records) == 6 + assert ( + caplog.records[0].message + == "Token succesfully granted by mail-provisioning API." + ) + assert ( + caplog.records[1].message + == f"Mailbox successfully created on domain {domain.name} by user None" + ) + assert ( + caplog.records[2].message + == f"Information for mailbox mock@{domain.name} sent to {mailbox1.secondary_email}." + ) + assert ( + caplog.records[4].message + == f"Mailbox successfully created on domain {domain.name} by user None" + ) + assert ( + caplog.records[5].message + == f"Information for mailbox mock@{domain.name} sent to {mailbox2.secondary_email}." + ) diff --git a/src/backend/mailbox_manager/utils/dimail.py b/src/backend/mailbox_manager/utils/dimail.py index d2c2f08..3664fe1 100644 --- a/src/backend/mailbox_manager/utils/dimail.py +++ b/src/backend/mailbox_manager/utils/dimail.py @@ -116,15 +116,15 @@ class DimailAPIClient: """Send a CREATE mailbox request to mail provisioning API.""" payload = { - "givenName": mailbox["first_name"], - "surName": mailbox["last_name"], - "displayName": f"{mailbox['first_name']} {mailbox['last_name']}", + "givenName": mailbox.first_name, + "surName": mailbox.last_name, + "displayName": f"{mailbox.first_name} {mailbox.last_name}", } headers = self.get_headers(user_sub) try: response = session.post( - f"{self.API_URL}/domains/{mailbox['domain']}/mailboxes/{mailbox['local_part']}", + f"{self.API_URL}/domains/{mailbox.domain.name}/mailboxes/{mailbox.local_part}", json=payload, headers=headers, verify=True, @@ -141,7 +141,7 @@ class DimailAPIClient: if response.status_code == status.HTTP_201_CREATED: logger.info( "Mailbox successfully created on domain %s by user %s", - str(mailbox["domain"]), + str(mailbox.domain), user_sub, ) return response @@ -149,7 +149,7 @@ class DimailAPIClient: if response.status_code == status.HTTP_403_FORBIDDEN: logger.error( "[DIMAIL] 403 Forbidden: you cannot access domain %s", - str(mailbox["domain"]), + str(mailbox.domain), ) raise exceptions.PermissionDenied( "Permission denied. Please check your MAIL_PROVISIONING_API_CREDENTIALS." @@ -413,6 +413,7 @@ class DimailAPIClient: domain.status != enums.MailDomainStatusChoices.ENABLED and dimail_status == "ok" ): + self.enable_pending_mailboxes(domain) domain.status = enums.MailDomainStatusChoices.ENABLED domain.save() elif ( @@ -423,3 +424,22 @@ class DimailAPIClient: domain.save() return response return self.raise_exception_for_unexpected_response(response) + + def enable_pending_mailboxes(self, domain): + """Send requests for all pending mailboxes of a domain.""" + + for mailbox in domain.mailboxes.filter( + status=enums.MailboxStatusChoices.PENDING + ): + response = self.create_mailbox(mailbox) + + # fix format to have actual json + dimail_data = json.loads(response.content.decode("utf-8").replace("'", '"')) + mailbox.status = enums.MailDomainStatusChoices.ENABLED + mailbox.save() + + # send confirmation email + self.notify_mailbox_creation( + recipient=mailbox.secondary_email, + mailbox_data=dimail_data, + )