290 lines
10 KiB
Python
290 lines
10 KiB
Python
|
|
"""
|
||
|
|
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"
|