Skip to content
Merged
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
2 changes: 1 addition & 1 deletion mailing/mailing.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ def send_sendgrid_email(self, **kwargs: Any) -> Any | None:
)
try:
response = sg.send(message)
return response
except Exception as e:
if hasattr(e, "body"):
logger.error("SendGrid error body: %s", e.body)
logger.exception("SendGrid exception")
return None
return response
File renamed without changes.
79 changes: 79 additions & 0 deletions mailing/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import os
import types
import pytest


@pytest.fixture(autouse=True)
def _env_settings(monkeypatch):
"""Ensure settings has required env vars for tests and patch settings module.
Autouse so all tests get consistent defaults.
"""
monkeypatch.setenv("SENDGRID_API_KEY", "test-api-key")
monkeypatch.setenv("SENDGRID_FROM_EMAIL", "from@example.com")

# Also patch the already-imported settings module so defaults apply even if
# settings was imported before this fixture ran.
import settings as _settings

monkeypatch.setattr(_settings, "SENDGRID_API_KEY", "test-api-key", raising=False)
monkeypatch.setattr(_settings, "SENDGRID_FROM_EMAIL", "from@example.com", raising=False)
yield


@pytest.fixture()
def email_data():
return {
"subject": "Hello",
"message": "Plain text body",
"recipient_list": ["to1@example.com", "to2@example.com"],
"from_email": "from@example.com",
"html_content": "<p>HTML</p>",
}


class DummyMail:
"""Lightweight substitute for sendgrid.helpers.mail.Mail used in tests."""

def __init__(self, **kwargs):
# store all kwargs to allow assertions in tests
self.kwargs = kwargs


class DummySendGridClient:
"""A dummy client whose send() we can customize per test."""

def __init__(self, *_, **__):
# send behavior can be overridden by tests via attribute replacement
self._response = "ok"

def send(self, mail): # noqa: D401
"""Return a predefined response; overridden in tests for error path."""
return self._response


@pytest.fixture()
def mock_sendgrid_success(monkeypatch):
"""Patch mailing.mailing with dummy Mail and SendGrid client that succeed."""
import mailing.mailing as mm

monkeypatch.setattr(mm, "Mail", DummyMail, raising=True)
monkeypatch.setattr(mm, "SendGridAPIClient", DummySendGridClient, raising=True)
return mm


@pytest.fixture()
def mock_sendgrid_error(monkeypatch):
"""Patch mailing.mailing with dummy Mail and a client that raises Exception."""
import mailing.mailing as mm

class ErroringClient(DummySendGridClient):
def send(self, mail):
# Simulate an exception that has a body attribute like SendGridError
err = Exception("send failed")
# attach a body attribute to mimic SDK errors
err.body = {"errors": ["some error"]}
raise err

monkeypatch.setattr(mm, "Mail", DummyMail, raising=True)
monkeypatch.setattr(mm, "SendGridAPIClient", ErroringClient, raising=True)
return mm
89 changes: 89 additions & 0 deletions mailing/tests/test_sendgrid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import logging

from mailing.mailing import Email


class TestSendgridMailing:
def test_send_sendgrid_email_success_returns_response(self, mock_sendgrid_success, email_data):
# Given
email = Email(**email_data)

# When
response = email.send_sendgrid_email()

# Then
assert response == "ok"

# Also verify Mail was constructed with expected kwargs
# The DummyMail stores kwargs; access the last constructed instance by recreating with same data
# Instead, we build another to compare keys; more robust is to rebuild and compare content
m = mock_sendgrid_success.Mail(
from_email=email_data["from_email"],
to_emails=email_data["recipient_list"],
subject=email_data["subject"],
html_content=email_data["html_content"],
plain_text_content=email_data["message"],
)
# ensure keys are as expected; values are validated via equality
expected = m.kwargs

# Now recreate the Email and intercept the actual Mail instance to assert args
captured = {}

class CapturingMail(mock_sendgrid_success.Mail):
def __init__(self, **kwargs):
super().__init__(**kwargs)
captured.update(kwargs)

# Patch in the capturing version
import mailing.mailing as mm

mm.Mail = CapturingMail
Email(**email_data).send_sendgrid_email()

assert captured == expected

def test_send_sendgrid_email_uses_default_from_email_when_not_provided(
self, mock_sendgrid_success
):
# Given: no from_email passed, should use settings.SENDGRID_FROM_EMAIL from env
data = {
"subject": "Subj",
"message": "Body",
"recipient_list": ["t@example.com"],
"html_content": None,
}
# Capture constructed kwargs
captured = {}

class CapturingMail(mock_sendgrid_success.Mail):
def __init__(self, **kwargs):
super().__init__(**kwargs)
captured.update(kwargs)

import mailing.mailing as mm

mm.Mail = CapturingMail

# When
Email(**data).send_sendgrid_email()

# Then
assert captured["from_email"] == "from@example.com"
assert captured["plain_text_content"] == "Body"
assert captured["html_content"] is None

def test_send_sendgrid_email_logs_and_returns_none_on_exception(
self, mock_sendgrid_error, caplog, email_data
):
# Given
email = Email(**email_data)

# When
with caplog.at_level(logging.ERROR):
result = email.send_sendgrid_email()

# Then
assert result is None
# Ensure an error was logged containing our simulated body
assert any("SendGrid error body" in rec.getMessage() for rec in caplog.records)
2 changes: 0 additions & 2 deletions tests/test_dummy.py

This file was deleted.