Skip to content

Commit f713ed3

Browse files
test: cover new branches to satisfy diff coverage
CI's diff-cover gate (--fail-under=90) flagged uncovered new lines: - QueryAuthSync had no test -- added one for first-observation no-op, clear-on-user-change, clear-on-sign-out, and no-clear on an unchanged id. - _retry_delay_seconds: a non-numeric X-RateLimit-Reset falls through to the cap instead of raising. - scan_results_repo.list_recent's new scanned_by_user_id filter, via a cockpit "own" BYO-PAT-scope test. - github_app.get_installation_token re-checks the cache after acquiring the per-installation mint lock (the raced-mint window). - event_consumer._rollback_quietly on both the happy path and a rollback that itself raises. Python diff coverage 97%, UI diff coverage 100%.
1 parent f581a18 commit f713ed3

5 files changed

Lines changed: 152 additions & 0 deletions

File tree

apps/api/tests/test_analytics_cockpit.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,39 @@ def test_cockpit_hides_score_trend_from_a_non_member_byo_pat_caller(http, db, mo
164164
assert body["score_trend"] == []
165165

166166

167+
def test_cockpit_shows_own_byo_pat_scans_when_caller_has_no_org_membership(http, db, mock_user):
168+
# An org row for "acme" exists but the caller has no membership and no installation
169+
# -- their only claim to its history is a scan they ran themselves, so
170+
# _user_history_scope is "own" and only their own scan rows feed the trend.
171+
# Mirrors test_analytics_history.py::test_personal_history_returns_seeded_rows_newest_first.
172+
org = org_repo.get_or_create(db, github_login="acme")
173+
other = User(email="other-cockpit@example.com", name=None, is_workspace_admin=False)
174+
db.add(other)
175+
db.flush()
176+
scan_results_repo.insert(
177+
db, owner="acme", score=60, total_checks=5, failed_checks=2, checks=[],
178+
tenant_id=org.tenant_id, scanned_by_user_id=mock_user.id,
179+
)
180+
scan_results_repo.insert(
181+
db, owner="acme", score=90, total_checks=5, failed_checks=0, checks=[],
182+
tenant_id=org.tenant_id, scanned_by_user_id=other.id,
183+
)
184+
185+
patchers = _patch_all()
186+
_start_all(patchers)
187+
try:
188+
with patch("src.routers.analytics.resolve_owner_token", return_value="ghp_test"):
189+
resp = http.get("/me/analytics/cockpit/acme")
190+
finally:
191+
_stop_all(patchers)
192+
193+
assert resp.status_code == 200
194+
body = resp.json()
195+
# Only mock_user's own scan (60), not the other user's (90).
196+
assert body["latest_score"] == 60
197+
assert body["score_trend"] == [60]
198+
199+
167200
def test_cockpit_no_cache_jobs_yet(http, db, mock_user):
168201
patchers = _patch_all()
169202
_start_all(patchers)

apps/api/tests/test_github_app.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ def test_expired_cache_refetches(app_configured):
9696
assert mock_client.post.call_count == 2
9797

9898

99+
def test_get_installation_token_rechecks_cache_after_acquiring_mint_lock(app_configured, monkeypatch):
100+
# Simulates another thread minting the token in the window between the lock-free
101+
# cache check and this call acquiring the per-installation mint lock: the second
102+
# check must short-circuit and no HTTP mint should happen.
103+
calls: list[int] = []
104+
105+
def fake_cached(installation_id: int):
106+
calls.append(installation_id)
107+
return None if len(calls) == 1 else "ghs_raced"
108+
109+
minted = MagicMock()
110+
monkeypatch.setattr(github_app, "_cached_token", fake_cached)
111+
monkeypatch.setattr(github_app, "_request_installation_token", minted)
112+
113+
assert github_app.get_installation_token(42) == "ghs_raced"
114+
minted.assert_not_called()
115+
116+
99117
def test_not_configured_raises(monkeypatch):
100118
monkeypatch.setattr(settings, "github_app_id", None)
101119
monkeypatch.setattr(settings, "github_app_private_key", None)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { cleanup, render } from "@testing-library/react"
2+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
3+
import { afterEach, describe, expect, it, vi } from "vitest"
4+
5+
let mockUser: { id: number } | null = null
6+
7+
vi.mock("@/lib/auth-context", () => ({
8+
useAuth: () => ({ user: mockUser }),
9+
}))
10+
11+
import { QueryAuthSync } from "@/components/query-auth-sync"
12+
13+
function renderWith(qc: QueryClient) {
14+
return render(
15+
<QueryClientProvider client={qc}>
16+
<QueryAuthSync />
17+
</QueryClientProvider>,
18+
)
19+
}
20+
21+
describe("QueryAuthSync", () => {
22+
afterEach(() => {
23+
cleanup()
24+
mockUser = null
25+
})
26+
27+
it("does not clear the cache on the first observation", () => {
28+
mockUser = { id: 1 }
29+
const qc = new QueryClient()
30+
const clear = vi.spyOn(qc, "clear")
31+
32+
renderWith(qc)
33+
34+
expect(clear).not.toHaveBeenCalled()
35+
})
36+
37+
it("clears the cache when the signed-in user changes", () => {
38+
mockUser = { id: 1 }
39+
const qc = new QueryClient()
40+
const clear = vi.spyOn(qc, "clear")
41+
const { rerender } = renderWith(qc)
42+
43+
mockUser = { id: 2 }
44+
rerender(
45+
<QueryClientProvider client={qc}>
46+
<QueryAuthSync />
47+
</QueryClientProvider>,
48+
)
49+
50+
expect(clear).toHaveBeenCalledTimes(1)
51+
})
52+
53+
it("clears the cache on sign-out (user becomes null) but not on an unchanged id", () => {
54+
mockUser = { id: 7 }
55+
const qc = new QueryClient()
56+
const clear = vi.spyOn(qc, "clear")
57+
const { rerender } = renderWith(qc)
58+
59+
const redraw = () =>
60+
rerender(
61+
<QueryClientProvider client={qc}>
62+
<QueryAuthSync />
63+
</QueryClientProvider>,
64+
)
65+
66+
redraw() // same id -> no clear
67+
expect(clear).not.toHaveBeenCalled()
68+
69+
mockUser = null
70+
redraw() // signed out -> clear
71+
expect(clear).toHaveBeenCalledTimes(1)
72+
})
73+
})

apps/worker/tests/test_event_consumer.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,20 @@ def test_parse_alert_timestamp_falls_back_on_missing_or_malformed_value():
583583
assert event_consumer._parse_alert_timestamp("not-a-real-timestamp", fallback) == fallback
584584

585585

586+
def test_rollback_quietly_clears_the_transaction():
587+
conn = MagicMock()
588+
event_consumer._rollback_quietly(conn)
589+
conn.rollback.assert_called_once_with()
590+
591+
592+
def test_rollback_quietly_swallows_a_failing_rollback():
593+
conn = MagicMock()
594+
conn.rollback.side_effect = psycopg.OperationalError("connection already gone")
595+
# Must not raise -- the outer loop reconnects on a dead connection.
596+
event_consumer._rollback_quietly(conn)
597+
conn.rollback.assert_called_once_with()
598+
599+
586600
def _org_member(conn, tenant_id, login):
587601
with conn.cursor() as cur:
588602
cur.execute(f"SET app.tenant_id = {int(tenant_id)}")

packages/checks/tests/test_github_checks.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,20 @@ def test_get_falls_back_to_ratelimit_reset_when_no_retry_after():
203203
assert 20 <= slept <= 30
204204

205205

206+
def test_get_ignores_a_malformed_ratelimit_reset_and_waits_the_cap():
207+
# A non-numeric X-RateLimit-Reset must not raise -- it falls through to the cap
208+
# (same as having no usable header at all).
209+
request = httpx.Request("GET", "https://x/y")
210+
rate_limited = httpx.Response(429, headers={"X-RateLimit-Reset": "soon"}, request=request)
211+
ok_response = httpx.Response(200, json={"login": "acme"}, request=request)
212+
responses = iter([rate_limited, ok_response])
213+
214+
with patch("time.sleep") as mock_sleep, patch("httpx.Client.get", side_effect=lambda url, headers: next(responses)):
215+
result = _get("https://x/y", "tok")
216+
assert result == {"login": "acme"}
217+
mock_sleep.assert_called_once_with(_MAX_RETRY_AFTER_SECONDS)
218+
219+
206220
def test_get_waits_the_cap_for_a_ratelimit_with_no_usable_header():
207221
# A 429 with neither Retry-After nor X-RateLimit-Reset waits the full documented
208222
# minimum backoff, not a fast exponential retry into the same limit.

0 commit comments

Comments
 (0)