import sqlite3 import httpx import pytest from src import main from src.later_store import LaterStore @pytest.mark.anyio async def test_transient_later_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 busy") monkeypatch.setattr(main, "current_user", user) monkeypatch.setattr(main, "_later_store", lambda: BusyStore()) payload = main.LaterOperation( operation_id="retry-me", action="defer", item_id="issue:r:1:", wake_at="2026-08-10T09:00:00.000Z", ) with pytest.raises(main.HTTPException) as raised: await main.update_later_plan(payload) assert raised.value.status_code == 503 assert raised.value.headers == {"Retry-After": "1"} def test_deferrals_are_durable_revisioned_idempotent_and_account_scoped(tmp_path): path = tmp_path / "later.sqlite3" store = LaterStore(path) deferred = store.apply( "Timmy", "op-1", "defer", "issue:stackchain/dashboard:363:", wake_at="2026-08-10T09:00:00.000Z", ) duplicate = store.apply( "timmy", "op-1", "defer", "issue:stackchain/dashboard:363:", wake_at="2026-08-11T09:00:00.000Z", ) assert duplicate == deferred == { "revision": 1, "records": { "issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z" }, } assert LaterStore(path).get("timmy") == deferred assert store.get("alexander") == {"revision": 0, "records": {}} assert store.apply( "timmy", "op-2", "restore", "issue:stackchain/dashboard:363:" ) == {"revision": 2, "records": {}} def test_batch_applies_in_one_ordered_idempotent_unit(tmp_path): store = LaterStore(tmp_path / "later.sqlite3") operations = [ {"operation_id": "first", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-10T09:00:00.000Z"}, {"operation_id": "second", "action": "restore", "item_id": "issue:r:1:"}, ] result = store.apply_batch("timmy", operations) assert result == { "revision": 2, "records": {}, "accepted_operation_ids": ["first", "second"], "duplicate_operation_ids": [], "rejected_operations": [], } replay = store.apply_batch("timmy", operations) assert replay["revision"] == 2 assert replay["accepted_operation_ids"] == [] assert replay["duplicate_operation_ids"] == ["first", "second"] @pytest.mark.anyio async def test_authenticated_later_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_LATER_DB", str(tmp_path / "later.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/later", json={ "operation_id": "mobile-1", "action": "defer", "item_id": "issue:stackchain/dashboard:363:", "wake_at": "2026-08-10T09:00:00.000Z", }, ) changed = await client.patch( "/api/v1/later", json={ "operations": [ {"operation_id": "mobile-1", "action": "defer", "item_id": "issue:stackchain/dashboard:363:", "wake_at": "2026-08-10T09:00:00.000Z"}, {"operation_id": "mobile-2", "action": "defer", "item_id": "issue:stackchain/dashboard:365:", "wake_at": "2026-08-11T09:00:00.000Z"}, ], }, headers={ "Origin": "https://test", "X-CSRF-Token": client.cookies["stackchain_csrf"], }, ) fetched = await client.get("/api/v1/later") assert forbidden.status_code == 403 assert changed.status_code == 200 assert changed.json() == { "revision": 2, "records": { "issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z", "issue:stackchain/dashboard:365:": "2026-08-11T09:00:00.000Z", }, "accepted_operation_ids": ["mobile-1", "mobile-2"], "duplicate_operation_ids": [], "rejected_operations": [], } assert fetched.json() == {"revision": 2, "records": { "issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z", "issue:stackchain/dashboard:365:": "2026-08-11T09:00:00.000Z", }} assert fetched.headers["cache-control"] == "no-store"