Skip to content

Commit

Permalink
Add sendgrid sandbox mode to test suite (#231)
Browse files Browse the repository at this point in the history
* Add coverage html output to git-ignore

* Make logging less verbose but more detailed

* Add sendgrid sandbox mode to test suite

* Make code coverage requirement stricter

* Remove parallelism to reduce build duplication
  • Loading branch information
c-w committed Sep 4, 2019
1 parent cfe33e4 commit 497231a
Show file tree
Hide file tree
Showing 9 changed files with 50 additions and 19 deletions.
1 change: 1 addition & 0 deletions .env
Expand Up @@ -8,6 +8,7 @@ LOKOLE_QUEUE_BROKER_SCHEME=amqp
LOKOLE_EMAIL_SERVER_QUEUES_SAS_NAME=
LOKOLE_EMAIL_SERVER_QUEUES_SAS_KEY=
LOKOLE_EMAIL_SERVER_QUEUES_NAMESPACE=
LOKOLE_SENDGRID_KEY=

CLOUDBROWSER_PORT=10001
AZURITE_PORT=10000
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -9,6 +9,7 @@ __pycache__/
venv*/
.coverage
cover/
htmlcov/
.mypy_cache/
dive.log

Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Expand Up @@ -34,6 +34,8 @@ x-shared-app-build:
build:
context: .
dockerfile: docker/app/Dockerfile
args:
LOKOLE_SENDGRID_KEY: ${LOKOLE_SENDGRID_KEY}

services:

Expand Down
1 change: 1 addition & 0 deletions docker/app/Dockerfile
Expand Up @@ -21,6 +21,7 @@ RUN pip wheel -r requirements.txt -w /deps

COPY . .

ARG LOKOLE_SENDGRID_KEY=""
RUN make ci clean

FROM python:${PYTHON_VERSION}-slim AS runtime
Expand Down
4 changes: 2 additions & 2 deletions makefile
Expand Up @@ -11,7 +11,7 @@ $(PY_ENV)/requirements.txt.out: requirements.txt requirements-dev.txt
venv: $(PY_ENV)/requirements.txt.out

tests: venv
$(PY_ENV)/bin/coverage run -m nose2 && $(PY_ENV)/bin/coverage report
LOKOLE_LOG_LEVEL=CRITICAL $(PY_ENV)/bin/coverage run -m nose2 -v && $(PY_ENV)/bin/coverage report

lint-swagger: venv
find opwen_email_server/swagger -type f -name '*.yaml' | while read file; do \
Expand Down Expand Up @@ -61,7 +61,7 @@ clean:

build:
docker-compose pull --ignore-pull-failures
docker-compose build --parallel
docker-compose build

start:
docker-compose up -d
Expand Down
29 changes: 21 additions & 8 deletions opwen_email_server/services/sendgrid.py
Expand Up @@ -2,13 +2,16 @@
from typing import Callable

from cached_property import cached_property
from python_http_client import BadRequestsError
from requests import post as http_post
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Attachment
from sendgrid.helpers.mail import Content
from sendgrid.helpers.mail import Email
from sendgrid.helpers.mail import Mail
from sendgrid.helpers.mail import MailSettings
from sendgrid.helpers.mail import Personalization
from sendgrid.helpers.mail import SandBoxMode

from opwen_email_server.constants.sendgrid import INBOX_URL
from opwen_email_server.constants.sendgrid import MAILBOX_URL
Expand All @@ -17,23 +20,30 @@


class SendSendgridEmail(LogMixin):
def __init__(self, key: str) -> None:
def __init__(self, key: str, sandbox: bool = False) -> None:
self._key = key
self._sandbox = sandbox

@cached_property
def _client(self) -> Callable[[dict], int]:
def _client(self) -> Callable[[Mail], int]:
if not self._key:

def send_email_fake(email: dict) -> int:
def send_email_fake(email: Mail) -> int:
self.log_warning('No key, not sending email %r', email)
return 202

return send_email_fake

client = SendGridAPIClient(api_key=self._key)

def send_email(email: dict) -> int:
response = client.send(email)
def send_email(email: Mail) -> int:
if self._sandbox:
self.log_warning('Sandbox mode, not delivering email %s', email)
email.mail_settings = MailSettings()
email.mail_settings.sandbox_mode = SandBoxMode()
email.mail_settings.sandbox_mode.enable = True

response = client.send(email.get())
return response.status_code

return send_email
Expand All @@ -45,12 +55,15 @@ def __call__(self, email: dict) -> bool:

def _send_email(self, email: Mail, email_id: str) -> bool:
self.log_debug('about to send email %s', email_id)
request = email.get()
try:
status = self._client(request)
status = self._client(email)
except BadRequestsError as ex:
status = ex.status_code
errors = ex.to_dict.get('errors', [])
self.log_exception(ex, 'error sending email %s:%s:%r', email_id, email, errors)
except Exception as ex:
status = getattr(ex, 'code', -1)
self.log_exception(ex, 'error sending email %s:%r', email_id, request)
self.log_exception(ex, 'error sending email %s:%s', email_id, email)
else:
self.log_debug('sent email %s', email_id)

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -7,6 +7,7 @@ connexion[flask,swagger-ui]==2.3.0
environs==5.2.1
flower==0.9.3
msgpack==0.6.1
python-http-client==3.1.0
pyzmail36==1.0.4
requests==2.22.0
sendgrid==6.0.5
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Expand Up @@ -6,7 +6,7 @@ omit =
opwen_email_server/integration/*

[coverage:report]
fail_under = 90
fail_under = 95

[flake8]
max-line-length = 120
Expand Down
28 changes: 20 additions & 8 deletions tests/opwen_email_server/services/test_sendgrid.py
@@ -1,11 +1,13 @@
from typing import Optional
from unittest import TestCase
from unittest import skipUnless
from unittest.mock import Mock
from unittest.mock import patch
from urllib.error import URLError

from responses import mock as mock_responses

from opwen_email_server.config import SENDGRID_KEY
from opwen_email_server.services.sendgrid import SendSendgridEmail
from opwen_email_server.services.sendgrid import SetupSendgridMailbox

Expand All @@ -17,15 +19,15 @@ class SendgridEmailSenderTests(TestCase):

def test_sends_email(self):
self.assertSendsEmail({
'to': [self.recipient1], 'from': self.sender, 'subject': self.test_sends_email.__name__, 'message':
'to': [self.recipient1], 'from': self.sender, 'subject': self.test_sends_email.__name__, 'body':
'simple email with <b>formatting</b>'
})

def test_sends_email_with_attachments(self):
self.assertSendsEmail({
'to': [self.recipient1], 'from':
self.sender, 'subject':
self.test_sends_email_with_attachments.__name__, 'message':
self.test_sends_email_with_attachments.__name__, 'body':
'simple email with attachments', 'attachments':
[{'filename': 'Some file.txt', 'content': b'first file'},
{'filename': 'Another file.txt', 'content': b'second file'}]
Expand All @@ -34,34 +36,34 @@ def test_sends_email_with_attachments(self):
def test_sends_email_to_multiple_recipients(self):
self.assertSendsEmail({
'to': [self.recipient1, self.recipient2], 'from': self.sender, 'subject':
self.test_sends_email_to_multiple_recipients.__name__, 'message': 'simple email with two recipients'
self.test_sends_email_to_multiple_recipients.__name__, 'body': 'simple email with two recipients'
})

def test_sends_email_to_cc(self):
self.assertSendsEmail({
'to': [self.recipient1], 'cc': [self.recipient2], 'from': self.sender, 'subject':
self.test_sends_email_to_cc.__name__, 'message': 'email with cc'
self.test_sends_email_to_cc.__name__, 'body': 'email with cc'
})

def test_sends_email_to_bcc(self):
self.assertSendsEmail({
'to': [self.recipient1], 'bcc': [self.recipient2], 'from': self.sender, 'subject':
self.test_sends_email_to_bcc.__name__, 'message': 'email with bcc'
self.test_sends_email_to_bcc.__name__, 'body': 'email with bcc'
})

def test_client_made_bad_request(self):
self.assertSendsEmail({'message': self.test_client_made_bad_request.__name__}, success=False, status=400)
self.assertSendsEmail({'body': self.test_client_made_bad_request.__name__}, success=False, status=400)

def test_client_had_exception(self):
self.assertSendsEmail({'message': self.test_client_had_exception.__name__},
self.assertSendsEmail({'body': self.test_client_had_exception.__name__},
success=False,
exception=URLError('sendgrid error'))

def test_does_not_send_email_without_key(self):
action = SendSendgridEmail(key='')

with patch.object(action, 'log_warning') as mock_log_warning:
action({'message': 'message'})
action({'body': 'message'})

self.assertEqual(mock_log_warning.call_count, 1)

Expand Down Expand Up @@ -95,6 +97,16 @@ def raise_exception(*args, **kargs):
mock_response.getcode.return_value = status


@skipUnless(SENDGRID_KEY, 'no sendgrid key configured')
class LiveSendgridEmailSenderTests(SendgridEmailSenderTests):
def assertSendsEmail(self, email: dict, success: bool = True, **kwargs):
send_email = SendSendgridEmail(key=SENDGRID_KEY, sandbox=True)

send_success = send_email(email)

self.assertTrue(send_success if success else not send_success)


class SetupSendgridMailboxTests(TestCase):
def test_does_not_make_request_when_key_is_missing(self):
action = SetupSendgridMailbox(key='')
Expand Down

0 comments on commit 497231a

Please sign in to comment.