stackchain-dashboard/tests/test_later_store.py
timmy 27fc74ced6
All checks were successful
CI / lint (pull_request) Successful in 41s
CI / build-release (pull_request) Successful in 4s
CI / release-candidate (pull_request) Has been skipped
fix: keep Later plans conflict-safe across devices (#377)
2026-08-09 06:37:30 +00:00

221 lines
7.5 KiB
Python

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"]
def test_batch_rejects_stale_same_item_intent_without_blocking_other_items(tmp_path):
store = LaterStore(tmp_path / "later.sqlite3")
first = store.apply_batch("timmy", [{
"operation_id": "newer-device",
"action": "defer",
"item_id": "issue:r:1:",
"wake_at": "2026-08-12T09:00:00.000Z",
"base_revision": 0,
}])
replay = store.apply_batch("timmy", [
{
"operation_id": "stale-device",
"action": "defer",
"item_id": "issue:r:1:",
"wake_at": "2026-08-10T09:00:00.000Z",
"base_revision": 0,
},
{
"operation_id": "unrelated-item",
"action": "defer",
"item_id": "issue:r:2:",
"wake_at": "2026-08-11T09:00:00.000Z",
"base_revision": 0,
},
])
assert first["revision"] == 1
assert replay == {
"revision": 2,
"records": {
"issue:r:1:": "2026-08-12T09:00:00.000Z",
"issue:r:2:": "2026-08-11T09:00:00.000Z",
},
"accepted_operation_ids": ["unrelated-item"],
"duplicate_operation_ids": [],
"rejected_operations": [
{"operation_id": "stale-device", "reason": "stale_intent"}
],
}
duplicate = store.apply_batch("timmy", [{
"operation_id": "stale-device",
"action": "restore",
"item_id": "issue:r:2:",
"base_revision": 2,
}])
assert duplicate["duplicate_operation_ids"] == ["stale-device"]
assert duplicate["revision"] == 2
assert duplicate["records"] == replay["records"]
@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"],
},
)
stale = await client.patch(
"/api/v1/later",
json={
"operations": [{
"operation_id": "offline-stale",
"action": "restore",
"item_id": "issue:stackchain/dashboard:363:",
"base_revision": 0,
}],
},
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 stale.json()["rejected_operations"] == [
{"operation_id": "offline-stale", "reason": "stale_intent"}
]
assert stale.json()["revision"] == 2
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"