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