Compare commits

...

13 Commits

Author SHA1 Message Date
daproclaima 85c789bb1a 🔖(patch) patch release to 1.0.1
- upgrade semver for all package.json, env.d files,
and pyproject.toml
- update CHANGELOG.md
2024-08-20 10:03:11 +02:00
daproclaima 049d8695be ️(frontend) updates links
- remove buttons inside links in panels and
teams index
- update component and e2e tests
2024-08-19 16:04:57 +02:00
daproclaima 49c238155c (frontend) user can add mail domain
- user can add an externally created mail domain
from UI and see the mail domain status on mail
domain page and left panel links.
- user can not create mailboxes to domain if mail
domain status is not equal to `enabled`
- update related tests and translations
2024-08-19 16:04:57 +02:00
daproclaima b79725acbe 💬(frontend) change text in mail domains list
- use more meaningful message
- update translations and e2e tests
2024-08-19 16:04:57 +02:00
Marie PUPO JEAMMET 439ddb9d4a 🔖(major) major release to 1.0.0
🎉! For changelog, see changelog.md
2024-08-09 15:21:42 +02:00
Sabrina Demagny a7a923e790 (mailboxes) manage bad secret sent to dimail API
- manage 403 returned by dimail API when mail domain secret is not valid
- improve some tests
- improve MailboxFactory to mock success for dimail API POST call
- override 403.html to return a nice failing error in django admin
- an error message is displayed on mailbox creation form of frontend
2024-08-09 13:37:20 +02:00
Marie PUPO JEAMMET 5ed63fc091 (test) add test list mailboxes non existing domain
Test that API raises a 404 when trying to list mailboxes
of a domain that does not exist.
2024-08-09 13:37:20 +02:00
Marie PUPO JEAMMET f55cb3a813 (mailboxes) add mail provisioning api integration
We want people to create new mailboxes in La Régie.
This commit adds integration with intermediary dimail-api,
which will in turn send our email creation request to Open-Xchange.
2024-08-09 13:37:20 +02:00
Marie PUPO JEAMMET 2c82f38c59 🗃️(domains) add "secret" field to domains
add a "secret" field to domain model. This secret will be used as
password in mail provisioning API.
2024-08-09 13:37:20 +02:00
Sabrina Demagny 8963f0bb3d (mail) add status on domain create or retrieve API
to display status on frontend
2024-08-08 23:47:49 +02:00
Anthony LC 733a1d8861 🔥(frontend) remove temporarily team feature
We want to make a first realease, but the
team feature is not ready yet.
So we will hide it for now by hiding the menu.
We will still let the feature in dev environment.
2024-08-08 16:29:06 +02:00
daproclaima 7916f7d7d0 (frontend) show names in mailbox table
- show a concatenation of first and last names
for each row in mailbox table
- update related e2e tests
2024-08-08 13:53:07 +02:00
daproclaima 45dbdd6c4c (frontend) restrict mailbox creation
- update mailbox creation feature by introducing the use of
new mail domain ability field to hide or show
mailbox creation button
- update related e2e tests
2024-08-08 13:53:07 +02:00
63 changed files with 2175 additions and 351 deletions
+17
View File
@@ -7,3 +7,20 @@ and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.1] - 2024-08-19
### Fixed
- ✨(frontend) user can add mail domains
## [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.1...main
[1.0.1]: https://github.com/numerique-gouv/people/releases/v1.0.1
[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",
+12 -1
View File
@@ -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
+37
View File
@@ -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'),
),
]
+30 -7
View File
@@ -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
+7
View File
@@ -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):
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "people"
version = "0.1.0"
version = "1.0.1"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1
View File
@@ -1 +1,2 @@
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
NEXT_PUBLIC_FEATURE_TEAM=true
+1
View File
@@ -1 +1,2 @@
NEXT_PUBLIC_API_ORIGIN=http://test.jest
NEXT_PUBLIC_FEATURE_TEAM=true
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-desk",
"version": "0.1.0",
"version": "1.0.1",
"private": true,
"scripts": {
"dev": "next dev",
@@ -5,6 +5,11 @@ import { AppWrapper } from '@/tests/utils';
import Page from '../pages';
jest.mock('next/navigation', () => ({
...jest.requireActual('next/navigation'),
useRouter: () => ({}),
}));
describe('Page', () => {
it('checks Page rendering', () => {
render(<Page />, { wrapper: AppWrapper });

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 617 B

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 500 B

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

@@ -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('link', {
name: /Teams button/i,
}),
).toBeInTheDocument();
expect(
screen.getByRole('link', {
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();
});
});
@@ -2,3 +2,4 @@ export * from './useMailDomains';
export * from './useMailDomain';
export * from './useCreateMailbox';
export * from './useMailboxes';
export * from './useCreateMailDomain';
@@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { MailDomain } from '@/features/mail-domains';
import { KEY_LIST_MAIL_DOMAIN } from './useMailDomains';
export const createMailDomain = async (name: string): Promise<MailDomain> => {
const response = await fetchAPI(`mail-domains/`, {
method: 'POST',
body: JSON.stringify({
name,
}),
});
if (!response.ok) {
throw new APIError(
'Failed to add the mail domain',
await errorCauses(response),
);
}
return response.json() as Promise<MailDomain>;
};
export function useCreateMailDomain({
onSuccess,
}: {
onSuccess: (data: MailDomain) => void;
}) {
const queryClient = useQueryClient();
return useMutation<MailDomain, APIError, string>({
mutationFn: createMailDomain,
onSuccess: (data) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_MAIL_DOMAIN],
});
onSuccess(data);
},
});
}
@@ -13,7 +13,7 @@ type MailDomainResponse = MailDomain;
export const getMailDomain = async ({
slug,
}: MailDomainParams): Promise<MailDomainResponse> => {
const response = await fetchAPI(`mail-domains/${slug}`);
const response = await fetchAPI(`mail-domains/${slug}/`);
if (!response.ok) {
throw new APIError(
@@ -40,7 +40,7 @@ export const getMailDomains = async ({
return response.json() as Promise<MailDomainsResponse>;
};
export const KEY_LIST_MAIL_DOMAINS = 'mail-domains';
export const KEY_LIST_MAIL_DOMAIN = 'mail-domains';
export function useMailDomains(
param: MailDomainsParams,
@@ -60,7 +60,7 @@ export function useMailDomains(
number
>({
initialPageParam: 1,
queryKey: [KEY_LIST_MAIL_DOMAINS, param],
queryKey: [KEY_LIST_MAIL_DOMAIN, param],
queryFn: ({ pageParam }) => getMailDomains({ ...param, page: pageParam }),
getNextPageParam(lastPage, allPages) {
return lastPage.next ? allPages.length + 1 : undefined;
@@ -1,14 +1,18 @@
import { UUID } from 'crypto';
import {
Alert,
Button,
DataGrid,
Loader,
SortModel,
VariantType,
usePagination,
} from '@openfun/cunningham-react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, Text, TextErrors } from '@/components';
import { Box, Card, Text, TextErrors, TextStyled } from '@/components';
import { useMailboxes } from '../api/useMailboxes';
import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
@@ -17,7 +21,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 = {
@@ -29,12 +37,6 @@ const defaultOrderingMapping: Record<string, string> = {
email: 'local_part',
};
/**
* Formats the sorting model based on a given mapping.
* @param {SortModelItem} sortModel The sorting model item containing field and sort direction.
* @param {Record<string, string>} mapping The mapping object to map field names.
* @returns {string} The formatted sorting string.
*/
function formatSortModel(
sortModel: SortModelItem,
mapping = defaultOrderingMapping,
@@ -69,6 +71,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 +79,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 +92,34 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
closeModal={() => setIsCreateMailboxFormVisible(false)}
/>
) : null}
<TopBanner
name={mailDomain.name}
setIsFormVisible={setIsCreateMailboxFormVisible}
mailDomain={mailDomain}
showMailBoxCreationForm={setIsCreateMailboxFormVisible}
/>
<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'),
@@ -125,35 +145,111 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
}
const TopBanner = ({
name,
setIsFormVisible,
mailDomain,
showMailBoxCreationForm,
}: {
name: string;
setIsFormVisible: (value: boolean) => void;
mailDomain: MailDomain;
showMailBoxCreationForm: (value: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<>
<Box
$direction="column"
$margin={{ all: 'big', bottom: 'tiny' }}
$gap="1rem"
>
<Box
$direction="row"
$align="center"
$margin={{ all: 'big', vertical: 'xbig' }}
$gap="2.25rem"
$justify="space-between"
>
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{name}
</Text>
<Box $direction="row" $margin="none" $gap="2.25rem">
<MailDomainsLogo aria-hidden="true" />
<Text $margin="none" as="h3" $size="h3">
{mailDomain?.name}
</Text>
</Box>
</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>
<Box $direction="row" $justify="space-between">
<AlertStatus status={mailDomain.status} />
</Box>
</>
{mailDomain?.abilities.post && (
<Box $direction="row-reverse">
<Box $display="inline">
<Button
aria-label={t('Create a mailbox in {{name}} domain', {
name: mailDomain?.name,
})}
disabled={mailDomain?.status !== 'enabled'}
onClick={() => showMailBoxCreationForm(true)}
>
{t('Create a mailbox')}
</Button>
</Box>
</Box>
)}
</Box>
);
};
const AlertStatus = ({ status }: { status: MailDomain['status'] }) => {
const { t } = useTranslation();
const getStatusAlertProps = (status?: string) => {
switch (status) {
case 'pending':
return {
variant: VariantType.WARNING,
message: t(
'Your domain name is being validated. ' +
'You will not be able to create mailboxes until your domain name has been validated by our team.',
),
};
case 'disabled':
return {
variant: VariantType.NEUTRAL,
message: t(
'This domain name is deactivated. No new mailboxes can be created.',
),
};
case 'failed':
return {
variant: VariantType.ERROR,
message: (
<Text $display="inline">
{t(
'The domain name encounters an error. Please contact our support team to solve the problem:',
)}{' '}
<TextStyled
as="a"
target="_blank"
$display="inline"
href="mailto:suiteterritoriale@anct.gouv.fr"
aria-label={t(
'Contact our support at "suiteterritoriale@anct.gouv.fr"',
)}
>
suiteterritoriale@anct.gouv.fr
</TextStyled>
.
</Text>
),
};
}
};
const alertStatusProps = getStatusAlertProps(status);
if (!alertStatusProps) {
return null;
}
return (
<Alert canClose={false} type={alertStatusProps.variant}>
<Text $display="inline">{alertStatusProps.message}</Text>
</Alert>
);
};
@@ -0,0 +1,143 @@
import { zodResolver } from '@hookform/resolvers/zod';
import {
Button,
Input,
Loader,
Modal,
ModalSize,
} from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { Box, StyledLink, Text, TextErrors } from '@/components';
import { useCreateMailDomain } from '@/features/mail-domains';
import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
const FORM_ID = 'form-add-mail-domain';
export const ModalCreateMailDomain = () => {
const { t } = useTranslation();
const router = useRouter();
const createMailDomainValidationSchema = z.object({
name: z.string().min(1, t('Example: saint-laurent.fr')),
});
const methods = useForm<{ name: string }>({
delayError: 0,
defaultValues: {
name: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
resolver: zodResolver(createMailDomainValidationSchema),
});
const {
mutate: createMailDomain,
isPending,
error,
} = useCreateMailDomain({
onSuccess: (mailDomain) => {
router.push(`/mail-domains/${mailDomain.slug}`);
},
});
const onSubmitCallback = () => {
void methods.handleSubmit(({ name }, event) => {
event?.preventDefault();
void createMailDomain(name);
})();
};
const causes = error?.cause?.filter((cause) => {
const isFound = cause === 'Mail domain with this name already exists.';
if (isFound) {
methods.setError('name', {
type: 'manual',
message: t(
'This mail domain is already used. Please, choose another one.',
),
});
}
return !isFound;
});
if (!methods) {
return null;
}
return (
<Modal
isOpen
leftActions={
<StyledLink href="/mail-domains">
<Button color="secondary" tabIndex={-1}>
{t('Cancel')}
</Button>
</StyledLink>
}
hideCloseButton
closeOnClickOutside
closeOnEsc
onClose={() => router.push('/mail-domains')}
rightActions={
<Button
onClick={onSubmitCallback}
disabled={!methods.watch('name') || isPending}
>
{t('Add the domain')}
</Button>
}
size={ModalSize.MEDIUM}
title={
<>
<MailDomainsLogo aria-hidden="true" />
<Text as="h3" $textAlign="center">
{t('Add your mail domain')}
</Text>
</>
}
>
<FormProvider {...methods}>
<form action="" id={FORM_ID}>
<Controller
control={methods.control}
name="name"
render={({ fieldState }) => (
<Input
fullWidth
type="text"
{...methods.register('name')}
aria-invalid={!!fieldState.error}
aria-required
required
autoComplete="off"
label={t('Domain name')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState?.error?.message
? fieldState.error.message
: t('Example: saint-laurent.fr')
}
/>
)}
/>
</form>
{!!causes?.length ? <TextErrors causes={causes} /> : null}
{isPending && (
<Box $align="center">
<Loader />
</Box>
)}
</FormProvider>
</Modal>
);
};
@@ -86,7 +86,7 @@ const ItemListState = ({
return (
<Box $justify="center" $margin="small">
<Text as="p" $margin={{ vertical: 'none' }}>
{t(`0 mail domain to display.`)}
{t(`No domains exist.`)}
</Text>
</Box>
);
@@ -1,11 +1,10 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import IconOpenClose from '@/assets/icons/icon-open-close.svg';
import { Box, BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import IconOpenClose from '../../assets/icon-open-close.svg';
import { ItemList } from './ItemList';
import { PanelActions } from './PanelActions';
@@ -21,6 +20,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 (
@@ -34,7 +38,7 @@ export const Panel = () => {
transition: ${transition};
`}
$height="inherit"
aria-label="mail domains panel"
aria-label={t('Mail domains panel')}
{...closedOverridingStyles}
>
<BoxButton
@@ -52,6 +56,7 @@ export const Panel = () => {
transition: ${transition};
`}
onClick={() => setIsOpen(!isOpen)}
{...styleNoTeam}
>
<IconOpenClose width={24} height={24} aria-hidden="true" />
</BoxButton>
@@ -1,13 +1,13 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton } from '@/components';
import IconAdd from '@/assets/icons/icon-add.svg';
import IconSort from '@/assets/icons/icon-sort.svg';
import { Box, BoxButton, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { EnumMailDomainsOrdering } from '@/features/mail-domains';
import { useMailDomainsStore } from '@/features/mail-domains/store/useMailDomainsStore';
import IconSort from '../../assets/icon-sort.svg';
export const PanelActions = () => {
const { t } = useTranslation();
const { changeOrdering, ordering } = useMailDomainsStore();
@@ -42,6 +42,16 @@ export const PanelActions = () => {
>
<IconSort width={30} height={30} aria-hidden="true" />
</BoxButton>
<StyledLink href="/mail-domains/add/">
<Text
$margin="auto"
aria-label={t('Add your mail domain')}
$theme="primary"
>
<IconAdd width={27} height={27} aria-hidden="true" />
</Text>
</StyledLink>
</Box>
);
};
@@ -1,5 +1,6 @@
import { useRouter } from 'next/router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
@@ -11,6 +12,7 @@ interface MailDomainProps {
}
export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
const { t } = useTranslation();
const { colorsTokens } = useCunninghamTheme();
const {
query: { slug },
@@ -18,10 +20,23 @@ export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
const isActive = mailDomain.slug === slug;
const getStatusText = (status: MailDomain['status']) => {
switch (status) {
case 'pending':
return t('[pending]');
case 'enabled':
return t('[enabled]');
case 'disabled':
return t('[disabled]');
case 'failed':
return t('[failed]');
}
};
const activeStyle = `
border-right: 4px solid ${colorsTokens()['primary-600']};
background: ${colorsTokens()['primary-400']};
span{
span {
color: ${colorsTokens()['primary-text']};
}
`;
@@ -31,12 +46,14 @@ export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
border-right: 4px solid ${colorsTokens()['primary-400']};
background: ${colorsTokens()['primary-300']};
span{
span {
color: ${colorsTokens()['primary-text']};
}
}
`;
const statusText = getStatusText(mailDomain.status);
return (
<Box
$margin="none"
@@ -49,29 +66,44 @@ export const PanelMailDomains = ({ mailDomain }: MailDomainProps) => {
>
<StyledLink
className="p-s pt-t pb-t"
$css="width: 100%"
href={`/mail-domains/${mailDomain.slug}`}
>
<Box $align="center" $direction="row" $gap="0.5rem">
<IconMailDomains
aria-hidden="true"
color={colorsTokens()['primary-500']}
className="p-t"
width="52"
style={{
borderRadius: '10px',
flexShrink: 0,
background: '#fff',
border: `1px solid ${colorsTokens()['primary-300']}`,
}}
/>
<Text
$weight="bold"
$color={colorsTokens()['greyscale-600']}
$css={`
min-width: 14rem;
<Box
$position="relative"
$align="center"
$direction="row"
$justify="space-between"
$gap="1rem"
>
<Box $direction="row" $gap="0.5rem" $justify="left" $align="center">
<IconMailDomains
aria-hidden="true"
color={colorsTokens()['primary-500']}
className="p-t"
width="52"
style={{
borderRadius: '10px',
flexShrink: 0,
background: '#fff',
border: `1px solid ${colorsTokens()['primary-300']}`,
}}
/>
<Text
$weight="bold"
$color={colorsTokens()['greyscale-600']}
$css={`
display: inline-block;
width: 10rem;
overflow: hidden;
text-overflow: ellipsis !important;
`}
>
{mailDomain.name}
>
{mailDomain.name}
</Text>
</Box>
<Text $size="s" $theme="greyscale">
{statusText}
</Text>
</Box>
</StyledLink>
@@ -6,10 +6,21 @@ export interface MailDomain {
created_at: string;
updated_at: string;
slug: string;
status: 'pending' | 'enabled' | 'failed' | 'disabled';
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;
}
@@ -70,7 +70,7 @@ const MenuItem = ({ Icon, label, href, alias }: MenuItemProps) => {
<BoxButton
aria-label={t(`{{label}} button`, { label })}
$color={color}
tabIndex={-1}
as="span"
>
<Icon
width="2.375rem"
@@ -1,4 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="11.5" transform="rotate(-180 12 12)" fill="white" stroke="currentColor"/>
<path d="M14.1683 16.232C14.4803 15.92 14.4803 15.416 14.1683 15.104L11.0643 12L14.1683 8.896C14.4803 8.584 14.4803 8.08 14.1683 7.768C13.8563 7.456 13.3523 7.456 13.0403 7.768L9.36834 11.44C9.05634 11.752 9.05634 12.256 9.36834 12.568L13.0403 16.24C13.3443 16.544 13.8563 16.544 14.1683 16.232Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 500 B

@@ -1,13 +0,0 @@
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_178_17837)">
<path
d="M11.25 3.75L6.25 8.7375H10V17.5H12.5V8.7375H16.25L11.25 3.75ZM20 21.2625V12.5H17.5V21.2625H13.75L18.75 26.25L23.75 21.2625H20Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_178_17837">
<rect width="30" height="30" fill="white" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 429 B

@@ -1,11 +1,10 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import IconOpenClose from '@/assets/icons/icon-open-close.svg';
import { Box, BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import IconOpenClose from '../assets/icon-open-close.svg';
import { PanelActions } from './PanelActions';
import { TeamList } from './TeamList';
@@ -1,12 +1,12 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import IconAdd from '@/assets/icons/icon-add.svg';
import IconSort from '@/assets/icons/icon-sort.svg';
import { Box, BoxButton, StyledLink } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { TeamsOrdering } from '@/features/teams/team-management/api';
import IconAdd from '../assets/icon-add.svg';
import IconSort from '../assets/icon-sort.svg';
import { useTeamStore } from '../store/useTeamsStore';
export const PanelActions = () => {
@@ -45,11 +45,13 @@ export const PanelActions = () => {
</BoxButton>
<StyledLink href="/teams/create">
<BoxButton
as="span"
$margin={{ all: 'auto' }}
aria-label={t('Add a team')}
$color={colorsTokens()['primary-600']}
tabIndex={-1}
>
<IconAdd width={30} height={30} aria-hidden="true" />
<IconAdd width={27} height={27} aria-hidden="true" />
</BoxButton>
</StyledLink>
</Box>
@@ -9,13 +9,14 @@
"fr": {
"translation": {
"0 group to display.": "0 groupe à afficher.",
"0 mail domain to display.": "0 domaine de mail à afficher.",
"Accessibility statement": "Déclaration d'accessibilité",
"Accessibility: non-compliant": "Accessibilité : non conforme",
"Add a member": "Ajouter un membre",
"Add a team": "Ajouter un groupe",
"Add members to the team": "Ajouter des membres à l'équipe",
"Add the domain": "Ajouter le domaine",
"Add to group": "Ajouter au groupe",
"Add your mail domain": "Ajouter votre nom de domaine",
"Address: National Agency for Territorial Cohesion - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07 Paris": "Adresse : Agence Nationale de la Cohésion des Territoires - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07",
"Administration": "Administration",
"All fields are mandatory.": "Tous les champs sont obligatoires.",
@@ -29,6 +30,7 @@
"Close the teams panel": "Fermer le panneau des groupes",
"Compliance status": "État de conformité",
"Confirm deletion": "Confirmer la suppression",
"Contact our support at \"suiteterritoriale@anct.gouv.fr\"": "Contacter notre support à \"suiteterritoriale@anct.gouv.fr\"",
"Content modal to delete the team": "Contenu modal pour supprimer le groupe",
"Content modal to update the team": "Contenu modal pour mettre à jour le groupe",
"Cookies placed": "Cookies déposés",
@@ -46,12 +48,14 @@
"Defender of Rights - Free response - 71120 75342 Paris CEDEX 07": "Défenseur des droits\nLibre réponse 71120 75342 Paris CEDEX 07",
"Delete the team": "Supprimer le groupe",
"Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}",
"Domain name": "Nom de domaine",
"E-mail:": "E-mail:",
"E.g. : jean.dupont@mail.fr": "Ex. : jean.dupont@mail.fr",
"Email address prefix": "Préfixe de l'adresse mail",
"Emails": "Emails",
"Empty team icon": "Icône équipe vide",
"Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné",
"Example: saint-laurent.fr": "Exemple : saint-laurent.fr",
"Failed to add {{name}} in the team": "Impossible d'ajouter {{name}} au groupe",
"Failed to create the invitation for {{email}}": "Impossible de créer l'invitation pour {{email}}",
"Find a member to add to the team": "Trouver un membre à ajouter au groupe",
@@ -79,6 +83,7 @@
"List members card": "Carte liste des membres",
"Logout": "Se déconnecter",
"Mail Domains": "Domaines de messagerie",
"Mail domains panel": "Panel des domaines de messagerie",
"Mailbox created!": "Boîte mail créée !",
"Mailboxes list": "Liste des boîtes mail",
"Marianne Logo": "Logo Marianne",
@@ -89,6 +94,7 @@
"My account": "Mon compte",
"Names": "Noms",
"New name...": "Nouveau nom...",
"No domains exist.": "Aucun domaine existant.",
"No mail box was created with this mail domain.": "Aucune boîte mail n'a été créée avec ce nom de domaine.",
"Nothing exceptional, no special privileges related to a .gouv.fr.": "Rien d'exceptionnel, pas de privilèges spéciaux liés à un .gouv.fr.",
"Open the mail domains panel": "Ouvrir le panneau des domaines de messagerie",
@@ -127,6 +133,7 @@
"Team name": "Nom du groupe",
"Teams": "Équipes",
"The National Agency for Territorial Cohesion undertakes to make its\n service accessible, in accordance with article 47 of law no. 2005-102\n of February 11, 2005.": "L'Agence Nationale de la Cohésion des Territoires sengage à rendre son service accessible, conformément à larticle 47 de la loi n° 2005-102 du 11 février 2005.",
"The domain name encounters an error. Please contact our support team to solve the problem:": "Le nom de domaine rencontre une erreur. Veuillez contacter notre support pour résoudre le problème :",
"The member has been removed from the team": "Le membre a été supprimé de votre groupe",
"The role has been updated": "Le rôle a bien été mis à jour",
"The team has been removed.": "Le groupe a été supprimé.",
@@ -134,7 +141,9 @@
"The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse",
"This accessibility statement applies to La Régie (Suite Territoriale)": "Cette déclaration daccessibilité sapplique à La Régie (Suite Territoriale)",
"This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.",
"This domain name is deactivated. No new mailboxes can be created.": "Ce nom de domaine est désactivé. Aucune nouvelle boîte mail ne peut être créée.",
"This email prefix is already used.": "Ce préfixe d'email est déjà utilisé.",
"This mail domain is already used. Please, choose another one.": "Ce domaine de messagerie est déjà utilisé. Veuillez en choisir un autre.",
"This procedure is to be used in the following case: you have reported to the website \n manager an accessibility defect which prevents you from accessing content or one of the \n portal's services and you have not obtained a satisfactory response.": "Cette procédure est à utiliser dans le cas suivant : vous avez signalé au responsable du site internet un défaut daccessibilité qui vous empêche daccéder à un contenu ou à un des services du portail et vous navez pas obtenu de réponse satisfaisante.",
"This site does not display a cookie consent banner, why?": "Ce site n'affiche pas de bannière de consentement des cookies, pourquoi?",
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
@@ -154,6 +163,11 @@
"You cannot remove other owner.": "Vous ne pouvez pas supprimer un autre propriétaire.",
"You cannot update the role of other owner.": "Vous ne pouvez pas mettre à jour les rôles d'autre propriétaire.",
"You must have minimum 1 character": "Vous devez entrer au moins 1 caractère",
"Your domain name is being validated. You will not be able to create mailboxes until your domain name has been validated by our team.": "Votre nom de domaine est en cours de validation. Vous ne pourrez créer de boîtes mail que lorsque votre nom de domaine sera validé par notre équipe.",
"[disabled]": "[désactivé]",
"[enabled]": "[actif]",
"[failed]": "[erroné]",
"[pending]": "[en attente]",
"accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>",
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
"mail domains list loading": "chargement de la liste des domaines de messagerie",
+5 -15
View File
@@ -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;
@@ -0,0 +1,20 @@
import React, { ReactElement } from 'react';
import { Box } from '@/components';
import { MailDomainsLayout } from '@/features/mail-domains';
import { ModalCreateMailDomain } from '@/features/mail-domains/components/ModalAddMailDomain';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return (
<Box $padding="large" $height="inherit">
<ModalCreateMailDomain />
</Box>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
return <MailDomainsLayout>{page}</MailDomainsLayout>;
};
export default Page;
@@ -1,10 +1,29 @@
import { ReactElement } from 'react';
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box } from '@/components';
import { MailDomainsLayout } from '@/features/mail-domains';
import { NextPageWithLayout } from '@/types/next';
const StyledButton = styled(Button)`
width: fit-content;
`;
const Page: NextPageWithLayout = () => {
return null;
const { t } = useTranslation();
const router = useRouter();
return (
<Box $align="center" $justify="center" $height="inherit">
<StyledButton onClick={() => void router.push('/mail-domains/add')}>
{t('Add your mail domain')}
</StyledButton>
</Box>
);
};
Page.getLayout = function getLayout(page: ReactElement) {
@@ -1,9 +1,10 @@
import { Button } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, StyledLink } from '@/components';
import { Box } from '@/components';
import { TeamLayout } from '@/features/teams/team-management';
import { NextPageWithLayout } from '@/types/next';
@@ -13,12 +14,13 @@ const StyledButton = styled(Button)`
const Page: NextPageWithLayout = () => {
const { t } = useTranslation();
const router = useNavigate();
return (
<Box $align="center" $justify="center" $height="inherit">
<StyledLink href="/teams/create">
<StyledButton tabIndex={-1}>{t('Create a new team')}</StyledButton>
</StyledLink>
<StyledButton onClick={() => void router.push('/teams/create')}>
{t('Create a new team')}
</StyledButton>
</Box>
);
};
@@ -35,7 +35,7 @@ export const createTeam = async (
const randomTeams = randomName(teamName, browserName, length);
for (let i = 0; i < randomTeams.length; i++) {
await panel.getByRole('button', { name: 'Add a team' }).click();
await panel.getByRole('link', { name: 'Add a team' }).click();
await page.getByText('Team name').fill(randomTeams[i]);
await expect(buttonCreate).toBeEnabled();
await buttonCreate.click();
@@ -15,6 +15,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'mails.fr',
@@ -22,6 +31,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'mailsfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'versailles.net',
@@ -29,6 +47,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'versaillesnet',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'paris.fr',
@@ -36,6 +63,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'parisfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
];
@@ -63,7 +99,7 @@ const interceptCommonApiRequests = (page: Page) => {
});
});
void page.route('**/api/v1.0/mail-domains/domainfr', (route) => {
void page.route('**/api/v1.0/mail-domains/domainfr/', (route) => {
void route.fulfill({
json: mailDomainDomainFrFixture,
});
@@ -99,8 +135,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 +259,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,
}) => {
@@ -5,42 +5,113 @@ import { keyCloakSignIn } from './common';
const currentDateIso = new Date().toISOString();
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
},
{
name: 'mails.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43e',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'mailsfr',
},
{
name: 'versailles.net',
id: '456ac6ca-0402-4615-8005-69bc1efde43g',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'versaillesnet',
},
{
name: 'paris.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43h',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'parisfr',
},
];
const interceptCommonApiCalls = async (
page: Page,
arrayMailDomains: MailDomain[],
) => {
const singleMailDomain = arrayMailDomains[0];
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: arrayMailDomains.length,
next: null,
previous: null,
results: arrayMailDomains,
},
});
});
const mailDomainDomainFrFixture = mailDomainsFixtures[0];
await page.route('**/api/v1.0/mail-domains/domainfr/', async (route) => {
await route.fulfill({
json: singleMailDomain,
});
});
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1',
async (route) => {
await route.fulfill({
json: {
count: 0,
next: null,
previous: null,
results: [],
},
});
},
);
};
const clickOnMailDomainsNavButton = async (page: Page): Promise<void> =>
await page.locator('menu').first().getByLabel(`Mail Domains button`).click();
const assertMailDomainUpperElementsAreVisible = async (page: Page) => {
await expect(page).toHaveURL(/mail-domains\//);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(/mail-domains\/domainfr\//);
await expect(page.getByRole('heading', { name: 'domain.fr' })).toBeVisible();
};
const assertFilledMailboxesTableElementsAreVisible = async (
page: Page,
domainFr: object & { name: string },
multiLevelArrayMailboxes: object & Array<{ local_part: string }[]>,
) => {
await expect(page).toHaveURL(/mail-domains\//);
await expect(
page.getByRole('button', { name: /Names/ }).first(),
).toBeVisible();
await expect(
page.getByRole('button', { name: /Emails/ }).first(),
).toBeVisible();
await Promise.all(
multiLevelArrayMailboxes[0].map((mailbox) =>
expect(
page.getByText(`${mailbox.local_part}@${domainFr.name}`),
).toBeVisible(),
),
);
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();
await expect(
page.locator('.c__pagination__list').getByText('navigate_next'),
).toBeVisible();
await page
.locator('.c__pagination__list')
.getByRole('button', { name: '2' })
.click();
await expect(
page.locator('.c__pagination__list').getByText('navigate_next'),
).toBeHidden();
await expect(
page.locator('.c__pagination__list').getByText('navigate_before'),
).toBeVisible();
await Promise.all(
multiLevelArrayMailboxes[1].map((mailbox) =>
expect(
page.getByText(`${mailbox.local_part}@${domainFr.name}`),
).toBeVisible(),
),
);
};
test.describe('Mail domain', () => {
test.beforeEach(async ({ page, browserName }) => {
await page.goto('/');
@@ -71,181 +142,645 @@ test.describe('Mail domain', () => {
});
});
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
page,
}) => {
const interceptApiCalls = async () => {
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1',
async (route) => {
await route.fulfill({
json: {
count: 0,
next: null,
previous: null,
results: [],
},
});
},
);
await page.route('**/api/v1.0/mail-domains/domainfr**', async (route) => {
await route.fulfill({
json: mailDomainDomainFrFixture,
});
});
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
test.describe('user is administrator or owner', () => {
test.describe('mail domain is enabled', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
});
},
{
name: 'mails.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43e',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'mailsfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'versailles.net',
id: '456ac6ca-0402-4615-8005-69bc1efde43g',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'versaillesnet',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'paris.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43h',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'parisfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
];
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
page,
}) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).toBeEnabled();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
};
await interceptApiCalls();
test('checks all the elements are visible when domain exists and contains 2 pages of mailboxes', async ({
page,
}) => {
const mailboxesFixtures = {
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}`,
})),
},
};
const interceptApiCalls = async () => {
await page.route(
'**/api/v1.0/mail-domains/?page=*',
async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/',
async (route) => {
await route.fulfill({
json: mailDomainsFixtures[0],
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: 'http://localhost:8071/api/v1.0/mail-domains/domainfr/mailboxes/?page=2',
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=2**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: null,
previous:
'http://localhost:8071/api/v1.0/mail-domains/domainfr/mailboxes/?page=1',
results: mailboxesFixtures.domainFr.page2,
},
});
},
);
};
await clickOnMailDomainsNavButton(page);
await interceptApiCalls();
await expect(page).toHaveURL(/mail-domains\//);
await clickOnMailDomainsNavButton(page);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(/mail-domains\/domainfr\//);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByRole('heading', { name: /domain\.fr/ }).first(),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).toBeEnabled();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
await assertFilledMailboxesTableElementsAreVisible(
page,
mailDomainsFixtures[0],
[mailboxesFixtures.domainFr.page1, mailboxesFixtures.domainFr.page2],
);
});
});
test.describe('mail domain creation is pending', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'pending',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
];
test('checks expected elements are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains\//);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(/mail-domains\/domainfr\//);
await expect(
page.getByRole('heading', { name: 'domain.fr' }),
).toBeVisible();
await expect(
page.getByText(
'Your domain name is being validated. ' +
'You will not be able to create mailboxes until your domain name has been validated by our team.',
),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).toBeDisabled();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
});
test.describe('mail domain is disabled', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'disabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
];
test('checks expected elements are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByText(
'This domain name is deactivated. No new mailboxes can be created.',
),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).toBeDisabled();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
});
test.describe('mail domain creation has failed', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'failed',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
];
test('checks expected elements are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByText(
'The domain name encounters an error. Please contact our support team to solve the problem:',
),
).toBeVisible();
await expect(
page.getByRole('link', { name: 'suiteterritoriale@anct.gouv.fr' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).toBeDisabled();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
});
});
test('checks all the elements are visible when domain exists and contains 2 pages of mailboxes', async ({
page,
}) => {
const mailboxesFixtures = {
domainFr: {
page1: Array.from({ length: 20 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}f`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
page2: Array.from({ length: 2 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}d`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
},
};
const interceptApiCalls = async () => {
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
test.describe('user is member', () => {
test.describe('mail domain is enabled', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
});
});
await page.route('**/api/v1.0/mail-domains/domainfr', async (route) => {
await route.fulfill({
json: mailDomainDomainFrFixture,
});
});
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: 'http://localhost:8071/api/v1.0/mail-domains/domainfr/mailboxes/?page=2',
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=2**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: null,
previous:
'http://localhost:8071/api/v1.0/mail-domains/domainfr/mailboxes/?page=1',
results: mailboxesFixtures.domainFr.page2,
},
});
{
name: 'mails.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43e',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'mailsfr',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
);
};
{
name: 'versailles.net',
id: '456ac6ca-0402-4615-8005-69bc1efde43g',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'versaillesnet',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
{
name: 'paris.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43h',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'parisfr',
status: 'enabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
];
await interceptApiCalls();
test('checks all the elements are visible when domain exist but contains no mailboxes', async ({
page,
}) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains\//);
await assertMailDomainUpperElementsAreVisible(page);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(/mail-domains\/domainfr\//);
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).not.toBeInViewport();
await expect(
page.getByRole('heading', { name: 'domain.fr' }),
).toBeVisible();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
await expect(
page.getByRole('button', { name: /Emails/ }).first(),
).toBeVisible();
test('checks all the elements are visible when domain exists and contains 2 pages of mailboxes', async ({
page,
}) => {
const mailboxesFixtures = {
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}`,
})),
},
};
const interceptApiCalls = async () => {
await page.route(
'**/api/v1.0/mail-domains/?page=*',
async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/',
async (route) => {
await route.fulfill({
json: mailDomainsFixtures[0],
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: 'http://localhost:8071/api/v1.0/mail-domains/domainfr/mailboxes/?page=2',
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
);
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=2**',
async (route) => {
await route.fulfill({
json: {
count:
mailboxesFixtures.domainFr.page1.length +
mailboxesFixtures.domainFr.page2.length,
next: null,
previous:
'http://localhost:8071/api/v1.0/mail-domains/domainfr/mailboxes/?page=1',
results: mailboxesFixtures.domainFr.page2,
},
});
},
);
};
await Promise.all(
mailboxesFixtures.domainFr.page1.map((mailbox) =>
expect(
await interceptApiCalls();
await clickOnMailDomainsNavButton(page);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).not.toBeInViewport();
await assertFilledMailboxesTableElementsAreVisible(
page,
mailDomainsFixtures[0],
[mailboxesFixtures.domainFr.page1, mailboxesFixtures.domainFr.page2],
);
});
});
test.describe('mail domain creation is pending', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'pending',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
];
test('checks expected elements are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await expect(page).toHaveURL(/mail-domains\//);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await expect(page).toHaveURL(/mail-domains\/domainfr\//);
await expect(
page.getByRole('heading', { name: 'domain.fr' }),
).toBeVisible();
await expect(
page.getByText(
`${mailbox.local_part}@${mailDomainDomainFrFixture.name}`,
'Your domain name is being validated. ' +
'You will not be able to create mailboxes until your domain name has been validated by our team.',
),
).toBeVisible(),
),
);
).toBeVisible();
await expect(
page.locator('.c__pagination__list').getByRole('button', { name: '1' }),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).not.toBeInViewport();
await expect(
page.locator('.c__pagination__list').getByText('navigate_next'),
).toBeVisible();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
});
await page
.locator('.c__pagination__list')
.getByRole('button', { name: '2' })
.click();
test.describe('mail domain is disabled', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'disabled',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
];
await expect(
page.locator('.c__pagination__list').getByText('navigate_next'),
).toBeHidden();
test('checks expected elements are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await expect(
page.locator('.c__pagination__list').getByText('navigate_before'),
).toBeVisible();
await clickOnMailDomainsNavButton(page);
await Promise.all(
mailboxesFixtures.domainFr.page2.map((mailbox) =>
expect(
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByText(
`${mailbox.local_part}@${mailDomainDomainFrFixture.name}`,
'This domain name is deactivated. No new mailboxes can be created.',
),
).toBeVisible(),
),
);
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).not.toBeInViewport();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
});
test.describe('mail domain creation has failed', () => {
const mailDomainsFixtures: MailDomain[] = [
{
name: 'domain.fr',
id: '456ac6ca-0402-4615-8005-69bc1efde43f',
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'failed',
abilities: {
get: true,
patch: false,
put: false,
post: false,
delete: false,
manage_accesses: false,
},
},
];
test('checks expected elements are visible', async ({ page }) => {
await interceptCommonApiCalls(page, mailDomainsFixtures);
await clickOnMailDomainsNavButton(page);
await assertMailDomainUpperElementsAreVisible(page);
await expect(
page.getByText(
'The domain name encounters an error. Please contact our support team to solve the problem:',
),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Create a mailbox' }),
).not.toBeInViewport();
await expect(
page.getByText('No mail box was created with this mail domain.'),
).toBeVisible();
});
});
});
});
@@ -0,0 +1,176 @@
import { expect, test } from '@playwright/test';
import { keyCloakSignIn, randomName } from './common';
test.beforeEach(async ({ page, browserName }) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
});
test.describe('Add Mail Domains', () => {
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/mail-domains');
const buttonFromHomePage = page.getByRole('button', {
name: 'Add your mail domain',
});
await expect(buttonFromHomePage).toBeVisible();
await buttonFromHomePage.click();
await expect(buttonFromHomePage).toBeHidden();
await expect(
page.getByRole('heading', {
name: 'Add your mail domain',
level: 3,
}),
).toBeVisible();
const form = page.locator('form');
await expect(form.getByLabel('Domain name')).toBeVisible();
await expect(page.getByText('Example: saint-laurent.fr')).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Add the domain',
}),
).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Cancel',
}),
).toBeVisible();
});
test('checks the cancel button interaction', async ({ page }) => {
await page.goto('/mail-domains');
const buttonFromHomePage = page.getByRole('button', {
name: 'Add your mail domain',
});
await buttonFromHomePage.click();
await expect(buttonFromHomePage).toBeHidden();
await page
.getByRole('button', {
name: 'Cancel',
})
.click();
await expect(buttonFromHomePage).toBeVisible();
});
test('checks form invalid status', async ({ page }) => {
await page.goto('/mail-domains');
const buttonFromHomePage = page.getByRole('button', {
name: 'Add your mail domain',
});
await buttonFromHomePage.click();
const form = page.locator('form');
const inputName = form.getByLabel('Domain name');
const buttonSubmit = page.getByRole('button', {
name: 'Add the domain',
});
await expect(inputName).toBeVisible();
await expect(page.getByText('Example: saint-laurent.fr')).toBeVisible();
await expect(
page.getByRole('button', {
name: 'Cancel',
}),
).toBeEnabled();
await expect(buttonSubmit).toBeDisabled();
await inputName.fill('s');
await expect(page.getByText('Example: saint-laurent.fr')).toBeVisible();
await inputName.clear();
await expect(page.getByText('Example: saint-laurent.fr')).toBeVisible();
});
test('checks the routing on new mail domain added', async ({
page,
browserName,
}) => {
const mailDomainName = randomName('versailles.fr', browserName, 1)[0];
const mailDomainSlug = mailDomainName.replace('.', '');
await page.goto('/mail-domains');
const panel = page.getByLabel('Mail domains panel').first();
await panel.getByRole('link', { name: 'Add your mail domain' }).click();
const form = page.locator('form');
await form.getByLabel('Domain name').fill(mailDomainName);
await page.getByRole('button', { name: 'Add the domain' }).click();
await expect(page).toHaveURL(`/mail-domains\/${mailDomainSlug}/`);
await expect(
page.getByRole('heading', {
name: mailDomainName,
}),
).toBeVisible();
});
test('checks error when duplicate mail domain', async ({
page,
browserName,
}) => {
await page.goto('/mail-domains');
const panel = page.getByLabel('Mail domains panel').first();
const additionLink = panel.getByRole('link', {
name: 'Add your mail domain',
});
const form = page.locator('form');
const inputName = form.getByLabel('Domain name');
const submitButton = page.getByRole('button', {
name: 'Add the domain',
});
const mailDomainName = randomName('duplicate.fr', browserName, 1)[0];
const mailDomainSlug = mailDomainName.replace('.', '');
await additionLink.click();
await inputName.fill(mailDomainName);
await submitButton.click();
await expect(page).toHaveURL(`/mail-domains\/${mailDomainSlug}\/`);
await additionLink.click();
await inputName.fill(mailDomainName);
await submitButton.click();
await expect(
page.getByText(
'This mail domain is already used. Please, choose another one.',
),
).toBeVisible();
});
test('checks 404 on mail-domains/[slug] page', async ({ page }) => {
await page.goto('/mail-domains/unknown-domain');
await expect(
page.getByText(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',
),
).toBeVisible({
timeout: 15000,
});
});
});
@@ -11,6 +11,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'domainfr',
status: 'pending',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'mails.fr',
@@ -18,6 +27,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'mailsfr',
status: 'enabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'versailles.net',
@@ -25,6 +43,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'versaillesnet',
status: 'disabled',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
{
name: 'paris.fr',
@@ -32,6 +59,15 @@ const mailDomainsFixtures: MailDomain[] = [
created_at: currentDateIso,
updated_at: currentDateIso,
slug: 'parisfr',
status: 'failed',
abilities: {
get: true,
patch: true,
put: true,
post: true,
delete: true,
manage_accesses: true,
},
},
];
@@ -67,7 +103,7 @@ test.describe('Mail domains', () => {
response.status() === 200,
);
const panel = page.getByLabel('mail domains panel').first();
const panel = page.getByLabel('Mail domains panel').first();
await panel
.getByRole('button', {
@@ -107,9 +143,9 @@ test.describe('Mail domains', () => {
.click();
await expect(page).toHaveURL(/mail-domains\//);
await expect(
page.getByLabel('mail domains panel', { exact: true }),
page.getByLabel('Mail domains panel', { exact: true }),
).toBeVisible();
await expect(page.getByText('0 mail domain to display.')).toBeVisible();
await expect(page.getByText('No domains exist.')).toBeVisible();
});
test('when 4 mail domains exist', async ({ page }) => {
@@ -131,13 +167,17 @@ test.describe('Mail domains', () => {
.click();
await expect(page).toHaveURL(/mail-domains\//);
await expect(
page.getByLabel('mail domains panel', { exact: true }),
page.getByLabel('Mail domains panel', { exact: true }),
).toBeVisible();
await expect(page.getByText('0 mail domain to display.')).toHaveCount(0);
await expect(page.getByText('domain.fr')).toBeVisible();
await expect(page.getByText('mails.fr')).toBeVisible();
await expect(page.getByText('versailles.net')).toBeVisible();
await expect(page.getByText('paris.fr')).toBeVisible();
await expect(page.getByText('No domains exist.')).toHaveCount(0);
await Promise.all(
mailDomainsFixtures.map(async ({ name, status }) => {
const linkName = page.getByRole('link', { name });
await expect(linkName).toBeVisible();
await expect(linkName.getByText(`[${status}]`)).toBeVisible();
}),
);
});
});
});
@@ -63,7 +63,7 @@ test.describe('Teams Create', () => {
}) => {
const panel = page.getByLabel('Teams panel').first();
await panel.getByRole('button', { name: 'Add a team' }).click();
await panel.getByRole('link', { name: 'Add a team' }).click();
const teamName = `My routing team ${browserName}-${Math.floor(Math.random() * 1000)}`;
await page.getByText('Team name').fill(teamName);
@@ -72,7 +72,7 @@ test.describe('Teams Create', () => {
const elTeam = page.getByRole('heading', { name: teamName });
await expect(elTeam).toBeVisible();
await panel.getByRole('button', { name: 'Add a team' }).click();
await panel.getByRole('link', { name: 'Add a team' }).click();
await expect(elTeam).toBeHidden();
await panel.locator('li').getByText(teamName).click();
@@ -96,13 +96,13 @@ test.describe('Teams Create', () => {
test('checks error when duplicate team', async ({ page, browserName }) => {
const panel = page.getByLabel('Teams panel').first();
await panel.getByRole('button', { name: 'Add a team' }).click();
await panel.getByRole('link', { name: 'Add a team' }).click();
const teamName = `My duplicate team ${browserName}-${Math.floor(Math.random() * 1000)}`;
await page.getByText('Team name').fill(teamName);
await page.getByRole('button', { name: 'Create the team' }).click();
await panel.getByRole('button', { name: 'Add a team' }).click();
await panel.getByRole('link', { name: 'Add a team' }).click();
await page.getByText('Team name').fill(teamName);
await page.getByRole('button', { name: 'Create the team' }).click();
@@ -22,7 +22,7 @@ test.describe('Teams Panel', () => {
).toBeVisible();
await expect(
panel.getByRole('button', {
panel.getByRole('link', {
name: 'Add a team',
}),
).toBeVisible();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "0.1.0",
"version": "1.0.1",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "people",
"version": "0.1.0",
"version": "1.0.1",
"private": true,
"workspaces": {
"packages": [
@@ -1,6 +1,6 @@
{
"name": "eslint-config-people",
"version": "0.1.0",
"version": "1.0.1",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "0.1.0",
"version": "1.0.1",
"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.1"
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.1"
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/people-backend
pullPolicy: Always
tag: "v0.1.0"
tag: "v1.0.1"
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.1"
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:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.1.0",
"version": "1.0.1",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "people-openapi-client-ts",
"version": "1.1.0",
"version": "1.0.1",
"private": true,
"description": "Tool to generate Typescript API client for the People application.",
"scripts": {