👔(dimail) allow creation of "pending" mailboxes

Previously, mailbox creation was restricted to "enabled" domains.
We now allow users to create mailboxes on pending and failed domains.
Mailboxes thus created have the "pending" mailboxes status.
This commit is contained in:
Marie PUPO JEAMMET
2024-11-15 18:14:10 +01:00
committed by Sabrina Demagny
parent 28a972e19e
commit 863c85e3f0
12 changed files with 334 additions and 139 deletions

View File

@@ -16,32 +16,45 @@ class MailboxSerializer(serializers.ModelSerializer):
class Meta:
model = models.Mailbox
fields = ["id", "first_name", "last_name", "local_part", "secondary_email"]
fields = [
"id",
"first_name",
"last_name",
"local_part",
"secondary_email",
"status",
]
# everything is actually read-only as we do not allow update for now
read_only_fields = ["id"]
read_only_fields = ["id", "status"]
def create(self, validated_data):
"""
Override create function to fire a request on mailbox creation.
"""
# send new mailbox request to dimail
client = DimailAPIClient()
response = client.send_mailbox_request(
validated_data, self.context["request"].user.sub
)
mailbox_status = enums.MailDomainStatusChoices.PENDING
# fix format to have actual json, and remove uuid
mailbox_data = json.loads(response.content.decode("utf-8").replace("'", '"'))
del mailbox_data["uuid"]
if validated_data["domain"].status == enums.MailDomainStatusChoices.ENABLED:
client = DimailAPIClient()
# send new mailbox request to dimail
response = client.send_mailbox_request(
validated_data, self.context["request"].user.sub
)
# fix format to have actual json, and remove uuid
mailbox_data = json.loads(
response.content.decode("utf-8").replace("'", '"')
)
del mailbox_data["uuid"]
mailbox_status = enums.MailDomainStatusChoices.ENABLED
# send confirmation email
client.send_new_mailbox_notification(
recipient=validated_data["secondary_email"], mailbox_data=mailbox_data
)
# actually save mailbox on our database
instance = models.Mailbox.objects.create(**validated_data)
# send confirmation email
client.send_new_mailbox_notification(
recipient=validated_data["secondary_email"], mailbox_data=mailbox_data
)
return instance
return models.Mailbox.objects.create(**validated_data, status=mailbox_status)
class MailDomainSerializer(serializers.ModelSerializer):

View File

@@ -1,3 +1,4 @@
# pylint: disable=too-many-ancestors
"""
Application enums declaration
"""
@@ -21,3 +22,12 @@ class MailDomainStatusChoices(models.TextChoices):
ENABLED = "enabled", _("Enabled")
FAILED = "failed", _("Failed")
DISABLED = "disabled", _("Disabled")
class MailboxStatusChoices(models.TextChoices):
"""Lists the possible statuses in which a mailbox can be."""
PENDING = "pending", _("Pending")
ENABLED = "enabled", _("Enabled")
FAILED = "failed", _("Failed")
DISABLED = "disabled", _("Disabled")

View File

@@ -75,3 +75,9 @@ class MailboxFactory(factory.django.DjangoModelFactory):
)
domain = factory.SubFactory(MailDomainEnabledFactory)
secondary_email = factory.Faker("email")
class MailboxEnabledFactory(MailboxFactory):
"""A factory to create mailbox enabled."""
status = enums.MailboxStatusChoices.ENABLED

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.3 on 2024-11-18 14:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mailbox_manager', '0013_remove_maildomain_secret'),
]
operations = [
migrations.AddField(
model_name='mailbox',
name='status',
field=models.CharField(choices=[('pending', 'Pending'), ('enabled', 'Enabled'), ('failed', 'Failed'), ('disabled', 'Disabled')], default='pending', max_length=20),
),
]

View File

@@ -10,7 +10,11 @@ from django.utils.translation import gettext_lazy as _
from core.models import BaseModel
from mailbox_manager.enums import MailDomainRoleChoices, MailDomainStatusChoices
from mailbox_manager.enums import (
MailboxStatusChoices,
MailDomainRoleChoices,
MailDomainStatusChoices,
)
class MailDomain(BaseModel):
@@ -184,6 +188,11 @@ class Mailbox(BaseModel):
secondary_email = models.EmailField(
_("secondary email address"), null=False, blank=False
)
status = models.CharField(
max_length=20,
choices=MailboxStatusChoices.choices,
default=MailboxStatusChoices.PENDING,
)
class Meta:
db_table = "people_mail_box"
@@ -196,14 +205,8 @@ class Mailbox(BaseModel):
def clean(self):
"""
Mailboxes can only be created on enabled domains.
Also, mail-provisioning API credentials must be set for dimail to allow auth.
Mail-provisioning API credentials must be set for dimail to allow auth.
"""
if self.domain.status != MailDomainStatusChoices.ENABLED:
raise exceptions.ValidationError(
"You can create mailbox only for a domain enabled"
)
# Won't be able to query user token if MAIL_PROVISIONING_API_CREDENTIALS are not set
if not settings.MAIL_PROVISIONING_API_CREDENTIALS:
raise exceptions.ValidationError(
@@ -216,6 +219,11 @@ class Mailbox(BaseModel):
"""
self.full_clean()
if self.domain.status == MailDomainStatusChoices.DISABLED:
raise exceptions.ValidationError(
_("You can't create a mailbox for a disabled domain.")
)
if self._state.adding:
return super().save(*args, **kwargs)

View File

@@ -137,6 +137,7 @@ def test_api_mailboxes__create_roles_success(role):
"last_name": str(mailbox.last_name),
"local_part": str(mailbox.local_part),
"secondary_email": str(mailbox.secondary_email),
"status": enums.MailboxStatusChoices.ENABLED,
}
@@ -194,6 +195,7 @@ def test_api_mailboxes__create_with_accent_success(role):
"last_name": str(mailbox.last_name),
"local_part": str(mailbox.local_part),
"secondary_email": str(mailbox.secondary_email),
"status": enums.MailboxStatusChoices.ENABLED,
}
@@ -236,6 +238,73 @@ def test_api_mailboxes__create_administrator_missing_fields():
assert response.json() == {"secondary_email": ["This field is required."]}
@pytest.mark.parametrize(
"role",
[
enums.MailDomainRoleChoices.OWNER,
enums.MailDomainRoleChoices.ADMIN,
],
)
def test_api_mailboxes__cannot_create_on_disabled_domain(role):
"""Admin and owner users should not be able to create mailboxes for a disabled domain"""
mail_domain = factories.MailDomainFactory(
status=enums.MailDomainStatusChoices.DISABLED
)
access = factories.MailDomainAccessFactory(role=role, domain=mail_domain)
client = APIClient()
client.force_login(access.user)
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",
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert not models.Mailbox.objects.exists()
assert response.json() == ["You can't create a mailbox for a disabled domain."]
@pytest.mark.parametrize(
"domain_status",
[
enums.MailDomainStatusChoices.PENDING,
enums.MailDomainStatusChoices.FAILED,
],
)
def test_api_mailboxes__create_pending_mailboxes(domain_status):
"""
Admin and owner users should be able to create mailboxes, including on pending and failed
domains.
Mailboxes created on pending and failed domains should have the "pending" status
"""
mail_domain = factories.MailDomainFactory(status=domain_status)
access = factories.MailDomainAccessFactory(
role=enums.MailDomainRoleChoices.ADMIN, domain=mail_domain
)
client = APIClient()
client.force_login(access.user)
mailbox_values = serializers.MailboxSerializer(
factories.MailboxFactory.build()
).data
with responses.RequestsMock():
# We add no response in RequestsMock
# because we expect no outside calls to be made
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()
assert mailbox.status == "pending"
### REACTING TO DIMAIL-API
### We mock dimail's responses to avoid testing dimail's container too
@@ -411,6 +480,7 @@ def test_api_mailboxes__domain_owner_or_admin_successful_creation_and_provisioni
"last_name": str(mailbox_data["last_name"]),
"local_part": str(mailbox_data["local_part"]),
"secondary_email": str(mailbox_data["secondary_email"]),
"status": enums.MailboxStatusChoices.ENABLED,
}
assert mailbox.first_name == mailbox_data["first_name"]
assert mailbox.last_name == mailbox_data["last_name"]

View File

@@ -53,8 +53,8 @@ def test_api_mailboxes__list_authenticated():
def test_api_mailboxes__list_roles(role):
"""Owner, admin and viewer users should be able to list mailboxes"""
mail_domain = factories.MailDomainEnabledFactory()
mailbox1 = factories.MailboxFactory(domain=mail_domain)
mailbox2 = factories.MailboxFactory(domain=mail_domain)
mailbox1 = factories.MailboxEnabledFactory(domain=mail_domain)
mailbox2 = factories.MailboxEnabledFactory(domain=mail_domain)
access = factories.MailDomainAccessFactory(role=role, domain=mail_domain)
client = APIClient()
@@ -69,6 +69,7 @@ def test_api_mailboxes__list_roles(role):
"last_name": str(mailbox2.last_name),
"local_part": str(mailbox2.local_part),
"secondary_email": str(mailbox2.secondary_email),
"status": enums.MailboxStatusChoices.ENABLED,
},
{
"id": str(mailbox1.id),
@@ -76,6 +77,7 @@ def test_api_mailboxes__list_roles(role):
"last_name": str(mailbox1.last_name),
"local_part": str(mailbox1.local_part),
"secondary_email": str(mailbox1.secondary_email),
"status": enums.MailboxStatusChoices.ENABLED,
},
]

View File

@@ -74,7 +74,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(models.MailDomain.DoesNotExist, match="Mailbox has no domain."):
with pytest.raises(exceptions.ValidationError, match="This field cannot be null"):
factories.MailboxFactory(domain=None)
@@ -93,44 +93,37 @@ def test_models_mailboxes__secondary_email_cannot_be_null():
factories.MailboxFactory(secondary_email=None)
def test_models_mailboxes__cannot_be_created_for_disabled_maildomain():
"""Mailbox creation is allowed only for a domain enabled.
@pytest.mark.parametrize(
"domain_status",
[
enums.MailDomainStatusChoices.PENDING,
enums.MailDomainStatusChoices.FAILED,
],
)
def test_models_mailboxes__can_create_pending_mailboxes_on_non_enabled_domain(
domain_status,
):
"""Mailbox creation is allowed for a domain pending and failed.
A pending mailbox is created."""
mailbox = factories.MailboxFactory(
domain=factories.MailDomainFactory(status=domain_status)
)
assert mailbox.status == enums.MailboxStatusChoices.PENDING
def test_models_mailboxes__cannot_create_mailboxes_on_disabled_domain():
"""Mailbox creation is not allowed for a domain disabled.
A disabled status for the mail domain raises an error."""
with pytest.raises(
exceptions.ValidationError,
match="You can create mailbox only for a domain enabled",
match="You can't create a mailbox for a disabled domain.",
):
factories.MailboxFactory(
domain=factories.MailDomainFactory(
status=enums.MailDomainStatusChoices.DISABLED
)
)
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(
exceptions.ValidationError,
match="You can create mailbox only for a domain enabled",
):
factories.MailboxFactory(
domain=factories.MailDomainFactory(
status=enums.MailDomainStatusChoices.FAILED
)
)
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(
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())
assert not models.Mailbox.objects.exist()
### REACTING TO DIMAIL-API

View File

@@ -37,6 +37,21 @@ def test_models_mail_domain__slug_inferred_from_name():
assert domain.slug == slugify(name)
# "STATUS" FIELD
def test_models_mail_domain__status_should_not_be_empty():
"""Status field should not be empty."""
with pytest.raises(ValidationError, match="This field cannot be blank"):
factories.MailDomainFactory(status="")
def test_models_mail_domain__status_should_not_be_null():
"""Status field should not be null."""
with pytest.raises(ValidationError, match="This field cannot be null."):
factories.MailDomainFactory(status=None)
# get_abilities