"""Unit tests for slot generation and calendar logic — no HTTP involved.""" import asyncio from datetime import date, time, timedelta from unittest.mock import AsyncMock, MagicMock import pytest from app.services.slot_service import generate_slots, _fmt_time # type: ignore from app.exceptions import AppError def make_mock_db(schedule=None, blocked=None): db = AsyncMock() db.transaction = MagicMock(return_value=_TxCtx()) db.fetch = AsyncMock(side_effect=lambda q, *a: _route_fetch(q, schedule, blocked)) db.fetchval = AsyncMock(return_value=None) db.execute = AsyncMock(return_value="INSERT 1") return db def _route_fetch(query, schedule, blocked): if "weekly_schedule" in query: return schedule or [] if "blocked_dates" in query: return blocked or [] return [] class _TxCtx: async def __aenter__(self): return self async def __aexit__(self, *a): return False MONDAY = 0 # weekday() value for Monday SCHED_ROW = { "day_of_week": MONDAY, "start_time": time(9, 0), "end_time": time(11, 0), "slot_duration_minutes": 60, } async def test_generate_slots_creates_correct_count(): """Mon 09:00–11:00 with 60-min slots → 2 slots per Monday.""" db = make_mock_db(schedule=[SCHED_ROW]) # Find the next Monday today = date.today() days_ahead = (7 - today.weekday()) % 7 or 7 next_monday = today + timedelta(days=days_ahead) db.fetchval = AsyncMock(return_value=None) # no existing slots created = await generate_slots(db, next_monday, next_monday) assert created == 2 async def test_generate_slots_skips_blocked_dates(): """A blocked Monday produces 0 slots.""" today = date.today() days_ahead = (7 - today.weekday()) % 7 or 7 next_monday = today + timedelta(days=days_ahead) blocked = [{"date": next_monday}] db = make_mock_db(schedule=[SCHED_ROW], blocked=blocked) db.fetchval = AsyncMock(return_value=None) created = await generate_slots(db, next_monday, next_monday) assert created == 0 async def test_generate_slots_skips_existing_slots(): """If a slot already exists, it is not duplicated.""" today = date.today() days_ahead = (7 - today.weekday()) % 7 or 7 next_monday = today + timedelta(days=days_ahead) db = make_mock_db(schedule=[SCHED_ROW]) db.fetchval = AsyncMock(return_value=1) # all slots already exist created = await generate_slots(db, next_monday, next_monday) assert created == 0 async def test_generate_slots_range_over_90_days_rejected(): with pytest.raises(AppError) as exc_info: db = AsyncMock() await generate_slots(db, date(2026, 1, 1), date(2026, 5, 1)) assert exc_info.value.code == "RANGE_TOO_LARGE" async def test_generate_slots_no_schedule_raises(): db = make_mock_db(schedule=[]) with pytest.raises(AppError) as exc_info: await generate_slots(db, date(2026, 6, 1), date(2026, 6, 7)) assert exc_info.value.code == "NO_SCHEDULE" # ── Time formatting ─────────────────────────────────────────────────────────── def test_fmt_time_strips_seconds(): from datetime import time as t assert _fmt_time(t(10, 0, 0)) == "10:00" assert _fmt_time(t(9, 30)) == "09:30" def test_fmt_time_string_input(): assert _fmt_time("14:30:00") == "14:30" assert _fmt_time("08:00") == "08:00" def test_fmt_time_none(): assert _fmt_time(None) == ""