mirror of
http://88.130.71.182:3000/BlitTech/badoHair_be.git
synced 2026-06-13 08:49:46 +00:00
191 lines
7.5 KiB
Python
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
|