Skip to content

[ENG-8172] Improve email testing #11170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: refactor-notifications
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,36 @@ jobs:
- name: Upload report
if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed
uses: ./.github/actions/gen-report

mailhog:
runs-on: ubuntu-22.04
permissions:
checks: write
needs: build-cache
services:
postgres:
image: postgres

env:
POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
mailhog:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v2
- uses: ./.github/actions/start-build
- name: Run tests
run: poetry run python3 -m invoke test-ci-mailhog -n 1 --junit
- name: Upload report
if: (github.event_name != 'pull_request') && (success() || failure()) # run this step even if previous step failed
uses: ./.github/actions/gen-report
83 changes: 83 additions & 0 deletions api_tests/mailhog/test_mailhog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import requests
import pytest
from website.mails import send_mail, TEST
from waffle.testutils import override_switch
from osf import features
from website import settings
from osf_tests.factories import (
fake_email,
AuthUserFactory,
)
from tests.base import (
capture_signals,
fake
)
from framework import auth
from unittest import mock
from osf.models import OSFUser
from tests.base import (
OsfTestCase,
)
from website.util import api_url_for
from conftest import start_mock_send_grid


@pytest.mark.django_db
@pytest.mark.usefixtures('mock_send_grid')
class TestMailHog:

def test_mailhog_recived_mail(self, mock_send_grid):
with override_switch(features.ENABLE_MAILHOG, active=True):
mailhog_v1 = f'{settings.MAILHOG_API_HOST}/api/v1/messages'
mailhog_v2 = f'{settings.MAILHOG_API_HOST}/api/v2/messages'
requests.delete(mailhog_v1)

send_mail('[email protected]', TEST, name='Mailhog')
res = requests.get(mailhog_v2).json()
assert res['count'] == 1
assert res['items'][0]['Content']['Headers']['To'][0] == '[email protected]'
assert res['items'][0]['Content']['Headers']['Subject'][0] == 'A test email to Mailhog'
mock_send_grid.assert_called()
requests.delete(mailhog_v1)


@pytest.mark.django_db
@mock.patch('website.mails.settings.USE_EMAIL', True)
@mock.patch('website.mails.settings.USE_CELERY', False)
class TestAuthMailhog(OsfTestCase):

def setUp(self):
super().setUp()
self.user = AuthUserFactory()
self.auth = self.user.auth

self.mock_send_grid = start_mock_send_grid(self)

def test_recived_confirmation(self):
url = api_url_for('register_user')
name, email, password = fake.name(), fake_email(), 'underpressure'
mailhog_v1 = f'{settings.MAILHOG_API_HOST}/api/v1/messages'
mailhog_v2 = f'{settings.MAILHOG_API_HOST}/api/v2/messages'
requests.delete(mailhog_v1)

with override_switch(features.ENABLE_MAILHOG, active=True):
with capture_signals() as mock_signals:
self.app.post(
url,
json={
'fullName': name,
'email1': email,
'email2': email,
'password': password,
}
)
res = requests.get(mailhog_v2).json()

assert mock_signals.signals_sent() == {auth.signals.user_registered, auth.signals.unconfirmed_user_created}
assert self.mock_send_grid.called

user = OSFUser.objects.get(username=email)
user_token = list(user.email_verifications.keys())[0]
ideal_link_path = f'/confirm/{user._id}/{user_token}/'

assert ideal_link_path in res['items'][0]['Content']['Body']
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -607,3 +607,11 @@ services:
stdin_open: true
volumes:
- /srv

mailhog:
image: mailhog/mailhog
container_name: mailhog
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
restart: unless-stopped
6 changes: 6 additions & 0 deletions osf/features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,9 @@ switches:
name: countedusage_unified_metrics_2024
note: use only `osf.metrics.counted_usage`-based metrics where possible; un-use PageCounter, PreprintView, PreprintDownload, etc
active: false

- flag_name: ENABLE_MAILHOG
name: enable_mailhog
note: This is used to enable the MailHog email testing service, this will allow emails to be sent to the
MailHog service before sending them to real email addresses.
active: false
17 changes: 17 additions & 0 deletions tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,9 @@ def test_module(ctx, module=None, numprocesses=None, nocapture=False, params=Non
ADMIN_TESTS = [
'admin_tests',
]
MAILHOG_TESTS = [
'api_tests/mailhog',
]


@task
Expand Down Expand Up @@ -431,6 +434,13 @@ def test_api3(ctx, numprocesses=None, coverage=False, testmon=False, junit=False
test_module(ctx, module=API_TESTS3 + OSF_TESTS, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit)


@task
def test_mailhog(ctx, numprocesses=None, coverage=False, testmon=False, junit=False):
"""Run the MAILHOG test suite."""
print(f'Testing modules "{MAILHOG_TESTS}"')
test_module(ctx, module=MAILHOG_TESTS, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit)


@task
def test_admin(ctx, numprocesses=None, coverage=False, testmon=False, junit=False):
"""Run the Admin test suite."""
Expand Down Expand Up @@ -463,6 +473,7 @@ def test(ctx, all=False, lint=False):
test_addons(ctx)
# TODO: Enable admin tests
test_admin(ctx)
test_mailhog(ctx)

@task
def remove_failures_from_testmon(ctx, db_path=None):
Expand Down Expand Up @@ -512,6 +523,12 @@ def test_ci_api3_and_osf(ctx, numprocesses=None, coverage=False, testmon=False,
#ci_setup(ctx)
test_api3(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit)


@task
def test_ci_mailhog(ctx, numprocesses=None, coverage=False, testmon=False, junit=False):
#ci_setup(ctx)
test_mailhog(ctx, numprocesses=numprocesses, coverage=coverage, testmon=testmon, junit=junit)

@task
def wheelhouse(ctx, addons=False, release=False, dev=False, pty=True):
"""Build wheels for python dependencies.
Expand Down
40 changes: 40 additions & 0 deletions website/mails/mails.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from framework.email import tasks
from osf import features
from website import settings
from django.core.mail import EmailMessage, get_connection

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -75,6 +76,34 @@ def render_message(tpl_name, **context):
return tpl.render(**context)


def send_to_mailhog(subject, message, from_email, to_email, attachment_name=None, attachment_content=None):
email = EmailMessage(
subject=subject,
body=message,
from_email=from_email,
to=[to_email],
connection=get_connection(
backend='django.core.mail.backends.smtp.EmailBackend',
host=settings.MAILHOG_HOST,
port=settings.MAILHOG_PORT,
username='',
password='',
use_tls=False,
use_ssl=False,
)
)
email.content_subtype = 'html'

if attachment_name and attachment_content:
email.attach(attachment_name, attachment_content)

try:
email.send()
except ConnectionRefusedError:
logger.debug('Mailhog is not running. Please start it to send emails.')
return


def send_mail(
to_addr,
mail,
Expand Down Expand Up @@ -119,6 +148,17 @@ def send_mail(
logger.debug('Sending email...')
logger.debug(f'To: {to_addr}\nFrom: {from_addr}\nSubject: {subject}\nMessage: {message}')

if waffle.switch_is_active(features.ENABLE_MAILHOG):
logger.debug('Intercepting email: sending via MailHog')
send_to_mailhog(
subject=subject,
message=message,
from_email=from_addr,
to_email=to_addr,
attachment_name=attachment_name,
attachment_content=attachment_content
)

kwargs = dict(
from_addr=from_addr,
to_addr=to_addr,
Expand Down
4 changes: 4 additions & 0 deletions website/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ def parent_dir(path):
MAIL_USERNAME = 'osf-smtp'
MAIL_PASSWORD = '' # Set this in local.py

MAILHOG_HOST = 'mailhog'
MAILHOG_PORT = 1025
MAILHOG_API_HOST = 'http://localhost:8025'

# OR, if using Sendgrid's API
# WARNING: If `SENDGRID_WHITELIST_MODE` is True,
# `tasks.send_email` would only email recipients included in `SENDGRID_EMAIL_WHITELIST`
Expand Down
4 changes: 4 additions & 0 deletions website/settings/local-ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
MAIL_USERNAME = 'osf-smtp'
MAIL_PASSWORD = 'CHANGEME'

MAILHOG_HOST = 'localhost'
MAILHOG_PORT = 1025
MAILHOG_API_HOST = 'http://localhost:8025'

# Session
COOKIE_NAME = 'osf'
SECRET_KEY = 'CHANGEME'
Expand Down
4 changes: 4 additions & 0 deletions website/settings/local-dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
MAIL_USERNAME = 'osf-smtp'
MAIL_PASSWORD = 'CHANGEME'

MAILHOG_HOST = 'mailhog'
MAILHOG_PORT = 1025
MAILHOG_API_HOST = 'http://localhost:8025'

# Mailchimp email subscriptions
ENABLE_EMAIL_SUBSCRIPTIONS = False

Expand Down
Loading