Add ruff linter/formatter for Python
- Add ruff as dev dependency - Configure ruff in pyproject.toml with strict 88-char line limit - Ignore B008 (FastAPI Depends pattern is standard) - Allow longer lines in tests for readability - Fix all lint issues in source files - Add Makefile targets: lint-backend, format-backend, fix-backend
This commit is contained in:
parent
69bc8413e0
commit
6c218130e9
31 changed files with 1234 additions and 876 deletions
|
|
@ -3,7 +3,9 @@ Booking API Tests
|
|||
|
||||
Tests for the user booking endpoints.
|
||||
"""
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from models import Appointment, AppointmentStatus
|
||||
|
|
@ -21,11 +23,14 @@ def in_days(n: int) -> date:
|
|||
# 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):
|
||||
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:
|
||||
|
|
@ -36,15 +41,19 @@ class TestBookingPermissions:
|
|||
"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())})
|
||||
|
||||
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):
|
||||
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:
|
||||
|
|
@ -55,22 +64,24 @@ class TestBookingPermissions:
|
|||
"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())})
|
||||
|
||||
response = await client.get(
|
||||
"/api/booking/slots", params={"date": str(tomorrow())}
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -85,18 +96,20 @@ class TestBookingPermissions:
|
|||
"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())})
|
||||
response = await client.get(
|
||||
"/api/booking/slots", params={"date": str(tomorrow())}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -113,6 +126,7 @@ class TestBookingPermissions:
|
|||
# Get Slots Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestGetSlots:
|
||||
"""Test getting available booking slots."""
|
||||
|
||||
|
|
@ -120,15 +134,19 @@ class TestGetSlots:
|
|||
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())})
|
||||
|
||||
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):
|
||||
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:
|
||||
|
|
@ -139,15 +157,17 @@ class TestGetSlots:
|
|||
"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())})
|
||||
|
||||
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"]
|
||||
|
|
@ -156,7 +176,9 @@ class TestGetSlots:
|
|||
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):
|
||||
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:
|
||||
|
|
@ -167,17 +189,19 @@ class TestGetSlots:
|
|||
"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())})
|
||||
|
||||
response = await client.get(
|
||||
"/api/booking/slots", params={"date": str(tomorrow())}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["slots"]) == 3
|
||||
|
|
@ -189,6 +213,7 @@ class TestGetSlots:
|
|||
# Booking Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestCreateBooking:
|
||||
"""Test creating bookings."""
|
||||
|
||||
|
|
@ -204,7 +229,7 @@ class TestCreateBooking:
|
|||
"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(
|
||||
|
|
@ -214,7 +239,7 @@ class TestCreateBooking:
|
|||
"note": "Discussion about project",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user_id"] == regular_user["user"]["id"]
|
||||
|
|
@ -235,20 +260,22 @@ class TestCreateBooking:
|
|||
"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):
|
||||
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:
|
||||
|
|
@ -259,7 +286,7 @@ class TestCreateBooking:
|
|||
"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(
|
||||
|
|
@ -267,19 +294,21 @@ class TestCreateBooking:
|
|||
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):
|
||||
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:
|
||||
|
|
@ -290,14 +319,14 @@ class TestCreateBooking:
|
|||
"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"]
|
||||
|
||||
|
|
@ -306,6 +335,7 @@ class TestCreateBooking:
|
|||
# Date Validation Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestBookingDateValidation:
|
||||
"""Test date validation for bookings."""
|
||||
|
||||
|
|
@ -317,9 +347,12 @@ class TestBookingDateValidation:
|
|||
"/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()
|
||||
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):
|
||||
|
|
@ -330,7 +363,7 @@ class TestBookingDateValidation:
|
|||
"/api/booking",
|
||||
json={"slot_start": f"{yesterday}T09:00:00Z"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -342,7 +375,7 @@ class TestBookingDateValidation:
|
|||
"/api/booking",
|
||||
json={"slot_start": f"{too_far}T09:00:00Z"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "30" in response.json()["detail"]
|
||||
|
||||
|
|
@ -350,8 +383,10 @@ class TestBookingDateValidation:
|
|||
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())})
|
||||
|
||||
response = await client.get(
|
||||
"/api/booking/slots", params={"date": str(date.today())}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -359,8 +394,10 @@ class TestBookingDateValidation:
|
|||
"""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)})
|
||||
|
||||
response = await client.get(
|
||||
"/api/booking/slots", params={"date": str(yesterday)}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
|
|
@ -368,11 +405,14 @@ class TestBookingDateValidation:
|
|||
# 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):
|
||||
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:
|
||||
|
|
@ -383,14 +423,14 @@ class TestBookingTimeValidation:
|
|||
"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"]
|
||||
|
||||
|
|
@ -399,6 +439,7 @@ class TestBookingTimeValidation:
|
|||
# Note Validation Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestBookingNoteValidation:
|
||||
"""Test note validation for bookings."""
|
||||
|
||||
|
|
@ -414,7 +455,7 @@ class TestBookingNoteValidation:
|
|||
"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:
|
||||
|
|
@ -422,11 +463,13 @@ class TestBookingNoteValidation:
|
|||
"/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):
|
||||
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:
|
||||
|
|
@ -437,7 +480,7 @@ class TestBookingNoteValidation:
|
|||
"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:
|
||||
|
|
@ -445,7 +488,7 @@ class TestBookingNoteValidation:
|
|||
"/api/booking",
|
||||
json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": note},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["note"] == note
|
||||
|
||||
|
|
@ -454,6 +497,7 @@ class TestBookingNoteValidation:
|
|||
# User Appointments Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestUserAppointments:
|
||||
"""Test user appointments endpoints."""
|
||||
|
||||
|
|
@ -462,12 +506,14 @@ class TestUserAppointments:
|
|||
"""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):
|
||||
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:
|
||||
|
|
@ -478,7 +524,7 @@ class TestUserAppointments:
|
|||
"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(
|
||||
|
|
@ -489,10 +535,10 @@ class TestUserAppointments:
|
|||
"/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
|
||||
|
|
@ -502,11 +548,13 @@ class TestUserAppointments:
|
|||
assert "Second" in notes
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_cannot_view_user_appointments(self, client_factory, admin_user):
|
||||
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
|
||||
|
|
@ -520,7 +568,9 @@ class TestCancelAppointment:
|
|||
"""Test cancelling appointments."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_own_appointment(self, client_factory, regular_user, admin_user):
|
||||
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:
|
||||
|
|
@ -531,7 +581,7 @@ class TestCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -539,17 +589,19 @@ class TestCancelAppointment:
|
|||
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):
|
||||
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:
|
||||
|
|
@ -560,7 +612,7 @@ class TestCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -568,24 +620,28 @@ class TestCancelAppointment:
|
|||
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):
|
||||
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):
|
||||
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:
|
||||
|
|
@ -596,7 +652,7 @@ class TestCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -605,23 +661,27 @@ class TestCancelAppointment:
|
|||
)
|
||||
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):
|
||||
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):
|
||||
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:
|
||||
|
|
@ -632,7 +692,7 @@ class TestCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -640,17 +700,17 @@ class TestCancelAppointment:
|
|||
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",
|
||||
|
|
@ -663,7 +723,7 @@ class TestCancelAppointment:
|
|||
"""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)
|
||||
past_time = datetime.now(UTC) - timedelta(hours=1)
|
||||
appointment = Appointment(
|
||||
user_id=regular_user["user"]["id"],
|
||||
slot_start=past_time,
|
||||
|
|
@ -674,11 +734,11 @@ class TestCancelAppointment:
|
|||
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()
|
||||
|
||||
|
|
@ -687,11 +747,14 @@ class TestCancelAppointment:
|
|||
# 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):
|
||||
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:
|
||||
|
|
@ -702,18 +765,18 @@ class TestAdminViewAppointments:
|
|||
"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
|
||||
|
|
@ -725,11 +788,13 @@ class TestAdminViewAppointments:
|
|||
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):
|
||||
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
|
||||
|
|
@ -743,7 +808,9 @@ class TestAdminCancelAppointment:
|
|||
"""Test admin cancelling appointments."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_can_cancel_any_appointment(self, client_factory, regular_user, admin_user):
|
||||
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:
|
||||
|
|
@ -754,7 +821,7 @@ class TestAdminCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -762,18 +829,22 @@ class TestAdminCancelAppointment:
|
|||
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")
|
||||
|
||||
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):
|
||||
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:
|
||||
|
|
@ -784,7 +855,7 @@ class TestAdminCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -792,22 +863,26 @@ class TestAdminCancelAppointment:
|
|||
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):
|
||||
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):
|
||||
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:
|
||||
|
|
@ -818,7 +893,7 @@ class TestAdminCancelAppointment:
|
|||
"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(
|
||||
|
|
@ -826,23 +901,27 @@ class TestAdminCancelAppointment:
|
|||
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")
|
||||
|
||||
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):
|
||||
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)
|
||||
past_time = datetime.now(UTC) - timedelta(hours=1)
|
||||
appointment = Appointment(
|
||||
user_id=regular_user["user"]["id"],
|
||||
slot_start=past_time,
|
||||
|
|
@ -853,11 +932,12 @@ class TestAdminCancelAppointment:
|
|||
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")
|
||||
|
||||
response = await admin_client.post(
|
||||
f"/api/admin/appointments/{apt_id}/cancel"
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "past" in response.json()["detail"].lower()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue