krz/hutch-stats
Server-side utility for calculating contributions for sourcehut users.
clone: git clone https://gitbay.org/krz/hutch-stats.git
main: tests/test_polling_api.py · raw
1from datetime import UTC, datetime
2
3from fastapi.testclient import TestClient
4from sqlalchemy import select
5
6from srht_contrib.config import Settings
7from srht_contrib.main import create_app
8from srht_contrib.models import ContributionEvent, TrackedActor
9from srht_contrib.services.srht_client import SourceHutClientError
10
11
12class _Closable:
13 def close(self) -> None:
14 return None
15
16
17class InsertingPoller:
18 def __init__(self) -> None:
19 service = type("Service", (), {"client": _Closable()})()
20 self.todo_service = service
21 self.git_service = service
22 self.tracked_poll_calls: list[str] = []
23
24 def track_actor_request(self, db, actor: str, *, update_last_requested: bool = True, prioritize: bool = False):
25 tracked_actor = db.scalar(select(TrackedActor).where(TrackedActor.actor == actor))
26 if tracked_actor is None:
27 tracked_actor = TrackedActor(actor=actor, is_active=True)
28 db.add(tracked_actor)
29 if update_last_requested:
30 tracked_actor.last_requested_at = datetime(2026, 3, 30, 9, 0, tzinfo=UTC)
31 db.flush()
32 return tracked_actor
33
34 def poll_all(self, db, actor: str) -> int:
35 db.add(
36 ContributionEvent(
37 service="todo",
38 event_type="ticket_created",
39 actor=actor,
40 repo_name="todo",
41 resource_id="1",
42 external_uid="todo:manual:1",
43 occurred_at=datetime(2026, 3, 30, 10, 0, tzinfo=UTC),
44 weight=1.0,
45 raw_payload_json=None,
46 )
47 )
48 db.commit()
49 return 1
50
51 def poll_tracked_actors(self, db, default_actor: str | None = None) -> dict[str, int]:
52 if default_actor is not None:
53 self.tracked_poll_calls.append(default_actor)
54 return {default_actor: self.poll_all(db, default_actor)}
55 return {}
56
57
58class FailingPoller:
59 def __init__(self) -> None:
60 service = type("Service", (), {"client": _Closable()})()
61 self.todo_service = service
62 self.git_service = service
63
64 def track_actor_request(self, db, actor: str, *, update_last_requested: bool = True, prioritize: bool = False):
65 tracked_actor = db.scalar(select(TrackedActor).where(TrackedActor.actor == actor))
66 if tracked_actor is None:
67 tracked_actor = TrackedActor(actor=actor, is_active=True)
68 db.add(tracked_actor)
69 db.flush()
70 return tracked_actor
71
72 def poll_all(self, db, actor: str) -> int:
73 raise SourceHutClientError("boom")
74
75 def poll_tracked_actors(self, db, default_actor: str | None = None) -> dict[str, int]:
76 raise SourceHutClientError("boom")
77
78
79def test_manual_poll_uses_same_database_session(settings: Settings, db_engine, session_factory) -> None:
80 app = create_app(settings, engine=db_engine, session_factory=session_factory, poller=InsertingPoller())
81 with TestClient(app) as client:
82 client.headers.update({"X-API-Key": settings.api_key})
83
84 poll_response = client.post("/api/contributions/poll?actor=~ccleberg")
85 calendar_response = client.get("/api/contributions/~ccleberg?from=2026-03-30&to=2026-03-30")
86
87 assert poll_response.status_code == 200
88 assert poll_response.json()["inserted_events"] == 1
89 assert calendar_response.status_code == 200
90 assert calendar_response.json()["is_indexed"] is True
91 assert calendar_response.json()["days"] == [{"date": "2026-03-30", "count": 1, "score": 1.0}]
92
93
94def test_manual_poll_maps_sourcehut_failures_to_502(settings: Settings, db_engine, session_factory) -> None:
95 app = create_app(settings, engine=db_engine, session_factory=session_factory, poller=FailingPoller())
96 with TestClient(app) as client:
97 client.headers.update({"X-API-Key": settings.api_key})
98 response = client.post("/api/contributions/poll?actor=~ccleberg")
99
100 assert response.status_code == 502
101 assert "SourceHut polling failed" in response.json()["detail"]
102
103
104def test_scheduler_runs_initial_poll_on_startup(settings: Settings, db_engine, session_factory) -> None:
105 scheduler_settings = settings.model_copy(update={"enable_scheduler": True})
106 poller = InsertingPoller()
107 app = create_app(scheduler_settings, engine=db_engine, session_factory=session_factory, poller=poller)
108
109 with TestClient(app):
110 pass
111
112 assert poller.tracked_poll_calls == ["~ccleberg"]
113
114
115def test_startup_poll_failure_does_not_block_app_start(settings: Settings, db_engine, session_factory) -> None:
116 scheduler_settings = settings.model_copy(update={"enable_scheduler": True})
117 app = create_app(scheduler_settings, engine=db_engine, session_factory=session_factory, poller=FailingPoller())
118
119 with TestClient(app) as client:
120 response = client.get("/health")
121
122 assert response.status_code == 200