(domains) check status after creation

Fetch domain status from dimail just after domain creation.
This commit is contained in:
Sabrina Demagny
2025-02-12 18:48:22 +01:00
parent a811431070
commit ab03cd9db9
4 changed files with 278 additions and 188 deletions

View File

@@ -10,6 +10,7 @@ and this project adheres to
### Added ### Added
- ✨(domains) check status after creation
- ✨(domains) display required actions to do on domain - ✨(domains) display required actions to do on domain
- ✨(plugin) add CommuneCreation plugin with domain provisioning #658 - ✨(plugin) add CommuneCreation plugin with domain provisioning #658
- ✨(frontend) display action required status on domain - ✨(frontend) display action required status on domain

View File

@@ -118,9 +118,17 @@ class MailDomainSerializer(serializers.ModelSerializer):
# send new domain request to dimail # send new domain request to dimail
client = DimailAPIClient() client = DimailAPIClient()
client.create_domain(validated_data["name"], self.context["request"].user.sub) client.create_domain(validated_data["name"], self.context["request"].user.sub)
domain = super().create(validated_data)
# no exception raised ? Then actually save domain on our database # check domain status and update it
return super().create(validated_data) try:
client.fetch_domain_status(domain)
except HTTPError as e:
logger.exception(
"[DIMAIL] domain status fetch after creation failed %s with error %s",
domain.name,
e,
)
return domain
class MailDomainAccessSerializer(serializers.ModelSerializer): class MailDomainAccessSerializer(serializers.ModelSerializer):

View File

@@ -2,6 +2,7 @@
Tests for MailDomains API endpoint in People's app mailbox_manager. Focus on "create" action. Tests for MailDomains API endpoint in People's app mailbox_manager. Focus on "create" action.
""" """
import json
import logging import logging
import re import re
@@ -14,6 +15,7 @@ from rest_framework.test import APIClient
from core import factories as core_factories from core import factories as core_factories
from mailbox_manager import enums, factories, models from mailbox_manager import enums, factories, models
from mailbox_manager.tests.fixtures.dimail import CHECK_DOMAIN_BROKEN, CHECK_DOMAIN_OK
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@@ -52,6 +54,7 @@ def test_api_mail_domains__create_name_unique():
assert response.json()["name"] == ["Mail domain with this name already exists."] assert response.json()["name"] == ["Mail domain with this name already exists."]
@responses.activate
def test_api_mail_domains__create_authenticated(): def test_api_mail_domains__create_authenticated():
""" """
Authenticated users should be able to create mail domains Authenticated users should be able to create mail domains
@@ -64,9 +67,8 @@ def test_api_mail_domains__create_authenticated():
domain_name = "test.domain.fr" domain_name = "test.domain.fr"
with responses.RequestsMock() as rsps: responses.add(
rsps.add( responses.POST,
rsps.POST,
re.compile(r".*/domains/"), re.compile(r".*/domains/"),
body=str( body=str(
{ {
@@ -76,8 +78,8 @@ def test_api_mail_domains__create_authenticated():
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
rsps.add( responses.add(
rsps.POST, responses.POST,
re.compile(r".*/users/"), re.compile(r".*/users/"),
body=str( body=str(
{ {
@@ -90,13 +92,22 @@ def test_api_mail_domains__create_authenticated():
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
rsps.add( responses.add(
rsps.POST, responses.POST,
re.compile(r".*/allows/"), re.compile(r".*/allows/"),
body=str({"user": "request-user-sub", "domain": str(domain_name)}), body=str({"user": "request-user-sub", "domain": str(domain_name)}),
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
body_content_domain1 = CHECK_DOMAIN_BROKEN.copy()
body_content_domain1["name"] = domain_name
responses.add(
responses.GET,
re.compile(rf".*/domains/{domain_name}/check/"),
body=json.dumps(body_content_domain1),
status=status.HTTP_200_OK,
content_type="application/json",
)
response = client.post( response = client.post(
"/api/v1.0/mail-domains/", "/api/v1.0/mail-domains/",
{ {
@@ -115,28 +126,42 @@ def test_api_mail_domains__create_authenticated():
"id": str(domain.id), "id": str(domain.id),
"name": domain.name, "name": domain.name,
"slug": domain.slug, "slug": domain.slug,
"status": enums.MailDomainStatusChoices.PENDING, "status": enums.MailDomainStatusChoices.ACTION_REQUIRED,
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"), "created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"), "updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
"abilities": domain.get_abilities(user), "abilities": domain.get_abilities(user),
"count_mailboxes": 0, "count_mailboxes": 0,
"support_email": domain.support_email, "support_email": domain.support_email,
"last_check_details": None, "last_check_details": body_content_domain1,
"action_required_details": {}, "action_required_details": {
"cname_imap": "Il faut un CNAME 'imap.example.fr' qui renvoie vers "
"'imap.ox.numerique.gouv.fr.'",
"cname_smtp": "Le CNAME pour 'smtp.example.fr' n'est pas bon, "
"il renvoie vers 'ns0.ovh.net.' et je veux 'smtp.ox.numerique.gouv.fr.'",
"cname_webmail": "Il faut un CNAME 'webmail.example.fr' qui "
"renvoie vers 'webmail.ox.numerique.gouv.fr.'",
"dkim": "Il faut un DKIM record, avec la bonne clef",
"mx": "Je veux que le MX du domaine soit mx.ox.numerique.gouv.fr., "
"or je trouve example-fr.mail.protection.outlook.com.",
"spf": "Le SPF record ne contient pas include:_spf.ox.numerique.gouv.fr",
},
} }
# a new domain with status "pending" is created and authenticated user is the owner # a new domain with status "action required" is created and authenticated user is the owner
assert domain.status == enums.MailDomainStatusChoices.PENDING assert domain.status == enums.MailDomainStatusChoices.ACTION_REQUIRED
assert domain.last_check_details == body_content_domain1
assert domain.name == domain_name assert domain.name == domain_name
assert domain.accesses.filter(role="owner", user=user).exists() assert domain.accesses.filter(role="owner", user=user).exists()
def test_api_mail_domains__create_authenticated__dimail_failure(): @responses.activate
def test_api_mail_domains__create_authenticated__dimail_failure(caplog):
""" """
Despite a dimail failure for user and/or allow creation, Despite a dimail failure for user and/or allow creation,
an authenticated user should be able to create a mail domain an authenticated user should be able to create a mail domain
and should automatically be added as owner of the newly created domain. and should automatically be added as owner of the newly created domain.
""" """
caplog.set_level(logging.ERROR)
user = core_factories.UserFactory() user = core_factories.UserFactory()
client = APIClient() client = APIClient()
@@ -144,9 +169,8 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
domain_name = "test.domain.fr" domain_name = "test.domain.fr"
with responses.RequestsMock() as rsps: responses.add(
rsps.add( responses.POST,
rsps.POST,
re.compile(r".*/domains/"), re.compile(r".*/domains/"),
body=str( body=str(
{ {
@@ -156,8 +180,8 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
rsps.add( responses.add(
rsps.POST, responses.POST,
re.compile(r".*/users/"), re.compile(r".*/users/"),
body=str( body=str(
{ {
@@ -170,13 +194,24 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
rsps.add( responses.add(
rsps.POST, responses.POST,
re.compile(r".*/allows/"), re.compile(r".*/allows/"),
body=str({"user": "request-user-sub", "domain": str(domain_name)}), body=str({"user": "request-user-sub", "domain": str(domain_name)}),
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
content_type="application/json", content_type="application/json",
) )
dimail_error = {
"status_code": status.HTTP_401_UNAUTHORIZED,
"detail": "Not authorized",
}
responses.add(
responses.GET,
re.compile(rf".*/domains/{domain_name}/check/"),
body=json.dumps(dimail_error),
status=dimail_error["status_code"],
content_type="application/json",
)
response = client.post( response = client.post(
"/api/v1.0/mail-domains/", "/api/v1.0/mail-domains/",
{ {
@@ -208,9 +243,12 @@ def test_api_mail_domains__create_authenticated__dimail_failure():
assert domain.status == enums.MailDomainStatusChoices.FAILED assert domain.status == enums.MailDomainStatusChoices.FAILED
assert domain.name == domain_name assert domain.name == domain_name
assert domain.accesses.filter(role="owner", user=user).exists() assert domain.accesses.filter(role="owner", user=user).exists()
assert caplog.records[0].levelname == "ERROR"
assert "Not authorized" in caplog.records[0].message
## SYNC TO DIMAIL ## SYNC TO DIMAIL
@responses.activate
def test_api_mail_domains__create_dimail_domain(caplog): def test_api_mail_domains__create_dimail_domain(caplog):
""" """
Creating a domain should trigger a call to create a domain on dimail too. Creating a domain should trigger a call to create a domain on dimail too.
@@ -222,9 +260,8 @@ def test_api_mail_domains__create_dimail_domain(caplog):
client.force_login(user) client.force_login(user)
domain_name = "test.fr" domain_name = "test.fr"
with responses.RequestsMock() as rsps: responses.add(
rsp = rsps.add( responses.POST,
rsps.POST,
re.compile(r".*/domains/"), re.compile(r".*/domains/"),
body=str( body=str(
{ {
@@ -234,8 +271,8 @@ def test_api_mail_domains__create_dimail_domain(caplog):
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
rsps.add( responses.add(
rsps.POST, responses.POST,
re.compile(r".*/users/"), re.compile(r".*/users/"),
body=str( body=str(
{ {
@@ -248,13 +285,23 @@ def test_api_mail_domains__create_dimail_domain(caplog):
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
rsps.add( responses.add(
rsps.POST, responses.POST,
re.compile(r".*/allows/"), re.compile(r".*/allows/"),
body=str({"user": "request-user-sub", "domain": str(domain_name)}), body=str({"user": "request-user-sub", "domain": str(domain_name)}),
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
content_type="application/json", content_type="application/json",
) )
body_content_domain1 = CHECK_DOMAIN_OK.copy()
body_content_domain1["name"] = domain_name
responses.add(
responses.GET,
re.compile(rf".*/domains/{domain_name}/check/"),
body=json.dumps(body_content_domain1),
status=status.HTTP_200_OK,
content_type="application/json",
)
response = client.post( response = client.post(
"/api/v1.0/mail-domains/", "/api/v1.0/mail-domains/",
{ {
@@ -265,7 +312,6 @@ def test_api_mail_domains__create_dimail_domain(caplog):
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
assert rsp.call_count == 1 # endpoint was called
# Logger # Logger
assert len(caplog.records) == 4 # should be 3. Last empty info still here. assert len(caplog.records) == 4 # should be 3. Last empty info still here.
@@ -283,6 +329,7 @@ def test_api_mail_domains__create_dimail_domain(caplog):
) )
@responses.activate
def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog): def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog):
"""No domain should be created when dimail returns a 409 conflict.""" """No domain should be created when dimail returns a 409 conflict."""
user = core_factories.UserFactory() user = core_factories.UserFactory()
@@ -294,15 +341,35 @@ def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog):
"status_code": status.HTTP_409_CONFLICT, "status_code": status.HTTP_409_CONFLICT,
"detail": "Domain already exists", "detail": "Domain already exists",
} }
responses.add(
with responses.RequestsMock() as rsps: responses.POST,
rsp = rsps.add( re.compile(r".*/users/"),
rsps.POST, body=str(
{
"name": "request-user-sub",
"is_admin": "false",
"uuid": "user-uuid-on-dimail",
"perms": [],
}
),
status=status.HTTP_201_CREATED,
content_type="application/json",
)
responses.add(
responses.POST,
re.compile(r".*/allows/"),
body=str({"user": "request-user-sub", "domain": str(domain_name)}),
status=status.HTTP_201_CREATED,
content_type="application/json",
)
responses.add(
responses.POST,
re.compile(r".*/domains/"), re.compile(r".*/domains/"),
body=str({"detail": dimail_error["detail"]}), body=str({"detail": dimail_error["detail"]}),
status=dimail_error["status_code"], status=dimail_error["status_code"],
content_type="application/json", content_type="application/json",
) )
with pytest.raises(HTTPError): with pytest.raises(HTTPError):
response = client.post( response = client.post(
"/api/v1.0/mail-domains/", "/api/v1.0/mail-domains/",
@@ -313,7 +380,6 @@ def test_api_mail_domains__no_creation_when_dimail_duplicate(caplog):
format="json", format="json",
) )
assert rsp.call_count == 1 # endpoint was called
assert response.status_code == dimail_error["status_code"] assert response.status_code == dimail_error["status_code"]
assert response.json() == {"detail": dimail_error["detail"]} assert response.json() == {"detail": dimail_error["detail"]}

View File

@@ -418,12 +418,19 @@ class DimailAPIClient:
def check_domain(self, domain): def check_domain(self, domain):
"""Send a request to dimail to check domain health.""" """Send a request to dimail to check domain health."""
try:
response = session.get( response = session.get(
f"{self.API_URL}/domains/{domain.name}/check/", f"{self.API_URL}/domains/{domain.name}/check/",
headers={"Authorization": f"Basic {self.API_CREDENTIALS}"}, headers={"Authorization": f"Basic {self.API_CREDENTIALS}"},
verify=True, verify=True,
timeout=10, timeout=10,
) )
except requests.exceptions.ConnectionError as error:
logger.error(
"Connection error while trying to reach %s.",
self.API_URL,
exc_info=error,
)
if response.status_code == status.HTTP_200_OK: if response.status_code == status.HTTP_200_OK:
return response.json() return response.json()
return self.raise_exception_for_unexpected_response(response) return self.raise_exception_for_unexpected_response(response)
@@ -448,6 +455,7 @@ class DimailAPIClient:
def fetch_domain_status(self, domain): def fetch_domain_status(self, domain):
"""Send a request to check and update status of a domain.""" """Send a request to check and update status of a domain."""
dimail_response = self.check_domain(domain) dimail_response = self.check_domain(domain)
if dimail_response:
dimail_state = dimail_response["state"] dimail_state = dimail_response["state"]
# if domain is not enabled and dimail returns ok status, enable it # if domain is not enabled and dimail returns ok status, enable it
if ( if (
@@ -461,15 +469,22 @@ class DimailAPIClient:
# if dimail returns broken status, we need to extract external and internal checks # if dimail returns broken status, we need to extract external and internal checks
# and manage the case where the domain has to be fixed by support # and manage the case where the domain has to be fixed by support
elif dimail_state == "broken": elif dimail_state == "broken":
external_checks = self._get_dimail_checks(dimail_response, internal=False) external_checks = self._get_dimail_checks(
internal_checks = self._get_dimail_checks(dimail_response, internal=True) dimail_response, internal=False
)
internal_checks = self._get_dimail_checks(
dimail_response, internal=True
)
# manage the case where the domain has to be fixed by support # manage the case where the domain has to be fixed by support
if not all(external_checks.values()): if not all(external_checks.values()):
domain.status = enums.MailDomainStatusChoices.ACTION_REQUIRED domain.status = enums.MailDomainStatusChoices.ACTION_REQUIRED
domain.last_check_details = dimail_response domain.last_check_details = dimail_response
domain.save() domain.save()
# if all external checks are ok but not internal checks, we need to fix internal checks # if all external checks are ok but not internal checks, we need to fix
elif all(external_checks.values()) and not all(internal_checks.values()): # internal checks
elif all(external_checks.values()) and not all(
internal_checks.values()
):
# a call to fix endpoint is needed because all external checks are ok # a call to fix endpoint is needed because all external checks are ok
dimail_response = self.fix_domain(domain) dimail_response = self.fix_domain(domain)
# we need to check again if all internal and external checks are ok # we need to check again if all internal and external checks are ok