Compare commits
9 Commits
fix/groups
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 439ddb9d4a | |||
| a7a923e790 | |||
| 5ed63fc091 | |||
| f55cb3a813 | |||
| 2c82f38c59 | |||
| 8963f0bb3d | |||
| 733a1d8861 | |||
| 7916f7d7d0 | |||
| 45dbdd6c4c |
@@ -7,3 +7,13 @@ and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0] - 2024-08-09
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(domains) create and manage domains on admin + API
|
||||
- ✨(domains) mailbox creation + link to email provisioning API
|
||||
|
||||
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.0.0...main
|
||||
[1.0.0]: https://github.com/numerique-gouv/people/releases/v1.0.0
|
||||
@@ -11,6 +11,8 @@ class MailboxSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Mailbox
|
||||
fields = ["id", "first_name", "last_name", "local_part", "secondary_email"]
|
||||
# everything is actually read-only as we do not allow update for now
|
||||
read_only_fields = ["id"]
|
||||
|
||||
|
||||
class MailDomainSerializer(serializers.ModelSerializer):
|
||||
@@ -25,6 +27,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
"id",
|
||||
"name",
|
||||
"slug",
|
||||
"status",
|
||||
"abilities",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
@@ -32,6 +35,7 @@ class MailDomainSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"slug",
|
||||
"status",
|
||||
"abilities",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
|
||||
@@ -74,7 +74,18 @@ class MailBoxViewSet(
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
"""MailBox ViewSet"""
|
||||
"""MailBox ViewSet
|
||||
|
||||
GET /api/<version>/mail-domains/<domain-slug>/mailboxes/
|
||||
Return a list of mailboxes on the domain
|
||||
|
||||
POST /api/<version>/mail-domains/<domain-slug>/mailboxes/ with expected data:
|
||||
- first_name: str
|
||||
- last_name: str
|
||||
- local_part: str
|
||||
- secondary_email: str
|
||||
Sends request to email provisioning API and returns newly created mailbox
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.MailBoxPermission]
|
||||
serializer_class = serializers.MailboxSerializer
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
Mailbox manager application factories
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.utils.text import slugify
|
||||
|
||||
import factory.fuzzy
|
||||
import responses
|
||||
from faker import Faker
|
||||
from rest_framework import status
|
||||
|
||||
from core import factories as core_factories
|
||||
from core import models as core_models
|
||||
@@ -27,6 +31,7 @@ class MailDomainFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
name = factory.Faker("domain_name")
|
||||
slug = factory.LazyAttribute(lambda o: slugify(o.name))
|
||||
secret = factory.Faker("password")
|
||||
|
||||
@factory.post_generation
|
||||
def users(self, create, extracted, **kwargs):
|
||||
@@ -75,3 +80,35 @@ class MailboxFactory(factory.django.DjangoModelFactory):
|
||||
)
|
||||
domain = factory.SubFactory(MailDomainEnabledFactory)
|
||||
secondary_email = factory.Faker("email")
|
||||
|
||||
@classmethod
|
||||
def _create(cls, model_class, *args, use_mock=True, **kwargs):
|
||||
domain = kwargs["domain"]
|
||||
if use_mock and isinstance(domain, models.MailDomain):
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
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"{kwargs['local_part']}@{domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
result = super()._create(model_class, *args, **kwargs)
|
||||
else:
|
||||
result = super()._create(model_class, *args, **kwargs)
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.6 on 2024-07-01 16:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mailbox_manager', '0010_alter_mailbox_first_name_alter_mailbox_last_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='maildomain',
|
||||
name='secret',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='secret'),
|
||||
),
|
||||
]
|
||||
@@ -3,15 +3,15 @@ Declare and configure the models for the People additional application : mailbox
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.core import exceptions, validators
|
||||
from django.db import models, transaction
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import BaseModel
|
||||
|
||||
from mailbox_manager.enums import MailDomainRoleChoices, MailDomainStatusChoices
|
||||
from mailbox_manager.utils.dimail import DimailAPIClient
|
||||
|
||||
|
||||
class MailDomain(BaseModel):
|
||||
@@ -26,6 +26,7 @@ class MailDomain(BaseModel):
|
||||
default=MailDomainStatusChoices.PENDING,
|
||||
choices=MailDomainStatusChoices.choices,
|
||||
)
|
||||
secret = models.CharField(_("secret"), max_length=255, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "people_mail_domain"
|
||||
@@ -137,8 +138,30 @@ class Mailbox(BaseModel):
|
||||
def __str__(self):
|
||||
return f"{self.local_part!s}@{self.domain.name:s}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
def clean(self):
|
||||
"""Mailboxes can be created only on enabled domains, with a set secret."""
|
||||
if self.domain.status != MailDomainStatusChoices.ENABLED:
|
||||
raise ValidationError("You can create mailbox only for a domain enabled")
|
||||
super().save(*args, **kwargs)
|
||||
raise exceptions.ValidationError(
|
||||
"You can create mailbox only for a domain enabled"
|
||||
)
|
||||
|
||||
if not self.domain.secret:
|
||||
raise exceptions.ValidationError(
|
||||
"Please configure your domain's secret before creating any mailbox."
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Override save function to fire a request on mailbox creation.
|
||||
Modification is forbidden for now.
|
||||
"""
|
||||
self.full_clean()
|
||||
|
||||
if self._state.adding:
|
||||
with transaction.atomic():
|
||||
client = DimailAPIClient()
|
||||
client.send_mailbox_request(self)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
# Update is not implemented for now
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<h1>403 Forbidden</h1>
|
||||
{{ exception }}
|
||||
@@ -52,12 +52,10 @@ def test_api_mail_domains__create_authenticated():
|
||||
Authenticated users should be able to create mail domains
|
||||
and should automatically be added as owner of the newly created domain.
|
||||
"""
|
||||
|
||||
user = core_factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/mail-domains/",
|
||||
{
|
||||
@@ -65,10 +63,21 @@ def test_api_mail_domains__create_authenticated():
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
# a new domain pending is created and the authenticated user is the owner
|
||||
domain = models.MailDomain.objects.get()
|
||||
|
||||
# response is as expected
|
||||
assert response.json() == {
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": enums.MailDomainStatusChoices.PENDING,
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
}
|
||||
|
||||
# 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.accesses.filter(role="owner", user=user).exists()
|
||||
|
||||
@@ -81,6 +81,7 @@ def test_api_mail_domains__retrieve_authenticated_related():
|
||||
"id": str(domain.id),
|
||||
"name": domain.name,
|
||||
"slug": domain.slug,
|
||||
"status": domain.status,
|
||||
"created_at": domain.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": domain.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"abilities": domain.get_abilities(user),
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
Unit tests for the mailbox API
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -91,11 +95,33 @@ def test_api_mailboxes__create_roles_success(role):
|
||||
mailbox_values = serializers.MailboxSerializer(
|
||||
factories.MailboxFactory.build()
|
||||
).data
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
|
||||
mailbox_values,
|
||||
format="json",
|
||||
)
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
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/{mail_domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_values['local_part']}@{mail_domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
|
||||
mailbox_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
mailbox = models.Mailbox.objects.get()
|
||||
@@ -130,12 +156,33 @@ def test_api_mailboxes__create_with_accent_success(role):
|
||||
mailbox_values = serializers.MailboxSerializer(
|
||||
factories.MailboxFactory.build(first_name="Aimé")
|
||||
).data
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
|
||||
mailbox_values,
|
||||
format="json",
|
||||
)
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
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/{mail_domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_values['local_part']}@{mail_domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{mail_domain.slug}/mailboxes/",
|
||||
mailbox_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
mailbox = models.Mailbox.objects.get()
|
||||
|
||||
@@ -187,3 +234,135 @@ def test_api_mailboxes__create_administrator_missing_fields():
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert not models.Mailbox.objects.exists()
|
||||
assert response.json() == {"secondary_email": ["This field is required."]}
|
||||
|
||||
|
||||
### SYNC TO PROVISIONING API
|
||||
|
||||
|
||||
def test_api_mailboxes__unrelated_user_provisioning_api_not_called():
|
||||
"""
|
||||
Provisioning API should not be called if an user tries
|
||||
to create a mailbox on a domain they have no access to.
|
||||
"""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(core_factories.UserFactory()) # user with no access
|
||||
body_values = serializers.MailboxSerializer(
|
||||
factories.MailboxFactory.build(domain=domain)
|
||||
).data
|
||||
with responses.RequestsMock():
|
||||
# We add no simulated response in RequestsMock
|
||||
# because we expected no "outside" calls to be made
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{domain.slug}/mailboxes/",
|
||||
body_values,
|
||||
format="json",
|
||||
)
|
||||
# No exception raised by RequestsMock means no call was sent
|
||||
# our API blocked the request before sending it
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
def test_api_mailboxes__domain_viewer_provisioning_api_not_called():
|
||||
"""
|
||||
Provisioning API should not be called if a domain viewer tries
|
||||
to create a mailbox on a domain they are not owner/admin of.
|
||||
"""
|
||||
access = factories.MailDomainAccessFactory(
|
||||
domain=factories.MailDomainEnabledFactory(),
|
||||
user=core_factories.UserFactory(),
|
||||
role=enums.MailDomainRoleChoices.VIEWER,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
body_values = serializers.MailboxSerializer(factories.MailboxFactory.build()).data
|
||||
with responses.RequestsMock():
|
||||
# We add no simulated response in RequestsMock
|
||||
# because we expected no "outside" calls to be made
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
|
||||
body_values,
|
||||
format="json",
|
||||
)
|
||||
# No exception raised by RequestsMock means no call was sent
|
||||
# our API blocked the request before sending it
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
[enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.OWNER],
|
||||
)
|
||||
def test_api_mailboxes__domain_owner_or_admin_successful_creation_and_provisioning(
|
||||
role,
|
||||
):
|
||||
"""
|
||||
Domain owner/admin should be able to create mailboxes.
|
||||
Provisioning API should be called when owner/admin makes a call.
|
||||
Expected response contains new email and password.
|
||||
"""
|
||||
# creating all needed objects
|
||||
access = factories.MailDomainAccessFactory(role=role)
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(access.user)
|
||||
mailbox_data = serializers.MailboxSerializer(
|
||||
factories.MailboxFactory.build(domain=access.domain)
|
||||
).data
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"access_token": "domain_owner_token"}',
|
||||
status=status.HTTP_200_OK,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsp = rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{access.domain.name}/mailboxes/"),
|
||||
body=str(
|
||||
{
|
||||
"email": f"{mailbox_data['local_part']}@{access.domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
|
||||
mailbox_data,
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Checks payload sent to email-provisioning API
|
||||
payload = json.loads(rsps.calls[1].request.body)
|
||||
assert payload == {
|
||||
"displayName": f"{mailbox_data['first_name']} {mailbox_data['last_name']}",
|
||||
"email": f"{mailbox_data['local_part']}@{access.domain.name}",
|
||||
"givenName": mailbox_data["first_name"],
|
||||
"surName": mailbox_data["last_name"],
|
||||
}
|
||||
|
||||
# Checks response
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert rsp.call_count == 1
|
||||
|
||||
mailbox = models.Mailbox.objects.get()
|
||||
assert response.json() == {
|
||||
"id": str(mailbox.id),
|
||||
"first_name": str(mailbox_data["first_name"]),
|
||||
"last_name": str(mailbox_data["last_name"]),
|
||||
"local_part": str(mailbox_data["local_part"]),
|
||||
"secondary_email": str(mailbox_data["secondary_email"]),
|
||||
}
|
||||
assert mailbox.first_name == mailbox_data["first_name"]
|
||||
assert mailbox.last_name == mailbox_data["last_name"]
|
||||
assert mailbox.local_part == mailbox_data["local_part"]
|
||||
assert mailbox.secondary_email == mailbox_data["secondary_email"]
|
||||
|
||||
@@ -78,3 +78,17 @@ def test_api_mailboxes__list_roles(role):
|
||||
"secondary_email": str(mailbox1.secondary_email),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_api_mailboxes__list_non_existing():
|
||||
"""
|
||||
User gets a 404 when trying to list mailboxes of a domain which does not exist.
|
||||
"""
|
||||
user = core_factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
factories.MailboxFactory.create_batch(5)
|
||||
|
||||
response = client.get("/api/v1.0/mail-domains/nonexistent.domain/mailboxes/")
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@@ -2,27 +2,34 @@
|
||||
Unit tests for the mailbox model
|
||||
"""
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
import json
|
||||
import re
|
||||
from logging import Logger
|
||||
from unittest import mock
|
||||
|
||||
from django.core import exceptions
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from rest_framework import status
|
||||
|
||||
from mailbox_manager import enums, factories
|
||||
from mailbox_manager import enums, factories, models
|
||||
from mailbox_manager.api import serializers
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
# LOCAL PART FIELD
|
||||
|
||||
|
||||
def test_models_mailboxes__local_part_cannot_be_empty():
|
||||
"""The "local_part" field should not be empty."""
|
||||
with pytest.raises(ValidationError, match="This field cannot be blank"):
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
||||
factories.MailboxFactory(local_part="")
|
||||
|
||||
|
||||
def test_models_mailboxes__local_part_cannot_be_null():
|
||||
"""The "local_part" field should not be null."""
|
||||
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be null"):
|
||||
factories.MailboxFactory(local_part=None)
|
||||
|
||||
|
||||
@@ -33,11 +40,13 @@ def test_models_mailboxes__local_part_matches_expected_format():
|
||||
"""
|
||||
factories.MailboxFactory(local_part="Marie-Jose.Perec+JO_2024")
|
||||
|
||||
with pytest.raises(ValidationError, match="Enter a valid value"):
|
||||
# other special characters (such as "@" or "!") should raise a validation error
|
||||
with pytest.raises(exceptions.ValidationError, match="Enter a valid value"):
|
||||
factories.MailboxFactory(local_part="mariejo@unnecessarydomain.com")
|
||||
|
||||
with pytest.raises(ValidationError, match="Enter a valid value"):
|
||||
factories.MailboxFactory(local_part="!")
|
||||
for character in ["!", "$", "%"]:
|
||||
with pytest.raises(exceptions.ValidationError, match="Enter a valid value"):
|
||||
factories.MailboxFactory(local_part=f"marie{character}jo")
|
||||
|
||||
|
||||
def test_models_mailboxes__local_part_unique_per_domain():
|
||||
@@ -50,7 +59,8 @@ def test_models_mailboxes__local_part_unique_per_domain():
|
||||
|
||||
# same local part on the same domain should not be possible
|
||||
with pytest.raises(
|
||||
ValidationError, match="Mailbox with this Local_part and Domain already exists."
|
||||
exceptions.ValidationError,
|
||||
match="Mailbox with this Local_part and Domain already exists.",
|
||||
):
|
||||
factories.MailboxFactory(
|
||||
local_part=existing_mailbox.local_part, domain=existing_mailbox.domain
|
||||
@@ -72,7 +82,7 @@ def test_models_mailboxes__domain_must_be_a_maildomain_instance():
|
||||
|
||||
def test_models_mailboxes__domain_cannot_be_null():
|
||||
"""The "domain" field should not be null."""
|
||||
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||
with pytest.raises(models.MailDomain.DoesNotExist, match="Mailbox has no domain."):
|
||||
factories.MailboxFactory(domain=None)
|
||||
|
||||
|
||||
@@ -81,13 +91,13 @@ def test_models_mailboxes__domain_cannot_be_null():
|
||||
|
||||
def test_models_mailboxes__secondary_email_cannot_be_empty():
|
||||
"""The "secondary_email" field should not be empty."""
|
||||
with pytest.raises(ValidationError, match="This field cannot be blank"):
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be blank"):
|
||||
factories.MailboxFactory(secondary_email="")
|
||||
|
||||
|
||||
def test_models_mailboxes__secondary_email_cannot_be_null():
|
||||
"""The "secondary_email" field should not be null."""
|
||||
with pytest.raises(ValidationError, match="This field cannot be null"):
|
||||
with pytest.raises(exceptions.ValidationError, match="This field cannot be null"):
|
||||
factories.MailboxFactory(secondary_email=None)
|
||||
|
||||
|
||||
@@ -95,7 +105,8 @@ def test_models_mailboxes__cannot_be_created_for_disabled_maildomain():
|
||||
"""Mailbox creation is allowed only for a domain enabled.
|
||||
A disabled status for the mail domain raises an error."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="You can create mailbox only for a domain enabled"
|
||||
exceptions.ValidationError,
|
||||
match="You can create mailbox only for a domain enabled",
|
||||
):
|
||||
factories.MailboxFactory(
|
||||
domain=factories.MailDomainFactory(
|
||||
@@ -108,7 +119,8 @@ def test_models_mailboxes__cannot_be_created_for_failed_maildomain():
|
||||
"""Mailbox creation is allowed only for a domain enabled.
|
||||
A failed status for the mail domain raises an error."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="You can create mailbox only for a domain enabled"
|
||||
exceptions.ValidationError,
|
||||
match="You can create mailbox only for a domain enabled",
|
||||
):
|
||||
factories.MailboxFactory(
|
||||
domain=factories.MailDomainFactory(
|
||||
@@ -121,8 +133,134 @@ def test_models_mailboxes__cannot_be_created_for_pending_maildomain():
|
||||
"""Mailbox creation is allowed only for a domain enabled.
|
||||
A pending status for the mail domain raises an error."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="You can create mailbox only for a domain enabled"
|
||||
exceptions.ValidationError,
|
||||
match="You can create mailbox only for a domain enabled",
|
||||
):
|
||||
# MailDomainFactory initializes a mail domain with default values,
|
||||
# so mail domain status is pending!
|
||||
factories.MailboxFactory(domain=factories.MailDomainFactory())
|
||||
|
||||
|
||||
### SYNC TO DIMAIL-API
|
||||
|
||||
|
||||
def test_models_mailboxes__no_secret():
|
||||
"""If no secret is declared on the domain, the function should raise an error."""
|
||||
domain = factories.MailDomainEnabledFactory(secret=None)
|
||||
|
||||
with pytest.raises(
|
||||
exceptions.ValidationError,
|
||||
match="Please configure your domain's secret before creating any mailbox.",
|
||||
):
|
||||
factories.MailboxFactory(domain=domain)
|
||||
|
||||
|
||||
def test_models_mailboxes__wrong_secret():
|
||||
"""If domain secret is inaccurate, the function should raise an error."""
|
||||
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response by scim provider using "responses":
|
||||
rsps.add(
|
||||
rsps.GET,
|
||||
re.compile(r".*/token/"),
|
||||
body='{"detail": "Permission denied"}',
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
content_type="application/json",
|
||||
)
|
||||
rsps.add(
|
||||
rsps.POST,
|
||||
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
|
||||
body='{"detail": "Permission denied"}',
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
exceptions.PermissionDenied,
|
||||
match=f"Please check secret of the mail domain {domain.name}",
|
||||
):
|
||||
mailbox = factories.MailboxFactory(use_mock=False, domain=domain)
|
||||
# Payload sent to mailbox provider
|
||||
payload = json.loads(rsps.calls[1].request.body)
|
||||
assert payload == {
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
"email": f"{mailbox.local_part}@{domain.name}",
|
||||
"givenName": mailbox.first_name,
|
||||
"surName": mailbox.last_name,
|
||||
}
|
||||
|
||||
|
||||
@mock.patch.object(Logger, "error")
|
||||
@mock.patch.object(Logger, "info")
|
||||
def test_models_mailboxes__create_mailbox_success(mock_info, mock_error):
|
||||
"""Creating a mailbox sends the expected information and get expected response before saving."""
|
||||
domain = factories.MailDomainEnabledFactory()
|
||||
|
||||
# generate mailbox data before mailbox, to mock responses
|
||||
mailbox_data = serializers.MailboxSerializer(
|
||||
factories.MailboxFactory.build(domain=domain)
|
||||
).data
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
# Ensure successful response using "responses":
|
||||
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"{mailbox_data['local_part']}@{domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
),
|
||||
status=status.HTTP_201_CREATED,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
mailbox = factories.MailboxFactory(
|
||||
use_mock=False, local_part=mailbox_data["local_part"], domain=domain
|
||||
)
|
||||
|
||||
# Check headers
|
||||
headers = rsps.calls[1].request.headers
|
||||
# assert "Authorization" not in headers
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
# Payload sent to mailbox provider
|
||||
payload = json.loads(rsps.calls[1].request.body)
|
||||
assert payload == {
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
"email": f"{mailbox.local_part}@{domain.name}",
|
||||
"givenName": mailbox.first_name,
|
||||
"surName": mailbox.last_name,
|
||||
}
|
||||
|
||||
# Logger
|
||||
assert not mock_error.called
|
||||
assert mock_info.call_count == 1
|
||||
assert mock_info.call_args_list[0][0] == (
|
||||
"Mailbox successfully created on domain %s",
|
||||
domain.name,
|
||||
)
|
||||
assert mock_info.call_args_list[0][1] == (
|
||||
{
|
||||
"extra": {
|
||||
"response": str(
|
||||
{
|
||||
"email": f"{mailbox.local_part}@{domain.name}",
|
||||
"password": "newpass",
|
||||
"uuid": "uuid",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""A minimalist client to synchronize with mailbox provisioning API."""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import exceptions
|
||||
|
||||
import requests
|
||||
from rest_framework import status
|
||||
from urllib3.util import Retry
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
adapter = requests.adapters.HTTPAdapter(
|
||||
max_retries=Retry(
|
||||
total=4,
|
||||
backoff_factor=0.1,
|
||||
status_forcelist=[500, 502],
|
||||
allowed_methods=["PATCH"],
|
||||
)
|
||||
)
|
||||
|
||||
session = requests.Session()
|
||||
session.mount("http://", adapter)
|
||||
session.mount("https://", adapter)
|
||||
|
||||
|
||||
class DimailAPIClient:
|
||||
"""A dimail-API client."""
|
||||
|
||||
def get_headers(self, domain):
|
||||
"""Build header dict from domain object."""
|
||||
# self.secret is the encoded basic auth, to request a new token from dimail-api
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
response = requests.get(
|
||||
f"{settings.MAIL_PROVISIONING_API_URL}/token/",
|
||||
headers={"Authorization": f"Basic {domain.secret}"},
|
||||
timeout=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
if response.json() == "{'detail': 'Permission denied'}":
|
||||
raise exceptions.PermissionDenied(
|
||||
"This secret does not allow for a new token."
|
||||
)
|
||||
|
||||
if "access_token" in response.json():
|
||||
headers["Authorization"] = f"Bearer {response.json()['access_token']}"
|
||||
|
||||
return headers
|
||||
|
||||
def send_mailbox_request(self, mailbox):
|
||||
"""Send a CREATE mailbox request to mail provisioning API."""
|
||||
|
||||
payload = {
|
||||
"email": f"{mailbox.local_part}@{mailbox.domain}",
|
||||
"givenName": mailbox.first_name,
|
||||
"surName": mailbox.last_name,
|
||||
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
|
||||
}
|
||||
|
||||
try:
|
||||
response = session.post(
|
||||
f"{settings.MAIL_PROVISIONING_API_URL}/domains/{mailbox.domain}/mailboxes/",
|
||||
json=payload,
|
||||
headers=self.get_headers(mailbox.domain),
|
||||
verify=True,
|
||||
timeout=10,
|
||||
)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(
|
||||
"Connection error while trying to reach %s.",
|
||||
settings.MAIL_PROVISIONING_API_URL,
|
||||
exc_info=e,
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
extra = {"response": response.content.decode("utf-8")}
|
||||
# This a temporary broken solution. Password will soon be sent
|
||||
# from OX servers but their prod is not ready.
|
||||
# In the meantime, we log mailbox info (including password !)
|
||||
logger.info(
|
||||
"Mailbox successfully created on domain %s",
|
||||
mailbox.domain.name,
|
||||
extra=extra,
|
||||
)
|
||||
elif response.status_code == status.HTTP_403_FORBIDDEN:
|
||||
logger.error(
|
||||
"[DIMAIL] 403 Forbidden: please check the mail domain secret of %s",
|
||||
mailbox.domain.name,
|
||||
)
|
||||
raise exceptions.PermissionDenied(
|
||||
f"Please check secret of the mail domain {mailbox.domain.name}"
|
||||
)
|
||||
return response
|
||||
@@ -364,6 +364,13 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# mailboxes provisioning API
|
||||
MAIL_PROVISIONING_API_URL = values.Value(
|
||||
default="https://main.dev.ox.numerique.gouv.fr",
|
||||
environ_name="MAIL_PROVISIONING_API_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "people"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_FEATURE_TEAM=true
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://test.jest
|
||||
NEXT_PUBLIC_FEATURE_TEAM=true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-desk",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -11,7 +11,7 @@ export function MainLayout({ children }: PropsWithChildren) {
|
||||
<Box $height="100vh">
|
||||
<Header />
|
||||
<Box $css="flex: 1;" $direction="row">
|
||||
<Menu />
|
||||
{process.env.NEXT_PUBLIC_FEATURE_TEAM === 'true' && <Menu />}
|
||||
<Box
|
||||
as="main"
|
||||
$height={`calc(100vh - ${HEADER_HEIGHT})`}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { MainLayout } from '../MainLayout';
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
...jest.requireActual('next/navigation'),
|
||||
usePathname: () => '/',
|
||||
}));
|
||||
|
||||
describe('MainLayout', () => {
|
||||
it('checks menu rendering', () => {
|
||||
render(<MainLayout />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /Teams button/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /Mail Domains button/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('checks menu rendering without team feature', () => {
|
||||
process.env.NEXT_PUBLIC_FEATURE_TEAM = 'false';
|
||||
|
||||
render(<MainLayout />, { wrapper: AppWrapper });
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: /Teams button/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', {
|
||||
name: /Mail Domains button/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { UUID } from 'crypto';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DataGrid,
|
||||
@@ -17,7 +19,11 @@ import { MailDomain, MailDomainMailbox } from '../types';
|
||||
|
||||
import { CreateMailboxForm } from './forms/CreateMailboxForm';
|
||||
|
||||
export type ViewMailbox = { email: string; id: string };
|
||||
export type ViewMailbox = {
|
||||
name: string;
|
||||
email: string;
|
||||
id: UUID;
|
||||
};
|
||||
|
||||
// FIXME : ask Cunningham to export this type
|
||||
type SortModelItem = {
|
||||
@@ -69,6 +75,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
|
||||
mailDomain && data?.results?.length
|
||||
? data.results.map((mailbox: MailDomainMailbox) => ({
|
||||
email: `${mailbox.local_part}@${mailDomain.name}`,
|
||||
name: `${mailbox.first_name} ${mailbox.last_name}`,
|
||||
id: mailbox.id,
|
||||
}))
|
||||
: [];
|
||||
@@ -76,6 +83,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
|
||||
useEffect(() => {
|
||||
setPagesCount(data?.count ? Math.ceil(data.count / pageSize) : 0);
|
||||
}, [data?.count, pageSize, setPagesCount]);
|
||||
|
||||
return isLoading ? (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Loader />
|
||||
@@ -88,18 +96,35 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
|
||||
closeModal={() => setIsCreateMailboxFormVisible(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TopBanner
|
||||
name={mailDomain.name}
|
||||
setIsFormVisible={setIsCreateMailboxFormVisible}
|
||||
abilities={mailDomain?.abilities}
|
||||
/>
|
||||
|
||||
<Card
|
||||
$padding={{ bottom: 'small' }}
|
||||
$margin={{ all: 'big', top: 'none' }}
|
||||
$overflow="auto"
|
||||
>
|
||||
{error && <TextErrors causes={error.cause} />}
|
||||
|
||||
<DataGrid
|
||||
columns={[
|
||||
{
|
||||
field: 'name',
|
||||
headerName: t('Names'),
|
||||
renderCell: ({ row }) => (
|
||||
<Text
|
||||
$weight="bold"
|
||||
$theme="primary"
|
||||
$css="text-transform: capitalize;"
|
||||
>
|
||||
{row.name}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
headerName: t('Emails'),
|
||||
@@ -127,9 +152,11 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
|
||||
const TopBanner = ({
|
||||
name,
|
||||
setIsFormVisible,
|
||||
abilities,
|
||||
}: {
|
||||
name: string;
|
||||
setIsFormVisible: (value: boolean) => void;
|
||||
abilities: MailDomain['abilities'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -147,12 +174,14 @@ const TopBanner = ({
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $margin={{ all: 'big', bottom: 'small' }} $align="flex-end">
|
||||
<Button
|
||||
aria-label={t(`Create a mailbox in {{name}} domain`, { name })}
|
||||
onClick={() => setIsFormVisible(true)}
|
||||
>
|
||||
{t('Create a mailbox')}
|
||||
</Button>
|
||||
{abilities.post ? (
|
||||
<Button
|
||||
aria-label={t(`Create a mailbox in {{name}} domain`, { name })}
|
||||
onClick={() => setIsFormVisible(true)}
|
||||
>
|
||||
{t('Create a mailbox')}
|
||||
</Button>
|
||||
) : null}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,11 @@ export const Panel = () => {
|
||||
$minWidth: '0',
|
||||
};
|
||||
|
||||
const styleNoTeam = process.env.NEXT_PUBLIC_FEATURE_TEAM !== 'true' && {
|
||||
$display: 'none',
|
||||
tabIndex: -1,
|
||||
};
|
||||
|
||||
const transition = 'all 0.5s ease-in-out';
|
||||
|
||||
return (
|
||||
@@ -52,6 +57,7 @@ export const Panel = () => {
|
||||
transition: ${transition};
|
||||
`}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
{...styleNoTeam}
|
||||
>
|
||||
<IconOpenClose width={24} height={24} aria-hidden="true" />
|
||||
</BoxButton>
|
||||
|
||||
@@ -6,10 +6,20 @@ export interface MailDomain {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
slug: string;
|
||||
abilities: {
|
||||
get: boolean;
|
||||
patch: boolean;
|
||||
put: boolean;
|
||||
post: boolean;
|
||||
delete: boolean;
|
||||
manage_accesses: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MailDomainMailbox {
|
||||
id: UUID;
|
||||
local_part: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
secondary_email: string;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import MailDomains from './mail-domains';
|
||||
import Teams from './teams';
|
||||
|
||||
import { TeamLayout } from '@/features/teams/team-management';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
import Teams from './teams/';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return <Teams />;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <TeamLayout>{page}</TeamLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default process.env.NEXT_PUBLIC_FEATURE_TEAM === 'true'
|
||||
? Teams
|
||||
: MailDomains;
|
||||
|
||||
@@ -15,6 +15,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'domainfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mails.fr',
|
||||
@@ -22,6 +30,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'mailsfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'versailles.net',
|
||||
@@ -29,6 +45,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'versaillesnet',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'paris.fr',
|
||||
@@ -36,6 +60,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'parisfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -99,8 +131,7 @@ test.describe('Mail domain create mailbox', () => {
|
||||
await keyCloakSignIn(page, browserName);
|
||||
});
|
||||
|
||||
// user should have administrator or owner role on this domain to be able perform this action
|
||||
test('checks user can create a mailbox for a mail domain', async ({
|
||||
test('checks user can create a mailbox when he has post ability', async ({
|
||||
page,
|
||||
}) => {
|
||||
const newMailbox = {
|
||||
@@ -224,6 +255,62 @@ test.describe('Mail domain create mailbox', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('checks user is not allowed to create a mailbox when he is missing post ability', async ({
|
||||
page,
|
||||
}) => {
|
||||
const localMailDomainsFixtures = [...mailDomainsFixtures];
|
||||
localMailDomainsFixtures[0].abilities.post = false;
|
||||
const localMailDomainDomainFr = localMailDomainsFixtures[0];
|
||||
const localMailboxFixtures = { ...mailboxesFixtures };
|
||||
|
||||
const interceptRequests = (page: Page) => {
|
||||
void page.route('**/api/v1.0/mail-domains/?page=*', (route) => {
|
||||
void route.fulfill({
|
||||
json: {
|
||||
count: localMailDomainsFixtures.length,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: localMailDomainsFixtures,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
void page.route('**/api/v1.0/mail-domains/domainfr', (route) => {
|
||||
void route.fulfill({
|
||||
json: localMailDomainDomainFr,
|
||||
});
|
||||
});
|
||||
|
||||
void page.route(
|
||||
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
|
||||
(route) => {
|
||||
void route.fulfill({
|
||||
json: {
|
||||
count: localMailboxFixtures.domainFr.page1.length,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: localMailboxFixtures.domainFr.page1,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
};
|
||||
|
||||
void interceptRequests(page);
|
||||
|
||||
await page
|
||||
.locator('menu')
|
||||
.first()
|
||||
.getByLabel(`Mail Domains button`)
|
||||
.click();
|
||||
await page.getByRole('listbox').first().getByText('domain.fr').click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Create a mailbox' }),
|
||||
).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('checks client invalidation messages are displayed and no mailbox creation request is sent when fields are not properly filled', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -12,6 +12,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'domainfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mails.fr',
|
||||
@@ -19,6 +27,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'mailsfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'versailles.net',
|
||||
@@ -26,6 +42,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'versaillesnet',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'paris.fr',
|
||||
@@ -33,6 +57,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'parisfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -131,11 +163,15 @@ test.describe('Mail domain', () => {
|
||||
domainFr: {
|
||||
page1: Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `456ac6ca-0402-4615-8005-69bc1efde${i}f`,
|
||||
first_name: 'john',
|
||||
last_name: 'doe',
|
||||
local_part: `local_part-${i}`,
|
||||
secondary_email: `secondary_email-${i}`,
|
||||
})),
|
||||
page2: Array.from({ length: 2 }, (_, i) => ({
|
||||
id: `456ac6ca-0402-4615-8005-69bc1efde${i}d`,
|
||||
first_name: 'john',
|
||||
last_name: 'doe',
|
||||
local_part: `local_part-${i}`,
|
||||
secondary_email: `secondary_email-${i}`,
|
||||
})),
|
||||
@@ -203,6 +239,10 @@ test.describe('Mail domain', () => {
|
||||
page.getByRole('heading', { name: 'domain.fr' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: /Names/ }).first(),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: /Emails/ }).first(),
|
||||
).toBeVisible();
|
||||
@@ -217,6 +257,12 @@ test.describe('Mail domain', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const table = page.locator('table');
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
const tdNames = await table.getByText('John Doe').all();
|
||||
expect(tdNames.length).toEqual(20);
|
||||
|
||||
await expect(
|
||||
page.locator('.c__pagination__list').getByRole('button', { name: '1' }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -11,6 +11,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'domainfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mails.fr',
|
||||
@@ -18,6 +26,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'mailsfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'versailles.net',
|
||||
@@ -25,6 +41,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'versaillesnet',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'paris.fr',
|
||||
@@ -32,6 +56,14 @@ const mailDomainsFixtures: MailDomain[] = [
|
||||
created_at: currentDateIso,
|
||||
updated_at: currentDateIso,
|
||||
slug: 'parisfr',
|
||||
abilities: {
|
||||
get: true,
|
||||
patch: true,
|
||||
put: true,
|
||||
post: true,
|
||||
delete: true,
|
||||
manage_accesses: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "people",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-people",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:desk",
|
||||
|
||||
@@ -50,6 +50,7 @@ backend:
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
MAIL_PROVISIONING_API_URL: "http://host.docker.internal:8000"
|
||||
command:
|
||||
- "gunicorn"
|
||||
- "-c"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/people-backend
|
||||
pullPolicy: Always
|
||||
tag: "main"
|
||||
tag: "v1.0.0"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -84,6 +84,7 @@ backend:
|
||||
secretKeyRef:
|
||||
name: redis.redis.libre.sh
|
||||
key: url
|
||||
MAIL_PROVISIONING_API_URL: "https://main.dev.ox.numerique.gouv.fr"
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
@@ -96,7 +97,7 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/people-frontend
|
||||
pullPolicy: Always
|
||||
tag: "main"
|
||||
tag: "v1.0.0"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
image:
|
||||
repository: lasuite/people-backend
|
||||
pullPolicy: Always
|
||||
tag: "v0.1.0"
|
||||
tag: "v1.0.0"
|
||||
|
||||
backend:
|
||||
migrateJobAnnotations:
|
||||
@@ -84,6 +84,7 @@ backend:
|
||||
secretKeyRef:
|
||||
name: redis.redis.libre.sh
|
||||
key: url
|
||||
MAIL_PROVISIONING_API_URL: "https://main.dev.ox.numerique.gouv.fr"
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
@@ -96,7 +97,7 @@ frontend:
|
||||
image:
|
||||
repository: lasuite/people-frontend
|
||||
pullPolicy: Always
|
||||
tag: "v0.1.0"
|
||||
tag: "v1.0.0"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
||||
@@ -84,6 +84,8 @@ backend:
|
||||
secretKeyRef:
|
||||
name: redis.redis.libre.sh
|
||||
key: url
|
||||
MAIL_PROVISIONING_API_URL: "https://main.dev.ox.numerique.gouv.fr"
|
||||
|
||||
|
||||
createsuperuser:
|
||||
command:
|
||||
|
||||
Reference in New Issue
Block a user