stackchain-dashboard/tests/test_following_api.py
timmy c3cb498ce1
All checks were successful
CI / lint (pull_request) Successful in 3m50s
CI / build-release (pull_request) Successful in 9s
CI / browser-journey (pull_request) Successful in 6m34s
CI / release-candidate (pull_request) Has been skipped
feat: keep Following changes for later (Closes #1320)
2026-08-23 20:24:14 +00:00

369 lines
14 KiB
Python

import httpx
import pytest
from src import main
from src.following_store import FollowingStore
@pytest.mark.anyio
async def test_confirmed_watch_updates_account_following_collection(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"a" * 32)
detail = {
"repository": "stackchain/api",
"kind": "issue",
"number": 42,
"title": "Make mobile review useful",
"state": "open",
"updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42",
"claimable": True,
}
async def preview(repository, kind, number):
assert (repository, kind, number) == ("stackchain/api", "issue", 42)
return detail
async def set_subscription(repository, number, watching):
return {"watching": watching}
async def user():
return {"login": "Timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store, raising=False)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
watched = await client.put(
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
)
following = await client.get("/api/v1/following")
assert watched.status_code == 200
assert watched.json() == {
"watching": True,
"following_synced": True,
"following_revision": 1,
"following_count": 1,
}
assert following.status_code == 200
assert following.headers["cache-control"] == "no-store"
assert following.json() == {"revision": 1, "degraded": False, "refresh_failures": 0, "items": [{
"repository": "stackchain/api",
"kind": "issue",
"number": 42,
"title": "Make mobile review useful",
"state": "open",
"updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42",
"has_unseen_change": False,
}]}
@pytest.mark.anyio
async def test_confirmed_pull_watch_round_trips_through_following(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"p" * 32)
detail = {
"repository": "stackchain/api", "kind": "pull", "number": 84,
"title": "Ship typed Following", "state": "open",
"updated_at": "2026-08-23T05:00:00Z",
"url": "https://forge.example/stackchain/api/pulls/84",
}
previews = []
async def preview(repository, kind, number):
previews.append((repository, kind, number))
return detail
async def set_subscription(repository, number, watching):
return {"watching": watching}
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
watched = await client.put(
"/api/v1/repos/stackchain/api/issues/84/preview/subscription?kind=pull"
)
following = await client.get("/api/v1/following")
assert watched.status_code == 200
assert following.status_code == 200
assert following.json()["items"][0]["kind"] == "pull"
assert previews == [
("stackchain/api", "pull", 84),
("stackchain/api", "pull", 84),
]
@pytest.mark.anyio
async def test_full_following_collection_rejects_watch_before_gitea_mutation(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"b" * 32, limit=1)
store.set_watching("timmy", {
"repository": "stackchain/api", "number": 7, "title": "Already followed",
"state": "open", "updated_at": "2026-08-23T02:00:00Z",
"url": "https://forge.example/stackchain/api/issues/7",
}, True)
calls = []
async def preview(repository, kind, number):
return {
"repository": repository, "kind": kind, "number": number,
"title": "Another issue", "state": "open",
"updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42", "claimable": True,
}
async def set_subscription(*args):
calls.append(args)
return {"watching": True}
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
)
assert response.status_code == 503
assert calls == []
assert store.get("timmy")["items"][0]["number"] == 7
@pytest.mark.anyio
async def test_following_write_failure_compensates_confirmed_gitea_watch(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"c" * 32)
calls = []
async def preview(repository, kind, number):
return {
"repository": repository, "kind": kind, "number": number,
"title": "Recoverable watch", "state": "open",
"updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42", "claimable": True,
}
async def set_subscription(repository, number, watching):
calls.append(watching)
return {"watching": watching}
async def user():
return {"login": "timmy"}
def fail_write(*args):
raise OSError("disk unavailable")
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(store, "set_watching", fail_write)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
)
assert response.status_code == 503
assert response.json() == {"error": "Watch status was not changed. Please retry."}
assert calls == [True, False]
@pytest.mark.anyio
async def test_uncompensated_following_write_reports_authoritative_partial_outcome(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"d" * 32)
calls = []
async def preview(repository, kind, number):
return {
"repository": repository, "kind": kind, "number": number,
"title": "Split watch", "state": "open",
"updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42", "claimable": True,
}
async def set_subscription(repository, number, watching):
calls.append(watching)
return {"watching": True}
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(store, "set_watching", lambda *args: (_ for _ in ()).throw(OSError()))
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
)
assert response.status_code == 200
assert response.json() == {
"watching": True,
"following_synced": False,
"error": "Watching in Gitea, but Following could not sync. Retry this action.",
}
assert calls == [True, False]
@pytest.mark.anyio
async def test_following_refreshes_changed_items_and_preserves_failed_items(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"e" * 32)
first = {
"repository": "stackchain/api", "number": 42, "title": "Old title",
"state": "open", "updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42",
}
failed = {**first, "number": 43, "title": "Last known",
"url": "https://forge.example/stackchain/api/issues/43"}
store.set_watching("timmy", failed, True)
store.set_watching("timmy", first, True)
async def preview(repository, kind, number):
assert (repository, kind) == ("stackchain/api", "issue")
if number == 43:
return {**failed, "kind": "issue", "title": ""}
return {**first, "kind": "issue", "title": "Fresh title", "state": "closed",
"updated_at": "2026-08-23T04:00:00Z"}
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/following")
assert response.status_code == 200
payload = response.json()
assert payload["degraded"] is True
assert payload["refresh_failures"] == 1
assert [(item["number"], item["has_unseen_change"]) for item in payload["items"]] == [
(42, True), (43, False)
]
assert payload["items"][0]["title"] == "Fresh title"
assert payload["items"][1]["title"] == "Last known"
@pytest.mark.anyio
async def test_following_acknowledges_only_the_exact_loaded_revision(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"f" * 32)
item = {
"repository": "stackchain/api", "kind": "pull", "number": 42, "title": "Changed",
"state": "open", "updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42",
}
store.set_watching("timmy", item, True)
store.refresh("timmy", [{**item, "updated_at": "2026-08-23T04:00:00Z"}])
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/api/v1/following/stackchain/api/issues/42/seen?kind=pull",
json={"updated_at": "2026-08-23T04:00:00Z"},
)
assert response.status_code == 200
assert response.json()["items"][0]["has_unseen_change"] is False
@pytest.mark.anyio
async def test_following_keep_restores_only_the_loaded_revision(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"f" * 32)
item = {
"repository": "stackchain/api", "kind": "pull", "number": 42, "title": "Changed",
"state": "open", "updated_at": "2026-08-23T03:00:00Z",
"url": "https://forge.example/stackchain/api/pulls/42",
}
store.set_watching("timmy", item, True)
changed = {**item, "updated_at": "2026-08-23T04:00:00Z"}
store.refresh("timmy", [changed])
store.acknowledge("timmy", item["repository"], item["number"], changed["updated_at"], kind="pull")
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.put(
"/api/v1/following/stackchain/api/issues/42/keep?kind=pull",
json={"updated_at": changed["updated_at"]},
)
assert response.status_code == 200
assert response.json()["items"][0]["has_unseen_change"] is True
@pytest.mark.anyio
async def test_closed_following_issue_can_be_unwatched_but_not_newly_watched(monkeypatch, tmp_path):
store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"g" * 32)
closed = {
"repository": "stackchain/api", "number": 42, "title": "Finished work",
"state": "closed", "updated_at": "2026-08-23T04:00:00Z",
"url": "https://forge.example/stackchain/api/issues/42",
}
store.set_watching("timmy", closed, True)
mutations = []
async def preview(repository, kind, number):
return {**closed, "kind": "issue"}
async def set_subscription(repository, number, watching):
mutations.append((repository, number, watching))
return {"watching": watching}
async def user():
return {"login": "timmy"}
monkeypatch.setattr(main, "_following_store", lambda: store)
monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
monkeypatch.setattr(main, "current_user", user)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
watch = await client.put(
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
)
unwatch = await client.delete(
"/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
)
assert watch.status_code == 404
assert unwatch.status_code == 200
assert unwatch.json() == {
"watching": False,
"following_synced": True,
"following_revision": 2,
"following_count": 0,
}
assert mutations == [("stackchain/api", 42, False)]
assert store.get("timmy")["items"] == []