import sqlite3 import pytest import httpx from src import main from src.today_store import TodayPlanFull, TodayStore @pytest.mark.anyio async def test_transient_today_store_failure_tells_clients_when_to_retry(monkeypatch): async def user(): return {"login": "timmy"} class BusyStore: def apply(self, *_args, **_kwargs): raise sqlite3.OperationalError("database is locked") monkeypatch.setattr(main, "current_user", user) monkeypatch.setattr(main, "_today_store", lambda: BusyStore()) payload = main.TodayOperation( operation_id="retry-me", action="add", item_id="issue:r:1:" ) with pytest.raises(main.HTTPException) as raised: await main.update_today_plan(payload) assert raised.value.status_code == 503 assert raised.value.headers == {"Retry-After": "1"} def test_operations_are_durable_ordered_idempotent_and_account_scoped(tmp_path): path = tmp_path / "today.sqlite3" store = TodayStore(path, limit=3) assert store.apply("timmy", "op-1", "add", "issue:stackchain/dashboard:1:") == { "revision": 1, "ids": ["issue:stackchain/dashboard:1:"], } store.apply("timmy", "op-2", "add", "issue:stackchain/dashboard:2:") store.apply("timmy", "op-3", "add", "issue:stackchain/dashboard:3:") moved = store.apply( "timmy", "op-4", "move", "issue:stackchain/dashboard:3:", direction="up" ) duplicate = store.apply( "timmy", "op-4", "move", "issue:stackchain/dashboard:3:", direction="up" ) assert moved == duplicate == { "revision": 4, "ids": [ "issue:stackchain/dashboard:1:", "issue:stackchain/dashboard:3:", "issue:stackchain/dashboard:2:", ], } assert TodayStore(path, limit=3).get("timmy") == moved assert store.get("alexander") == {"revision": 0, "ids": []} def test_limit_is_atomic_and_remove_frees_capacity(tmp_path): store = TodayStore(tmp_path / "today.sqlite3", limit=2) store.apply("timmy", "one", "add", "issue:r:1:") store.apply("timmy", "two", "add", "issue:r:2:") with pytest.raises(TodayPlanFull): store.apply("timmy", "three", "add", "issue:r:3:") assert store.get("timmy") == { "revision": 2, "ids": ["issue:r:1:", "issue:r:2:"], } store.apply("timmy", "remove", "remove", "issue:r:1:") assert store.apply("timmy", "retry-three", "add", "issue:r:3:")["ids"] == [ "issue:r:2:", "issue:r:3:", ] def test_batch_applies_ordered_operations_once_and_rejects_only_capacity_conflicts(tmp_path): store = TodayStore(tmp_path / "today.sqlite3", limit=2) result = store.apply_batch( "timmy", [ {"operation_id": "one", "action": "add", "item_id": "issue:r:1:"}, {"operation_id": "two", "action": "add", "item_id": "issue:r:2:"}, {"operation_id": "full", "action": "add", "item_id": "issue:r:3:"}, {"operation_id": "remove", "action": "remove", "item_id": "issue:r:1:"}, {"operation_id": "three", "action": "add", "item_id": "issue:r:3:"}, ], ) assert result == { "revision": 4, "ids": ["issue:r:2:", "issue:r:3:"], "accepted_operation_ids": ["one", "two", "remove", "three"], "duplicate_operation_ids": [], "rejected_operations": [{"operation_id": "full", "reason": "today_full"}], } replay = store.apply_batch("timmy", [ {"operation_id": "two", "action": "add", "item_id": "issue:r:2:"}, {"operation_id": "three", "action": "add", "item_id": "issue:r:3:"}, ]) assert replay["revision"] == 4 assert replay["accepted_operation_ids"] == [] assert replay["duplicate_operation_ids"] == ["two", "three"] @pytest.mark.anyio async def test_authenticated_today_api_uses_confirmed_account_and_csrf(monkeypatch, tmp_path): monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator") monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "correct horse battery staple") monkeypatch.setenv( "STACKCHAIN_DASHBOARD_SESSION_SECRET", "a-separate-session-signing-secret-with-enough-entropy", ) monkeypatch.setenv("STACKCHAIN_SESSION_DB", str(tmp_path / "sessions.sqlite3")) monkeypatch.setenv("STACKCHAIN_LOGIN_ATTEMPT_DB", str(tmp_path / "login.sqlite3")) monkeypatch.setenv("STACKCHAIN_TODAY_DB", str(tmp_path / "today.sqlite3")) async def user(): return {"id": 1, "login": "Timmy"} monkeypatch.setattr(main, "current_user", user) transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="https://test") as client: await client.post( "/api/v1/session", json={"access_token": "correct horse battery staple"} ) forbidden = await client.patch( "/api/v1/today", json={ "operation_id": "mobile-1", "action": "add", "item_id": "issue:stackchain/dashboard:357:", }, ) changed = await client.patch( "/api/v1/today", json={ "operations": [ {"operation_id": "mobile-1", "action": "add", "item_id": "issue:stackchain/dashboard:357:"}, {"operation_id": "mobile-2", "action": "add", "item_id": "issue:stackchain/dashboard:359:"}, ], }, headers={ "Origin": "https://test", "X-CSRF-Token": client.cookies["stackchain_csrf"], }, ) fetched = await client.get("/api/v1/today") assert forbidden.status_code == 403 assert changed.status_code == 200 assert changed.json() == { "revision": 2, "ids": ["issue:stackchain/dashboard:357:", "issue:stackchain/dashboard:359:"], "accepted_operation_ids": ["mobile-1", "mobile-2"], "duplicate_operation_ids": [], "rejected_operations": [], } assert fetched.json() == {"revision": 2, "ids": ["issue:stackchain/dashboard:357:", "issue:stackchain/dashboard:359:"]} assert fetched.headers["cache-control"] == "no-store"