diff --git a/.env.example b/.env.example index 4fadffe..bc46029 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8080 DJANGO_DEBUG=false CORS_ALLOWED_ORIGINS=http://localhost:4200,http://localhost:8080 +SITE_BASE_URL=http://localhost:8080 TIME_ZONE=Europe/Rome POSTGRES_DB=azionelab diff --git a/backend/azionelab/settings.py b/backend/azionelab/settings.py index cf89f8f..45fff65 100644 --- a/backend/azionelab/settings.py +++ b/backend/azionelab/settings.py @@ -23,6 +23,7 @@ def csv_env(name, default=""): ALLOWED_HOSTS = csv_env("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1") CSRF_TRUSTED_ORIGINS = csv_env("DJANGO_CSRF_TRUSTED_ORIGINS") CORS_ALLOWED_ORIGINS = csv_env("CORS_ALLOWED_ORIGINS") +SITE_BASE_URL = os.environ.get("SITE_BASE_URL", "http://localhost:8080").rstrip("/") INSTALLED_APPS = [ "django.contrib.admin", diff --git a/backend/bookings/emailing.py b/backend/bookings/emailing.py index 1d12343..e8719fb 100644 --- a/backend/bookings/emailing.py +++ b/backend/bookings/emailing.py @@ -1,5 +1,6 @@ import logging +from django.conf import settings from django.core.mail import send_mail @@ -9,7 +10,7 @@ CONFIRMATION_PATH = "/api/reservations/confirm/" def build_confirmation_link(raw_confirmation_token): - return f"{CONFIRMATION_PATH}?token={raw_confirmation_token}" + return f"{settings.SITE_BASE_URL}{CONFIRMATION_PATH}?token={raw_confirmation_token}" def send_confirmation_email(*, reservation, raw_confirmation_token): diff --git a/backend/bookings/qr.py b/backend/bookings/qr.py index 40cd6ff..6d69e08 100644 --- a/backend/bookings/qr.py +++ b/backend/bookings/qr.py @@ -2,6 +2,7 @@ import base64 from io import BytesIO import qrcode +from django.conf import settings from .models import Reservation @@ -10,7 +11,7 @@ CHECK_IN_PREVIEW_PATH = "/api/check-ins/preview/" def build_check_in_preview_url(raw_check_in_token): - return f"{CHECK_IN_PREVIEW_PATH}?token={raw_check_in_token}" + return f"{settings.SITE_BASE_URL}{CHECK_IN_PREVIEW_PATH}?token={raw_check_in_token}" def generate_check_in_qr_png(raw_check_in_token): diff --git a/backend/bookings/test_api.py b/backend/bookings/test_api.py index 151a8dc..c9b22e8 100644 --- a/backend/bookings/test_api.py +++ b/backend/bookings/test_api.py @@ -2,6 +2,7 @@ from datetime import timedelta from django.core import mail from django.urls import reverse +from django.test.utils import override_settings from django.utils import timezone from rest_framework import status from rest_framework.test import APITestCase @@ -60,6 +61,7 @@ class BookingApiTests(APITestCase): self.assertEqual(response.data["performances"][0]["id"], self.performance.id) self.assertEqual(response.data["performances"][0]["available_seats"], 3) + @override_settings(SITE_BASE_URL="https://tickets.azionelab.example") def test_reservation_creation_success(self): response = self.client.post( reverse("api-reservation-create", kwargs={"performance_id": self.performance.id}), @@ -80,7 +82,10 @@ class BookingApiTests(APITestCase): self.assertNotIn("email", response.data) self.assertEqual(Reservation.objects.count(), 1) self.assertEqual(len(mail.outbox), 1) - self.assertIn("/api/reservations/confirm/?token=", mail.outbox[0].body) + self.assertIn( + "https://tickets.azionelab.example/api/reservations/confirm/?token=", + mail.outbox[0].body, + ) def test_reservation_creation_with_insufficient_seats(self): response = self.client.post( @@ -97,6 +102,7 @@ class BookingApiTests(APITestCase): self.assertEqual(response.data["status"], "booking_unavailable") self.assertEqual(Reservation.objects.count(), 0) + @override_settings(SITE_BASE_URL="https://tickets.azionelab.example") def test_confirmation_success(self): reservation = self.create_reservation() _, raw_token = generate_confirmation_token(reservation) @@ -112,7 +118,11 @@ class BookingApiTests(APITestCase): self.assertEqual(response.data["reservation_id"], reservation.id) self.assertEqual(response.data["status"], Reservation.Status.CONFIRMED) self.assertEqual(response.data["party_size"], reservation.party_size) - self.assertTrue(response.data["qr_code_url"].startswith("/api/check-ins/preview/?token=")) + self.assertTrue( + response.data["qr_code_url"].startswith( + "https://tickets.azionelab.example/api/check-ins/preview/?token=" + ) + ) self.assertNotIn("token", response.data) self.assertTrue(response.data["qr_code_image"].startswith("data:image/png;base64,")) self.assertEqual(reservation.status, Reservation.Status.CONFIRMED) diff --git a/backend/bookings/test_services.py b/backend/bookings/test_services.py index 891ab77..231b11a 100644 --- a/backend/bookings/test_services.py +++ b/backend/bookings/test_services.py @@ -60,7 +60,10 @@ class BookingServiceTests(TestCase): ) self.assertNotIn("maria@example.com", result.raw_confirmation_token) - @override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend") + @override_settings( + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + SITE_BASE_URL="https://tickets.azionelab.example", + ) def test_create_pending_reservation_sends_confirmation_email(self): result = create_pending_reservation( performance_id=self.performance.id, @@ -72,7 +75,10 @@ class BookingServiceTests(TestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to, ["maria@example.com"]) self.assertIn(result.raw_confirmation_token, mail.outbox[0].body) - self.assertIn("/api/reservations/confirm/?token=", mail.outbox[0].body) + self.assertIn( + "https://tickets.azionelab.example/api/reservations/confirm/?token=", + mail.outbox[0].body, + ) @patch("bookings.emailing.send_mail", side_effect=RuntimeError("SMTP down")) def test_create_pending_reservation_logs_email_failure_without_crashing(self, mocked_send_mail): @@ -96,6 +102,7 @@ class BookingServiceTests(TestCase): self.assertEqual(token.token_hash, ReservationToken.hash_token(raw_token)) self.assertGreater(token.expires_at, timezone.now()) + @override_settings(SITE_BASE_URL="https://tickets.azionelab.example") def test_confirm_reservation_from_valid_token(self): reservation = self.create_reservation(party_size=2) token, raw_token = generate_confirmation_token(reservation) @@ -118,8 +125,14 @@ class BookingServiceTests(TestCase): result.qr_code_url, build_check_in_preview_url(result.raw_check_in_token), ) + self.assertTrue( + result.qr_code_url.startswith( + "https://tickets.azionelab.example/api/check-ins/preview/?token=" + ) + ) self.assertTrue(result.qr_code_image.startswith("data:image/png;base64,")) + @override_settings(SITE_BASE_URL="https://tickets.azionelab.example") def test_qr_code_is_generated_for_confirmed_reservation(self): reservation = self.create_reservation( status=Reservation.Status.CONFIRMED, @@ -134,6 +147,10 @@ class BookingServiceTests(TestCase): self.assertTrue(qr_code_image.startswith("data:image/png;base64,")) self.assertGreater(len(qr_code_image), len("data:image/png;base64,")) + self.assertEqual( + build_check_in_preview_url(raw_check_in_token), + "https://tickets.azionelab.example/api/check-ins/preview/?token=opaque-check-in-token", + ) def test_qr_code_is_not_generated_for_pending_reservation(self): reservation = self.create_reservation()