Fix: Prevent cancellation of past appointments

Add check to both user and admin cancel endpoints to reject
cancellation of appointments whose slot_start is in the past.
This matches the spec requirement that cancellations can only
happen 'before the appointment'.

Added tests for both user and admin cancel endpoints.

Also includes frontend styling updates.
This commit is contained in:
counterweight 2025-12-21 17:27:23 +01:00
parent 89eec1e9c4
commit 63cf46c230
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
5 changed files with 679 additions and 291 deletions

View file

@ -3,9 +3,11 @@ Booking API Tests
Tests for the user booking endpoints.
"""
from datetime import date, timedelta
from datetime import date, datetime, timedelta, timezone
import pytest
from models import Appointment, AppointmentStatus
def tomorrow() -> date:
return date.today() + timedelta(days=1)
@ -656,6 +658,30 @@ class TestCancelAppointment:
)
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
@ -806,3 +832,27 @@ class TestAdminCancelAppointment:
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()