314 lines
9.3 KiB
Python
314 lines
9.3 KiB
Python
import asyncio
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from src import main
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_live_snapshot_task():
|
|
main._live_snapshot_task = None
|
|
main._live_snapshot_value = None
|
|
main._live_snapshot_created_at = None
|
|
yield
|
|
main._live_snapshot_task = None
|
|
main._live_snapshot_value = None
|
|
main._live_snapshot_created_at = None
|
|
|
|
|
|
def payload(response):
|
|
return json.loads(response.body)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_live_snapshot_fetches_user_once_and_updates_work_and_activity(monkeypatch):
|
|
calls = {"user": 0}
|
|
|
|
async def user():
|
|
calls["user"] += 1
|
|
return {"id": 1, "login": "timmy"}
|
|
|
|
async def empty():
|
|
return []
|
|
|
|
async def events(authenticated_user):
|
|
assert authenticated_user["login"] == "timmy"
|
|
return [{"type": "push"}]
|
|
|
|
async def updates():
|
|
return {
|
|
"items": [{"id": 42, "title": "Mentioned you"}],
|
|
"page": 1,
|
|
"total": 125,
|
|
"has_more": True,
|
|
}
|
|
|
|
monkeypatch.setattr(main, "current_user", user)
|
|
monkeypatch.setattr(main, "repos", empty)
|
|
monkeypatch.setattr(main, "issues", empty)
|
|
monkeypatch.setattr(main, "pull_requests", empty)
|
|
monkeypatch.setattr(main, "activity_events", events)
|
|
monkeypatch.setattr(main, "notifications", updates)
|
|
|
|
response = await main.live_snapshot()
|
|
result = payload(response)
|
|
|
|
assert calls["user"] == 1
|
|
assert result["context"]["user"]["login"] == "timmy"
|
|
assert result["events"] == [{"type": "push"}]
|
|
assert result["notifications"] == [{"id": 42, "title": "Mentioned you"}]
|
|
assert result["notification_pagination"] == {
|
|
"page": 1, "total": 125, "has_more": True
|
|
}
|
|
assert result["sections"] == {
|
|
"context": "fresh", "events": "fresh", "notifications": "fresh"
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_live_snapshot_keeps_fresh_context_when_activity_fails(monkeypatch):
|
|
async def user():
|
|
return {"id": 1, "login": "timmy"}
|
|
|
|
async def empty():
|
|
return []
|
|
|
|
async def failing_events(authenticated_user):
|
|
raise ConnectionError("secret upstream detail")
|
|
|
|
monkeypatch.setattr(main, "current_user", user)
|
|
monkeypatch.setattr(main, "repos", empty)
|
|
monkeypatch.setattr(main, "issues", empty)
|
|
monkeypatch.setattr(main, "pull_requests", empty)
|
|
monkeypatch.setattr(main, "activity_events", failing_events)
|
|
monkeypatch.setattr(main, "notifications", empty)
|
|
|
|
result = payload(await main.live_snapshot())
|
|
|
|
assert result["context"]["user"]["login"] == "timmy"
|
|
assert result["events"] is None
|
|
assert result["sections"] == {
|
|
"context": "fresh",
|
|
"events": "temporarily unavailable",
|
|
"notifications": "fresh",
|
|
}
|
|
assert "secret" not in json.dumps(result)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_live_snapshot_keeps_work_and_activity_when_notifications_fail(monkeypatch):
|
|
async def user():
|
|
return {"id": 1, "login": "timmy"}
|
|
|
|
async def empty():
|
|
return []
|
|
|
|
async def events(_authenticated_user):
|
|
return [{"type": "push"}]
|
|
|
|
async def failing_updates():
|
|
raise ConnectionError("private notification failure")
|
|
|
|
monkeypatch.setattr(main, "current_user", user)
|
|
monkeypatch.setattr(main, "repos", empty)
|
|
monkeypatch.setattr(main, "issues", empty)
|
|
monkeypatch.setattr(main, "pull_requests", empty)
|
|
monkeypatch.setattr(main, "activity_events", events)
|
|
monkeypatch.setattr(main, "notifications", failing_updates)
|
|
|
|
result = payload(await main.live_snapshot())
|
|
|
|
assert result["context"]["user"]["login"] == "timmy"
|
|
assert result["events"] == [{"type": "push"}]
|
|
assert result["notifications"] is None
|
|
assert result["sections"]["notifications"] == "temporarily unavailable"
|
|
assert "private" not in json.dumps(result)
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_live_snapshot_keeps_fresh_activity_when_work_fails(monkeypatch):
|
|
async def user():
|
|
return {"id": 1, "login": "timmy"}
|
|
|
|
async def failing_repos():
|
|
raise ConnectionError("work unavailable")
|
|
|
|
async def empty():
|
|
return []
|
|
|
|
async def events(authenticated_user):
|
|
return [{"type": "push"}]
|
|
|
|
monkeypatch.setattr(main, "current_user", user)
|
|
monkeypatch.setattr(main, "repos", failing_repos)
|
|
monkeypatch.setattr(main, "issues", empty)
|
|
monkeypatch.setattr(main, "pull_requests", empty)
|
|
monkeypatch.setattr(main, "activity_events", events)
|
|
monkeypatch.setattr(main, "notifications", empty)
|
|
|
|
result = payload(await main.live_snapshot())
|
|
|
|
assert result["context"] is None
|
|
assert result["events"] == [{"type": "push"}]
|
|
assert result["sections"] == {
|
|
"context": "temporarily unavailable",
|
|
"events": "fresh",
|
|
"notifications": "fresh",
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_live_snapshot_reuses_completed_snapshot_inside_freshness_window(monkeypatch):
|
|
user_calls = 0
|
|
release = asyncio.Event()
|
|
|
|
async def user():
|
|
nonlocal user_calls
|
|
user_calls += 1
|
|
return {"id": 1, "login": "timmy"}
|
|
|
|
async def blocked_repos():
|
|
await release.wait()
|
|
return []
|
|
|
|
async def empty():
|
|
return []
|
|
|
|
async def events(authenticated_user):
|
|
return []
|
|
|
|
monkeypatch.setattr(main, "current_user", user)
|
|
monkeypatch.setattr(main, "repos", blocked_repos)
|
|
monkeypatch.setattr(main, "issues", empty)
|
|
monkeypatch.setattr(main, "pull_requests", empty)
|
|
monkeypatch.setattr(main, "activity_events", events)
|
|
monkeypatch.setattr(main, "notifications", empty)
|
|
|
|
first = asyncio.create_task(main.live_snapshot())
|
|
await asyncio.sleep(0)
|
|
second = asyncio.create_task(main.live_snapshot())
|
|
await asyncio.sleep(0)
|
|
assert user_calls == 1
|
|
|
|
release.set()
|
|
await asyncio.gather(first, second)
|
|
await main.live_snapshot()
|
|
|
|
assert user_calls == 1
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_stale_snapshot_returns_immediately_while_one_refresh_revalidates(monkeypatch):
|
|
now = 100.0
|
|
refresh_started = asyncio.Event()
|
|
release_refresh = asyncio.Event()
|
|
builds = 0
|
|
|
|
async def snapshot():
|
|
nonlocal builds
|
|
builds += 1
|
|
if builds == 2:
|
|
refresh_started.set()
|
|
await release_refresh.wait()
|
|
return {
|
|
"context": {"generation": builds},
|
|
"events": [],
|
|
"notifications": [],
|
|
"sections": {},
|
|
}
|
|
|
|
monkeypatch.setattr(main.time, "monotonic", lambda: now)
|
|
monkeypatch.setattr(main, "_build_live_snapshot", snapshot)
|
|
|
|
first = payload(await main.live_snapshot())
|
|
now += main.LIVE_SNAPSHOT_FRESHNESS_SECONDS + 1
|
|
stale_request = asyncio.create_task(main.live_snapshot())
|
|
await refresh_started.wait()
|
|
await asyncio.sleep(0)
|
|
returned_immediately = stale_request.done()
|
|
release_refresh.set()
|
|
stale = payload(await stale_request)
|
|
|
|
assert returned_immediately is True
|
|
assert first["context"]["generation"] == 1
|
|
assert stale["context"]["generation"] == 1
|
|
assert stale["freshness"]["stale"] is True
|
|
assert stale["freshness"]["revalidating"] is True
|
|
assert builds == 2
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_cancelling_one_waiter_does_not_cancel_the_shared_snapshot(monkeypatch):
|
|
started = asyncio.Event()
|
|
release = asyncio.Event()
|
|
snapshot_calls = 0
|
|
|
|
async def blocked_snapshot():
|
|
nonlocal snapshot_calls
|
|
snapshot_calls += 1
|
|
started.set()
|
|
await release.wait()
|
|
return {
|
|
"context": {},
|
|
"events": [],
|
|
"notifications": [],
|
|
"sections": {},
|
|
}
|
|
|
|
monkeypatch.setattr(main, "_build_live_snapshot", blocked_snapshot)
|
|
|
|
disconnected = asyncio.create_task(main.live_snapshot())
|
|
await started.wait()
|
|
survivor = asyncio.create_task(main.live_snapshot())
|
|
await asyncio.sleep(0)
|
|
|
|
disconnected.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await disconnected
|
|
|
|
assert main._live_snapshot_task is not None
|
|
assert not main._live_snapshot_task.done()
|
|
|
|
release.set()
|
|
response = await survivor
|
|
|
|
assert response.status_code == 200
|
|
assert snapshot_calls == 1
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_shared_snapshot_deadline_cancels_upstream_work_for_all_waiters(monkeypatch):
|
|
started = asyncio.Event()
|
|
cancelled = asyncio.Event()
|
|
|
|
async def snapshot_that_exceeds_deadline():
|
|
started.set()
|
|
try:
|
|
await asyncio.Event().wait()
|
|
finally:
|
|
cancelled.set()
|
|
|
|
monkeypatch.setattr(main, "_build_live_snapshot", snapshot_that_exceeds_deadline)
|
|
monkeypatch.setattr(main, "CONTEXT_TIMEOUT_SECONDS", 0.01)
|
|
|
|
first = asyncio.create_task(main.live_snapshot())
|
|
await started.wait()
|
|
second = asyncio.create_task(main.live_snapshot())
|
|
|
|
try:
|
|
responses = await asyncio.gather(first, second)
|
|
await asyncio.wait_for(cancelled.wait(), timeout=0.1)
|
|
|
|
assert [response.status_code for response in responses] == [503, 503]
|
|
assert [payload(response)["error"] for response in responses] == [
|
|
"Gitea live snapshot timed out after 0.01s",
|
|
"Gitea live snapshot timed out after 0.01s",
|
|
]
|
|
finally:
|
|
task = main._live_snapshot_task
|
|
if task is not None and not task.done():
|
|
task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|