diff --git a/docker/integtest/1-register-client.sh b/docker/integtest/1-register-client.sh index 8911a089..46901988 100755 --- a/docker/integtest/1-register-client.sh +++ b/docker/integtest/1-register-client.sh @@ -31,3 +31,22 @@ curl -fs \ -d '{"domain":"developer2.lokole.ca"}' \ "http://nginx:8888/api/email/register/" \ | tee "${out_dir}/register2.json" + +# after creating a client, creating the same one again should fail but we should be able to delete it +curl -fs \ + -H "Content-Type: application/json" \ + -u "${REGISTRATION_CREDENTIALS}" \ + -d '{"domain":"developer3.lokole.ca"}' \ + "http://nginx:8888/api/email/register/" + +if curl -fs \ + -H "Content-Type: application/json" \ + -u "${REGISTRATION_CREDENTIALS}" \ + -d '{"domain":"developer3.lokole.ca"}' \ + "http://nginx:8888/api/email/register/" \ +; then echo "Was able to register a duplicate client" >&2; exit 5; fi + +curl -fs \ + -u "${REGISTRATION_CREDENTIALS}" \ + -X DELETE \ + "http://nginx:8888/api/email/register/developer3.lokole.ca" diff --git a/opwen_email_server/actions.py b/opwen_email_server/actions.py index 2540c41a..4a491a96 100644 --- a/opwen_email_server/actions.py +++ b/opwen_email_server/actions.py @@ -326,7 +326,7 @@ def _action(self, client, **auth_args): # type: ignore self._setup_mailbox(client_id, domain) self._setup_mx_records(domain) self._client_storage.ensure_exists() - self._auth.insert(client_id, domain) + self._auth.insert(client_id, domain, auth_args.get('user')) self.log_event(events.NEW_CLIENT_REGISTERED, {'domain': domain}) # noqa: E501 # yapf: disable return { @@ -337,6 +337,33 @@ def _action(self, client, **auth_args): # type: ignore } +class DeleteClient(_Action): + def __init__(self, auth: AzureAuth, delete_mailbox: Callable[[str, str], None], + delete_mx_records: Callable[[str], None]): + + self._auth = auth + self._delete_mailbox = delete_mailbox + self._delete_mx_records = delete_mx_records + + def _action(self, domain, **auth_args): # type: ignore + if not is_lowercase(domain): + return 'domain must be lowercase', 400 + + client_id = self._auth.client_id_for(domain) + if client_id is None: + return 'client does not exist', 404 + + if not self._auth.is_owner(domain, auth_args.get('user')): + return 'client does not belong to the user', 403 + + self._delete_mailbox(client_id, domain) + self._delete_mx_records(domain) + self._auth.delete(client_id, domain) + + self.log_event(events.CLIENT_DELETED, {'domain': domain}) # noqa: E501 # yapf: disable + return 'OK', 200 + + class CalculatePendingEmailsMetric(_Action): def __init__(self, auth: AzureAuth, pending_factory: Callable[[str], AzureTextStorage]): diff --git a/opwen_email_server/constants/events.py b/opwen_email_server/constants/events.py index 87c1929d..1840bb72 100644 --- a/opwen_email_server/constants/events.py +++ b/opwen_email_server/constants/events.py @@ -1,5 +1,6 @@ from typing_extensions import Final # noqa: F401 +CLIENT_DELETED = 'client_deleted' # type: Final NEW_CLIENT_REGISTERED = 'new_client_registered' # type: Final UNREGISTERED_CLIENT = 'unregistered_client' # type: Final UNKNOWN_USER = 'unknown_user' # type: Final diff --git a/opwen_email_server/constants/sendgrid.py b/opwen_email_server/constants/sendgrid.py index 92c58e69..cc55fe22 100644 --- a/opwen_email_server/constants/sendgrid.py +++ b/opwen_email_server/constants/sendgrid.py @@ -1,6 +1,7 @@ from typing_extensions import Final # noqa: F401 -MAILBOX_URL = 'https://api.sendgrid.com/v3/user/webhooks/parse/settings' # type: Final # noqa: E501 # yapf: disable +MAILBOX_CREATE_URL = 'https://api.sendgrid.com/v3/user/webhooks/parse/settings' # type: Final # noqa: E501 # yapf: disable +MAILBOX_DELETE_URL = 'https://api.sendgrid.com/v3/user/webhooks/parse/settings/{}' # type: Final # noqa: E501 # yapf: disable INBOX_URL = 'https://mailserver.lokole.ca/api/email/sendgrid/{}' # type: Final diff --git a/opwen_email_server/integration/azure.py b/opwen_email_server/integration/azure.py index d662c800..d8e3d232 100644 --- a/opwen_email_server/integration/azure.py +++ b/opwen_email_server/integration/azure.py @@ -4,7 +4,9 @@ from opwen_email_server.constants import azure as constants from opwen_email_server.constants.cache import PENDING_STORAGE_CACHE_SIZE from opwen_email_server.services.auth import AzureAuth +from opwen_email_server.services.dns import DeleteMxRecords from opwen_email_server.services.dns import SetupMxRecords +from opwen_email_server.services.sendgrid import DeleteSendgridMailbox from opwen_email_server.services.sendgrid import SendSendgridEmail from opwen_email_server.services.sendgrid import SetupSendgridMailbox from opwen_email_server.services.storage import AzureFileStorage @@ -60,6 +62,11 @@ def get_mailbox_setup() -> SetupSendgridMailbox: return SetupSendgridMailbox(key=config.SENDGRID_KEY) +@singleton +def get_mailbox_deletion() -> DeleteSendgridMailbox: + return DeleteSendgridMailbox(key=config.SENDGRID_KEY) + + @singleton def get_mx_setup() -> SetupMxRecords: return SetupMxRecords( @@ -69,6 +76,15 @@ def get_mx_setup() -> SetupMxRecords: ) +@singleton +def get_mx_deletion() -> DeleteMxRecords: + return DeleteMxRecords( + account=config.DNS_ACCOUNT, + secret=config.DNS_SECRET, + provider=config.DNS_PROVIDER, + ) + + @singleton def get_email_storage() -> AzureObjectStorage: return AzureObjectStorage( diff --git a/opwen_email_server/integration/connexion.py b/opwen_email_server/integration/connexion.py index e1015ac7..a49e3121 100644 --- a/opwen_email_server/integration/connexion.py +++ b/opwen_email_server/integration/connexion.py @@ -1,5 +1,6 @@ from opwen_email_server import config from opwen_email_server.actions import CalculatePendingEmailsMetric +from opwen_email_server.actions import DeleteClient from opwen_email_server.actions import DownloadClientEmails from opwen_email_server.actions import Ping from opwen_email_server.actions import ReceiveInboundEmail @@ -8,7 +9,9 @@ from opwen_email_server.integration.azure import get_auth from opwen_email_server.integration.azure import get_client_storage from opwen_email_server.integration.azure import get_email_storage +from opwen_email_server.integration.azure import get_mailbox_deletion from opwen_email_server.integration.azure import get_mailbox_setup +from opwen_email_server.integration.azure import get_mx_deletion from opwen_email_server.integration.azure import get_mx_setup from opwen_email_server.integration.azure import get_pending_storage from opwen_email_server.integration.azure import get_raw_email_storage @@ -43,6 +46,12 @@ setup_mx_records=get_mx_setup(), ) +client_delete = DeleteClient( + auth=get_auth(), + delete_mailbox=get_mailbox_deletion(), + delete_mx_records=get_mx_deletion(), +) + metrics_pending = CalculatePendingEmailsMetric( auth=get_auth(), pending_factory=get_pending_storage, diff --git a/opwen_email_server/services/auth.py b/opwen_email_server/services/auth.py index 4a2d4957..c2f9e685 100644 --- a/opwen_email_server/services/auth.py +++ b/opwen_email_server/services/auth.py @@ -1,4 +1,5 @@ from functools import lru_cache +from json import JSONDecodeError from typing import Callable from typing import Dict from typing import Iterable @@ -14,6 +15,8 @@ from opwen_email_server.constants.cache import AUTH_DOMAIN_CACHE_SIZE from opwen_email_server.services.storage import AzureTextStorage from opwen_email_server.utils.log import LogMixin +from opwen_email_server.utils.serialization import from_json +from opwen_email_server.utils.serialization import to_json class AnyOfBasicAuth(LogMixin): @@ -137,18 +140,45 @@ class AzureAuth(LogMixin): def __init__(self, storage: AzureTextStorage) -> None: self._storage = storage - def insert(self, client_id: str, domain: str): + def insert(self, client_id: str, domain: str, owner: str): self._storage.store_text(client_id, domain) - self._storage.store_text(domain, client_id) + self._storage.store_text(domain, to_json({'client_id': client_id, 'owner': owner})) self.log_debug('Registered client %s at domain %s', client_id, domain) + def is_owner(self, domain: str, username: str) -> bool: + try: + raw_auth = self._storage.fetch_text(domain) + except ObjectDoesNotExistError: + self.log_debug('Unrecognized domain %s', domain) + return False + + try: + auth = from_json(raw_auth) + except JSONDecodeError: + # fallback for clients registered before November 2019 + self.log_debug('Unable to lookup owner for domain %s', domain) + return False + + return auth.get('owner') == username + + def delete(self, client_id: str, domain: str) -> bool: + self._storage.delete(domain) + self._storage.delete(client_id) + self._domain_for_cached.cache_clear() + return True + def client_id_for(self, domain: str) -> Optional[str]: try: - client_id = self._storage.fetch_text(domain) + raw_auth = self._storage.fetch_text(domain) except ObjectDoesNotExistError: self.log_debug('Unrecognized domain %s', domain) return None else: + try: + client_id = from_json(raw_auth)['client_id'] + except JSONDecodeError: + # fallback for clients registered before November 2019 + client_id = raw_auth self.log_debug('Domain %s has client %s', domain, client_id) return client_id diff --git a/opwen_email_server/services/dns.py b/opwen_email_server/services/dns.py index 3dc78908..c94e5fc0 100644 --- a/opwen_email_server/services/dns.py +++ b/opwen_email_server/services/dns.py @@ -1,5 +1,6 @@ from cached_property import cached_property from libcloud.dns.base import DNSDriver +from libcloud.dns.base import Zone from libcloud.dns.providers import get_driver from libcloud.dns.types import Provider from libcloud.dns.types import RecordType @@ -8,7 +9,7 @@ from opwen_email_server.utils.log import LogMixin -class SetupMxRecords(LogMixin): +class _MxRecords(LogMixin): def __init__(self, account: str, secret: str, provider: str) -> None: self._account = account self._secret = secret @@ -30,6 +31,23 @@ def __call__(self, domain: str) -> None: zone = next(zone for zone in self._driver.iterate_zones() if zone.domain == zone_name) + self._run(client_name, zone) + + def _run(self, client_name: str, zone: Zone) -> None: + raise NotImplementedError # pragma: no cover + + +class DeleteMxRecords(_MxRecords): + def _run(self, client_name: str, zone: Zone) -> None: + record = next(record for record in self._driver.iterate_records(zone) if record.name == client_name) + + self._driver.delete_record(record) + + self.log_debug('Deleted MX records for client %s.%s', client_name, zone.domain) + + +class SetupMxRecords(_MxRecords): + def _run(self, client_name: str, zone: Zone) -> None: self._driver.create_record( zone=zone, name=client_name, @@ -37,4 +55,4 @@ def __call__(self, domain: str) -> None: data=MX_RECORD, ) - self.log_debug('Set up MX records for %s', domain) + self.log_debug('Set up MX records for client %s.%s', client_name, zone.domain) diff --git a/opwen_email_server/services/sendgrid.py b/opwen_email_server/services/sendgrid.py index 6e563a21..b20f81c2 100644 --- a/opwen_email_server/services/sendgrid.py +++ b/opwen_email_server/services/sendgrid.py @@ -3,6 +3,7 @@ from cached_property import cached_property from python_http_client import BadRequestsError +from requests import delete as http_delete from requests import post as http_post from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Attachment @@ -14,7 +15,8 @@ from sendgrid.helpers.mail import SandBoxMode from opwen_email_server.constants.sendgrid import INBOX_URL -from opwen_email_server.constants.sendgrid import MAILBOX_URL +from opwen_email_server.constants.sendgrid import MAILBOX_CREATE_URL +from opwen_email_server.constants.sendgrid import MAILBOX_DELETE_URL from opwen_email_server.utils.log import LogMixin from opwen_email_server.utils.serialization import to_base64 @@ -119,7 +121,7 @@ def _create_attachment(cls, attachment: dict) -> Attachment: ) -class SetupSendgridMailbox(LogMixin): +class _SendgridManagement(LogMixin): def __init__(self, key: str) -> None: self._key = key @@ -128,8 +130,28 @@ def __call__(self, client_id: str, domain: str) -> None: self.log_warning('No key, skipping mailbox setup for %s', domain) return + self._run(client_id, domain) + + def _run(self, client_id: str, domain: str) -> None: + raise NotImplementedError # pragma: no cover + + +class DeleteSendgridMailbox(_SendgridManagement): + def _run(self, client_id: str, domain: str) -> None: + http_delete( + url=MAILBOX_DELETE_URL.format(domain), + headers={ + 'Authorization': f'Bearer {self._key}', + }, + ).raise_for_status() + + self.log_debug('Deleted mailbox for %s', domain) + + +class SetupSendgridMailbox(_SendgridManagement): + def _run(self, client_id: str, domain: str) -> None: http_post( - url=MAILBOX_URL, + url=MAILBOX_CREATE_URL, json={ 'hostname': domain, 'url': INBOX_URL.format(client_id), diff --git a/opwen_email_server/swagger/client-register.yaml b/opwen_email_server/swagger/client-register.yaml index ae27fd77..4dd33c1e 100644 --- a/opwen_email_server/swagger/client-register.yaml +++ b/opwen_email_server/swagger/client-register.yaml @@ -31,6 +31,24 @@ paths: security: - basic: [] + '/{domain}': + delete: + operationId: opwen_email_server.integration.connexion.client_delete + summary: Endpoint where Lokole clients can be deleted. + parameters: + - $ref: '#/parameters/Domain' + responses: + 200: + description: The client was successfully deleted. + 400: + description: The supplied client is malformed. + 403: + description: The client does not belong to the user. + 404: + description: The supplied client does not exist. + security: + - basic: [] + securityDefinitions: basic: type: basic @@ -46,6 +64,13 @@ parameters: $ref: '#/definitions/RegistrationInfo' required: true + Domain: + name: domain + description: Domain of the Lokole client. + in: path + type: string + required: true + definitions: RegistrationInfo: diff --git a/tests/opwen_email_server/services/test_auth.py b/tests/opwen_email_server/services/test_auth.py index 9f0ec292..efdfd067 100644 --- a/tests/opwen_email_server/services/test_auth.py +++ b/tests/opwen_email_server/services/test_auth.py @@ -187,19 +187,38 @@ def test_with_correct_password(self): class AzureAuthTests(TestCase): def setUp(self): self._folder = mkdtemp() - self._auth = AzureAuth(storage=AzureTextStorage( + self._storage = AzureTextStorage( account=self._folder, key='key', container='auth', provider='LOCAL', - )) + ) + self._auth = AzureAuth(storage=self._storage) def tearDown(self): rmtree(self._folder) def test_inserts_and_retrieves_client(self): - self._auth.insert('client', 'domain') + self._auth.insert('client', 'domain', 'owner') self.assertEqual(self._auth.domain_for('client'), 'domain') self.assertEqual(self._auth.client_id_for('domain'), 'client') self.assertIsNone(self._auth.domain_for('unknown-client')) self.assertIsNone(self._auth.client_id_for('unknown-client')) + self.assertTrue(self._auth.is_owner('domain', 'owner')) + self.assertFalse(self._auth.is_owner('domain', 'unknown-user')) + self.assertFalse(self._auth.is_owner('unknown-domain', 'owner')) + + def test_deletes_client(self): + self._auth.insert('client', 'domain', 'owner') + self.assertIsNotNone(self._auth.domain_for('client')) + self._auth.delete('client', 'domain') + self.assertIsNone(self._auth.domain_for('client')) + + def test_inserts_and_retrieves_client_backwards_compatibility_pre_november_2019(self): + # emulate pre november 2019 version of self._auth.insert + self._storage.store_text('client', 'domain') + self._storage.store_text('domain', 'client') + + self.assertEqual(self._auth.domain_for('client'), 'domain') + self.assertEqual(self._auth.client_id_for('domain'), 'client') + self.assertFalse(self._auth.is_owner('domain', 'owner')) diff --git a/tests/opwen_email_server/services/test_dns.py b/tests/opwen_email_server/services/test_dns.py index 56dd5dca..7d5574a6 100644 --- a/tests/opwen_email_server/services/test_dns.py +++ b/tests/opwen_email_server/services/test_dns.py @@ -2,11 +2,47 @@ from unittest.mock import PropertyMock from unittest.mock import patch +from libcloud.dns.base import Record +from libcloud.dns.types import RecordType from libcloud.dns.base import Zone +from opwen_email_server.services.dns import DeleteMxRecords from opwen_email_server.services.dns import SetupMxRecords +class DeleteMxRecordsTests(TestCase): + def test_does_not_make_request_when_key_is_missing(self): + action = DeleteMxRecords(account='', secret='', provider='CLOUDFLARE') + + with patch.object(action, 'log_warning') as mock_log_warning: + action(domain='') + + self.assertEqual(mock_log_warning.call_count, 1) + + def test_makes_request_when_key_is_set(self): + action = DeleteMxRecords('my-user', 'my-key', provider='CLOUDFLARE') + + with patch.object(action, '_driver', new_callable=PropertyMock) as mock_driver: + zones = [ + Zone(id='1', domain='foo.com', type='master', ttl=1, driver=mock_driver), + Zone(id='2', domain='my-zone', type='master', ttl=1, driver=mock_driver), + ] + + records = [ + Record(id='1', name='bar', type=RecordType.A, data='', zone=zones[1], ttl=1, driver=mock_driver), + Record(id='2', name='my-domain', type=RecordType.MX, data='', zone=zones[1], ttl=1, driver=mock_driver), + ] + + mock_driver.iterate_zones.return_value = zones + mock_driver.iterate_records.return_value = records + + action('my-domain.my-zone') + + self.assertEqual(mock_driver.iterate_zones.call_count, 1) + self.assertEqual(mock_driver.iterate_records.call_count, 1) + self.assertEqual(mock_driver.delete_record.call_count, 1) + + class SetupMxRecordsTests(TestCase): def test_does_not_make_request_when_key_is_missing(self): action = SetupMxRecords(account='', secret='', provider='CLOUDFLARE') diff --git a/tests/opwen_email_server/services/test_sendgrid.py b/tests/opwen_email_server/services/test_sendgrid.py index 85f0ae48..62afa236 100644 --- a/tests/opwen_email_server/services/test_sendgrid.py +++ b/tests/opwen_email_server/services/test_sendgrid.py @@ -8,6 +8,7 @@ from responses import mock as mock_responses from opwen_email_server.config import SENDGRID_KEY +from opwen_email_server.services.sendgrid import DeleteSendgridMailbox from opwen_email_server.services.sendgrid import SendSendgridEmail from opwen_email_server.services.sendgrid import SetupSendgridMailbox @@ -107,6 +108,26 @@ def assertSendsEmail(self, email: dict, success: bool = True, **kwargs): self.assertTrue(send_success if success else not send_success) +class DeleteSendgridMailboxTests(TestCase): + def test_does_not_make_request_when_key_is_missing(self): + action = SetupSendgridMailbox(key='') + + with patch.object(action, 'log_warning') as mock_log_warning: + action(client_id='', domain='') + + self.assertEqual(mock_log_warning.call_count, 1) + + @mock_responses.activate + def test_makes_request_when_key_is_set(self): + mock_responses.add(mock_responses.DELETE, 'https://api.sendgrid.com/v3/user/webhooks/parse/settings/my-domain') + + action = DeleteSendgridMailbox('my-key') + + action('my-client-id', 'my-domain') + + self.assertEqual(len(mock_responses.calls), 1) + + class SetupSendgridMailboxTests(TestCase): def test_does_not_make_request_when_key_is_missing(self): action = SetupSendgridMailbox(key='') diff --git a/tests/opwen_email_server/test_actions.py b/tests/opwen_email_server/test_actions.py index af745ad5..da504cb6 100644 --- a/tests/opwen_email_server/test_actions.py +++ b/tests/opwen_email_server/test_actions.py @@ -37,7 +37,6 @@ def test_200(self): self.assertEqual(status, 200) -# noinspection PyTypeChecker class SendOutboundEmailsTests(TestCase): def setUp(self): self.email_storage = Mock() @@ -78,7 +77,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class StoreInboundEmailsTests(TestCase): def setUp(self): self.raw_email_storage = Mock() @@ -143,7 +141,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class IndexReceivedEmailForMailboxTests(TestCase): def setUp(self): self.email_storage = Mock() @@ -175,7 +172,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class IndexSentEmailForMailboxTests(TestCase): def setUp(self): self.email_storage = Mock() @@ -205,7 +201,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class StoreWrittenClientEmailsTests(TestCase): def setUp(self): self.client_storage = Mock() @@ -256,7 +251,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class ReceiveInboundEmailTests(TestCase): def setUp(self): self.auth = Mock() @@ -321,7 +315,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class DownloadClientEmailsTests(TestCase): def setUp(self): self.auth = Mock() @@ -418,7 +411,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class UploadClientEmailsTests(TestCase): def setUp(self): self.auth = Mock() @@ -459,7 +451,6 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) -# noinspection PyTypeChecker class RegisterClientTests(TestCase): def setUp(self): self.auth = Mock() @@ -470,17 +461,19 @@ def setUp(self): def test_400(self): domain = 'TEST.com' + user = 'user' - _, status = self._execute_action({'domain': domain}) + _, status = self._execute_action({'domain': domain}, user=user) self.assertEqual(status, 400) def test_409(self): domain = 'test.com' + user = 'user' self.auth.client_id_for.return_value = '123' - _, status = self._execute_action({'domain': domain}) + _, status = self._execute_action({'domain': domain}, user=user) self.assertEqual(status, 409) self.auth.client_id_for.assert_called_once_with(domain) @@ -491,6 +484,7 @@ def test_200(self): client_storage_key = 'key' client_storage_container = 'container' domain = 'test.com' + user = 'user' self.client_id_source.return_value = client_id self.auth.client_id_for.return_value = None @@ -500,14 +494,14 @@ def test_200(self): container=client_storage_container, ) - response = self._execute_action({'domain': domain}) + response = self._execute_action({'domain': domain}, user=user) self.assertEqual(response['client_id'], client_id) self.assertEqual(response['storage_account'], client_storage_account) self.assertEqual(response['storage_key'], client_storage_key) self.assertEqual(response['resource_container'], client_storage_container) self.auth.client_id_for.assert_called_once_with(domain) - self.auth.insert.assert_called_with(client_id, domain) + self.auth.insert.assert_called_with(client_id, domain, user) self.assertEqual(self.client_storage.ensure_exists.call_count, 1) self.setup_mailbox.assert_called_once_with(client_id, domain) self.setup_mx_records.assert_called_once_with(domain) @@ -524,6 +518,72 @@ def _execute_action(self, *args, **kwargs): return action(*args, **kwargs) +class DeleteClientTests(TestCase): + def setUp(self): + self.auth = Mock() + self.delete_mailbox = MagicMock() + self.delete_mx_records = MagicMock() + + def test_400(self): + domain = 'TEST.com' + user = 'user' + + _, status = self._execute_action(domain, user=user) + + self.assertEqual(status, 400) + + def test_404(self): + domain = 'test.com' + user = 'user' + + self.auth.client_id_for.return_value = None + + _, status = self._execute_action(domain, user=user) + + self.assertEqual(status, 404) + self.auth.client_id_for.assert_called_once_with(domain) + + def test_403(self): + domain = 'test.com' + user = 'user' + client_id = '187ba644-4d46-49f6-a634-017d7f58e338' + + self.auth.client_id_for.return_value = client_id + self.auth.is_owner.return_value = False + + _, status = self._execute_action(domain, user=user) + + self.assertEqual(status, 403) + self.auth.client_id_for.assert_called_once_with(domain) + self.auth.is_owner.assert_called_once_with(domain, user) + + def test_200(self): + client_id = '187ba644-4d46-49f6-a634-017d7f58e338' + domain = 'test.com' + user = 'user' + + self.auth.client_id_for.return_value = client_id + self.auth.is_owner.return_value = True + + _, status = self._execute_action(domain, user=user) + + self.assertEqual(status, 200) + self.auth.client_id_for.assert_called_once_with(domain) + self.auth.is_owner.assert_called_once_with(domain, user) + self.auth.delete.assert_called_once_with(client_id, domain) + self.delete_mailbox.assert_called_once_with(client_id, domain) + self.delete_mx_records.assert_called_once_with(domain) + + def _execute_action(self, *args, **kwargs): + action = actions.DeleteClient( + auth=self.auth, + delete_mailbox=self.delete_mailbox, + delete_mx_records=self.delete_mx_records, + ) + + return action(*args, **kwargs) + + class CalculatePendingEmailsMetricTests(TestCase): def setUp(self): self.auth = Mock()