""" Availability API Tests Tests for the admin availability management endpoints. """ from datetime import date, time, timedelta import pytest def tomorrow() -> date: return date.today() + timedelta(days=1) def in_days(n: int) -> date: return date.today() + timedelta(days=n) # ============================================================================= # Permission Tests # ============================================================================= class TestAvailabilityPermissions: """Test that only admins can access availability endpoints.""" @pytest.mark.asyncio async def test_admin_can_get_availability(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.get( "/api/admin/availability", params={"from": str(tomorrow()), "to": str(in_days(7))}, ) assert response.status_code == 200 @pytest.mark.asyncio async def test_admin_can_set_availability(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 200 @pytest.mark.asyncio async def test_regular_user_cannot_get_availability(self, client_factory, regular_user): async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.get( "/api/admin/availability", params={"from": str(tomorrow()), "to": str(in_days(7))}, ) assert response.status_code == 403 @pytest.mark.asyncio async def test_regular_user_cannot_set_availability(self, client_factory, regular_user): async with client_factory.create(cookies=regular_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 403 @pytest.mark.asyncio async def test_unauthenticated_cannot_get_availability(self, client): response = await client.get( "/api/admin/availability", params={"from": str(tomorrow()), "to": str(in_days(7))}, ) assert response.status_code == 401 @pytest.mark.asyncio async def test_unauthenticated_cannot_set_availability(self, client): response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 401 # ============================================================================= # Set Availability Tests # ============================================================================= class TestSetAvailability: """Test setting availability for a date.""" @pytest.mark.asyncio async def test_set_single_slot(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 200 data = response.json() assert data["date"] == str(tomorrow()) assert len(data["slots"]) == 1 assert data["slots"][0]["start_time"] == "09:00:00" assert data["slots"][0]["end_time"] == "12:00:00" @pytest.mark.asyncio async def test_set_multiple_slots(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [ {"start_time": "09:00:00", "end_time": "12:00:00"}, {"start_time": "14:00:00", "end_time": "17:00:00"}, ], }, ) assert response.status_code == 200 data = response.json() assert len(data["slots"]) == 2 @pytest.mark.asyncio async def test_set_empty_slots_clears_availability(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # First set some availability await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # Then clear it response = await client.put( "/api/admin/availability", json={"date": str(tomorrow()), "slots": []}, ) assert response.status_code == 200 data = response.json() assert len(data["slots"]) == 0 @pytest.mark.asyncio async def test_set_replaces_existing_availability(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set initial availability await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # Replace with different slots response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "14:00:00", "end_time": "16:00:00"}], }, ) # Verify the replacement get_response = await client.get( "/api/admin/availability", params={"from": str(tomorrow()), "to": str(tomorrow())}, ) data = get_response.json() assert len(data["days"]) == 1 assert len(data["days"][0]["slots"]) == 1 assert data["days"][0]["slots"][0]["start_time"] == "14:00:00" # ============================================================================= # Validation Tests # ============================================================================= class TestAvailabilityValidation: """Test validation rules for availability.""" @pytest.mark.asyncio async def test_cannot_set_past_date(self, client_factory, admin_user): yesterday = date.today() - timedelta(days=1) async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(yesterday), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 400 assert "past" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_cannot_set_today(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(date.today()), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 400 assert "past" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_cannot_set_beyond_30_days(self, client_factory, admin_user): too_far = in_days(31) async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(too_far), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 400 assert "30" in response.json()["detail"] @pytest.mark.asyncio async def test_time_must_be_15min_boundary(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "09:05:00", "end_time": "12:00:00"}], }, ) assert response.status_code == 422 # Pydantic validation error assert "15-minute" in response.json()["detail"][0]["msg"] @pytest.mark.asyncio async def test_end_time_must_be_after_start_time(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [{"start_time": "12:00:00", "end_time": "09:00:00"}], }, ) assert response.status_code == 400 assert "after" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_slots_cannot_overlap(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.put( "/api/admin/availability", json={ "date": str(tomorrow()), "slots": [ {"start_time": "09:00:00", "end_time": "12:00:00"}, {"start_time": "11:00:00", "end_time": "14:00:00"}, ], }, ) assert response.status_code == 400 assert "overlap" in response.json()["detail"].lower() # ============================================================================= # Get Availability Tests # ============================================================================= class TestGetAvailability: """Test retrieving availability.""" @pytest.mark.asyncio async def test_get_empty_availability(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.get( "/api/admin/availability", params={"from": str(tomorrow()), "to": str(in_days(7))}, ) assert response.status_code == 200 data = response.json() assert data["days"] == [] @pytest.mark.asyncio async def test_get_availability_in_range(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set availability for multiple days for i in range(1, 4): await client.put( "/api/admin/availability", json={ "date": str(in_days(i)), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # Get range that includes all response = await client.get( "/api/admin/availability", params={"from": str(in_days(1)), "to": str(in_days(3))}, ) assert response.status_code == 200 data = response.json() assert len(data["days"]) == 3 @pytest.mark.asyncio async def test_get_availability_partial_range(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set availability for 5 days for i in range(1, 6): await client.put( "/api/admin/availability", json={ "date": str(in_days(i)), "slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}], }, ) # Get only a subset response = await client.get( "/api/admin/availability", params={"from": str(in_days(2)), "to": str(in_days(4))}, ) assert response.status_code == 200 data = response.json() assert len(data["days"]) == 3 @pytest.mark.asyncio async def test_get_availability_invalid_range(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.get( "/api/admin/availability", params={"from": str(in_days(7)), "to": str(in_days(1))}, ) assert response.status_code == 400 assert "before" in response.json()["detail"].lower() # ============================================================================= # Copy Availability Tests # ============================================================================= class TestCopyAvailability: """Test copying availability from one day to others.""" @pytest.mark.asyncio async def test_copy_to_single_day(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set source availability await client.put( "/api/admin/availability", json={ "date": str(in_days(1)), "slots": [ {"start_time": "09:00:00", "end_time": "12:00:00"}, {"start_time": "14:00:00", "end_time": "17:00:00"}, ], }, ) # Copy to another day response = await client.post( "/api/admin/availability/copy", json={ "source_date": str(in_days(1)), "target_dates": [str(in_days(2))], }, ) assert response.status_code == 200 data = response.json() assert len(data["days"]) == 1 assert data["days"][0]["date"] == str(in_days(2)) assert len(data["days"][0]["slots"]) == 2 @pytest.mark.asyncio async def test_copy_to_multiple_days(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set source availability await client.put( "/api/admin/availability", json={ "date": str(in_days(1)), "slots": [{"start_time": "10:00:00", "end_time": "11:00:00"}], }, ) # Copy to multiple days response = await client.post( "/api/admin/availability/copy", json={ "source_date": str(in_days(1)), "target_dates": [str(in_days(2)), str(in_days(3)), str(in_days(4))], }, ) assert response.status_code == 200 data = response.json() assert len(data["days"]) == 3 @pytest.mark.asyncio async def test_copy_replaces_existing(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set different availability on target await client.put( "/api/admin/availability", json={ "date": str(in_days(2)), "slots": [{"start_time": "08:00:00", "end_time": "09:00:00"}], }, ) # Set source availability await client.put( "/api/admin/availability", json={ "date": str(in_days(1)), "slots": [{"start_time": "14:00:00", "end_time": "15:00:00"}], }, ) # Copy (should replace) await client.post( "/api/admin/availability/copy", json={ "source_date": str(in_days(1)), "target_dates": [str(in_days(2))], }, ) # Verify target was replaced response = await client.get( "/api/admin/availability", params={"from": str(in_days(2)), "to": str(in_days(2))}, ) data = response.json() assert len(data["days"]) == 1 assert len(data["days"][0]["slots"]) == 1 assert data["days"][0]["slots"][0]["start_time"] == "14:00:00" @pytest.mark.asyncio async def test_copy_no_source_availability(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: response = await client.post( "/api/admin/availability/copy", json={ "source_date": str(in_days(1)), "target_dates": [str(in_days(2))], }, ) assert response.status_code == 400 assert "no availability" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_copy_skips_self(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set source availability await client.put( "/api/admin/availability", json={ "date": str(in_days(1)), "slots": [{"start_time": "09:00:00", "end_time": "10:00:00"}], }, ) # Copy including self in targets response = await client.post( "/api/admin/availability/copy", json={ "source_date": str(in_days(1)), "target_dates": [str(in_days(1)), str(in_days(2))], }, ) assert response.status_code == 200 data = response.json() # Should only have copied to day 2, not day 1 (self) assert len(data["days"]) == 1 assert data["days"][0]["date"] == str(in_days(2)) @pytest.mark.asyncio async def test_copy_validates_target_dates(self, client_factory, admin_user): async with client_factory.create(cookies=admin_user["cookies"]) as client: # Set source availability await client.put( "/api/admin/availability", json={ "date": str(in_days(1)), "slots": [{"start_time": "09:00:00", "end_time": "10:00:00"}], }, ) # Try to copy to a date beyond 30 days response = await client.post( "/api/admin/availability/copy", json={ "source_date": str(in_days(1)), "target_dates": [str(in_days(31))], }, ) assert response.status_code == 400 assert "30" in response.json()["detail"]