import pytest import httpx from unittest.mock import patch, AsyncMock from datetime import date from room_cal.api import create_app from room_cal.api._routes import _serialize # ── _serialize ───────────────────────────────────────────────────────────────── def test_serialize_date(): assert _serialize(date(2026, 1, 15)) == '2026-01-15' def test_serialize_nested_dict(): result = _serialize({'a': date(2026, 1, 1), 'b': 42}) assert result == {'a': '2026-01-01', 'b': 42} def test_serialize_list(): result = _serialize([date(2026, 2, 1), 'x', 7]) assert result == ['2026-02-01', 'x', 7] def test_serialize_passthrough(): assert _serialize(42) == 42 assert _serialize('hello') == 'hello' assert _serialize(None) is None # ── app factory ──────────────────────────────────────────────────────────────── def test_create_app(): from fastapi import FastAPI assert isinstance(create_app(), FastAPI) # ── HTTP tests ───────────────────────────────────────────────────────────────── @pytest.fixture def app(): return create_app() @pytest.fixture async def client(app): async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url='http://test') as c: yield c _AUTHED = {'sub': 'u', 'email': 'u@test.com', 'name': 'User'} @pytest.mark.asyncio async def test_calc_net_to_list(client): r = await client.get('/calc/net_to_list?service=airbnb&amount=100') assert r.status_code == 200 data = r.json() assert data['net'] == 100 assert data['list'] > data['net'] assert 'retail' in data assert 'details' in data @pytest.mark.asyncio async def test_calc_unknown_transformation(client): r = await client.get('/calc/bad_transform?service=airbnb&amount=100') assert r.status_code == 404 @pytest.mark.asyncio async def test_calc_bad_service(client): r = await client.get('/calc/net_to_list?service=nonexistent&amount=100') assert r.status_code == 400 @pytest.mark.asyncio async def test_auth_middleware_calc_is_public(client): # /calc/ prefix is public — no auth cookie needed r = await client.get('/calc/net_to_list?service=airbnb&amount=50') assert r.status_code == 200 @pytest.mark.asyncio async def test_auth_middleware_private_api_returns_401(client): r = await client.get('/prices/2026-01-01') assert r.status_code == 401 @pytest.mark.asyncio async def test_auth_middleware_html_path_redirects_to_login(client): r = await client.get('/calendar', follow_redirects=False) assert r.status_code in (302, 307) assert '/auth/login' in r.headers['location'] @pytest.mark.asyncio async def test_workflow_discount_invalid_type(client): with patch('room_cal.api.get_user', return_value=_AUTHED): r = await client.post('/workflow/discount/2026-01-15', json={'type': 'bad', 'value': 10}) assert r.status_code == 422 @pytest.mark.asyncio async def test_action_dispatch_unknown_action(client): with patch('room_cal.api.get_user', return_value=_AUTHED): r = await client.post('/action/nonexistent-action/2026-01-15') assert r.status_code == 404 @pytest.mark.asyncio async def test_reload_clears_caches(client): import room_cal.cal as _cal cal = _cal.get_calendar() cal._event_cache[('fake_key',)] = [] cal._cached_url = 'http://example.com/cal' with patch('room_cal.api.get_user', return_value=_AUTHED): r = await client.post('/reload') assert r.status_code == 200 data = r.json() assert 'cleared_calendars' in data assert data['cleared_calendars'] >= 1 assert cal._event_cache == {} assert cal._cached_url is None