diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py index 22159d87c..c2b621fd8 100644 --- a/modoboa/admin/api/v1/serializers.py +++ b/modoboa/admin/api/v1/serializers.py @@ -171,8 +171,9 @@ class MailboxSerializer(serializers.ModelSerializer): def validate(self, data): """Check if quota is required.""" + method = self.context["request"].method if not data.get("use_domain_quota", False): - if "quota" not in data: + if "quota" not in data and method != "PATCH": raise serializers.ValidationError({ "quota": _("This field is required") }) diff --git a/modoboa/admin/tests/test_api.py b/modoboa/admin/api/v1/tests.py similarity index 91% rename from modoboa/admin/tests/test_api.py rename to modoboa/admin/api/v1/tests.py index 727154239..6f6ca230a 100644 --- a/modoboa/admin/tests/test_api.py +++ b/modoboa/admin/api/v1/tests.py @@ -12,11 +12,11 @@ from django.urls import reverse from rest_framework.authtoken.models import Token from reversion.models import Version -from modoboa.admin import models as admin_models +from modoboa.admin import factories +from modoboa.admin import models +from modoboa.admin.tests import utils from modoboa.core import factories as core_factories, models as core_models from modoboa.lib.tests import ModoAPITestCase -from . import utils -from .. import factories, models class DomainAPITestCase(ModoAPITestCase): @@ -32,13 +32,13 @@ class DomainAPITestCase(ModoAPITestCase): def test_get_domains(self): """Retrieve a list of domains.""" - url = reverse("api:domain-list") + url = reverse("v1:domain-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) domain = response.data[0] url = reverse( - "api:domain-detail", args=[domain["pk"]]) + "v1:domain-detail", args=[domain["pk"]]) response = self.client.get(url) data = response.json() self.assertEqual(response.status_code, 200) @@ -47,7 +47,7 @@ class DomainAPITestCase(ModoAPITestCase): def test_create_domain(self): """Check domain creation.""" - url = reverse("api:domain-list") + url = reverse("v1:domain-list") response = self.client.post( url, {"name": "test3.com", "quota": 0, "default_mailbox_quota": 10} ) @@ -83,7 +83,7 @@ class DomainAPITestCase(ModoAPITestCase): token = Token.objects.create(user=reseller) self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) - url = reverse("api:domain-list") + url = reverse("v1:domain-list") mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result response = self.client.post( @@ -107,7 +107,7 @@ class DomainAPITestCase(ModoAPITestCase): models.Mailbox.objects.filter( domain__name="test.com", address="user").update( use_domain_quota=True) - url = reverse("api:domain-detail", args=[domain.pk]) + url = reverse("v1:domain-detail", args=[domain.pk]) response = self.client.put( url, {"name": "test.com", "default_mailbox_quota": 1000}) self.assertEqual(response.status_code, 200) @@ -127,7 +127,7 @@ class DomainAPITestCase(ModoAPITestCase): def test_patch_domain(self): """Check domain partial update.""" domain = models.Domain.objects.get(name="test.com") - url = reverse("api:domain-detail", args=[domain.pk]) + url = reverse("v1:domain-detail", args=[domain.pk]) response = self.client.put(url, {"name": "toto.test"}) self.assertEqual(response.status_code, 200) domain.refresh_from_db() @@ -136,7 +136,7 @@ class DomainAPITestCase(ModoAPITestCase): def test_delete_domain(self): """Try to delete a domain.""" domain = models.Domain.objects.get(name="test.com") - url = reverse("api:domain-detail", args=[domain.pk]) + url = reverse("v1:domain-detail", args=[domain.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertFalse(models.Domain.objects.filter(pk=domain.pk).exists()) @@ -159,30 +159,30 @@ class DomainAliasAPITestCase(ModoAPITestCase): def test_get(self): """Retrieve a list of domain aliases.""" - url = reverse("api:domain_alias-list") + url = reverse("v1:domain_alias-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) url = reverse( - "api:domain_alias-detail", args=[response.data[0]["pk"]]) + "v1:domain_alias-detail", args=[response.data[0]["pk"]]) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data["name"], "dalias1.com") - url = reverse("api:domain_alias-list") + url = reverse("v1:domain_alias-list") response = self.client.get("{}?domain=test.com".format(url)) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) self.client.credentials( HTTP_AUTHORIZATION="Token " + self.da_token.key) - response = self.client.get(reverse("api:domain_alias-list")) + response = self.client.get(reverse("v1:domain_alias-list")) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 0) def test_post(self): """Try to create a new domain alias.""" - url = reverse("api:domain_alias-list") + url = reverse("v1:domain_alias-list") target = models.Domain.objects.get(name="test.com") data = { "name": "dalias3.com", @@ -205,7 +205,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): def test_put(self): """Try to update a domain alias.""" dalias = models.DomainAlias.objects.get(name="dalias1.com") - url = reverse("api:domain_alias-detail", args=[dalias.pk]) + url = reverse("v1:domain_alias-detail", args=[dalias.pk]) data = { "name": "dalias3.com", "target": dalias.target.pk } @@ -219,7 +219,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): def test_delete(self): """Try to delete an existing domain alias.""" dalias = models.DomainAlias.objects.get(name="dalias1.com") - url = reverse("api:domain_alias-detail", args=[dalias.pk]) + url = reverse("v1:domain_alias-detail", args=[dalias.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertFalse( @@ -257,7 +257,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_get_accounts(self): """Retrieve a list of accounts.""" - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) response = response.json() @@ -280,7 +280,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_create_account(self): """Try to create a new account.""" - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, self.ACCOUNT_DATA, format="json") self.assertEqual(response.status_code, 201) @@ -306,7 +306,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_create_account_with_random_password(self): """Try to create a new account with random password.""" - url = reverse("api:account-list") + url = reverse("v1:account-list") data = dict(self.ACCOUNT_DATA) data["random_password"] = True del data["password"] @@ -316,7 +316,7 @@ class AccountAPITestCase(ModoAPITestCase): @override_settings(LANGUAGE_CODE="fr") def test_create_account_default_lang(self): """Try to create a new account with(out) language.""" - url = reverse("api:account-list") + url = reverse("v1:account-list") data = dict(self.ACCOUNT_DATA) response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) @@ -337,16 +337,16 @@ class AccountAPITestCase(ModoAPITestCase): data = copy.deepcopy(self.ACCOUNT_DATA) data["domains"] = ["test.com"] data["role"] = "DomainAdmins" - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) - domain = admin_models.Domain.objects.get(name="test.com") + domain = models.Domain.objects.get(name="test.com") admin = core_models.User.objects.get( pk=response.json()["pk"]) self.assertIn(admin, domain.admins) response = self.client.get( - reverse("api:account-detail", args=[admin.pk])) + reverse("v1:account-detail", args=[admin.pk])) self.assertEqual(response.status_code, 200) self.assertIn("domains", response.json()) @@ -359,7 +359,7 @@ class AccountAPITestCase(ModoAPITestCase): """Try to create a new account.""" data = copy.deepcopy(self.ACCOUNT_DATA) del data["mailbox"] - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) account = response.json() @@ -373,7 +373,7 @@ class AccountAPITestCase(ModoAPITestCase): """Check if unicity is respected.""" data = copy.deepcopy(self.ACCOUNT_DATA) data["username"] = "user@test.com" - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) @@ -386,7 +386,7 @@ class AccountAPITestCase(ModoAPITestCase): """Try to create a new account.""" data = copy.deepcopy(self.ACCOUNT_DATA) data["password"] = "toto" - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) errors = response.json() @@ -398,13 +398,13 @@ class AccountAPITestCase(ModoAPITestCase): HTTP_AUTHORIZATION="Token " + self.da_token.key) data = copy.deepcopy(self.ACCOUNT_DATA) data["mailbox"]["quota"] = 1000 - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) data["username"] = "fromapi@test2.com" data["mailbox"].update({"full_address": "fromapi@test2.com", "quota": 10}) - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) errors = response.json() @@ -414,7 +414,7 @@ class AccountAPITestCase(ModoAPITestCase): """Try to create a new account.""" data = copy.deepcopy(self.ACCOUNT_DATA) data["master_user"] = True - url = reverse("api:account-list") + url = reverse("v1:account-list") response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) errors = response.json() @@ -423,7 +423,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_update_account(self): """Try to update an account.""" account = core_models.User.objects.get(username="user@test.com") - url = reverse("api:account-detail", args=[account.pk]) + url = reverse("v1:account-detail", args=[account.pk]) data = { "username": "fromapi@test.com", "role": account.role, @@ -450,7 +450,7 @@ class AccountAPITestCase(ModoAPITestCase): """Try to disable an account.""" account = core_factories.UserFactory( username="reseller", groups=("Resellers", )) - url = reverse("api:account-detail", args=[account.pk]) + url = reverse("v1:account-detail", args=[account.pk]) data = { "username": "reseller", "role": account.role, @@ -463,7 +463,7 @@ class AccountAPITestCase(ModoAPITestCase): """Try to patch an account.""" account = core_models.User.objects.get(username="user@test.com") old_quota = account.mailbox.quota - url = reverse("api:account-detail", args=[account.pk]) + url = reverse("v1:account-detail", args=[account.pk]) data = { "username": "fromapi@test.com", "mailbox": { @@ -479,7 +479,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_update_domain_admin_account(self): """Try to change administered domains.""" account = core_models.User.objects.get(username="admin@test.com") - url = reverse("api:account-detail", args=[account.pk]) + url = reverse("v1:account-detail", args=[account.pk]) data = { "username": account.username, "role": account.role, @@ -492,21 +492,21 @@ class AccountAPITestCase(ModoAPITestCase): } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) - domains = admin_models.Domain.objects.get_for_admin(account) + domains = models.Domain.objects.get_for_admin(account) self.assertEqual(domains.count(), 2) self.assertTrue(domains.filter(name="test2.com").exists()) data["domains"] = ["test2.com"] response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) - domains = admin_models.Domain.objects.get_for_admin(account) + domains = models.Domain.objects.get_for_admin(account) self.assertEqual(domains.count(), 1) self.assertTrue(domains.filter(name="test2.com").exists()) def test_update_account_wrong_address(self): """Try to update an account.""" account = core_models.User.objects.get(username="user@test.com") - url = reverse("api:account-detail", args=[account.pk]) + url = reverse("v1:account-detail", args=[account.pk]) data = { "username": "fromapi@test3.com", "role": account.role, @@ -524,7 +524,7 @@ class AccountAPITestCase(ModoAPITestCase): account = core_models.User.objects.get(username="user@test.com") domadmin = core_models.User.objects.get(username="admin@test.com") self.assertTrue(domadmin.can_access(account)) - url = reverse("api:account-detail", args=[account.pk]) + url = reverse("v1:account-detail", args=[account.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertFalse( @@ -533,7 +533,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_account_exists(self): """Validate /exists/ service.""" - url = reverse("api:account-exists") + url = reverse("v1:account-exists") response = self.client.get( "{}?email={}".format(url, "user@test.com")) self.assertEqual(response.status_code, 200) @@ -551,7 +551,7 @@ class AccountAPITestCase(ModoAPITestCase): """Check the change password service.""" account = core_models.User.objects.get(username="user@test.com") url = reverse( - "api:account-password", args=[account.pk]) + "v1:account-password", args=[account.pk]) response = self.client.put( url, {"password": "toto", "new_password": "pass"}, format="json") @@ -568,7 +568,7 @@ class AccountAPITestCase(ModoAPITestCase): @mock.patch("ovh.Client.get") @mock.patch("ovh.Client.post") def test_reset_password(self, client_post, client_get): - url = reverse("api:account-reset-password") + url = reverse("v1:account-reset-password") # SMS password recovery not enabled response = self.client.post(url) self.assertEqual(response.status_code, 404) @@ -621,7 +621,7 @@ class AliasAPITestCase(ModoAPITestCase): def test_get_aliases(self): """Retrieve a list of aliases.""" - url = reverse("api:alias-list") + url = reverse("v1:alias-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) response = response.json() @@ -635,7 +635,7 @@ class AliasAPITestCase(ModoAPITestCase): def test_get_alias(self): """Retrieve an alias.""" al = models.Alias.objects.get(address="alias@test.com") - url = reverse("api:alias-detail", args=[al.pk]) + url = reverse("v1:alias-detail", args=[al.pk]) response = self.client.get(url) self.assertEqual(response.status_code, 200) response = response.json() @@ -643,7 +643,7 @@ class AliasAPITestCase(ModoAPITestCase): def test_create_alias(self): """Try to create a new alias.""" - url = reverse("api:alias-list") + url = reverse("v1:alias-list") response = self.client.post(url, self.ALIAS_DATA, format="json") self.assertEqual(response.status_code, 201) @@ -670,7 +670,7 @@ class AliasAPITestCase(ModoAPITestCase): """As DomainAdmin, try to create a new alias.""" self.client.credentials( HTTP_AUTHORIZATION="Token " + self.da_token.key) - url = reverse("api:alias-list") + url = reverse("v1:alias-list") response = self.client.post(url, self.ALIAS_DATA, format="json") self.assertEqual(response.status_code, 201) @@ -684,7 +684,7 @@ class AliasAPITestCase(ModoAPITestCase): def test_update_alias(self): """Try to update an alias.""" alias = models.Alias.objects.get(address="alias@test.com") - url = reverse("api:alias-detail", args=[alias.pk]) + url = reverse("v1:alias-detail", args=[alias.pk]) data = { "address": "alias@test.com", "recipients": ["user@test.com", "user@nonlocal.com"] @@ -715,7 +715,7 @@ class AliasAPITestCase(ModoAPITestCase): alias = models.Alias.objects.get(address="alias@test.com") domadmin = core_models.User.objects.get(username="admin@test.com") self.assertTrue(domadmin.can_access(alias)) - url = reverse("api:alias-detail", args=[alias.pk]) + url = reverse("v1:alias-detail", args=[alias.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) self.assertFalse( @@ -750,7 +750,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_list(self): """Retrieve a list of sender addresses.""" - url = reverse("api:sender_address-list") + url = reverse("v1:sender_address-list") response = self.client.get(url) self.assertEqual(response.status_code, 200) response = response.json() @@ -758,7 +758,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_create(self): """Create a new sender addresses.""" - url = reverse("api:sender_address-list") + url = reverse("v1:sender_address-list") mbox = models.Mailbox.objects.get( address="admin", domain__name="test.com") data = {"address": "@extdomain.test", "mailbox": mbox.pk} @@ -785,7 +785,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_patch(self): """Patch an existing sender address.""" - url = reverse("api:sender_address-detail", args=[self.sa1.pk]) + url = reverse("v1:sender_address-detail", args=[self.sa1.pk]) mbox = models.Mailbox.objects.get( address="user", domain__name="test.com") data = {"address": "@extdomain.test"} @@ -797,7 +797,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_update(self): """Update an existing sender address.""" - url = reverse("api:sender_address-detail", args=[self.sa1.pk]) + url = reverse("v1:sender_address-detail", args=[self.sa1.pk]) mbox = models.Mailbox.objects.get( address="user", domain__name="test.com") data = {"address": "@extdomain.test", "mailbox": mbox.pk} @@ -809,7 +809,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_delete(self): """Delete an existing sender address.""" - url = reverse("api:sender_address-detail", args=[self.sa1.pk]) + url = reverse("v1:sender_address-detail", args=[self.sa1.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) with self.assertRaises(models.SenderAddress.DoesNotExist): diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index 2b17dd27b..9ccac780a 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -111,13 +111,11 @@ def mailbox_deleted_handler(sender, **kwargs): alias.delete() models.Quota.objects.filter(username=mb.full_address).delete() request = lib_signals.get_request() - print(request) if request: if not request.localconfig.parameters.get_value( "handle_mailboxes", raise_exception=False): return keepdir = request.POST.get("keepdir", "false") == "true" - print(keepdir) if keepdir: return mb.delete_dir() diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index 3a8364e39..6ee7405fe 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -54,11 +54,13 @@ class AliasManager(models.Manager): def create(self, *args, **kwargs): """Custom create method.""" - creator = kwargs.pop("creator") - recipients = kwargs.pop("recipients") + creator = kwargs.pop("creator", None) + recipients = kwargs.pop("recipients", None) alias = super().create(*args, **kwargs) - alias.post_create(creator) - alias.set_recipients(recipients) + if creator: + alias.post_create(creator) + if recipients: + alias.set_recipients(recipients) return alias @@ -208,8 +210,6 @@ class Alias(AdminObject): raise BadRequest(_("Invalid line: {}").format(row)) self.address = row[1].strip().lower() local_part, self.domain = validate_alias_address(self.address, user) - if Alias.objects.filter(address=self.address, internal=False).exists(): - raise Conflict self.enabled = (row[2].strip().lower() in ["true", "1", "yes", "y"]) self.save() self.set_recipients([raddress.strip() for raddress in row[3:]]) diff --git a/modoboa/core/tests/test_api.py b/modoboa/core/api/v1/tests.py similarity index 91% rename from modoboa/core/tests/test_api.py rename to modoboa/core/api/v1/tests.py index 043222304..c75aafc88 100644 --- a/modoboa/core/tests/test_api.py +++ b/modoboa/core/api/v1/tests.py @@ -4,7 +4,7 @@ from modoboa.lib.tests import ModoTestCase class OpenAPITestCase(ModoTestCase): - openapi_schema_url = reverse_lazy('schema') + openapi_schema_url = reverse_lazy('schema-v1-legacy') def test_unauthorized(self): self.client.logout() diff --git a/modoboa/limits/api/__init__.py b/modoboa/limits/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/limits/api/v1/__init__.py b/modoboa/limits/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/limits/serializers.py b/modoboa/limits/api/v1/serializers.py similarity index 53% rename from modoboa/limits/serializers.py rename to modoboa/limits/api/v1/serializers.py index 38d9200db..2b4aa3d51 100644 --- a/modoboa/limits/serializers.py +++ b/modoboa/limits/api/v1/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers -from . import lib +from ... import lib class ResourcesSerializer(serializers.Serializer): @@ -32,23 +32,3 @@ class ResourcesSerializer(serializers.Serializer): limit.max_value = max_value limit.save(update_fields=["max_value"]) return instance - - -class LimitsGlobalParemetersSerializer(serializers.Serializer): - """A serializer for global parameters.""" - - # Per-admin limits - enable_admin_limits = serializers.BooleanField(default=True) - deflt_user_domain_admins_limit = serializers.IntegerField(default=0) - deflt_user_domains_limit = serializers.IntegerField(default=0) - deflt_user_domain_aliases_limit = serializers.IntegerField(default=0) - deflt_user_mailboxes_limit = serializers.IntegerField(default=0) - deflt_user_mailbox_aliases_limit = serializers.IntegerField(default=0) - deflt_user_quota_limit = serializers.IntegerField(default=0) - - # Per-domain limits - enable_domain_limits = serializers.BooleanField(default=False) - deflt_domain_domain_admins_limit = serializers.IntegerField(default=0) - deflt_domain_domain_aliases_limit = serializers.IntegerField(default=0) - deflt_domain_mailboxes_limit = serializers.IntegerField(default=0) - deflt_domain_mailbox_aliases_limit = serializers.IntegerField(default=0) diff --git a/modoboa/limits/tests/test_api.py b/modoboa/limits/api/v1/tests.py similarity index 94% rename from modoboa/limits/tests/test_api.py rename to modoboa/limits/api/v1/tests.py index 63aa057f2..5d3db87c8 100644 --- a/modoboa/limits/tests/test_api.py +++ b/modoboa/limits/api/v1/tests.py @@ -11,7 +11,7 @@ from modoboa.admin.models import Domain from modoboa.core import factories as core_factories from modoboa.core.models import User from modoboa.lib import permissions, tests as lib_tests -from .. import utils +from ... import utils class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): @@ -39,7 +39,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): HTTP_AUTHORIZATION="Token " + self.r_token.key) limit = self.reseller.userobjectlimit_set.get(name="domain_admins") - url = reverse("api:account-list") + url = reverse("v1:account-list") data = { "username": "fromapi@test.com", "role": "DomainAdmins", @@ -61,7 +61,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): user = User.objects.get(username="user@test.com") domain = Domain.objects.get(name="test.com") domain.add_admin(self.reseller) - url = reverse("api:account-detail", args=[user.pk]) + url = reverse("v1:account-detail", args=[user.pk]) data = { "username": user.username, "role": "DomainAdmins", @@ -82,7 +82,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): quota = self.reseller.userobjectlimit_set.get(name="quota") quota.max_value = 3 quota.save(update_fields=["max_value"]) - url = reverse("api:domain-list") + url = reverse("v1:domain-list") data = {"name": "test3.com", "quota": 1} response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) @@ -102,9 +102,10 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): response.content.decode("utf-8"), '"Domains: limit reached"' ) - self.client.delete( - reverse("api:domain-detail", + resp = self.client.delete( + reverse("v1:domain-detail", args=[Domain.objects.get(name="test4.com").pk])) + self.assertEqual(resp.status_code, 204) data["quota"] = 0 response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) @@ -119,7 +120,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): domain = Domain.objects.get(name="test.com") domain.add_admin(self.reseller) limit = self.reseller.userobjectlimit_set.get(name="domain_aliases") - url = reverse("api:domain_alias-list") + url = reverse("v1:domain_alias-list") data = {"name": "dalias1.com", "target": domain.pk} response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) @@ -140,7 +141,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): HTTP_AUTHORIZATION="Token " + self.da_token.key) limit = self.user.userobjectlimit_set.get(name="mailboxes") - url = reverse("api:account-list") + url = reverse("v1:account-list") data = { "username": "fromapi@test.com", "role": "SimpleUsers", @@ -171,7 +172,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): HTTP_AUTHORIZATION="Token " + self.da_token.key) limit = self.user.userobjectlimit_set.get(name="mailbox_aliases") - url = reverse("api:alias-list") + url = reverse("v1:alias-list") data = { "address": "alias_fromapi@test.com", "recipients": [ @@ -212,7 +213,7 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): domain = Domain.objects.get(name="test.com") limit = domain.domainobjectlimit_set.get(name="mailboxes") self.assertTrue(limit.is_exceeded()) - url = reverse("api:account-list") + url = reverse("v1:account-list") data = { "username": "fromapi@test.com", "role": "SimpleUsers", @@ -229,7 +230,7 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): """Check domain_aliases limit.""" domain = Domain.objects.get(name="test.com") limit = domain.domainobjectlimit_set.get(name="domain_aliases") - url = reverse("api:domain_alias-list") + url = reverse("v1:domain_alias-list") data = {"name": "dalias1.com", "target": domain.pk} response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) @@ -246,7 +247,7 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): domain = Domain.objects.get(name="test.com") limit = domain.domainobjectlimit_set.get(name="mailbox_aliases") self.assertTrue(limit.is_exceeded()) - url = reverse("api:alias-list") + url = reverse("v1:alias-list") data = { "address": "alias_fromapi@test.com", "recipients": [ @@ -278,7 +279,7 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): def test_get_admin_resources(self): """Retrieve admin resources.""" - url = reverse("api:resources-detail", args=[self.user.pk]) + url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.get(url) self.assertEqual(response.status_code, 200) expected = { @@ -305,7 +306,7 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): def test_update_resources(self): """Update resources.""" - url = reverse("api:resources-detail", args=[self.reseller.pk]) + url = reverse("v1:resources-detail", args=[self.reseller.pk]) response = self.client.get(url) resources = response.data resources.update({"domains": 1000, "mailboxes": 1000}) @@ -319,7 +320,7 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): self.client.credentials( HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) resources.update({"domains": 2, "mailboxes": 2}) - url = reverse("api:resources-detail", args=[self.user.pk]) + url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.put(url, resources) self.assertEqual(response.status_code, 404) @@ -328,7 +329,7 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): self.client.credentials( HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) resources.update({"domains": 500, "mailboxes": 500}) - url = reverse("api:resources-detail", args=[self.user.pk]) + url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.put(url, resources) self.assertEqual(response.status_code, 200) self.assertEqual( diff --git a/modoboa/limits/urls_api.py b/modoboa/limits/api/v1/urls.py similarity index 100% rename from modoboa/limits/urls_api.py rename to modoboa/limits/api/v1/urls.py diff --git a/modoboa/limits/viewsets.py b/modoboa/limits/api/v1/viewsets.py similarity index 100% rename from modoboa/limits/viewsets.py rename to modoboa/limits/api/v1/viewsets.py diff --git a/modoboa/limits/api/v2/__init__.py b/modoboa/limits/api/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/limits/api/v2/serializers.py b/modoboa/limits/api/v2/serializers.py new file mode 100644 index 000000000..113eba183 --- /dev/null +++ b/modoboa/limits/api/v2/serializers.py @@ -0,0 +1,23 @@ +"""Limits serializers for API v2.""" + +from rest_framework import serializers + + +class LimitsGlobalParemetersSerializer(serializers.Serializer): + """A serializer for global parameters.""" + + # Per-admin limits + enable_admin_limits = serializers.BooleanField(default=True) + deflt_user_domain_admins_limit = serializers.IntegerField(default=0) + deflt_user_domains_limit = serializers.IntegerField(default=0) + deflt_user_domain_aliases_limit = serializers.IntegerField(default=0) + deflt_user_mailboxes_limit = serializers.IntegerField(default=0) + deflt_user_mailbox_aliases_limit = serializers.IntegerField(default=0) + deflt_user_quota_limit = serializers.IntegerField(default=0) + + # Per-domain limits + enable_domain_limits = serializers.BooleanField(default=False) + deflt_domain_domain_admins_limit = serializers.IntegerField(default=0) + deflt_domain_domain_aliases_limit = serializers.IntegerField(default=0) + deflt_domain_mailboxes_limit = serializers.IntegerField(default=0) + deflt_domain_mailbox_aliases_limit = serializers.IntegerField(default=0) diff --git a/modoboa/limits/apps.py b/modoboa/limits/apps.py index 12238562f..fa419ac58 100644 --- a/modoboa/limits/apps.py +++ b/modoboa/limits/apps.py @@ -8,7 +8,7 @@ def load_limits_settings(): """Load settings.""" from modoboa.parameters import tools as param_tools from . import app_settings - from . import serializers + from .api.v2 import serializers param_tools.registry.add("global", app_settings.ParametersForm, _("Limits")) param_tools.registry.add2( diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index 9e9f58f70..9c87f8494 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -2,7 +2,6 @@ from django.conf import settings from django.db import models -from django.utils.encoding import python_2_unicode_compatible from django.utils.functional import cached_property from django.utils.translation import ugettext as _ @@ -59,7 +58,6 @@ class ObjectLimitMixin(object): return "{}%".format(self.usage) -@python_2_unicode_compatible class UserObjectLimit(ObjectLimitMixin, models.Model): """Object level limit.""" diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index e59002b01..a1d868026 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -1,3 +1,4 @@ +from django.core import exceptions from django.core.files.base import ContentFile from django.urls import reverse @@ -107,7 +108,7 @@ account; user3@{domain}; toto; User; One; True; SimpleUsers; user3@{domain}; 5 f = ContentFile("""alias; alias1@{domain}; True; user@{domain} alias; alias2@{domain}; True; user@{domain} """.format(domain=self.domain), name="aliases.csv") - response = self.client.post( + self.client.post( reverse("admin:identity_import"), { "sourcefile": f } @@ -117,12 +118,12 @@ alias; alias2@{domain}; True; user@{domain} f = ContentFile(""" alias; alias3@{domain}; True; user@{domain} """.format(domain=self.domain), name="aliases.csv") - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) - self.assertContains(response, "Mailbox aliases: limit reached") + with self.assertRaises(exceptions.ValidationError): + self.client.post( + reverse("admin:identity_import"), { + "sourcefile": f + } + ) class UserLimitImportTestCase(LimitImportTestCase): diff --git a/modoboa/relaydomains/api/__init__.py b/modoboa/relaydomains/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/relaydomains/api/v1/__init__.py b/modoboa/relaydomains/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/relaydomains/serializers.py b/modoboa/relaydomains/api/v1/serializers.py similarity index 100% rename from modoboa/relaydomains/serializers.py rename to modoboa/relaydomains/api/v1/serializers.py diff --git a/modoboa/relaydomains/api/v1/tests.py b/modoboa/relaydomains/api/v1/tests.py new file mode 100644 index 000000000..55be375a3 --- /dev/null +++ b/modoboa/relaydomains/api/v1/tests.py @@ -0,0 +1,108 @@ +"""relaydomains API v1 unit tests.""" + +import json + +from django.urls import reverse + +from modoboa.admin import factories as admin_factories, models as admin_models +from modoboa.lib.tests import ModoAPITestCase +from modoboa.transport import factories as tr_factories, models as tr_models + + +class DataMixin(object): + """A mixin to provide test data.""" + + @classmethod + def setUpTestData(cls): # NOQA:N802 + super(DataMixin, cls).setUpTestData() + transport = tr_factories.TransportFactory( + pattern="test.com", _settings={ + "relay_target_host": "external.host.tld", + "relay_target_port": 25, + "relay_verify_recipients": False + } + ) + cls.domain1 = admin_factories.DomainFactory( + name="test.com", type="relaydomain", transport=transport) + transport = tr_factories.TransportFactory( + pattern="domain2.test", _settings={ + "relay_target_host": "external.host.tld", + "relay_target_port": 25, + "relay_verify_recipients": True + } + ) + cls.domain2 = admin_factories.DomainFactory( + name="test2.com", type="relaydomain", transport=transport) + + +class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): + """API test cases.""" + + def test_list(self): + """Test list service.""" + url = reverse("api:relaydomain-list") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 2) + + def test_create(self): + """Test create service.""" + url = reverse("api:relaydomain-list") + settings = { + "relay_target_host": "1.2.3.4" + } + data = { + "name": "test3.com", + "transport": { + "service": "relay", + "_settings": json.dumps(settings) + } + } + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.data["transport"]["_settings"], + ["relay_target_port: This field is required"] + ) + + settings.update({"relay_target_port": 25}) + data["transport"]["_settings"] = json.dumps(settings) + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, 201) + domain = admin_models.Domain.objects.get(name="test3.com") + self.assertEqual( + domain.transport.next_hop, "[{}]:{}".format( + settings["relay_target_host"], settings["relay_target_port"]) + ) + + def test_update(self): + """Test update service.""" + url = reverse("api:relaydomain-detail", args=[self.domain1.pk]) + settings = self.domain1.transport._settings.copy() + settings.update({"relay_target_port": 1000}) + data = { + "name": "test3.com", + "transport": { + "service": "relay", + "_settings": json.dumps(settings) + } + } + response = self.client.put(url, data, format="json") + self.assertEqual(response.status_code, 200) + self.domain1.refresh_from_db() + self.domain1.transport.refresh_from_db() + self.assertEqual(self.domain1.name, data["name"]) + self.assertEqual( + self.domain1.transport.next_hop, "[{}]:{}".format( + settings["relay_target_host"], settings["relay_target_port"]) + ) + + def test_delete(self): + """Test delete service.""" + url = reverse("api:relaydomain-detail", args=[self.domain1.pk]) + response = self.client.delete(url) + self.assertEqual(response.status_code, 204) + with self.assertRaises(admin_models.Domain.DoesNotExist): + self.domain1.refresh_from_db() + self.assertFalse( + tr_models.Transport.objects.filter(pattern="test.com").exists()) diff --git a/modoboa/relaydomains/urls_api.py b/modoboa/relaydomains/api/v1/urls.py similarity index 100% rename from modoboa/relaydomains/urls_api.py rename to modoboa/relaydomains/api/v1/urls.py diff --git a/modoboa/relaydomains/viewsets.py b/modoboa/relaydomains/api/v1/viewsets.py similarity index 100% rename from modoboa/relaydomains/viewsets.py rename to modoboa/relaydomains/api/v1/viewsets.py diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index 2c610814c..dd00901d4 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -1,7 +1,5 @@ """relaydomains unit tests.""" -import json - from django.core.files.base import ContentFile from django.test import TestCase from django.urls import reverse @@ -9,7 +7,7 @@ from django.urls import reverse from modoboa.admin import factories as admin_factories, models as admin_models from modoboa.core.factories import UserFactory from modoboa.lib.test_utils import MapFilesTestCaseMixin -from modoboa.lib.tests import ModoAPITestCase, ModoTestCase +from modoboa.lib.tests import ModoTestCase from modoboa.limits import utils as limits_utils from modoboa.transport import factories as tr_factories, models as tr_models from . import models @@ -363,102 +361,3 @@ class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): "sql-relaydomains.cf", "sql-relay-recipient-verification.cf" ] - - -class DataMixin(object): - """A mixin to provide test data.""" - - @classmethod - def setUpTestData(cls): # NOQA:N802 - super(DataMixin, cls).setUpTestData() - transport = tr_factories.TransportFactory( - pattern="test.com", _settings={ - "relay_target_host": "external.host.tld", - "relay_target_port": 25, - "relay_verify_recipients": False - } - ) - cls.domain1 = admin_factories.DomainFactory( - name="test.com", type="relaydomain", transport=transport) - transport = tr_factories.TransportFactory( - pattern="domain2.test", _settings={ - "relay_target_host": "external.host.tld", - "relay_target_port": 25, - "relay_verify_recipients": True - } - ) - cls.domain2 = admin_factories.DomainFactory( - name="test2.com", type="relaydomain", transport=transport) - - -class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): - """API test cases.""" - - def test_list(self): - """Test list service.""" - url = reverse("api:relaydomain-list") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 2) - - def test_create(self): - """Test create service.""" - url = reverse("api:relaydomain-list") - settings = { - "relay_target_host": "1.2.3.4" - } - data = { - "name": "test3.com", - "transport": { - "service": "relay", - "_settings": json.dumps(settings) - } - } - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.data["transport"]["_settings"], - ["relay_target_port: This field is required"] - ) - - settings.update({"relay_target_port": 25}) - data["transport"]["_settings"] = json.dumps(settings) - response = self.client.post(url, data, format="json") - self.assertEqual(response.status_code, 201) - domain = admin_models.Domain.objects.get(name="test3.com") - self.assertEqual( - domain.transport.next_hop, "[{}]:{}".format( - settings["relay_target_host"], settings["relay_target_port"]) - ) - - def test_update(self): - """Test update service.""" - url = reverse("api:relaydomain-detail", args=[self.domain1.pk]) - settings = self.domain1.transport._settings.copy() - settings.update({"relay_target_port": 1000}) - data = { - "name": "test3.com", - "transport": { - "service": "relay", - "_settings": json.dumps(settings) - } - } - response = self.client.put(url, data, format="json") - self.assertEqual(response.status_code, 200) - self.domain1.refresh_from_db() - self.domain1.transport.refresh_from_db() - self.assertEqual(self.domain1.name, data["name"]) - self.assertEqual( - self.domain1.transport.next_hop, "[{}]:{}".format( - settings["relay_target_host"], settings["relay_target_port"]) - ) - - def test_delete(self): - """Test delete service.""" - url = reverse("api:relaydomain-detail", args=[self.domain1.pk]) - response = self.client.delete(url) - self.assertEqual(response.status_code, 204) - with self.assertRaises(admin_models.Domain.DoesNotExist): - self.domain1.refresh_from_db() - self.assertFalse( - tr_models.Transport.objects.filter(pattern="test.com").exists()) diff --git a/modoboa/urls.py b/modoboa/urls.py index 2a098df62..159d2583e 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -60,6 +60,10 @@ if extra_routes: # API urls urlpatterns += [ + # FIXME: legacy, to remove ASAP + path('docs/openapi.json', + SpectacularJSONAPIView.as_view(api_version="v1"), name='schema-v1-legacy'), + path('api/schema-v1/', SpectacularJSONAPIView.as_view(api_version="v1"), name='schema-v1'), path('api/schema-v1/swagger/', diff --git a/modoboa/urls_api_v1.py b/modoboa/urls_api_v1.py index 0db20c44b..7697ba96b 100644 --- a/modoboa/urls_api_v1.py +++ b/modoboa/urls_api_v1.py @@ -9,8 +9,8 @@ app_name = "api" urlpatterns = [ path('', include("modoboa.core.api.v1.urls")), path('', include("modoboa.admin.api.v1.urls")), - path('', include("modoboa.limits.urls_api")), - path('', include("modoboa.relaydomains.urls_api")) + path('', include("modoboa.limits.api.v1.urls")), + path('', include("modoboa.relaydomains.api.v1.urls")) ] urlpatterns += exts_pool.get_urls(category="api") diff --git a/modoboa/urls_api_v2.py b/modoboa/urls_api_v2.py index 57695e43f..dd890e3ed 100644 --- a/modoboa/urls_api_v2.py +++ b/modoboa/urls_api_v2.py @@ -18,8 +18,8 @@ urlpatterns = [ path('', include("modoboa.core.api.v2.urls")), path('', include("modoboa.admin.api.v2.urls")), path('', include("modoboa.parameters.api.v2.urls")), - path('', include("modoboa.limits.urls_api")), - path('', include("modoboa.relaydomains.urls_api")), + path('', include("modoboa.limits.api.v1.urls")), + path('', include("modoboa.relaydomains.api.v1.urls")), path('', include("modoboa.dnstools.api.v2.urls")), path('', include("modoboa.maillog.api.v2.urls")), ]