diff --git a/docker/app/run-celery.sh b/docker/app/run-celery.sh index 3be1e419..1186e2c8 100755 --- a/docker/app/run-celery.sh +++ b/docker/app/run-celery.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash if [[ "${CELERY_QUEUE_NAMES}" = "all" ]]; then - CELERY_QUEUE_NAMES="inbound,written,send,mailboxreceived,mailboxsent" + CELERY_QUEUE_NAMES="register,inbound,written,send,mailboxreceived,mailboxsent" fi "${PY_ENV}/bin/celery" \ diff --git a/docker/integtest/1-register-client.sh b/docker/integtest/1-register-client.sh index 46901988..a7e44b91 100755 --- a/docker/integtest/1-register-client.sh +++ b/docker/integtest/1-register-client.sh @@ -13,8 +13,12 @@ curl -fs \ -H "Content-Type: application/json" \ -u "${REGISTRATION_CREDENTIALS}" \ -d '{"domain":"developer1.lokole.ca"}' \ - "http://nginx:8888/api/email/register/" \ -| tee "${out_dir}/register1.json" + "http://nginx:8888/api/email/register/" + +while ! curl -fs -u "${REGISTRATION_CREDENTIALS}" "http://nginx:8888/api/email/register/developer1.lokole.ca" | tee "${out_dir}/register1.json"; do + log "Waiting for client 1 registration" + sleep 1s +done # registering a client with bad credentials should fail if curl -fs \ @@ -29,8 +33,12 @@ curl -fs \ -H "Content-Type: application/json" \ -u "${REGISTRATION_CREDENTIALS}" \ -d '{"domain":"developer2.lokole.ca"}' \ - "http://nginx:8888/api/email/register/" \ -| tee "${out_dir}/register2.json" + "http://nginx:8888/api/email/register/" + +while ! curl -fs -u "${REGISTRATION_CREDENTIALS}" "http://nginx:8888/api/email/register/developer2.lokole.ca" | tee "${out_dir}/register2.json"; do + log "Waiting for client 2 registration" + sleep 1s +done # after creating a client, creating the same one again should fail but we should be able to delete it curl -fs \ @@ -39,6 +47,11 @@ curl -fs \ -d '{"domain":"developer3.lokole.ca"}' \ "http://nginx:8888/api/email/register/" +while ! curl -fs -u "${REGISTRATION_CREDENTIALS}" "http://nginx:8888/api/email/register/developer3.lokole.ca"; do + log "Waiting for client 3 registration" + sleep 1s +done + if curl -fs \ -H "Content-Type: application/json" \ -u "${REGISTRATION_CREDENTIALS}" \ diff --git a/opwen_email_server/actions.py b/opwen_email_server/actions.py index bb5c79c3..21f305ae 100644 --- a/opwen_email_server/actions.py +++ b/opwen_email_server/actions.py @@ -313,6 +313,23 @@ def __init__(self, self._setup_mx_records = setup_mx_records self._client_id_source = client_id_source or new_client_id + def _action(self, domain, owner): # type: ignore + client_id = self._client_id_source() + + self._setup_mailbox(client_id, domain) + self._setup_mx_records(domain) + self._client_storage.ensure_exists() + self._auth.insert(client_id, domain, owner) + + self.log_event(events.NEW_CLIENT_REGISTERED, {'domain': domain}) # noqa: E501 # yapf: disable + return 'OK', 200 + + +class CreateClient(_Action): + def __init__(self, auth: AzureAuth, task: Callable[[str, str], None]): + self._auth = auth + self._task = task + def _action(self, client, **auth_args): # type: ignore domain = client['domain'] if not is_lowercase(domain): @@ -320,15 +337,31 @@ def _action(self, client, **auth_args): # type: ignore if self._auth.client_id_for(domain) is not None: return 'client already exists', 409 - client_id = self._client_id_source() - access_info = self._client_storage.access_info() + self._task(domain, auth_args.get('user')) - self._setup_mailbox(client_id, domain) - self._setup_mx_records(domain) - self._client_storage.ensure_exists() - self._auth.insert(client_id, domain, auth_args.get('user')) + self.log_event(events.CLIENT_CREATED, {'domain': domain}) # noqa: E501 # yapf: disable + return 'accepted', 201 - self.log_event(events.NEW_CLIENT_REGISTERED, {'domain': domain}) # noqa: E501 # yapf: disable + +class GetClient(_Action): + def __init__(self, auth: AzureAuth, client_storage: AzureObjectsStorage): + self._auth = auth + self._client_storage = client_storage + + 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 + + access_info = self._client_storage.access_info() + + self.log_event(events.CLIENT_FETCHED, {'domain': domain}) # noqa: E501 # yapf: disable return { 'client_id': client_id, 'storage_account': access_info.account, diff --git a/opwen_email_server/constants/cache.py b/opwen_email_server/constants/cache.py index 3a258c9e..4365b901 100644 --- a/opwen_email_server/constants/cache.py +++ b/opwen_email_server/constants/cache.py @@ -1,4 +1,3 @@ from typing_extensions import Final # noqa: F401 -AUTH_DOMAIN_CACHE_SIZE = 128 # type: Final PENDING_STORAGE_CACHE_SIZE = 128 # type: Final diff --git a/opwen_email_server/constants/events.py b/opwen_email_server/constants/events.py index 1840bb72..cc336eb4 100644 --- a/opwen_email_server/constants/events.py +++ b/opwen_email_server/constants/events.py @@ -1,6 +1,8 @@ from typing_extensions import Final # noqa: F401 CLIENT_DELETED = 'client_deleted' # type: Final +CLIENT_FETCHED = 'client_fetched' # type: Final +CLIENT_CREATED = 'client_created' # 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/queues.py b/opwen_email_server/constants/queues.py index b92014a0..ddc55dee 100644 --- a/opwen_email_server/constants/queues.py +++ b/opwen_email_server/constants/queues.py @@ -1,5 +1,6 @@ from typing_extensions import Final # noqa: F401 +REGISTER_CLIENT_QUEUE = 'register' # type: Final INBOUND_STORE_QUEUE = 'inbound' # type: Final WRITTEN_STORE_QUEUE = 'written' # type: Final SEND_QUEUE = 'send' # type: Final diff --git a/opwen_email_server/constants/sendgrid.py b/opwen_email_server/constants/sendgrid.py index cc55fe22..1f292ff3 100644 --- a/opwen_email_server/constants/sendgrid.py +++ b/opwen_email_server/constants/sendgrid.py @@ -1,7 +1,7 @@ from typing_extensions import Final # noqa: F401 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 +MAILBOX_DETAIL_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/celery.py b/opwen_email_server/integration/celery.py index 1b054c3d..86616d0d 100644 --- a/opwen_email_server/integration/celery.py +++ b/opwen_email_server/integration/celery.py @@ -2,6 +2,7 @@ from opwen_email_server.actions import IndexReceivedEmailForMailbox from opwen_email_server.actions import IndexSentEmailForMailbox +from opwen_email_server.actions import RegisterClient from opwen_email_server.actions import SendOutboundEmails from opwen_email_server.actions import StoreInboundEmails from opwen_email_server.actions import StoreWrittenClientEmails @@ -9,18 +10,34 @@ from opwen_email_server.constants.queues import INBOUND_STORE_QUEUE from opwen_email_server.constants.queues import MAILBOX_RECEIVED_QUEUE from opwen_email_server.constants.queues import MAILBOX_SENT_QUEUE +from opwen_email_server.constants.queues import REGISTER_CLIENT_QUEUE from opwen_email_server.constants.queues import SEND_QUEUE from opwen_email_server.constants.queues import WRITTEN_STORE_QUEUE +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_sender from opwen_email_server.integration.azure import get_email_storage +from opwen_email_server.integration.azure import get_mailbox_setup from opwen_email_server.integration.azure import get_mailbox_storage +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 celery = Celery(broker=QUEUE_BROKER) +@celery.task(ignore_result=True) +def register_client(domain: str, owner: str) -> None: + action = RegisterClient( + auth=get_auth(), + client_storage=get_client_storage(), + setup_mailbox=get_mailbox_setup(), + setup_mx_records=get_mx_setup(), + ) + + action(domain, owner) + + @celery.task(ignore_result=True) def index_received_email_for_mailbox(resource_id: str) -> None: action = IndexReceivedEmailForMailbox( @@ -85,6 +102,7 @@ def _fqn(task): celery.conf.update( task_routes={ + _fqn(register_client): {'queue': REGISTER_CLIENT_QUEUE}, _fqn(index_received_email_for_mailbox): {'queue': MAILBOX_RECEIVED_QUEUE}, _fqn(index_sent_email_for_mailbox): {'queue': MAILBOX_SENT_QUEUE}, _fqn(inbound_store): {'queue': INBOUND_STORE_QUEUE}, diff --git a/opwen_email_server/integration/connexion.py b/opwen_email_server/integration/connexion.py index a49e3121..ecce555b 100644 --- a/opwen_email_server/integration/connexion.py +++ b/opwen_email_server/integration/connexion.py @@ -1,21 +1,21 @@ from opwen_email_server import config from opwen_email_server.actions import CalculatePendingEmailsMetric +from opwen_email_server.actions import CreateClient from opwen_email_server.actions import DeleteClient from opwen_email_server.actions import DownloadClientEmails +from opwen_email_server.actions import GetClient from opwen_email_server.actions import Ping from opwen_email_server.actions import ReceiveInboundEmail -from opwen_email_server.actions import RegisterClient from opwen_email_server.actions import UploadClientEmails 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 from opwen_email_server.integration.celery import inbound_store +from opwen_email_server.integration.celery import register_client from opwen_email_server.integration.celery import written_store from opwen_email_server.services.auth import AnyOfBasicAuth from opwen_email_server.services.auth import BasicAuth @@ -39,11 +39,14 @@ pending_factory=get_pending_storage, ) -client_register = RegisterClient( +client_create = CreateClient( + auth=get_auth(), + task=register_client.delay, +) + +client_get = GetClient( auth=get_auth(), client_storage=get_client_storage(), - setup_mailbox=get_mailbox_setup(), - setup_mx_records=get_mx_setup(), ) client_delete = DeleteClient( diff --git a/opwen_email_server/services/auth.py b/opwen_email_server/services/auth.py index c2f9e685..40974eab 100644 --- a/opwen_email_server/services/auth.py +++ b/opwen_email_server/services/auth.py @@ -1,4 +1,3 @@ -from functools import lru_cache from json import JSONDecodeError from typing import Callable from typing import Dict @@ -12,7 +11,6 @@ from opwen_email_server.constants import events from opwen_email_server.constants import github -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 @@ -164,7 +162,6 @@ def is_owner(self, domain: str, username: str) -> bool: 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]: @@ -184,14 +181,10 @@ def client_id_for(self, domain: str) -> Optional[str]: def domain_for(self, client_id: str) -> Optional[str]: try: - domain = self._domain_for_cached(client_id) + domain = self._storage.fetch_text(client_id) except ObjectDoesNotExistError: self.log_debug('Unrecognized client %s', client_id) return None else: self.log_debug('Client %s has domain %s', client_id, domain) return domain - - @lru_cache(maxsize=AUTH_DOMAIN_CACHE_SIZE) - def _domain_for_cached(self, client_id: str) -> str: - return self._storage.fetch_text(client_id) diff --git a/opwen_email_server/services/dns.py b/opwen_email_server/services/dns.py index c94e5fc0..f5d97767 100644 --- a/opwen_email_server/services/dns.py +++ b/opwen_email_server/services/dns.py @@ -1,4 +1,5 @@ from cached_property import cached_property +from libcloud.common.types import LibcloudError from libcloud.dns.base import DNSDriver from libcloud.dns.base import Zone from libcloud.dns.providers import get_driver @@ -48,11 +49,14 @@ def _run(self, client_name: str, zone: Zone) -> None: class SetupMxRecords(_MxRecords): def _run(self, client_name: str, zone: Zone) -> None: - self._driver.create_record( - zone=zone, - name=client_name, - type=RecordType.MX, - data=MX_RECORD, - ) - - self.log_debug('Set up MX records for client %s.%s', client_name, zone.domain) + try: + self._driver.create_record( + zone=zone, + name=client_name, + type=RecordType.MX, + data=MX_RECORD, + ) + except LibcloudError: + self.log_debug('MX records for client %s.%s already exist', client_name, zone.domain) + else: + 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 b20f81c2..0e2cb844 100644 --- a/opwen_email_server/services/sendgrid.py +++ b/opwen_email_server/services/sendgrid.py @@ -1,9 +1,12 @@ +from itertools import count from mimetypes import guess_type +from time import sleep from typing import Callable from cached_property import cached_property from python_http_client import BadRequestsError from requests import delete as http_delete +from requests import get as http_get from requests import post as http_post from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Attachment @@ -16,7 +19,7 @@ from opwen_email_server.constants.sendgrid import INBOX_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.constants.sendgrid import MAILBOX_DETAIL_URL from opwen_email_server.utils.log import LogMixin from opwen_email_server.utils.serialization import to_base64 @@ -139,7 +142,7 @@ def _run(self, client_id: str, domain: str) -> None: class DeleteSendgridMailbox(_SendgridManagement): def _run(self, client_id: str, domain: str) -> None: http_delete( - url=MAILBOX_DELETE_URL.format(domain), + url=MAILBOX_DETAIL_URL.format(domain), headers={ 'Authorization': f'Bearer {self._key}', }, @@ -149,18 +152,43 @@ def _run(self, client_id: str, domain: str) -> None: class SetupSendgridMailbox(_SendgridManagement): - def _run(self, client_id: str, domain: str) -> None: - http_post( - url=MAILBOX_CREATE_URL, - json={ - 'hostname': domain, - 'url': INBOX_URL.format(client_id), - 'spam_check': True, - 'send_raw': True, - }, - headers={ - 'Authorization': f'Bearer {self._key}', - }, - ).raise_for_status() + def __init__(self, key: str, max_retries: int = 10, retry_interval_seconds: float = 1): + super().__init__(key) + self._max_retries = max_retries + self._retry_interval_seconds = retry_interval_seconds - self.log_debug('Set up mailbox for %s', domain) + def _run(self, client_id: str, domain: str) -> None: + for retry in count(): + get_response = http_get( + url=MAILBOX_DETAIL_URL.format(domain), + headers={ + 'Authorization': f'Bearer {self._key}', + }, + ) + + if get_response.ok: + self.log_debug('Mailbox %s already exists', domain) + break + + create_response = http_post( + url=MAILBOX_CREATE_URL, + json={ + 'hostname': domain, + 'url': INBOX_URL.format(client_id), + 'spam_check': True, + 'send_raw': True, + }, + headers={ + 'Authorization': f'Bearer {self._key}', + }, + ) + + if create_response.ok: + self.log_debug('Set up mailbox for %s', domain) + break + + if retry > self._max_retries: + self.log_debug('Too many attempts to set up mailbox for %s', domain) + create_response.raise_for_status() + + sleep(self._retry_interval_seconds) diff --git a/opwen_email_server/swagger/client-register.yaml b/opwen_email_server/swagger/client-register.yaml index 4dd33c1e..8c49c54d 100644 --- a/opwen_email_server/swagger/client-register.yaml +++ b/opwen_email_server/swagger/client-register.yaml @@ -11,27 +11,45 @@ paths: '/': post: - operationId: opwen_email_server.integration.connexion.client_register + operationId: opwen_email_server.integration.connexion.client_create summary: Endpoint where Lokole clients register themselves. consumes: - application/json + parameters: + - $ref: '#/parameters/Client' + responses: + 201: + description: The client registration has been accepted. + 400: + description: The supplied client is malformed. + 409: + description: The supplied client already exists. + security: + - basic: [] + + '/{domain}': + + get: + operationId: opwen_email_server.integration.connexion.client_get + summary: Endpoint where Lokole clients can be looked up. produces: - application/json parameters: - - $ref: '#/parameters/Client' + - $ref: '#/parameters/Domain' responses: 200: - description: The client was successfully registered. + description: Information about the client. schema: $ref: '#/definitions/RegisteredClient' 400: description: The supplied client is malformed. - 409: - description: The supplied client already exists. + 403: + description: The client does not belong to the user. + 404: + description: The supplied client does not exist. security: - basic: [] - '/{domain}': delete: operationId: opwen_email_server.integration.connexion.client_delete summary: Endpoint where Lokole clients can be deleted. diff --git a/tests/opwen_email_server/helpers.py b/tests/opwen_email_server/helpers.py new file mode 100644 index 00000000..16b24716 --- /dev/null +++ b/tests/opwen_email_server/helpers.py @@ -0,0 +1,24 @@ +class MockResponses: + def __init__(self, responses): + self._i = 0 + self._responses = responses + + def __call__(self, *args, **kwargs): + try: + status, headers, body = self._responses[self._i] + except ValueError: + body = self._responses[self._i] + status = 200 + headers = {} + + self._i += 1 + + return status, headers, body + + +def throw(exception): + # noinspection PyUnusedLocal + def throws(*args, **kwargs): + raise exception + + return throws diff --git a/tests/opwen_email_server/services/test_auth.py b/tests/opwen_email_server/services/test_auth.py index efdfd067..aec5f75e 100644 --- a/tests/opwen_email_server/services/test_auth.py +++ b/tests/opwen_email_server/services/test_auth.py @@ -11,24 +11,7 @@ from opwen_email_server.services.auth import BasicAuth from opwen_email_server.services.auth import GithubBasicAuth from opwen_email_server.services.storage import AzureTextStorage - - -class _MockResponses: - def __init__(self, responses): - self._i = 0 - self._responses = responses - - def __call__(self, *args, **kwargs): - try: - status, headers, body = self._responses[self._i] - except ValueError: - body = self._responses[self._i] - status = 200 - headers = {} - - self._i += 1 - - return status, headers, body +from tests.opwen_email_server.helpers import MockResponses class AnyOfBasicAuthTests(TestCase): @@ -143,7 +126,7 @@ def test_with_correct_password(self): mock_responses.add_callback( mock_responses.POST, github.GRAPHQL_URL, - callback=_MockResponses([ + callback=MockResponses([ '''{ "data": { "organization": { diff --git a/tests/opwen_email_server/services/test_dns.py b/tests/opwen_email_server/services/test_dns.py index 7d5574a6..97aab2b7 100644 --- a/tests/opwen_email_server/services/test_dns.py +++ b/tests/opwen_email_server/services/test_dns.py @@ -2,12 +2,14 @@ from unittest.mock import PropertyMock from unittest.mock import patch +from libcloud.common.types import LibcloudError 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 +from tests.opwen_email_server.helpers import throw class DeleteMxRecordsTests(TestCase): @@ -65,3 +67,18 @@ def test_makes_request_when_key_is_set(self): self.assertEqual(mock_driver.iterate_zones.call_count, 1) self.assertEqual(mock_driver.create_record.call_count, 1) + + def test_returns_when_record_already_exists(self): + action = SetupMxRecords('my-user', 'my-key', provider='CLOUDFLARE') + + with patch.object(action, '_driver', new_callable=PropertyMock) as mock_driver: + mock_driver.iterate_zones.return_value = [ + 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), + ] + mock_driver.create_record.side_effect = throw(LibcloudError(None, mock_driver)) + + action('my-domain.my-zone') + + self.assertEqual(mock_driver.iterate_zones.call_count, 1) + self.assertEqual(mock_driver.create_record.call_count, 1) diff --git a/tests/opwen_email_server/services/test_sendgrid.py b/tests/opwen_email_server/services/test_sendgrid.py index 62afa236..0468cc9b 100644 --- a/tests/opwen_email_server/services/test_sendgrid.py +++ b/tests/opwen_email_server/services/test_sendgrid.py @@ -11,6 +11,8 @@ 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 tests.opwen_email_server.helpers import MockResponses +from tests.opwen_email_server.helpers import throw class SendgridEmailSenderTests(TestCase): @@ -84,15 +86,11 @@ def assertSendsEmail(self, @classmethod def given_response_status(cls, mock_build_opener, status, exception): - # noinspection PyUnusedLocal - def raise_exception(*args, **kargs): - raise exception - mock_opener = Mock() mock_response = Mock() mock_build_opener.return_value = mock_opener if exception: - mock_opener.open.side_effect = raise_exception + mock_opener.open.side_effect = throw(exception) else: mock_opener.open.return_value = mock_response mock_response.getcode.return_value = status @@ -139,11 +137,55 @@ def test_does_not_make_request_when_key_is_missing(self): @mock_responses.activate def test_makes_request_when_key_is_set(self): - mock_responses.add(mock_responses.POST, 'https://api.sendgrid.com/v3/user/webhooks/parse/settings') + mock_responses.add(mock_responses.GET, + 'https://api.sendgrid.com/v3/user/webhooks/parse/settings/my-domain', + status=404) + mock_responses.add(mock_responses.POST, 'https://api.sendgrid.com/v3/user/webhooks/parse/settings', status=200) + + action = SetupSendgridMailbox('my-key') + + action('my-client-id', 'my-domain') + + self.assertEqual(len(mock_responses.calls), 2) + self.assertIn(b'"hostname": "my-domain"', mock_responses.calls[1].request.body) + + @mock_responses.activate + def test_skips_request_when_domain_already_exists(self): + mock_responses.add(mock_responses.GET, + 'https://api.sendgrid.com/v3/user/webhooks/parse/settings/my-domain', + status=200) action = SetupSendgridMailbox('my-key') action('my-client-id', 'my-domain') self.assertEqual(len(mock_responses.calls), 1) - self.assertIn(b'"hostname": "my-domain"', mock_responses.calls[0].request.body) + + @mock_responses.activate + def test_retries_request_when_creation_failed(self): + mock_responses.add(mock_responses.GET, + 'https://api.sendgrid.com/v3/user/webhooks/parse/settings/my-domain', + status=404) + mock_responses.add_callback(mock_responses.POST, + 'https://api.sendgrid.com/v3/user/webhooks/parse/settings', + callback=MockResponses([(500, '', ''), (200, '', '')])) + + action = SetupSendgridMailbox('my-key', max_retries=3, retry_interval_seconds=0.001) + + action('my-client-id', 'my-domain') + + self.assertEqual(len(mock_responses.calls), 4) + + @mock_responses.activate + def test_fails_request_when_retry_limit_is_exceeded(self): + mock_responses.add(mock_responses.GET, + 'https://api.sendgrid.com/v3/user/webhooks/parse/settings/my-domain', + status=404) + mock_responses.add_callback(mock_responses.POST, + 'https://api.sendgrid.com/v3/user/webhooks/parse/settings', + callback=MockResponses([(500, '', '') for _ in range(5)])) + + action = SetupSendgridMailbox('my-key', max_retries=3, retry_interval_seconds=0.001) + + with self.assertRaises(Exception): + action('my-client-id', 'my-domain') diff --git a/tests/opwen_email_server/services/test_storage.py b/tests/opwen_email_server/services/test_storage.py index 8ef4ad5a..92b08968 100644 --- a/tests/opwen_email_server/services/test_storage.py +++ b/tests/opwen_email_server/services/test_storage.py @@ -26,6 +26,7 @@ from opwen_email_server.utils.serialization import to_jsonl_bytes from opwen_email_server.utils.temporary import create_tempfilename from opwen_email_server.utils.temporary import removing +from tests.opwen_email_server.helpers import throw class AzureTextStorageTests(TestCase): @@ -66,12 +67,8 @@ def get_container(*args, **kwargs): return container - # noinspection PyUnusedLocal - def create_container(*args, **kwargs): - raise ContainerAlreadyExistsError(None, driver, self._container) - driver.get_container.side_effect = get_container - driver.create_container.side_effect = create_container + driver.create_container.side_effect = throw(ContainerAlreadyExistsError(None, driver, self._container)) self.assertIs(self._storage._client, container) diff --git a/tests/opwen_email_server/test_actions.py b/tests/opwen_email_server/test_actions.py index da504cb6..1802f99a 100644 --- a/tests/opwen_email_server/test_actions.py +++ b/tests/opwen_email_server/test_actions.py @@ -12,6 +12,7 @@ from opwen_email_server.services.storage import AccessInfo from opwen_email_server.utils.serialization import from_jsonl_bytes from opwen_email_server.utils.serialization import to_jsonl_bytes +from tests.opwen_email_server.helpers import throw class ActionTests(TestCase): @@ -87,13 +88,9 @@ def setUp(self): self.next_task = MagicMock() def test_202(self): - # noinspection PyUnusedLocal - def throw(*args, **kwargs): - raise ObjectDoesNotExistError(None, None, None) - resource_id = 'eb93fde9-0cc6-4339-b7d6-f6e838e78f1c' - self.raw_email_storage.fetch_text.side_effect = throw + self.raw_email_storage.fetch_text.side_effect = throw(ObjectDoesNotExistError(None, None, None)) _, status = self._execute_action(resource_id) @@ -459,6 +456,47 @@ def setUp(self): self.setup_mx_records = MagicMock() self.client_id_source = MagicMock() + def test_200(self): + client_id = '187ba644-4d46-49f6-a634-017d7f58e338' + client_storage_account = 'account' + client_storage_key = 'key' + client_storage_container = 'container' + domain = 'test.com' + user = 'user' + + self.client_id_source.return_value = client_id + self.client_storage.access_info.return_value = AccessInfo( + account=client_storage_account, + key=client_storage_key, + container=client_storage_container, + ) + + _, status = self._execute_action(domain, user) + + self.assertEqual(status, 200) + self.client_id_source.assert_called_once_with() + 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) + + def _execute_action(self, *args, **kwargs): + action = actions.RegisterClient( + auth=self.auth, + client_storage=self.client_storage, + setup_mailbox=self.setup_mailbox, + setup_mx_records=self.setup_mx_records, + client_id_source=self.client_id_source, + ) + + return action(*args, **kwargs) + + +class CreateClientTests(TestCase): + def setUp(self): + self.auth = Mock() + self.task = MagicMock() + def test_400(self): domain = 'TEST.com' user = 'user' @@ -478,6 +516,66 @@ def test_409(self): self.assertEqual(status, 409) self.auth.client_id_for.assert_called_once_with(domain) + def test_200(self): + domain = 'test.com' + user = 'user' + + self.auth.client_id_for.return_value = None + + _, status = self._execute_action({'domain': domain}, user=user) + + self.assertEqual(status, 201) + self.auth.client_id_for.assert_called_once_with(domain) + self.task.assert_called_once_with(domain, user) + + def _execute_action(self, *args, **kwargs): + action = actions.CreateClient( + auth=self.auth, + task=self.task, + ) + + return action(*args, **kwargs) + + +class GetClientTests(TestCase): + def setUp(self): + self.auth = Mock() + self.client_storage = Mock() + self.setup_mailbox = MagicMock() + self.setup_mx_records = MagicMock() + self.client_id_source = 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) + def test_200(self): client_id = '187ba644-4d46-49f6-a634-017d7f58e338' client_storage_account = 'account' @@ -486,33 +584,25 @@ def test_200(self): domain = 'test.com' user = 'user' - self.client_id_source.return_value = client_id - self.auth.client_id_for.return_value = None + self.auth.client_id_for.return_value = client_id self.client_storage.access_info.return_value = AccessInfo( account=client_storage_account, key=client_storage_key, container=client_storage_container, ) - response = self._execute_action({'domain': domain}, user=user) + response = self._execute_action(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, 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) def _execute_action(self, *args, **kwargs): - action = actions.RegisterClient( + action = actions.GetClient( auth=self.auth, client_storage=self.client_storage, - setup_mailbox=self.setup_mailbox, - setup_mx_records=self.setup_mx_records, - client_id_source=self.client_id_source, ) return action(*args, **kwargs) diff --git a/tests/opwen_email_server/utils/test_email_parser.py b/tests/opwen_email_server/utils/test_email_parser.py index 06aaf3fc..aaec86bd 100644 --- a/tests/opwen_email_server/utils/test_email_parser.py +++ b/tests/opwen_email_server/utils/test_email_parser.py @@ -9,6 +9,7 @@ from responses import mock as mock_responses from opwen_email_server.utils import email_parser +from tests.opwen_email_server.helpers import throw TEST_DATA_DIRECTORY = abspath( join(dirname(__file__), '..', '..', 'files', 'opwen_email_server', 'utils', 'test_email_parser')) @@ -141,10 +142,7 @@ def test_format_inline_images_with_img_tag(self): @mock_responses.activate @patch.object(email_parser, 'Image') def test_handles_exceptions_when_processing_image(self, mock_pil): - def throw(): - raise IOError() - - mock_pil.open.side_effect = throw + mock_pil.open.side_effect = throw(IOError()) handled_errors = [] def on_error(*args):