From cca6c77f00f5d08cbdd5709bf52cf3038af0090f Mon Sep 17 00:00:00 2001 From: Marie PUPO JEAMMET Date: Mon, 8 Apr 2024 21:29:26 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F(models)=20add=20MailDomai?= =?UTF-8?q?n,=20MailDomainAccess=20and=20Mailbox=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additional app and models to handle email addresses creation in Desk. --- src/backend/mailbox_manager/__init__.py | 0 src/backend/mailbox_manager/apps.py | 1 + src/backend/mailbox_manager/factories.py | 49 ++++++++++ .../migrations/0001_initial.py | 67 ++++++++++++++ .../mailbox_manager/migrations/__init__.py | 0 src/backend/mailbox_manager/models.py | 88 ++++++++++++++++++ src/backend/mailbox_manager/tests/__init__.py | 0 .../tests/test_models_mailboxes.py | 91 +++++++++++++++++++ .../tests/test_models_maildomain.py | 26 ++++++ .../tests/test_models_maildomainaccess.py | 60 ++++++++++++ src/backend/people/settings.py | 1 + 11 files changed, 383 insertions(+) create mode 100644 src/backend/mailbox_manager/__init__.py create mode 100644 src/backend/mailbox_manager/apps.py create mode 100644 src/backend/mailbox_manager/factories.py create mode 100644 src/backend/mailbox_manager/migrations/0001_initial.py create mode 100644 src/backend/mailbox_manager/migrations/__init__.py create mode 100644 src/backend/mailbox_manager/models.py create mode 100644 src/backend/mailbox_manager/tests/__init__.py create mode 100644 src/backend/mailbox_manager/tests/test_models_mailboxes.py create mode 100644 src/backend/mailbox_manager/tests/test_models_maildomain.py create mode 100644 src/backend/mailbox_manager/tests/test_models_maildomainaccess.py diff --git a/src/backend/mailbox_manager/__init__.py b/src/backend/mailbox_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/mailbox_manager/apps.py b/src/backend/mailbox_manager/apps.py new file mode 100644 index 0000000..ce062d2 --- /dev/null +++ b/src/backend/mailbox_manager/apps.py @@ -0,0 +1 @@ +"""People additionnal application, to manage email adresses.""" diff --git a/src/backend/mailbox_manager/factories.py b/src/backend/mailbox_manager/factories.py new file mode 100644 index 0000000..43cb8a5 --- /dev/null +++ b/src/backend/mailbox_manager/factories.py @@ -0,0 +1,49 @@ +""" +Mailbox manager application factories +""" + +import factory.fuzzy +from faker import Faker + +from core import factories as core_factories +from core import models as core_models + +from mailbox_manager import models + +fake = Faker() + + +class MailDomainFactory(factory.django.DjangoModelFactory): + """A factory to create mail domain.""" + + class Meta: + model = models.MailDomain + + name = factory.Faker("domain_name") + + +class MailDomainAccessFactory(factory.django.DjangoModelFactory): + """A factory to create mail domain accesses.""" + + class Meta: + model = models.MailDomainAccess + + user = factory.SubFactory(core_factories.UserFactory) + domain = factory.SubFactory(MailDomainFactory) + role = factory.fuzzy.FuzzyChoice([r[0] for r in core_models.RoleChoices.choices]) + + +class MailboxFactory(factory.django.DjangoModelFactory): + """A factory to create mailboxes for mail domain members.""" + + class Meta: + model = models.Mailbox + + class Params: + """Parameters for fields.""" + + full_name = factory.Faker("name") + + local_part = factory.LazyAttribute(lambda a: a.full_name.lower().replace(" ", ".")) + domain = factory.SubFactory(MailDomainFactory) + secondary_email = factory.Faker("email") diff --git a/src/backend/mailbox_manager/migrations/0001_initial.py b/src/backend/mailbox_manager/migrations/0001_initial.py new file mode 100644 index 0000000..1a07261 --- /dev/null +++ b/src/backend/mailbox_manager/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 5.0.3 on 2024-04-16 12:51 + +import django.core.validators +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='MailDomain', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')), + ('name', models.CharField(max_length=150, unique=True, verbose_name='name')), + ], + options={ + 'verbose_name': 'Mail domain', + 'verbose_name_plural': 'Mail domains', + 'db_table': 'people_mail_domain', + }, + ), + migrations.CreateModel( + name='Mailbox', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')), + ('local_part', models.CharField(max_length=150, validators=[django.core.validators.RegexValidator(regex='^[a-zA-Z0-9_.+-]+$')], verbose_name='local_part')), + ('secondary_email', models.EmailField(max_length=254, verbose_name='secondary email address')), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_domain', to='mailbox_manager.maildomain')), + ], + options={ + 'verbose_name': 'Mailbox', + 'verbose_name_plural': 'Mailboxes', + 'db_table': 'people_mail_box', + 'unique_together': {('local_part', 'domain')}, + }, + ), + migrations.CreateModel( + name='MailDomainAccess', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')), + ('role', models.CharField(choices=[('member', 'Member'), ('administrator', 'Administrator'), ('owner', 'Owner')], default='member', max_length=20)), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_domain_accesses', to='mailbox_manager.maildomain')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_domain_accesses', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'User/mail domain relation', + 'verbose_name_plural': 'User/mail domain relations', + 'db_table': 'people_mail_domain_accesses', + 'unique_together': {('user', 'domain')}, + }, + ), + ] diff --git a/src/backend/mailbox_manager/migrations/__init__.py b/src/backend/mailbox_manager/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py new file mode 100644 index 0000000..1683b73 --- /dev/null +++ b/src/backend/mailbox_manager/models.py @@ -0,0 +1,88 @@ +""" +Declare and configure the models for the People additional application : mailbox_manager +""" + +from django.conf import settings +from django.core import validators +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.models import BaseModel, RoleChoices + + +class MailDomain(BaseModel): + """Domain names from which we will create email addresses (mailboxes).""" + + name = models.CharField( + _("name"), max_length=150, null=False, blank=False, unique=True + ) + + class Meta: + db_table = "people_mail_domain" + verbose_name = _("Mail domain") + verbose_name_plural = _("Mail domains") + + def __str__(self): + return self.name + + +class MailDomainAccess(BaseModel): + """Allow to manage users' accesses to mail domains.""" + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="mail_domain_accesses", + null=False, + blank=False, + ) + domain = models.ForeignKey( + MailDomain, + on_delete=models.CASCADE, + related_name="mail_domain_accesses", + null=False, + blank=False, + ) + role = models.CharField( + max_length=20, choices=RoleChoices.choices, default=RoleChoices.MEMBER + ) + + class Meta: + db_table = "people_mail_domain_accesses" + verbose_name = _("User/mail domain relation") + verbose_name_plural = _("User/mail domain relations") + unique_together = ("user", "domain") + + def __str__(self): + return f"Access of user {self.user!s} on domain {self.domain:s}." + + +class Mailbox(BaseModel): + """Mailboxes for users from mail domain.""" + + local_part = models.CharField( + _("local_part"), + max_length=150, + null=False, + blank=False, + validators=[validators.RegexValidator(regex="^[a-zA-Z0-9_.+-]+$")], + ) + domain = models.ForeignKey( + MailDomain, + on_delete=models.CASCADE, + related_name="mail_domain", + null=False, + blank=False, + ) + secondary_email = models.EmailField( + _("secondary email address"), null=False, blank=False + ) + + class Meta: + db_table = "people_mail_box" + verbose_name = _("Mailbox") + verbose_name_plural = _("Mailboxes") + unique_together = ("local_part", "domain") + + def __str__(self): + return f"{self.local_part!s}@{self.domain.name:s}." diff --git a/src/backend/mailbox_manager/tests/__init__.py b/src/backend/mailbox_manager/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/mailbox_manager/tests/test_models_mailboxes.py b/src/backend/mailbox_manager/tests/test_models_mailboxes.py new file mode 100644 index 0000000..1fed666 --- /dev/null +++ b/src/backend/mailbox_manager/tests/test_models_mailboxes.py @@ -0,0 +1,91 @@ +""" +Unit tests for the mailbox model +""" + +from django.core.exceptions import ValidationError + +import pytest + +from mailbox_manager import factories + +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"): + 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"): + factories.MailboxFactory(local_part=None) + + +def test_models_mailboxes__local_part_matches_expected_format(): + """ + The local part should contain alpha-numeric caracters + and a limited set of special caracters ("+", "-", ".", "_"). + """ + factories.MailboxFactory(local_part="Marie-Jose.Perec+JO_2024") + + with pytest.raises(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="!") + + +def test_models_mailboxes__local_part_unique_per_domain(): + """Local parts should be unique per domain.""" + + existing_mailbox = factories.MailboxFactory() + + # same local part on another domain should not be a problem + factories.MailboxFactory(local_part=existing_mailbox.local_part) + + # 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." + ): + factories.MailboxFactory( + local_part=existing_mailbox.local_part, domain=existing_mailbox.domain + ) + + +# DOMAIN FIELD + + +def test_models_mailboxes__domain_must_be_a_maildomain_instance(): + """The "domain" field should be an instance of MailDomain.""" + expected_error = '"Mailbox.domain" must be a "MailDomain" instance.' + with pytest.raises(ValueError, match=expected_error): + factories.MailboxFactory(domain="") + + with pytest.raises(ValueError, match=expected_error): + factories.MailboxFactory(domain="domain-as-string.com") + + +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"): + factories.MailboxFactory(domain=None) + + +# SECONDARY_EMAIL FIELD + + +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"): + 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"): + factories.MailboxFactory(secondary_email=None) diff --git a/src/backend/mailbox_manager/tests/test_models_maildomain.py b/src/backend/mailbox_manager/tests/test_models_maildomain.py new file mode 100644 index 0000000..5bf54fc --- /dev/null +++ b/src/backend/mailbox_manager/tests/test_models_maildomain.py @@ -0,0 +1,26 @@ +""" +Unit tests for the MailDomain model +""" + +from django.core.exceptions import ValidationError + +import pytest + +from mailbox_manager import factories + +pytestmark = pytest.mark.django_db + + +# NAME FIELD + + +def test_models_mail_domain__domain_name_should_not_be_empty(): + """The domain name field should not be empty.""" + with pytest.raises(ValidationError, match="This field cannot be blank"): + factories.MailDomainFactory(name="") + + +def test_models_mail_domain__domain_name_should_not_be_null(): + """The domain name field should not be null.""" + with pytest.raises(ValidationError, match="This field cannot be null."): + factories.MailDomainFactory(name=None) diff --git a/src/backend/mailbox_manager/tests/test_models_maildomainaccess.py b/src/backend/mailbox_manager/tests/test_models_maildomainaccess.py new file mode 100644 index 0000000..180cdf9 --- /dev/null +++ b/src/backend/mailbox_manager/tests/test_models_maildomainaccess.py @@ -0,0 +1,60 @@ +""" +Unit tests for the MailDomainAccess model +""" + +from django.core.exceptions import ValidationError + +import pytest + +from mailbox_manager import factories + +pytestmark = pytest.mark.django_db + +# USER FIELD + + +def test_models_maildomainaccess__user_be_a_user_instance(): + """The "user" field should be a user instance.""" + expected_error = '"MailDomainAccess.user" must be a "User" instance.' + with pytest.raises(ValueError, match=expected_error): + factories.MailDomainAccessFactory(user="") + + +def test_models_maildomainaccess__user_should_not_be_null(): + """The user field should not be null.""" + with pytest.raises(ValidationError, match="This field cannot be null."): + factories.MailDomainAccessFactory(user=None) + + +# DOMAIN FIELD + + +def test_models_maildomainaccesses__domain_must_be_a_maildomain_instance(): + """The "domain" field should be an instance of MailDomain.""" + expected_error = '"MailDomainAccess.domain" must be a "MailDomain" instance.' + with pytest.raises(ValueError, match=expected_error): + factories.MailDomainAccessFactory(domain="") + + with pytest.raises(ValueError, match=expected_error): + factories.MailDomainAccessFactory(domain="domain-as-string.com") + + +def test_models_maildomainaccesses__domain_cannot_be_null(): + """The "domain" field should not be null.""" + with pytest.raises(ValidationError, match="This field cannot be null"): + factories.MailDomainAccessFactory(domain=None) + + +# ROLE FIELD + + +def test_models_maildomainaccesses__role_cannot_be_empty(): + """The "role" field cannot be empty.""" + with pytest.raises(ValidationError, match="This field cannot be blank"): + factories.MailDomainAccessFactory(role="") + + +def test_models_maildomainaccesses__role_cannot_be_null(): + """The "role" field cannot be null.""" + with pytest.raises(ValidationError, match="This field cannot be null"): + factories.MailDomainAccessFactory(role=None) diff --git a/src/backend/people/settings.py b/src/backend/people/settings.py index 6b156e6..c4175e2 100755 --- a/src/backend/people/settings.py +++ b/src/backend/people/settings.py @@ -190,6 +190,7 @@ class Base(Configuration): # People "core", "demo", + "mailbox_manager", "drf_spectacular", # Third party apps "corsheaders",