""" Booking API Tests Tests for the user booking endpoints. """ from datetime import date, datetime, timedelta, timezone import pytest from models import Appointment, AppointmentStatus def tomorrow() -> date: return date.today() + timedelta(days=1) def in_days(n: int) -> date: return date.today() + timedelta(days=n) # ============================================================================= # Permission Tests # ============================================================================= class TestBookingPermissions: """Test that only regular users can book appointments.""" @pytest.mark.asyncio async def test_regular_user_can_get_slots(self, client_factory, regular_user, admin_user): """Regular user can get available slots.""" # First, admin sets up availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # Regular user gets slots async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/booking/slots", params={"date": str(tomorrow())}) assert response.status_code == 200 @pytest.mark.asyncio async def test_regular_user_can_book(self, client_factory, regular_user, admin_user): """Regular user can book an appointment.""" # Admin sets up availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # Regular user books async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": "Test booking"}, ) assert response.status_code == 200 @pytest.mark.asyncio async def test_admin_cannot_get_slots(self, client_factory, admin_user): """Admin cannot access booking slots endpoint.""" async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.get("/api/booking/slots", params={"date": str(tomorrow())}) assert response.status_code == 403 @pytest.mark.asyncio async def test_admin_cannot_book(self, client_factory, admin_user): """Admin cannot book appointments.""" # Admin sets up availability first async with client_factory.create(cookies=admin_user["cookies"]) as client: await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) assert response.status_code == 403 @pytest.mark.asyncio async def test_unauthenticated_cannot_get_slots(self, client): """Unauthenticated user cannot get slots.""" response = await client.get("/api/booking/slots", params={"date": str(tomorrow())}) assert response.status_code == 401 @pytest.mark.asyncio async def test_unauthenticated_cannot_book(self, client): """Unauthenticated user cannot book.""" response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) assert response.status_code == 401 # ============================================================================= # Get Slots Tests # ============================================================================= class TestGetSlots: """Test getting available booking slots.""" @pytest.mark.asyncio async def test_get_slots_no_availability(self, client_factory, regular_user): """Returns empty slots when no availability set.""" async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/booking/slots", params={"date": str(tomorrow())}) assert response.status_code == 200 data = response.json() assert data["date"] == str(tomorrow()) assert data["slots"] == [] @pytest.mark.asyncio async def test_get_slots_expands_to_15min(self, client_factory, regular_user, admin_user): """Availability is expanded into 15-minute slots.""" # Admin sets 1-hour availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "10:00:00"}], }, ) # User gets slots - should be 4 x 15-minute slots async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/booking/slots", params={"date": str(tomorrow())}) assert response.status_code == 200 data = response.json() assert len(data["slots"]) == 4 # Verify times assert "09:00:00" in data["slots"][0]["start_time"] assert "09:15:00" in data["slots"][0]["end_time"] assert "09:15:00" in data["slots"][1]["start_time"] assert "09:45:00" in data["slots"][3]["start_time"] assert "10:00:00" in data["slots"][3]["end_time"] @pytest.mark.asyncio async def test_get_slots_excludes_booked(self, client_factory, regular_user, admin_user): """Already booked slots are excluded from available slots.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "10:00:00"}], }, ) # User books first slot async with client_factory.create(cookies=regular_user["cookies"]) as client: await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) # Get slots again - should have 3 left response = await client.get("/api/booking/slots", params={"date": str(tomorrow())}) assert response.status_code == 200 data = response.json() assert len(data["slots"]) == 3 # First slot should now be 09:15 assert "09:15:00" in data["slots"][0]["start_time"] # ============================================================================= # Booking Tests # ============================================================================= class TestCreateBooking: """Test creating bookings.""" @pytest.mark.asyncio async def test_book_slot_success(self, client_factory, regular_user, admin_user): """Can successfully book an available slot.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={ "slot_start": f"{tomorrow()}T09:00:00Z", "note": "Discussion about project", }, ) assert response.status_code == 200 data = response.json() assert data["user_id"] == regular_user["user"]["id"] assert data["note"] == "Discussion about project" assert data["status"] == "booked" assert "09:00:00" in data["slot_start"] assert "09:15:00" in data["slot_end"] @pytest.mark.asyncio async def test_book_without_note(self, client_factory, regular_user, admin_user): """Can book without a note.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books without note async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) assert response.status_code == 200 data = response.json() assert data["note"] is None @pytest.mark.asyncio async def test_cannot_double_book_slot(self, client_factory, regular_user, admin_user, alt_regular_user): """Cannot book a slot that's already booked.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # First user books async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) assert response.status_code == 200 # Second user tries to book same slot async with client_factory.create(cookies=alt_regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) assert response.status_code == 409 assert "already been booked" in response.json()["detail"] @pytest.mark.asyncio async def test_cannot_book_outside_availability(self, client_factory, regular_user, admin_user): """Cannot book a slot outside of availability.""" # Admin sets availability for morning only async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User tries to book afternoon slot async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T14:00:00Z"}, ) assert response.status_code == 400 assert "not within available" in response.json()["detail"] # ============================================================================= # Date Validation Tests # ============================================================================= class TestBookingDateValidation: """Test date validation for bookings.""" @pytest.mark.asyncio async def test_cannot_book_today(self, client_factory, regular_user): """Cannot book for today (same day).""" async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{date.today()}T09:00:00Z"}, ) assert response.status_code == 400 assert "past" in response.json()["detail"].lower() or "today" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_cannot_book_past_date(self, client_factory, regular_user): """Cannot book for past date.""" yesterday = date.today() - timedelta(days=1) async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{yesterday}T09:00:00Z"}, ) assert response.status_code == 400 @pytest.mark.asyncio async def test_cannot_book_beyond_30_days(self, client_factory, regular_user): """Cannot book more than 30 days in advance.""" too_far = in_days(31) async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{too_far}T09:00:00Z"}, ) assert response.status_code == 400 assert "30" in response.json()["detail"] @pytest.mark.asyncio async def test_cannot_get_slots_today(self, client_factory, regular_user): """Cannot get slots for today.""" async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/booking/slots", params={"date": str(date.today())}) assert response.status_code == 400 @pytest.mark.asyncio async def test_cannot_get_slots_past(self, client_factory, regular_user): """Cannot get slots for past date.""" yesterday = date.today() - timedelta(days=1) async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/booking/slots", params={"date": str(yesterday)}) assert response.status_code == 400 # ============================================================================= # Time Validation Tests # ============================================================================= class TestBookingTimeValidation: """Test time validation for bookings.""" @pytest.mark.asyncio async def test_slot_must_be_15min_boundary(self, client_factory, regular_user, admin_user): """Slot start time must be on 15-minute boundary.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User tries to book at 09:05 async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:05:00Z"}, ) assert response.status_code == 400 assert "15-minute" in response.json()["detail"] # ============================================================================= # Note Validation Tests # ============================================================================= class TestBookingNoteValidation: """Test note validation for bookings.""" @pytest.mark.asyncio async def test_note_max_length(self, client_factory, regular_user, admin_user): """Note cannot exceed 144 characters.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User tries to book with long note long_note = "x" * 145 async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": long_note}, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_note_exactly_144_chars(self, client_factory, regular_user, admin_user): """Note of exactly 144 characters is allowed.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books with exactly 144 char note note = "x" * 144 async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": note}, ) assert response.status_code == 200 assert response.json()["note"] == note # ============================================================================= # User Appointments Tests # ============================================================================= class TestUserAppointments: """Test user appointments endpoints.""" @pytest.mark.asyncio async def test_get_my_appointments_empty(self, client_factory, regular_user): """Returns empty list when user has no appointments.""" async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/appointments") assert response.status_code == 200 assert response.json() == [] @pytest.mark.asyncio async def test_get_my_appointments_with_bookings(self, client_factory, regular_user, admin_user): """Returns user's appointments.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books two slots async with client_factory.create(cookies=regular_user["cookies"]) as client: await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": "First"}, ) await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:15:00Z", "note": "Second"}, ) # Get appointments response = await client.get("/api/appointments") assert response.status_code == 200 data = response.json() assert len(data) == 2 # Sorted by date descending notes = [apt["note"] for apt in data] assert "First" in notes assert "Second" in notes @pytest.mark.asyncio async def test_admin_cannot_view_user_appointments(self, client_factory, admin_user): """Admin cannot access user appointments endpoint.""" async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.get("/api/appointments") assert response.status_code == 403 @pytest.mark.asyncio async def test_unauthenticated_cannot_view_appointments(self, client): """Unauthenticated user cannot view appointments.""" response = await client.get("/api/appointments") assert response.status_code == 401 class TestCancelAppointment: """Test cancelling appointments.""" @pytest.mark.asyncio async def test_cancel_own_appointment(self, client_factory, regular_user, admin_user): """User can cancel their own appointment.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] # Cancel response = await client.post(f"/api/appointments/{apt_id}/cancel") assert response.status_code == 200 data = response.json() assert data["status"] == "cancelled_by_user" assert data["cancelled_at"] is not None @pytest.mark.asyncio async def test_cannot_cancel_others_appointment(self, client_factory, regular_user, alt_regular_user, admin_user): """User cannot cancel another user's appointment.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # First user books async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] # Second user tries to cancel async with client_factory.create(cookies=alt_regular_user["cookies"]) as client: response = await client.post(f"/api/appointments/{apt_id}/cancel") assert response.status_code == 403 assert "another user" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_cannot_cancel_nonexistent_appointment(self, client_factory, regular_user): """Returns 404 for non-existent appointment.""" async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post("/api/appointments/99999/cancel") assert response.status_code == 404 @pytest.mark.asyncio async def test_cannot_cancel_already_cancelled(self, client_factory, regular_user, admin_user): """Cannot cancel an already cancelled appointment.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books and cancels async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] await client.post(f"/api/appointments/{apt_id}/cancel") # Try to cancel again response = await client.post(f"/api/appointments/{apt_id}/cancel") assert response.status_code == 400 assert "cancelled_by_user" in response.json()["detail"] @pytest.mark.asyncio async def test_admin_cannot_use_user_cancel_endpoint(self, client_factory, admin_user): """Admin cannot use user cancel endpoint.""" async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.post("/api/appointments/1/cancel") assert response.status_code == 403 @pytest.mark.asyncio async def test_cancelled_slot_becomes_available(self, client_factory, regular_user, admin_user): """After cancelling, the slot becomes available again.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "09:30:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] # Check slots - should have 1 slot left (09:15) slots_response = await client.get( "/api/booking/slots", params={"date": str(tomorrow())}, ) assert len(slots_response.json()["slots"]) == 1 # Cancel await client.post(f"/api/appointments/{apt_id}/cancel") # Check slots - should have 2 slots now slots_response = await client.get( "/api/booking/slots", params={"date": str(tomorrow())}, ) assert len(slots_response.json()["slots"]) == 2 @pytest.mark.asyncio async def test_cannot_cancel_past_appointment(self, client_factory, regular_user): """User cannot cancel a past appointment.""" # Create a past appointment directly in DB async with client_factory.get_db_session() as db: past_time = datetime.now(timezone.utc) - timedelta(hours=1) appointment = Appointment( user_id=regular_user["user"]["id"], slot_start=past_time, slot_end=past_time + timedelta(minutes=15), status=AppointmentStatus.BOOKED, ) db.add(appointment) await db.commit() await db.refresh(appointment) apt_id = appointment.id # Try to cancel async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.post(f"/api/appointments/{apt_id}/cancel") assert response.status_code == 400 assert "past" in response.json()["detail"].lower() # ============================================================================= # Admin Appointments Tests # ============================================================================= class TestAdminViewAppointments: """Test admin viewing all appointments.""" @pytest.mark.asyncio async def test_admin_can_view_all_appointments(self, client_factory, regular_user, admin_user): """Admin can view all appointments.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": "Test"}, ) # Admin views all appointments async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: response = await admin_client.get("/api/admin/appointments") assert response.status_code == 200 data = response.json() assert len(data) >= 1 assert any(apt["note"] == "Test" for apt in data) @pytest.mark.asyncio async def test_regular_user_cannot_view_all_appointments(self, client_factory, regular_user): """Regular user cannot access admin appointments endpoint.""" async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get("/api/admin/appointments") assert response.status_code == 403 @pytest.mark.asyncio async def test_unauthenticated_cannot_view_all_appointments(self, client): """Unauthenticated user cannot view appointments.""" response = await client.get("/api/admin/appointments") assert response.status_code == 401 class TestAdminCancelAppointment: """Test admin cancelling appointments.""" @pytest.mark.asyncio async def test_admin_can_cancel_any_appointment(self, client_factory, regular_user, admin_user): """Admin can cancel any user's appointment.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] # Admin cancels async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: response = await admin_client.post(f"/api/admin/appointments/{apt_id}/cancel") assert response.status_code == 200 data = response.json() assert data["status"] == "cancelled_by_admin" assert data["cancelled_at"] is not None @pytest.mark.asyncio async def test_regular_user_cannot_use_admin_cancel(self, client_factory, regular_user, admin_user): """Regular user cannot use admin cancel endpoint.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] # User tries to use admin cancel endpoint response = await client.post(f"/api/admin/appointments/{apt_id}/cancel") assert response.status_code == 403 @pytest.mark.asyncio async def test_admin_cancel_nonexistent_appointment(self, client_factory, admin_user): """Returns 404 for non-existent appointment.""" async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.post("/api/admin/appointments/99999/cancel") assert response.status_code == 404 @pytest.mark.asyncio async def test_admin_cannot_cancel_already_cancelled(self, client_factory, regular_user, admin_user): """Admin cannot cancel an already cancelled appointment.""" # Admin sets availability async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: await admin_client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # User books async with client_factory.create(cookies=regular_user["cookies"]) as client: book_response = await client.post( "/api/booking", json={"slot_start": f"{tomorrow()}T09:00:00Z"}, ) apt_id = book_response.json()["id"] # User cancels their own appointment await client.post(f"/api/appointments/{apt_id}/cancel") # Admin tries to cancel again async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: response = await admin_client.post(f"/api/admin/appointments/{apt_id}/cancel") assert response.status_code == 400 assert "cancelled_by_user" in response.json()["detail"] @pytest.mark.asyncio async def test_admin_cannot_cancel_past_appointment(self, client_factory, regular_user, admin_user): """Admin cannot cancel a past appointment.""" # Create a past appointment directly in DB async with client_factory.get_db_session() as db: past_time = datetime.now(timezone.utc) - timedelta(hours=1) appointment = Appointment( user_id=regular_user["user"]["id"], slot_start=past_time, slot_end=past_time + timedelta(minutes=15), status=AppointmentStatus.BOOKED, ) db.add(appointment) await db.commit() await db.refresh(appointment) apt_id = appointment.id # Admin tries to cancel async with client_factory.create(cookies=admin_user["cookies"]) as admin_client: response = await admin_client.post(f"/api/admin/appointments/{apt_id}/cancel") assert response.status_code == 400 assert "past" in response.json()["detail"].lower()