generated from bisco/codex-bootstrap
Merge branch 'fix/dev-email-confirmation-logging' into develop
This commit is contained in:
@@ -32,3 +32,4 @@ ENVIRONMENT=local
|
|||||||
|
|
||||||
# Local convention: nginx is the public entrypoint on http://localhost.
|
# Local convention: nginx is the public entrypoint on http://localhost.
|
||||||
# If you change the published nginx port, update SITE_BASE_URL and trusted origins to match.
|
# If you change the published nginx port, update SITE_BASE_URL and trusted origins to match.
|
||||||
|
# In local/debug mode, failed or attempted reservation emails also log the confirmation URL for manual browser testing.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
|
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
|
||||||
|
ENVIRONMENT = os.environ.get("ENVIRONMENT", "production").lower()
|
||||||
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
|
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
|
||||||
if not SECRET_KEY:
|
if not SECRET_KEY:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@@ -25,6 +26,7 @@ ALLOWED_HOSTS = csv_env("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1")
|
|||||||
CSRF_TRUSTED_ORIGINS = csv_env("DJANGO_CSRF_TRUSTED_ORIGINS")
|
CSRF_TRUSTED_ORIGINS = csv_env("DJANGO_CSRF_TRUSTED_ORIGINS")
|
||||||
CORS_ALLOWED_ORIGINS = csv_env("CORS_ALLOWED_ORIGINS")
|
CORS_ALLOWED_ORIGINS = csv_env("CORS_ALLOWED_ORIGINS")
|
||||||
SITE_BASE_URL = os.environ.get("SITE_BASE_URL", "http://localhost").rstrip("/")
|
SITE_BASE_URL = os.environ.get("SITE_BASE_URL", "http://localhost").rstrip("/")
|
||||||
|
LOG_RESERVATION_CONFIRMATION_URLS = DEBUG or ENVIRONMENT == "local"
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ def build_confirmation_link(raw_confirmation_token):
|
|||||||
return f"{settings.SITE_BASE_URL}{CONFIRMATION_PATH}?token={raw_confirmation_token}"
|
return f"{settings.SITE_BASE_URL}{CONFIRMATION_PATH}?token={raw_confirmation_token}"
|
||||||
|
|
||||||
|
|
||||||
|
def _log_confirmation_link_for_local_debug(*, reservation, confirmation_link, reason):
|
||||||
|
if not settings.LOG_RESERVATION_CONFIRMATION_URLS:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Local reservation confirmation link for manual testing (%s) for reservation %s: %s",
|
||||||
|
reason,
|
||||||
|
reservation.id,
|
||||||
|
confirmation_link,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_confirmation_email(*, reservation, raw_confirmation_token):
|
def send_confirmation_email(*, reservation, raw_confirmation_token):
|
||||||
confirmation_link = build_confirmation_link(raw_confirmation_token)
|
confirmation_link = build_confirmation_link(raw_confirmation_token)
|
||||||
subject = f"Confirm your reservation for {reservation.performance.show.title}"
|
subject = f"Confirm your reservation for {reservation.performance.show.title}"
|
||||||
@@ -23,6 +35,12 @@ def send_confirmation_email(*, reservation, raw_confirmation_token):
|
|||||||
"If you did not request this reservation, you can ignore this email."
|
"If you did not request this reservation, you can ignore this email."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_log_confirmation_link_for_local_debug(
|
||||||
|
reservation=reservation,
|
||||||
|
confirmation_link=confirmation_link,
|
||||||
|
reason="email send attempt",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
send_mail(
|
send_mail(
|
||||||
subject=subject,
|
subject=subject,
|
||||||
@@ -32,6 +50,11 @@ def send_confirmation_email(*, reservation, raw_confirmation_token):
|
|||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
_log_confirmation_link_for_local_debug(
|
||||||
|
reservation=reservation,
|
||||||
|
confirmation_link=confirmation_link,
|
||||||
|
reason="email send failure fallback",
|
||||||
|
)
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to send confirmation email for reservation %s.",
|
"Failed to send confirmation email for reservation %s.",
|
||||||
reservation.id,
|
reservation.id,
|
||||||
|
|||||||
@@ -96,6 +96,34 @@ class BookingServiceTests(TestCase):
|
|||||||
self.assertEqual(len(callbacks), 1)
|
self.assertEqual(len(callbacks), 1)
|
||||||
self.assertEqual(len(mail.outbox), 0)
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
|
||||||
|
LOG_RESERVATION_CONFIRMATION_URLS=True,
|
||||||
|
SITE_BASE_URL="https://tickets.azionelab.example",
|
||||||
|
)
|
||||||
|
def test_create_pending_reservation_logs_confirmation_link_in_local_mode(self):
|
||||||
|
with self.assertLogs("bookings.emailing", level="INFO") as captured_logs:
|
||||||
|
with self.captureOnCommitCallbacks(execute=True):
|
||||||
|
result = create_pending_reservation(
|
||||||
|
performance_id=self.performance.id,
|
||||||
|
name="Maria Rossi",
|
||||||
|
email="maria@example.com",
|
||||||
|
party_size=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.reservation.status, Reservation.Status.PENDING)
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
(
|
||||||
|
"Local reservation confirmation link for manual testing "
|
||||||
|
"(email send attempt)"
|
||||||
|
) in log_entry
|
||||||
|
and result.raw_confirmation_token in log_entry
|
||||||
|
for log_entry in captured_logs.output
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(LOG_RESERVATION_CONFIRMATION_URLS=False)
|
||||||
@patch("bookings.emailing.send_mail", side_effect=RuntimeError("SMTP down"))
|
@patch("bookings.emailing.send_mail", side_effect=RuntimeError("SMTP down"))
|
||||||
def test_create_pending_reservation_logs_email_failure_without_crashing(self, mocked_send_mail):
|
def test_create_pending_reservation_logs_email_failure_without_crashing(self, mocked_send_mail):
|
||||||
with self.assertLogs("bookings.emailing", level="ERROR") as captured_logs:
|
with self.assertLogs("bookings.emailing", level="ERROR") as captured_logs:
|
||||||
@@ -116,6 +144,40 @@ class BookingServiceTests(TestCase):
|
|||||||
for log_entry in captured_logs.output
|
for log_entry in captured_logs.output
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any(result.raw_confirmation_token in log_entry for log_entry in captured_logs.output)
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
LOG_RESERVATION_CONFIRMATION_URLS=True,
|
||||||
|
SITE_BASE_URL="https://tickets.azionelab.example",
|
||||||
|
)
|
||||||
|
@patch("bookings.emailing.send_mail", side_effect=RuntimeError("SMTP down"))
|
||||||
|
def test_create_pending_reservation_logs_confirmation_link_on_email_failure_in_local_mode(
|
||||||
|
self,
|
||||||
|
mocked_send_mail,
|
||||||
|
):
|
||||||
|
with self.assertLogs("bookings.emailing", level="INFO") as captured_logs:
|
||||||
|
with self.captureOnCommitCallbacks(execute=True):
|
||||||
|
result = create_pending_reservation(
|
||||||
|
performance_id=self.performance.id,
|
||||||
|
name="Maria Rossi",
|
||||||
|
email="maria@example.com",
|
||||||
|
party_size=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.reservation.status, Reservation.Status.PENDING)
|
||||||
|
mocked_send_mail.assert_called_once()
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
(
|
||||||
|
"Local reservation confirmation link for manual testing "
|
||||||
|
"(email send failure fallback)"
|
||||||
|
) in log_entry
|
||||||
|
and result.raw_confirmation_token in log_entry
|
||||||
|
for log_entry in captured_logs.output
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_generate_confirmation_token_returns_raw_token_once(self):
|
def test_generate_confirmation_token_returns_raw_token_once(self):
|
||||||
reservation = self.create_reservation()
|
reservation = self.create_reservation()
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ Local Docker convention:
|
|||||||
- set `SITE_BASE_URL=http://localhost`;
|
- set `SITE_BASE_URL=http://localhost`;
|
||||||
- keep `DJANGO_CSRF_TRUSTED_ORIGINS` and browser-facing `CORS_ALLOWED_ORIGINS` aligned with that public URL;
|
- keep `DJANGO_CSRF_TRUSTED_ORIGINS` and browser-facing `CORS_ALLOWED_ORIGINS` aligned with that public URL;
|
||||||
- if you publish nginx on a different port, update `SITE_BASE_URL` and trusted origins to the same host and port.
|
- if you publish nginx on a different port, update `SITE_BASE_URL` and trusted origins to the same host and port.
|
||||||
|
- local/debug reservation email sends also log the confirmation URL so browser testing can continue even if SMTP is missing or fails.
|
||||||
|
|
||||||
Required database configuration:
|
Required database configuration:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user