Files
badoHair_be/tests/test_bookings.py
belviskhoremk d2dc43b16f Initial Commit
2026-05-12 00:34:21 +00:00

191 lines
7.5 KiB
Python

"""Booking endpoint tests — guest booking, auth booking, slots, admin actions."""
from datetime import date, time
from unittest.mock import AsyncMock
from tests.conftest import SAMPLE_BOOKING, SAMPLE_SLOT, SLOT_ID, BOOKING_ID, USER_ID
# ── Available slots ───────────────────────────────────────────────────────────
async def test_get_available_slots(anon_client, mock_db):
mock_db.fetch = AsyncMock(return_value=[SAMPLE_SLOT])
r = await anon_client.get("/api/v1/bookings/slots?from_date=2026-06-01&to_date=2026-06-07")
assert r.status_code == 200
body = r.json()
assert body["success"] is True
assert isinstance(body["data"], list)
async def test_get_slots_missing_date_param(anon_client):
r = await anon_client.get("/api/v1/bookings/slots?from_date=2026-06-01")
assert r.status_code == 422
# ── Guest booking ─────────────────────────────────────────────────────────────
async def test_guest_booking_success(anon_client, mock_db):
mock_db.fetchrow = AsyncMock(side_effect=[
SAMPLE_SLOT, # slot availability check
SAMPLE_BOOKING, # INSERT booking
])
mock_db.fetchval = AsyncMock(return_value=50.0) # default booking price
r = await anon_client.post("/api/v1/bookings", json={
"slot_id": SLOT_ID,
"service_note": "Box braids",
"guest_name": "Marie Dupont",
"guest_email": "marie@test.com",
"guest_phone": "+49123456789",
})
assert r.status_code == 201
body = r.json()
assert body["success"] is True
assert "client_secret" in body["data"]
assert "booking_id" in body["data"]
async def test_guest_booking_missing_email_rejected(anon_client, mock_db):
"""Unauthenticated booking without guest_email must fail with 422."""
r = await anon_client.post("/api/v1/bookings", json={
"slot_id": SLOT_ID,
"guest_name": "Marie",
# guest_email missing
})
assert r.status_code == 422
async def test_guest_booking_slot_unavailable(anon_client, mock_db):
mock_db.fetchrow = AsyncMock(return_value=None) # slot check returns nothing → unavailable
r = await anon_client.post("/api/v1/bookings", json={
"slot_id": SLOT_ID,
"guest_name": "Marie",
"guest_email": "marie@test.com",
})
assert r.status_code == 409
assert r.json()["error"]["code"] == "SLOT_UNAVAILABLE"
# ── Authenticated booking ─────────────────────────────────────────────────────
async def test_auth_booking_success(auth_client, mock_db):
mock_db.fetchrow = AsyncMock(side_effect=[
SAMPLE_SLOT,
SAMPLE_BOOKING,
])
mock_db.fetchval = AsyncMock(return_value=50.0)
r = await auth_client.post("/api/v1/bookings", json={
"slot_id": SLOT_ID,
"service_note": "Tape-in maintenance",
})
assert r.status_code == 201
assert r.json()["success"] is True
# ── List / get my bookings ────────────────────────────────────────────────────
async def test_list_my_bookings(auth_client, mock_db):
mock_db.fetchval = AsyncMock(return_value=1)
mock_db.fetch = AsyncMock(return_value=[SAMPLE_BOOKING])
r = await auth_client.get("/api/v1/bookings")
assert r.status_code == 200
body = r.json()
assert body["meta"]["total"] == 1
assert body["data"][0]["status"] == "confirmed"
async def test_get_booking_by_id(auth_client, mock_db):
mock_db.fetchrow = AsyncMock(return_value=SAMPLE_BOOKING)
r = await auth_client.get(f"/api/v1/bookings/{BOOKING_ID}")
assert r.status_code == 200
assert r.json()["data"]["id"] == BOOKING_ID
async def test_get_booking_not_found(auth_client, mock_db):
mock_db.fetchrow = AsyncMock(return_value=None)
r = await auth_client.get(f"/api/v1/bookings/00000000-0000-0000-0000-000000000000")
assert r.status_code == 404
async def test_list_bookings_requires_auth(anon_client):
r = await anon_client.get("/api/v1/bookings")
assert r.status_code == 401
# ── Cancel booking ────────────────────────────────────────────────────────────
async def test_cancel_booking_success(auth_client, mock_db):
pending_booking = {**SAMPLE_BOOKING, "status": "pending", "user_id": USER_ID}
mock_db.fetchrow = AsyncMock(return_value=pending_booking)
mock_db.execute = AsyncMock(return_value="UPDATE 1")
r = await auth_client.delete(f"/api/v1/bookings/{BOOKING_ID}")
assert r.status_code == 204
async def test_cancel_already_cancelled_booking(auth_client, mock_db):
cancelled = {**SAMPLE_BOOKING, "status": "cancelled", "user_id": USER_ID}
mock_db.fetchrow = AsyncMock(return_value=cancelled)
r = await auth_client.delete(f"/api/v1/bookings/{BOOKING_ID}")
assert r.status_code == 400
assert r.json()["error"]["code"] == "CANNOT_CANCEL"
async def test_cancel_booking_not_found(auth_client, mock_db):
mock_db.fetchrow = AsyncMock(return_value=None)
r = await auth_client.delete(f"/api/v1/bookings/00000000-0000-0000-0000-000000000000")
assert r.status_code == 404
# ── Admin booking management ──────────────────────────────────────────────────
async def test_admin_list_bookings(admin_client, mock_db):
mock_db.fetchval = AsyncMock(return_value=1)
mock_db.fetch = AsyncMock(return_value=[SAMPLE_BOOKING])
r = await admin_client.get("/api/v1/admin/bookings")
assert r.status_code == 200
body = r.json()
assert body["meta"]["total"] == 1
# Verify admin response includes resolved client info
record = body["data"][0]
assert "client_name" in record
assert "client_email" in record
assert "slot_date" in record
assert "slot_start" in record
async def test_admin_confirm_booking(admin_client, mock_db):
mock_db.fetchrow = AsyncMock(side_effect=[
SAMPLE_BOOKING, # UPDATE bookings RETURNING *
None, # SELECT email FROM profiles (skip email sending)
SAMPLE_BOOKING, # get_booking JOIN query at the end
])
mock_db.execute = AsyncMock(return_value="INSERT 1") # activity_log
r = await admin_client.patch(f"/api/v1/admin/bookings/{BOOKING_ID}", json={"status": "confirmed"})
assert r.status_code == 200
async def test_admin_delete_booking(admin_client, mock_db):
mock_db.execute = AsyncMock(return_value="DELETE 1")
r = await admin_client.delete(f"/api/v1/admin/bookings/{BOOKING_ID}")
assert r.status_code == 204
async def test_admin_delete_booking_not_found(admin_client, mock_db):
mock_db.execute = AsyncMock(return_value="DELETE 0")
r = await admin_client.delete(f"/api/v1/admin/bookings/00000000-0000-0000-0000-000000000000")
assert r.status_code == 404
async def test_admin_bookings_list_filter_by_status(admin_client, mock_db):
mock_db.fetchval = AsyncMock(return_value=0)
mock_db.fetch = AsyncMock(return_value=[])
r = await admin_client.get("/api/v1/admin/bookings?status=pending")
assert r.status_code == 200