arbret/backend/tests/test_availability.py

527 lines
20 KiB
Python
Raw Normal View History

"""
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"]