From 4cb695c2bf03d80078e373a70553cb298c4353b9 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Thu, 6 Feb 2025 11:50:27 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(plugin)=20add=20CommuneCreation=20plu?= =?UTF-8?q?gin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add unit tests and refactor name normalization and zone naming. --- CHANGELOG.md | 1 + src/backend/plugins/organizations.py | 34 ++++++++++++++----- .../organizations/test_commune_creation.py | 20 +++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e7ceb..98c4ceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Added +- ✨(plugin) add CommuneCreation plugin with domain provisioning #658 - ✨(frontend) display action required status on domain - ✨(domains) store last health check details on MailDomain - ✨(domains) add support email field on domain diff --git a/src/backend/plugins/organizations.py b/src/backend/plugins/organizations.py index 53f765c..e3be3f6 100644 --- a/src/backend/plugins/organizations.py +++ b/src/backend/plugins/organizations.py @@ -1,8 +1,10 @@ """Organization related plugins.""" import logging +import re from django.conf import settings +from django.utils.text import slugify import requests from requests.adapters import HTTPAdapter, Retry @@ -139,6 +141,8 @@ class CommuneCreation(BaseOrganizationPlugin): def dns_call(self, spec): """Call to add a DNS record""" + zone_name = self.zone_name(spec.inputs["name"]) + records = [ { "name": item["target"], @@ -151,16 +155,24 @@ class CommuneCreation(BaseOrganizationPlugin): result = ApiCall() result.method = "PATCH" result.base = "https://api.scaleway.com" - result.url = ( - f"/domain/v2beta1/dns-zones/{spec.inputs['name']}.collectivite.fr/records" - ) + result.url = f"/domain/v2beta1/dns-zones/{zone_name}/records" result.params = {"changes": [{"add": {"records": records}}]} result.headers = {"X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS} return result + def normalize_name(self, name: str) -> str: + """Map the name to a standard form""" + name = re.sub("'", "-", name) + return slugify(name) + + def zone_name(self, name: str) -> str: + """Derive the zone name from the commune name""" + normalized = self.normalize_name(name) + return f"{normalized}.collectivite.fr" + def complete_commune_creation(self, name: str) -> ApiCall: """Specify the tasks to be completed after a commune is created.""" - inputs = {"name": name.lower()} + inputs = {"name": self.normalize_name(name)} create_zone = ApiCall() create_zone.method = "POST" @@ -175,15 +187,17 @@ class CommuneCreation(BaseOrganizationPlugin): "X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS } + zone_name = self.zone_name(inputs["name"]) + create_domain = ApiCall() create_domain.method = "POST" create_domain.base = settings.MAIL_PROVISIONING_API_URL create_domain.url = "/domains" create_domain.params = { - "name": f"{inputs['name']}.collectivite.fr", + "name": zone_name, "delivery": "virtual", "features": ["webmail", "mailbox"], - "context_name": f"{inputs['name']}.collectivite.fr", + "context_name": zone_name, } create_domain.headers = { "Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}" @@ -192,7 +206,7 @@ class CommuneCreation(BaseOrganizationPlugin): spec_domain = ApiCall() spec_domain.inputs = inputs spec_domain.base = settings.MAIL_PROVISIONING_API_URL - spec_domain.url = f"/domains/{inputs['name']}.collectivite.fr/spec" + spec_domain.url = f"/domains/{zone_name}/spec" spec_domain.headers = { "Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}" } @@ -234,7 +248,9 @@ class CommuneCreation(BaseOrganizationPlugin): organization.save(update_fields=["name", "updated_at"]) logger.info("Organization %s name updated to %s", organization, name) - MailDomain.objects.get_or_create(name=f"{name.lower()}.collectivite.fr") + zone_name = self.zone_name(name) + support = f"support@{zone_name}" + MailDomain.objects.get_or_create(name=zone_name, support_email=support) # Compute and execute the rest of the process tasks = self.complete_commune_creation(name) @@ -247,7 +263,7 @@ class CommuneCreation(BaseOrganizationPlugin): """After granting an organization access, check for needed domain access grant.""" orga = organization_access.organization user = organization_access.user - zone_name = orga.name.lower() + ".collectivite.fr" + zone_name = self.zone_name(orga.name) try: domain = MailDomain.objects.get(name=zone_name) diff --git a/src/backend/plugins/tests/organizations/test_commune_creation.py b/src/backend/plugins/tests/organizations/test_commune_creation.py index 6d899cc..b326329 100644 --- a/src/backend/plugins/tests/organizations/test_commune_creation.py +++ b/src/backend/plugins/tests/organizations/test_commune_creation.py @@ -169,3 +169,23 @@ def test_tasks_on_commune_creation_include_dns_records(): assert ( zone_call.headers["X-Auth-Token"] == settings.DNS_PROVISIONING_API_CREDENTIALS ) + + +def test_normalize_name(): + """Test name normalization""" + plugin = CommuneCreation() + assert plugin.normalize_name("Asnières-sur-Saône") == "asnieres-sur-saone" + assert plugin.normalize_name("Bâgé-le-Châtel") == "bage-le-chatel" + assert plugin.normalize_name("Courçais") == "courcais" + assert plugin.normalize_name("Moÿ-de-l'Aisne") == "moy-de-l-aisne" + assert plugin.normalize_name("Salouël") == "salouel" + assert ( + plugin.normalize_name("Bors (Canton de Tude-et-Lavalette)") + == "bors-canton-de-tude-et-lavalette" + ) + + +def test_zone_name(): + """Test transforming a commune name to a sub-zone of collectivite.fr""" + plugin = CommuneCreation() + assert plugin.zone_name("Bâgé-le-Châtel") == "bage-le-chatel.collectivite.fr"