""" End-to-end tests: create a timetable, add capacity, create a form definition with a reservation element, then submit a booking through that form. Fixture chain (all module-scoped so resources are created once): timetable_id -> capacity_id timetable_id -> form_id form_id + timetable_id + capacity_id -> submission tests Run: pytest Reservations/test_timetable_reservation.py -v """ import os from datetime import date, timedelta import requests import pytest BASE_URL = os.environ.get("WP_BASE_URL", "http://localhost/wordpress") API = f"{BASE_URL}/wp-json/reservations/v1" BOOKING_DATE = (date.today() + timedelta(days=7)).isoformat() def slot_dt(time: str = "08:00:00") -> str: """Return an ISO 8601 UTC datetime string for the booking date at the given time.""" return f"{BOOKING_DATE}T{time}+00:00" # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def timetable_id(): r = requests.post(f"{API}/timetable", json={ "name": "Test timetable", "block_size": 60, }) assert r.status_code == 201, f"Failed to create timetable: {r.text}" tid = r.json()["id"] assert isinstance(tid, int) and tid > 0 return tid @pytest.fixture(scope="module") def capacity_id(timetable_id): r = requests.post(f"{API}/timetable/{timetable_id}/capacity", json={ "capacity": 2, "min_lead_time_minutes": 0, "date": BOOKING_DATE, "start_time": "08:00", "end_time": "18:00", "repeat_period_in_days": 0, "repeat_times": 0, }) assert r.status_code == 201, f"Failed to add capacity: {r.text}" cid = r.json()["ids"][0] assert isinstance(cid, int) and cid > 0 return cid @pytest.fixture(scope="module") def form_id(timetable_id): r = requests.post(f"{API}/form-definition", json={ "name": "Reservation form", "definition": { "email_key": "email", "elements": [ { "type": "reservation", "name": "reservation", "label": "Book a slot", "calendar_id": str(timetable_id), "required": True, }, { "type": "input-text", "name": "email", "label": "Email", "required": True, "validation": "email", }, { "type": "button", "name": "submit", "label": "Submit", }, ] }, }) assert r.status_code == 201, f"Failed to create form definition: {r.text}" fid = r.json()["id"] assert isinstance(fid, int) and fid > 0 return fid def submit(form_id, payload): return requests.post(f"{API}/form/{form_id}", json=payload) def valid_payload(timetable_id, time: str = "08:00:00"): return { "email": "user@example.com", "reservation": { "timetable_id": timetable_id, "timetable_reservations": [slot_dt(time)], }, } # --------------------------------------------------------------------------- # Setup verification # --------------------------------------------------------------------------- class TestSetup: def test_timetable_created(self, timetable_id): r = requests.get(f"{API}/timetable") assert r.status_code == 200 ids = [row["id"] for row in r.json()["data"]] assert timetable_id in ids or str(timetable_id) in [str(i) for i in ids] def test_capacity_listed(self, timetable_id, capacity_id): r = requests.get(f"{API}/timetable/{timetable_id}/capacity") assert r.status_code == 200 ids = [row["id"] for row in r.json()["data"]] assert capacity_id in ids or str(capacity_id) in [str(i) for i in ids] def test_form_definition_listed(self, form_id): r = requests.get(f"{API}/form-definition") assert r.status_code == 200 ids = [row["form_id"] for row in r.json()["data"]] assert form_id in ids or str(form_id) in [str(i) for i in ids] # --------------------------------------------------------------------------- # Reservation element validation # --------------------------------------------------------------------------- class TestReservationValidation: def test_missing_reservation_field_returns_error(self, form_id): r = submit(form_id, {"email": "user@example.com"}) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "reservation"), None) assert error is not None assert error["code"] == "required" def test_reservation_missing_timetable_id(self, form_id): r = submit(form_id, { "email": "user@example.com", "reservation": {"timetable_reservations": [slot_dt()]}, }) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "reservation"), None) assert error is not None assert error["code"] == "invalid" def test_reservation_missing_timetable_reservations(self, form_id, timetable_id): r = submit(form_id, { "email": "user@example.com", "reservation": {"timetable_id": timetable_id}, }) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "reservation"), None) assert error is not None assert error["code"] == "missing_field" def test_reservation_payload_must_be_object(self, form_id): r = submit(form_id, { "email": "user@example.com", "reservation": "not-an-object", }) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "reservation"), None) assert error is not None def test_slot_outside_capacity_fails(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 r = submit(form_id, valid_payload(timetable_id, "20:00:00")) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "reservation"), None) assert error is not None assert error["code"] == "not_available" def test_missing_email_with_valid_reservation(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 r = submit(form_id, { "reservation": { "timetable_id": timetable_id, "timetable_reservations": [slot_dt()], }, }) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "email"), None) assert error is not None assert error["code"] == "required" def test_invalid_email_with_valid_reservation(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 r = submit(form_id, { "email": "not-an-email", "reservation": { "timetable_id": timetable_id, "timetable_reservations": [slot_dt()], }, }) body = r.json() assert body["success"] is False error = next((e for e in body["errors"] if e["element"] == "email"), None) assert error is not None assert error["code"] == "invalid_email" # --------------------------------------------------------------------------- # Booking fixture + tests # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def booking(form_id, timetable_id, capacity_id): assert capacity_id > 0 r = requests.post(f"{API}/form/{form_id}", json=valid_payload(timetable_id)) assert r.status_code == 200, f"Booking failed: {r.text}" return r.json() class TestBooking: def test_booking_succeeds(self, booking): assert booking["success"] is True def test_booking_has_submit_id(self, booking): assert "submit_id" in booking assert isinstance(booking["submit_id"], int) assert booking["submit_id"] > 0 def test_booking_values_contain_email(self, booking): assert booking["values"]["email"] == "user@example.com" def test_second_booking_gets_different_submit_id(self, form_id, timetable_id, capacity_id, booking): assert capacity_id > 0 r = requests.post(f"{API}/form/{form_id}", json=valid_payload(timetable_id)) assert r.status_code == 200 assert r.json()["submit_id"] != booking["submit_id"] # --------------------------------------------------------------------------- # Full submission # --------------------------------------------------------------------------- class TestReservationSubmission: def test_valid_submission_returns_200(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 r = submit(form_id, valid_payload(timetable_id, "13:00:00")) assert r.status_code == 200 def test_valid_submission_succeeds(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 r = submit(form_id, valid_payload(timetable_id, "14:00:00")) assert r.json()["success"] is True def test_valid_submission_returns_submit_id(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 r = submit(form_id, valid_payload(timetable_id, "15:00:00")) body = r.json() assert "submit_id" in body assert isinstance(body["submit_id"], int) assert body["submit_id"] > 0 class TestReservationOverlapSubmission: def test_overlapping_submission_fails(self, form_id, timetable_id, capacity_id): assert capacity_id > 0 # capacity=2, so first two succeed and third is rejected r = submit(form_id, valid_payload(timetable_id, "10:00:00")) assert r.status_code == 200 assert r.json()["success"] is True r = submit(form_id, valid_payload(timetable_id, "10:00:00")) assert r.status_code == 200 assert r.json()["success"] is True r = submit(form_id, valid_payload(timetable_id, "10:00:00")) assert r.json()["success"] is False error = next((e for e in r.json()["errors"] if e["element"] == "reservation"), None) assert error is not None assert error["code"] == "not_available"