"""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