340 lines
13 KiB
Python
340 lines
13 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_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"] == []
|