From edc292986fa8fc31afeee0d20296f3cb74c7f843 Mon Sep 17 00:00:00 2001 From: counterweight Date: Mon, 22 Dec 2025 20:23:09 +0100 Subject: [PATCH] Fix: Delete deprecated test_booking.py --- backend/tests/test_booking.py | 943 ---------------------------------- 1 file changed, 943 deletions(-) delete mode 100644 backend/tests/test_booking.py diff --git a/backend/tests/test_booking.py b/backend/tests/test_booking.py deleted file mode 100644 index ef58872..0000000 --- a/backend/tests/test_booking.py +++ /dev/null @@ -1,943 +0,0 @@ -""" -Booking API Tests - -Tests for the user booking endpoints. -""" - -from datetime import UTC, date, datetime, timedelta - -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 any available time ranges" 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(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() - # Paginated response - assert "records" in data - assert "total" in data - assert "page" in data - assert "per_page" in data - assert len(data["records"]) >= 1 - assert any(apt["note"] == "Test" for apt in data["records"]) - - @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(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()