93 lines
4.2 KiB
Python
93 lines
4.2 KiB
Python
import httpx
|
|
import pytest
|
|
|
|
from src import main
|
|
from src.unfiled_draft_store import UnfiledDraftConflict, UnfiledDraftStore
|
|
|
|
|
|
def draft(draft_id="phone-capture", *, title="Broken checkout", evidence=None):
|
|
return {
|
|
"id": draft_id,
|
|
"title": title,
|
|
"body": "Steps from the field",
|
|
"saved_at": 1_723_600_000_000,
|
|
"blockers": [{"repository": "stackchain/api", "number": 7, "title": "API rollout"}],
|
|
"evidence": (evidence if evidence is not None else [
|
|
{
|
|
"filename": "checkout.png",
|
|
"content_type": "image/png",
|
|
"note": "Error after tapping Pay",
|
|
"data": "cG5nLWJ5dGVz",
|
|
}
|
|
]),
|
|
}
|
|
|
|
|
|
def test_unfiled_drafts_are_revisioned_ordered_and_account_scoped(tmp_path):
|
|
store = UnfiledDraftStore(tmp_path / "unfiled.sqlite3")
|
|
|
|
created = store.replace(" Timmy ", 0, [draft(), draft("second", title="Second")])
|
|
|
|
assert created == {"revision": 1, "drafts": [draft(), draft("second", title="Second")]}
|
|
assert store.get("timmy") == created
|
|
assert store.get("alexander") == {"revision": 0, "drafts": []}
|
|
with pytest.raises(UnfiledDraftConflict) as conflict:
|
|
store.replace("timmy", 0, [draft(title="Stale overwrite")])
|
|
assert conflict.value.snapshot == created
|
|
|
|
|
|
def test_unfiled_drafts_bound_collection_and_decoded_evidence(tmp_path):
|
|
store = UnfiledDraftStore(tmp_path / "unfiled.sqlite3", limit=2, max_total_bytes=12)
|
|
|
|
with pytest.raises(ValueError, match="limited to 2"):
|
|
store.replace("timmy", 0, [draft("one", evidence=[]), draft("two", evidence=[]), draft("three", evidence=[])])
|
|
with pytest.raises(ValueError, match="evidence is too large"):
|
|
store.replace("timmy", 0, [draft(evidence=[{
|
|
"filename": "large.png", "content_type": "image/png", "data": "eHh4eHh4eHh4eHh4eHh4eHh4eHg=",
|
|
}])])
|
|
with pytest.raises(ValueError, match="valid base64"):
|
|
store.replace("timmy", 0, [draft(evidence=[{
|
|
"filename": "bad.png", "content_type": "image/png", "data": "not base64!",
|
|
}])])
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_unfiled_draft_api_is_account_scoped_csrf_protected_and_conflict_safe(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_UNFILED_DRAFT_DB", str(tmp_path / "drafts.sqlite3"))
|
|
identity = {"login": "Timmy"}
|
|
|
|
async def user():
|
|
return {"id": 1, "login": identity["login"]}
|
|
|
|
monkeypatch.setattr(main, "current_user", user)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
payload = {"revision": 0, "drafts": [draft(evidence=[])]}
|
|
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.put("/api/v1/unfiled-drafts", json=payload)
|
|
headers = {"Origin": "https://test", "X-CSRF-Token": client.cookies["stackchain_csrf"]}
|
|
saved = await client.put("/api/v1/unfiled-drafts", json=payload, headers=headers)
|
|
stale = await client.put("/api/v1/unfiled-drafts", json=payload, headers=headers)
|
|
fetched = await client.get("/api/v1/unfiled-drafts")
|
|
identity["login"] = "Alexander"
|
|
isolated = await client.get("/api/v1/unfiled-drafts")
|
|
|
|
assert forbidden.status_code == 403
|
|
assert saved.status_code == 200
|
|
expected = draft(evidence=[])
|
|
expected.pop("evidence")
|
|
assert saved.json() == {"revision": 1, "drafts": [expected]}
|
|
assert stale.status_code == 409
|
|
assert stale.json()["detail"]["snapshot"] == saved.json()
|
|
assert fetched.json() == saved.json()
|
|
assert fetched.headers["cache-control"] == "no-store"
|
|
assert isolated.json() == {"revision": 0, "drafts": []}
|