From 21bf431940e3180ff28d52ad596e532f76cd14bb Mon Sep 17 00:00:00 2001 From: Marie PUPO JEAMMET Date: Wed, 23 Oct 2024 18:44:14 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(dimail)=20send=20domain=20creation=20?= =?UTF-8?q?request=20to=20dimail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Send domain creation request to dimail when someone creates a domain in people. --- CHANGELOG.md | 1 + docs/interoperability/dimail.md | 2 + .../mailbox_manager/api/serializers.py | 13 ++ .../test_api_mail_domains_create.py | 124 ++++++++++++++++-- src/backend/mailbox_manager/utils/dimail.py | 34 +++++ 5 files changed, 166 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d90d401..67e0879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to ### Added +- ✨(dimail) send domain creation requests to dimail #454 - ✨(dimail) synchronize mailboxes from dimail to our db #453 - ✨(ci) add security scan #429 - ✨(teams) register contacts on admin views diff --git a/docs/interoperability/dimail.md b/docs/interoperability/dimail.md index 031c015..702eb56 100644 --- a/docs/interoperability/dimail.md +++ b/docs/interoperability/dimail.md @@ -15,6 +15,8 @@ As dimail's primary goal is to act as an interface between People and OX, its ar Upon creating a domain on People, the same domain is created on dimail and will undergo a series of checks. When all checks have passed, the domain is considered valid and mailboxes can be created on it. +Domains in OX have a field called "context". Context is a shared space between domains, allowing users to discover users not only on their domain but on their entire context. + ### Users The ones issuing requests. Dimail users will reflect domains owners and administrators on People, for logging purposes and to allow direct use of dimail, if desired. User reconciliation is made on user uuid provided by ProConnect. diff --git a/src/backend/mailbox_manager/api/serializers.py b/src/backend/mailbox_manager/api/serializers.py index 25fea76..f5871dc 100644 --- a/src/backend/mailbox_manager/api/serializers.py +++ b/src/backend/mailbox_manager/api/serializers.py @@ -77,6 +77,19 @@ class MailDomainSerializer(serializers.ModelSerializer): return domain.get_abilities(request.user) return {} + def create(self, validated_data): + """ + Override create function to fire a request to dimail upon domain creation. + """ + # send new domain request to dimail + client = DimailAPIClient() + client.send_domain_creation_request( + validated_data["name"], self.context["request"].user.sub + ) + + # no exception raised ? Then actually save domain on our database + return models.MailDomain.objects.create(**validated_data) + class MailDomainAccessSerializer(serializers.ModelSerializer): """Serialize mail domain access.""" diff --git a/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_create.py b/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_create.py index 13dfe4d..f9769b4 100644 --- a/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_create.py +++ b/src/backend/mailbox_manager/tests/api/mail_domain/test_api_mail_domains_create.py @@ -2,7 +2,13 @@ Tests for MailDomains API endpoint in People's app mailbox_manager. Focus on "create" action. """ +import re +from logging import Logger +from unittest import mock + import pytest +import responses +from requests.exceptions import HTTPError from rest_framework import status from rest_framework.test import APIClient @@ -56,13 +62,26 @@ def test_api_mail_domains__create_authenticated(): client = APIClient() client.force_login(user) - response = client.post( - "/api/v1.0/mail-domains/", - { - "name": "mydomain.com", - }, - format="json", - ) + + domain_name = "test.domain.fr" + + with responses.RequestsMock() as rsps: + rsps.add( + rsps.POST, + re.compile(r".*/domains/"), + body=str( + { + "name": domain_name, + } + ), + status=status.HTTP_201_CREATED, + content_type="application/json", + ) + response = client.post( + "/api/v1.0/mail-domains/", + {"name": domain_name, "context": "null", "features": ["webmail"]}, + format="json", + ) assert response.status_code == status.HTTP_201_CREATED domain = models.MailDomain.objects.get() @@ -79,5 +98,94 @@ def test_api_mail_domains__create_authenticated(): # a new domain with status "pending" is created and authenticated user is the owner assert domain.status == enums.MailDomainStatusChoices.PENDING - assert domain.name == "mydomain.com" + assert domain.name == domain_name assert domain.accesses.filter(role="owner", user=user).exists() + + +## SYNC TO DIMAIL +@mock.patch.object(Logger, "info") +def test_api_mail_domains__create_dimail_domain(mock_info): + """ + Creating a domain should trigger a call to create a domain on dimail too. + """ + user = core_factories.UserFactory() + + client = APIClient() + client.force_login(user) + domain_name = "test.fr" + + with responses.RequestsMock() as rsps: + rsp = rsps.add( + rsps.POST, + re.compile(r".*/domains/"), + body=str( + { + "name": domain_name, + } + ), + status=status.HTTP_201_CREATED, + content_type="application/json", + ) + response = client.post( + "/api/v1.0/mail-domains/", + { + "name": domain_name, + }, + format="json", + ) + + assert response.status_code == status.HTTP_201_CREATED + assert rsp.call_count == 1 # endpoint was called + + # Logger + assert ( + mock_info.call_count == 2 + ) # should be 1. A new empty info has been added. To be investigated + assert mock_info.call_args_list[0][0] == ( + "Domain %s successfully created on dimail by user %s", + domain_name, + user.sub, + ) + + +def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog): + """No domain should be created when dimail returns a 409 conflict.""" + user = core_factories.UserFactory() + + client = APIClient() + client.force_login(user) + domain_name = "test.fr" + dimail_error = { + "status_code": status.HTTP_409_CONFLICT, + "detail": "Domain already exists", + } + + with responses.RequestsMock() as rsps: + rsp = rsps.add( + rsps.POST, + re.compile(r".*/domains/"), + body=str({"detail": dimail_error["detail"]}), + status=dimail_error["status_code"], + content_type="application/json", + ) + with pytest.raises(HTTPError): + response = client.post( + "/api/v1.0/mail-domains/", + { + "name": domain_name, + }, + format="json", + ) + + assert rsp.call_count == 1 # endpoint was called + assert response.status_code == dimail_error["status_code"] + assert response.json() == {"detail": dimail_error["detail"]} + + # check logs + assert len(caplog.records) == 2 # 1 + new empty info. To be investigated + record = caplog.records[0] + assert record.levelname == "ERROR" + assert ( + record.message + == f"[DIMAIL] {dimail_error['status_code']}: {dimail_error['detail']}" + ) diff --git a/src/backend/mailbox_manager/utils/dimail.py b/src/backend/mailbox_manager/utils/dimail.py index f2856c4..49c40b2 100644 --- a/src/backend/mailbox_manager/utils/dimail.py +++ b/src/backend/mailbox_manager/utils/dimail.py @@ -76,6 +76,40 @@ class DimailAPIClient: return self.pass_dimail_unexpected_response(response) + def send_domain_creation_request(self, domain_name, request_user): + """Send a domain creation request to dimail API.""" + + payload = { + "domain": domain_name, + "context": domain_name, # for now, we put each domain on its own context + "features": ["webmail", "mailboxes"], + } + try: + response = session.post( + f"{self.API_URL}/domains/", + json=payload, + headers={"Authorization": f"Basic {self.API_CREDENTIALS}"}, + verify=True, + timeout=10, + ) + except requests.exceptions.ConnectionError as error: + logger.error( + "Connection error while trying to reach %s.", + self.API_URL, + exc_info=error, + ) + raise error + + if response.status_code == status.HTTP_201_CREATED: + logger.info( + "Domain %s successfully created on dimail by user %s", + domain_name, + request_user, + ) + return response + + return self.pass_dimail_unexpected_response(response) + def send_mailbox_request(self, mailbox, user_sub=None): """Send a CREATE mailbox request to mail provisioning API."""