This commit is contained in:
Martin Slachta
2026-06-11 19:03:29 +02:00
commit 0d829845c4
150 changed files with 38582 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.venv/
+105
View File
@@ -0,0 +1,105 @@
"""
Form submission API tests.
Install dependencies:
pip install pytest requests
Run:
pytest tests/Forms/FormTests.py -v
The base URL is read from the WP_BASE_URL environment variable,
defaulting to http://localhost/wordpress.
"""
import os
import pytest
import requests
BASE_URL = os.environ.get("WP_BASE_URL", "http://localhost/wordpress")
FORM_ENDPOINT = f"{BASE_URL}/wp-json/reservations/v1/form/1"
def post_form(data: dict) -> requests.Response:
return requests.post(FORM_ENDPOINT, json=data)
def find_error(errors: list, element: str) -> dict | None:
return next((e for e in errors if e.get("element") == element), None)
class TestFormResponseShape:
def test_response_is_json(self):
r = post_form({})
assert r.headers["Content-Type"].startswith("application/json")
def test_response_has_success_field(self):
r = post_form({})
body = r.json()
assert "success" in body
def test_failed_response_has_errors_list(self):
r = post_form({})
body = r.json()
assert body["success"] is False
assert isinstance(body["errors"], list)
def test_error_object_has_required_fields(self):
r = post_form({})
errors = r.json()["errors"]
assert len(errors) > 0
for error in errors:
assert "element" in error
assert "code" in error
assert "message" in error
class TestEmailValidation:
def test_missing_email_returns_required_error(self):
r = post_form({"name": "John"})
errors = r.json()["errors"]
email_error = find_error(errors, "email")
assert email_error is not None
assert email_error["code"] == "required"
def test_invalid_email_returns_invalid_email_error(self):
r = post_form({"email": "not-an-email", "name": "John"})
errors = r.json()["errors"]
email_error = find_error(errors, "email")
assert email_error is not None
assert email_error["code"] == "invalid_email"
def test_empty_email_returns_required_error(self):
r = post_form({"email": "", "name": "John"})
errors = r.json()["errors"]
email_error = find_error(errors, "email")
assert email_error is not None
assert email_error["code"] == "required"
def test_valid_email_produces_no_email_error(self):
r = post_form({"email": "valid@example.com", "name": "John"})
errors = r.json().get("errors", [])
email_error = find_error(errors, "email")
assert email_error is None
class TestNameField:
def test_missing_name_produces_no_name_error(self):
r = post_form({"email": "valid@example.com"})
errors = r.json().get("errors", [])
name_error = find_error(errors, "name")
assert name_error is None
def test_empty_name_produces_no_name_error(self):
r = post_form({"email": "valid@example.com", "name": ""})
errors = r.json().get("errors", [])
name_error = find_error(errors, "name")
assert name_error is None
class TestFormDefinition:
def test_form_definition_returns_correct_fields(self):
r = post_form({})
body = r.json()
assert "success" in body
assert "definition" in body
definition = body["definition"]
assert isinstance(definition, dict)
+2
View File
@@ -0,0 +1,2 @@
pytest
requests
+81
View File
@@ -0,0 +1,81 @@
"""
Tests for the FormDefinition creation endpoint.
Endpoint: POST /wp-json/reservations/v1/form-definition
Body: { "name": str, "definition": object }
Run:
pytest tests/Forms/test_form_definition.py -v
Override the WordPress base URL:
WP_BASE_URL=http://mysite.local pytest tests/Forms/test_form_definition.py -v
"""
import os
import requests
BASE_URL = os.environ.get("WP_BASE_URL", "http://localhost/wordpress")
ENDPOINT = f"{BASE_URL}/wp-json/reservations/v1/form-definition"
VALID_DEFINITION = {
"email_key": "email",
"elements": [
{"type": "text", "name": "name", "label": "Your name", "required": False},
{"type": "text", "name": "email", "label": "Your email", "required": True},
]
}
def post_definition(payload: dict) -> requests.Response:
return requests.post(ENDPOINT, json=payload)
class TestFormDefinitionCreation:
def test_can_create_form_definition(self):
r = post_definition({"name": "Contact form", "definition": VALID_DEFINITION})
assert r.status_code == 201
body = r.json()
assert "id" in body
assert isinstance(body["id"], int)
assert body["id"] > 0
def test_response_content_type_is_json(self):
r = post_definition({"name": "Contact form", "definition": VALID_DEFINITION})
assert r.headers["Content-Type"].startswith("application/json")
def test_each_creation_returns_a_new_id(self):
r1 = post_definition({"name": "Form A", "definition": VALID_DEFINITION})
r2 = post_definition({"name": "Form B", "definition": VALID_DEFINITION})
assert r1.status_code == 201
assert r2.status_code == 201
assert r1.json()["id"] != r2.json()["id"]
class TestFormDefinitionValidation:
def test_missing_name_returns_400(self):
r = post_definition({"definition": VALID_DEFINITION})
assert r.status_code == 400
def test_empty_name_returns_400(self):
r = post_definition({"name": "", "definition": VALID_DEFINITION})
assert r.status_code == 400
def test_missing_definition_returns_400(self):
r = post_definition({"name": "Contact form"})
assert r.status_code == 400
def test_definition_must_be_object_not_string(self):
r = post_definition({"name": "Contact form", "definition": "not-an-object"})
assert r.status_code == 400
def test_missing_both_fields_returns_400(self):
r = post_definition({})
assert r.status_code == 400
def test_definition_with_no_elements_is_accepted(self):
r = post_definition({"name": "Empty form", "definition": {}})
assert r.status_code == 201
def test_definition_with_empty_elements_list_is_accepted(self):
r = post_definition({"name": "Empty elements form", "definition": {"elements": []}})
assert r.status_code == 201
+121
View File
@@ -0,0 +1,121 @@
"""
Tests for form submission.
A form definition is created once per session, then submissions are POSTed
against that definition's ID.
Run:
pytest Forms/test_form_submission.py -v
"""
import os
import pytest
import requests
BASE_URL = os.environ.get("WP_BASE_URL", "http://localhost/wordpress")
DEFINITION_ENDPOINT = f"{BASE_URL}/wp-json/reservations/v1/form-definition"
FORM_ENDPOINT = f"{BASE_URL}/wp-json/reservations/v1/form"
FORM_DEFINITION = {
"elements": [
{
"type": "input-text",
"name": "email",
"label": "Email",
"required": True,
"validation": "email",
},
{
"type": "input-text",
"name": "name",
"label": "Name",
"required": False,
},
{
"type": "button",
"name": "submit",
"label": "Submit",
},
]
}
@pytest.fixture(scope="module")
def form_id():
r = requests.post(DEFINITION_ENDPOINT, json={"name": "Test submission form", "definition": FORM_DEFINITION})
assert r.status_code == 201, f"Failed to create form definition: {r.text}"
return r.json()["id"]
def submit(form_id: int, data: dict) -> requests.Response:
return requests.post(f"{FORM_ENDPOINT}/{form_id}", json=data)
class TestFormSubmissionSuccess:
def test_valid_submission_returns_200(self, form_id):
r = submit(form_id, {"email": "user@example.com", "name": "Alice"})
assert r.status_code == 200
def test_valid_submission_returns_success_true(self, form_id):
r = submit(form_id, {"email": "user@example.com", "name": "Alice"})
assert r.json()["success"] is True
def test_valid_submission_returns_submit_id(self, form_id):
r = submit(form_id, {"email": "user@example.com", "name": "Alice"})
body = r.json()
assert "submit_id" in body
assert isinstance(body["submit_id"], int)
assert body["submit_id"] > 0
def test_valid_submission_has_values(self, form_id):
r = submit(form_id, {"email": "user@example.com", "name": "Alice"})
values = r.json()["values"]
assert values["email"] == "user@example.com"
assert values["name"] == "Alice"
def test_optional_name_can_be_omitted(self, form_id):
r = submit(form_id, {"email": "user@example.com"})
assert r.status_code == 200
assert r.json()["success"] is True
class TestFormSubmissionValidation:
def test_missing_email_returns_400(self, form_id):
r = submit(form_id, {"name": "Alice"})
assert r.status_code == 400
def test_missing_email_returns_required_error(self, form_id):
r = submit(form_id, {"name": "Alice"})
errors = r.json()["errors"]
email_error = next((e for e in errors if e["element"] == "email"), None)
assert email_error is not None
assert email_error["code"] == "required"
def test_invalid_email_returns_400(self, form_id):
r = submit(form_id, {"email": "not-an-email", "name": "Alice"})
assert r.status_code == 400
def test_invalid_email_returns_invalid_email_error(self, form_id):
r = submit(form_id, {"email": "not-an-email", "name": "Alice"})
errors = r.json()["errors"]
email_error = next((e for e in errors if e["element"] == "email"), None)
assert email_error is not None
assert email_error["code"] == "invalid_email"
def test_empty_email_returns_required_error(self, form_id):
r = submit(form_id, {"email": "", "name": "Alice"})
errors = r.json()["errors"]
email_error = next((e for e in errors if e["element"] == "email"), None)
assert email_error is not None
assert email_error["code"] == "required"
class TestFormSubmissionNotFound:
def test_unknown_form_id_returns_400(self):
r = submit(999999, {"email": "user@example.com"})
assert r.status_code == 400
def test_unknown_form_id_returns_not_found_error(self):
r = submit(999999, {"email": "user@example.com"})
errors = r.json()["errors"]
assert any(e["code"] == "not_found" for e in errors)
+50
View File
@@ -0,0 +1,50 @@
# Reservair Tests
Integration & system tests for the Reservair WordPress plugin REST API. Written in Python.
*System tests* are testing if the whole system is behaving as it should. You should first and foremost define, what that even mean. You can build a test client like `ReservationClient` that has `create_reservation_at()` method.
*Integration tests* are testing if interface is working correctly. It is testing if the interface the implemented client for _system tests_ is using, is in fact correct.
## Requirements
```bash
pip install -r Forms/requirements.txt
```
## Configuration
By default tests run against `http://localhost/wordpress`. Override with:
```bash
export WP_BASE_URL=http://mysite.local
```
## Running
All tests:
```bash
cd tests
pytest
```
Specific subfolder:
```bash
pytest Forms/
```
Specific test file:
```bash
pytest Forms/test_form_definition.py
```
Specific test:
```bash
pytest Forms/test_form_definition.py::TestFormDefinitionCreation::test_can_create_form_definition
```
Add `-v` for verbose output.
@@ -0,0 +1,164 @@
"""
Tests for weekly repeating capacity.
A single capacity row is created for the nearest future Thursday, repeating
every 7 days. The tests verify:
- each Thursday in the series accepts bookings
- days that are not Thursday are rejected
- a slot that has reached capacity (capacity=1) is rejected on re-submission
Fixture chain (module-scoped):
repeating_timetable_id -> repeating_capacity_id
repeating_timetable_id -> repeating_form_id
"""
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"
def _next_thursday() -> date:
"""Nearest Thursday at least 7 days from today."""
d = date.today() + timedelta(days=7)
while d.weekday() != 3: # 3 = Thursday
d += timedelta(days=1)
return d
THURSDAY = _next_thursday()
FRIDAY = THURSDAY + timedelta(days=1)
THURSDAY_WEEK_2 = THURSDAY + timedelta(weeks=1)
THURSDAY_WEEK_3 = THURSDAY + timedelta(weeks=2)
def slot(day: date, time: str = "09:00:00") -> str:
return f"{day.isoformat()}T{time}+00:00"
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def repeating_timetable_id():
r = requests.post(f"{API}/timetable", json={
"name": "Weekly Thursday timetable",
"block_size": 60,
})
assert r.status_code == 201, r.text
return r.json()["id"]
@pytest.fixture(scope="module")
def repeating_capacity_id(repeating_timetable_id):
r = requests.post(f"{API}/timetable/{repeating_timetable_id}/capacity", json={
"capacity": 1,
"min_lead_time_minutes": 0,
"date": THURSDAY.isoformat(),
"start_time": "08:00",
"end_time": "18:00",
"repeat_period_in_days": 7,
"repeat_times": 52,
})
print(r.json())
assert r.status_code == 201, r.text
return r.json()["ids"][0]
@pytest.fixture(scope="module")
def repeating_form_id(repeating_timetable_id):
r = requests.post(f"{API}/form-definition", json={
"name": "Weekly Thursday form",
"definition": {
"email_key": "email",
"elements": [
{
"type": "reservation",
"name": "reservation",
"label": "Book a slot",
"calendar_id": str(repeating_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, r.text
return r.json()["id"]
def book(form_id, timetable_id, day: date, time: str = "09:00:00"):
return requests.post(f"{API}/form/{form_id}", json={
"email": "user@example.com",
"reservation": {
"timetable_id": timetable_id,
"timetable_reservations": [slot(day, time)],
},
})
def assert_not_available(r):
body = r.json()
assert body["success"] is False, f"Expected failure but got: {body}"
error = next((e for e in body["errors"] if e["element"] == "reservation"), None)
assert error is not None
assert error["code"] == "not_available"
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestRepeatingCapacity:
def test_first_thursday_is_available(
self, repeating_form_id, repeating_timetable_id, repeating_capacity_id
):
r = book(repeating_form_id, repeating_timetable_id, THURSDAY)
assert r.json()["success"] is True, r.text
def test_first_thursday_is_full_after_booking(
self, repeating_form_id, repeating_timetable_id, repeating_capacity_id
):
# capacity=1; the slot booked above is now exhausted
r = book(repeating_form_id, repeating_timetable_id, THURSDAY)
assert_not_available(r)
def test_friday_is_not_available(
self, repeating_form_id, repeating_timetable_id, repeating_capacity_id
):
r = book(repeating_form_id, repeating_timetable_id, FRIDAY)
assert_not_available(r)
def test_second_thursday_is_available(
self, repeating_form_id, repeating_timetable_id, repeating_capacity_id
):
r = book(repeating_form_id, repeating_timetable_id, THURSDAY_WEEK_2)
assert r.json()["success"] is True, r.text
def test_third_thursday_is_available(
self, repeating_form_id, repeating_timetable_id, repeating_capacity_id
):
r = book(repeating_form_id, repeating_timetable_id, THURSDAY_WEEK_3)
assert r.json()["success"] is True, r.text
def test_wednesday_before_second_thursday_is_not_available(
self, repeating_form_id, repeating_timetable_id, repeating_capacity_id
):
wednesday = THURSDAY_WEEK_2 - timedelta(days=1)
r = book(repeating_form_id, repeating_timetable_id, wednesday)
assert_not_available(r)
@@ -0,0 +1,289 @@
"""
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"
+60
View File
@@ -0,0 +1,60 @@
import json
import os
import textwrap
import pytest
import requests as _requests
BASE_URL = os.environ.get("WP_BASE_URL", "http://localhost/wordpress")
# Capture every HTTP exchange made during a test so we can print it on failure.
_exchanges: list[_requests.Response] = []
_original_request = _requests.Session.request
def _patched_request(self, method, url, **kwargs):
resp = _original_request(self, method, url, **kwargs)
_exchanges.append(resp)
return resp
_requests.Session.request = _patched_request # type: ignore[method-assign]
@pytest.fixture(scope="session")
def base_url():
return BASE_URL
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item): # noqa: ARG001
_exchanges.clear()
yield
def _pretty(raw) -> str:
try:
return json.dumps(json.loads(raw), indent=2)
except Exception:
return raw if isinstance(raw, str) else raw.decode(errors="replace")
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call): # noqa: ARG001
outcome = yield
report = outcome.get_result()
if not (report.failed and call.when == "call" and _exchanges):
return
lines = []
for i, resp in enumerate(_exchanges, 1):
req = resp.request
lines.append(f"[{i}] {req.method} {req.url}")
if req.body:
lines.append(textwrap.indent(_pretty(req.body), " req "))
lines.append(f" {resp.status_code}")
lines.append(textwrap.indent(_pretty(resp.text), " resp "))
report.sections.append(("HTTP", "\n".join(lines)))
@@ -0,0 +1,21 @@
# Tests if reservation can be created
# Create reservation
POST http://localhost/wordpress/wp-json/reservations/v1/reservation
{
"metadata": {
"email": "test@test.cz",
"phone": "608 349 219"
},
"is_confirmed": null
}
HTTP 200
[Captures]
object_id: jsonpath "$"
# Get timetable
GET http://localhost/wordpress/wp-json/reservations/v1/reservation/{{object_id}}
HTTP 200
[Asserts]
jsonpath "$.name" == "Test Timetable"
jsonpath "$.id" == "{{object_id}}"
@@ -0,0 +1,47 @@
# Tests if reservation can be created
# Create reservation type
POST http://localhost/wordpress/wp-json/reservations/v1/type
{
"name": "test",
"description": "testik",
"configuration": {
"steps": [
{
"index": 0,
"type": "timetable",
"configuration": {
}
}
]
}
}
HTTP 200
[Captures]
reservation_type_id: jsonpath "$"
# Get timetable
GET http://localhost/wordpress/wp-json/reservations/v1/type/{{reservation_type_id}}
HTTP 200
# Create reservation
POST http://localhost/wordpress/wp-json/reservations/v1/reservation
{
"metadata": {
"email": "test@test.cz",
"phone": "+420 608 349 219"
},
"is_confirmed": null,
"reservation_type_id": {{reservation_type_id}},
"steps": {
"0": {
"timetable_id": 1,
"date": "2025-1-1",
"start_time": "8:00",
"end_time": "9:00"
}
}
}
HTTP 200
[Captures]
@@ -0,0 +1,22 @@
# Tests if timetable can be created
# Create timetable
POST http://localhost/wordpress/wp-json/reservations/v1/timetable
{
"name": "Test Timetable"
}
HTTP 200
[Captures]
object_id: jsonpath "$"
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/capacity
{
"capacity": 2,
"min_lead_time_minutes": 60,
"date": "2025-1-1",
"start_time": "8:00",
"end_time": "12:00",
"repeat_period_in_days": 7,
"repeat_times": 0,
"requires_confirmation": true
}
@@ -0,0 +1,17 @@
# Tests if timetable can be created
# Create timetable
POST http://localhost/wordpress/wp-json/reservations/v1/timetable
{
"name": "Test Timetable"
}
HTTP 200
[Captures]
object_id: jsonpath "$"
# Get timetable
GET http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}
HTTP 200
[Asserts]
jsonpath "$.name" == "Test Timetable"
jsonpath "$.id" == "{{object_id}}"
@@ -0,0 +1,22 @@
# Create timetable
POST http://localhost/wordpress/wp-json/reservations/v1/timetable
{
"name": "Test Timetable"
}
HTTP 200
[Captures]
object_id: jsonpath "$"
# Update timetable
PUT http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}
{
"name": "Test Timetable Updated"
}
HTTP 200
# Check timetable
GET http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}
HTTP 200
[Asserts]
jsonpath "$.name" == "Test Timetable Updated"
jsonpath "$.id" == {{object_id}}
@@ -0,0 +1,81 @@
# Tests if timetable reservation can be created
# Create timetable
POST http://localhost/wordpress/wp-json/reservations/v1/timetable
{
"name": "Test Timetable"
}
HTTP 200
[Captures]
object_id: jsonpath "$"
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/capacity
{
"capacity": 1,
"min_lead_time_minutes": 60,
"date": "2025-1-1",
"start_time": "8:00",
"end_time": "12:00",
"repeat_period_in_days": 0,
"repeat_times": 0,
"requires_confirmation": true
}
HTTP 200
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/capacity
{
"capacity": 1,
"min_lead_time_minutes": 60,
"date": "2025-1-1",
"start_time": "13:00",
"end_time": "18:00",
"repeat_period_in_days": 0,
"repeat_times": 0,
"requires_confirmation": true
}
HTTP 200
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/reservation
{
"timetable_id": {{object_id}},
"reservation_id": 161,
"date": "2025-1-1",
"start_time": "8:00",
"end_time": "9:00"
}
HTTP 200
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/reservation
{
"timetable_id": {{object_id}},
"reservation_id": 161,
"date": "2025-1-1",
"start_time": "11:30",
"end_time": "12:30"
}
HTTP 400
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/reservation
{
"timetable_id": {{object_id}},
"reservation_id": 161,
"date": "2025-1-1",
"start_time": "11:30",
"end_time": "13:30"
}
HTTP 400
POST http://localhost/wordpress/wp-json/reservations/v1/timetable/{{object_id}}/reservation
{
"timetable_id": {{object_id}},
"reservation_id": 161,
"date": "2025-1-1",
"start_time": "12:30",
"end_time": "13:30"
}
HTTP 400
+13
View File
@@ -0,0 +1,13 @@
GET http://localhost/wordpress/wp-json/reservations/v1/object/1/action
HTTP 200
PATCH http://localhost/wordpress/wp-json/reservations/v1/object/1/action
[8, 9]
HTTP 204
GET http://localhost/wordpress/wp-json/reservations/v1/object/1/action
HTTP 200
[Asserts]
jsonpath "$" count == 2
jsonpath "$[0].reservation_action_id" == "8"
jsonpath "$[1].reservation_action_id" == "9"
+188
View File
@@ -0,0 +1,188 @@
# Create object with timetable
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1",
"timetable": {
"block_len": 30,
"block_capacity": 1
}
}
HTTP 200
[Captures]
object_id: jsonpath "$[0]"
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
{
"timetable_id": {{object_id}},
"first_date": "2025-08-31",
"start": "8:00",
"end": "16:00",
"is_repeating": false,
"requires_confirmation": false
}
HTTP 200
[Asserts]
jsonpath "$" count == 1
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
{
"timetable_id": {{object_id}},
"first_date": "2025-08-31",
"start": "8:00",
"end": "16:00",
"is_repeating": true,
"repeat_period": 7,
"repeat_times": 0,
"monday": true,
"tuesday": false,
"wednesday": true,
"thursday": false,
"friday": true,
"saturday": false,
"sunday": true,
"requires_confirmation": false
}
HTTP 200
[Asserts]
jsonpath "$" count == 4
jsonpath "$[0].first_date" == "2025-08-31"
jsonpath "$[1].first_date" == "2025-08-31"
jsonpath "$[2].first_date" == "2025-08-31"
jsonpath "$[3].first_date" == "2025-08-31"
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available?date=2025-08-31
HTTP 200
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 1
}
]
}
HTTP 200
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
HTTP 200
[Captures]
count: jsonpath "$" count
# Overlapping
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 1
}
]
}
HTTP 400
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
HTTP 200
[Asserts]
jsonpath "$" count == {{count}}
# Cannot create outside available time
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 34,
"num_blocks": 1
}
]
}
HTTP 400
# Not even end outside available time
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 30,
"num_blocks": 4
}
]
}
HTTP 400
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
HTTP 200
[Asserts]
jsonpath "$" count == {{count}}
# When one fails, none is inserted
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 20,
"num_blocks": 1
}, {
"from": 36,
"num_blocks": 1
}
]
}
HTTP 400
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
HTTP 200
[Asserts]
jsonpath "$" count == {{count}}
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 31,
"num_blocks": 1
}
]
}
HTTP 200
DELETE http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}
HTTP 200
+49
View File
@@ -0,0 +1,49 @@
# If available times are created properly
#
# Create object with timetable
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1",
"timetable": {
"block_len": 30,
"block_capacity": 1
}
}
HTTP 200
[Captures]
object_id: jsonpath "$[0]"
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
{
"timetable_id": {{object_id}},
"first_date": "2025-08-31",
"start": "8:00",
"end": "16:00",
"is_repeating": true,
"repeat_period": 7,
"repeat_times": 0,
"monday": true,
"tuesday": false,
"wednesday": true,
"thursday": false,
"friday": true,
"saturday": false,
"sunday": true,
"requires_confirmation": false
}
HTTP 200
[Asserts]
jsonpath "$" count == 4
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
HTTP 200
[Asserts]
jsonpath "$[0].first_date" == "2025-09-01"
jsonpath "$[1].first_date" == "2025-09-03"
jsonpath "$[2].first_date" == "2025-09-05"
jsonpath "$[3].first_date" == "2025-08-31"
DELETE http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}
HTTP 200
+13
View File
@@ -0,0 +1,13 @@
# create reservation type
POST http://localhost/wordpress/wp-json/reservations/v1/type
{
"name": "test",
"description": "description of test",
"configuration": [
{
"index": 1,
"type": "Timetable"
}
]
}
HTTP 201
+29
View File
@@ -0,0 +1,29 @@
# test objects
GET http://localhost/wordpress/wp-json/reservations/v1/object
HTTP 200
[Asserts]
jsonpath "$" isEmpty
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1"
}
HTTP 200
GET http://localhost/wordpress/wp-json/reservations/v1/object
HTTP 200
[Captures]
object_id: jsonpath "$[0].reservation_object_id"
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].name" == "TestObject1"
jsonpath "$[0].is_readonly" == "0"
DELETE http://localhost/wordpress/wp-json/reservations/v1/object/a
HTTP 404
DELETE http://localhost/wordpress/wp-json/reservations/v1/object/1
HTTP 404
DELETE http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}
HTTP 200
+64
View File
@@ -0,0 +1,64 @@
# Create object with timetable
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1",
"timetable": {
"block_len": 30,
"block_capacity": 1
}
}
HTTP 200
[Captures]
object_id: jsonpath "$[0]"
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
{
"timetable_id": {{object_id}},
"first_date": "2025-08-31",
"start": "8:00",
"end": "10:00",
"is_repeating": false,
"requires_confirmation": false
}
HTTP 200
[Asserts]
jsonpath "$" count == 1
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-08-31
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 0
jsonpath "$.occupancy[1]" == 0
jsonpath "$.occupancy[2]" == 0
jsonpath "$.occupancy[3]" == 0
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 2
}
]
}
HTTP 200
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-08-31
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 1
jsonpath "$.occupancy[1]" == 1
jsonpath "$.occupancy[2]" == 0
jsonpath "$.occupancy[3]" == 0
+172
View File
@@ -0,0 +1,172 @@
# Create object with timetable
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1",
"timetable": {
"block_len": 30,
"block_capacity": 2
}
}
HTTP 200
[Captures]
object_id: jsonpath "$[0]"
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
{
"timetable_id": {{object_id}},
"first_date": "2025-08-31",
"start": "8:00",
"end": "10:00",
"is_repeating": true,
"requires_confirmation": false,
"repeat_period": 7,
"repeat_times": 0,
"monday": true,
"tuesday": false,
"wednesday": true,
"thursday": false,
"friday": true,
"saturday": false,
"sunday": true
}
HTTP 200
[Asserts]
jsonpath "$" count == 4
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-08-31
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 0
jsonpath "$.occupancy[1]" == 0
jsonpath "$.occupancy[2]" == 0
jsonpath "$.occupancy[3]" == 0
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 2
}
]
}
HTTP 200
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-09-07",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 2
}
]
}
HTTP 200
# not available time
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-09-06",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 2
}
]
}
HTTP 400
# Availabel time, still has capacity
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-09-07",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 17,
"num_blocks": 2
}
]
}
HTTP 200
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-09-07
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 1
jsonpath "$.occupancy[1]" == 2
jsonpath "$.occupancy[2]" == 1
jsonpath "$.occupancy[3]" == 0
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-09-07",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 18,
"num_blocks": 1
}
]
}
HTTP 200
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-09-07",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 18,
"num_blocks": 1
}
]
}
HTTP 400
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-09-07",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 1
}
]
}
HTTP 400
+144
View File
@@ -0,0 +1,144 @@
# Create object with timetable
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1",
"timetable": {
"block_len": 30,
"block_capacity": 2
}
}
HTTP 200
[Captures]
object_id: jsonpath "$[0]"
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/available
{
"timetable_id": {{object_id}},
"first_date": "2025-08-31",
"start": "8:00",
"end": "10:00",
"is_repeating": false,
"requires_confirmation": false
}
HTTP 200
[Asserts]
jsonpath "$" count == 1
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-08-31
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 0
jsonpath "$.occupancy[1]" == 0
jsonpath "$.occupancy[2]" == 0
jsonpath "$.occupancy[3]" == 0
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 2
}
]
}
HTTP 200
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-08-31
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 1
jsonpath "$.occupancy[1]" == 1
jsonpath "$.occupancy[2]" == 0
jsonpath "$.occupancy[3]" == 0
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 17,
"num_blocks": 2
}
]
}
HTTP 200
GET http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation/occupancy?date=2025-08-31
HTTP 200
[Asserts]
jsonpath "$.occupancy" count == 4
jsonpath "$.occupancy[0]" == 1
jsonpath "$.occupancy[1]" == 2
jsonpath "$.occupancy[2]" == 1
jsonpath "$.occupancy[3]" == 0
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 17,
"num_blocks": 2
}
]
}
HTTP 400
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 2
}
]
}
HTTP 400
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable/reservation
{
"timetable_id": {{object_id}},
"date": "2025-08-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 17,
"num_blocks": 1
}
]
}
HTTP 400
+15
View File
@@ -0,0 +1,15 @@
POST http://localhost/wordpress/wp-json/reservations/v1/object/2/timetable/reservation
{
"timetable_id": 2,
"date": "2025-8-31",
"email": "test@test",
"full_name": "Test Test",
"phone": "123 456 789",
"reservations": [
{
"start_block_idx": 16,
"num_blocks": 1
}
]
}
HTTP 200
+1
View File
@@ -0,0 +1 @@
GET
+20
View File
@@ -0,0 +1,20 @@
POST http://localhost/wordpress/wp-json/reservations/v1/object
{
"name": "TestObject1"
}
HTTP 200
[Captures]
object_id: jsonpath "$[0]"
GET http://localhost/wordpress/wp-json/reservations/v1/object
HTTP 200
[Asserts]
jsonpath "$[-1:].reservation_object_id" count == 1
jsonpath "$[-1:].reservation_object_id[0]" == {{object_id}}
# Create it's timetable
POST http://localhost/wordpress/wp-json/reservations/v1/object/{{object_id}}/timetable
{
"block_len": 30
}
HTTP 200
+4
View File
@@ -0,0 +1,4 @@
{
"venvPath": ".",
"venv": ".venv"
}
+2
View File
@@ -0,0 +1,2 @@
[pytest]
addopts = -s