Coverage for app / backend / src / tests / test_bugs.py: 95%
162 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
1from datetime import UTC
2from unittest.mock import patch
4import grpc
5import pytest
6from google.protobuf import empty_pb2, timestamp_pb2
7from sqlalchemy import select
9from couchers.config import config
10from couchers.crypto import random_hex
11from couchers.db import session_scope
12from couchers.models.logging import EventLog, EventSource
13from couchers.proto import bugs_pb2
14from tests.fixtures.db import generate_user
15from tests.fixtures.sessions import bugs_session
18@pytest.fixture(autouse=True)
19def _(testconfig):
20 pass
23def test_bugs_disabled():
24 with bugs_session() as bugs, pytest.raises(grpc.RpcError) as e:
25 bugs.ReportBug(
26 bugs_pb2.ReportBugReq(
27 subject="subject",
28 description="description",
29 results="results",
30 frontend_version="frontend_version",
31 user_agent="user_agent",
32 page="page",
33 )
34 )
35 assert e.value.code() == grpc.StatusCode.UNAVAILABLE
38def test_bugs(db):
39 with bugs_session() as bugs:
41 def dud_post(url, auth, json):
42 assert url == "https://api.github.com/repos/org/repo/issues"
43 assert auth == ("user", "token")
44 assert json == {
45 "title": "subject",
46 "body": (
47 "Subject: subject\nDescription:\ndescription\n\nResults:\nresults\n\nBackend version: "
48 + config["VERSION"]
49 + "\nFrontend version: frontend_version\nUser Agent: user_agent\nScreen resolution: 1920x1080\nPage: page\nUser: <not logged in>"
50 ),
51 "labels": ["bug tool", "bug: triage needed"],
52 }
54 class _PostReturn:
55 status_code = 201
57 def json(self):
58 return {"number": 11}
60 return _PostReturn()
62 new_config = config.copy()
63 new_config["BUG_TOOL_ENABLED"] = True
65 with patch("couchers.servicers.bugs.config", new_config):
66 with patch("couchers.servicers.bugs.requests.post", dud_post): 66 ↛ anywhereline 66 didn't jump anywhere: it always raised an exception.
67 res = bugs.ReportBug(
68 bugs_pb2.ReportBugReq(
69 subject="subject",
70 description="description",
71 results="results",
72 frontend_version="frontend_version",
73 user_agent="user_agent",
74 screen_resolution=bugs_pb2.ScreenResolution(width=1920, height=1080),
75 page="page",
76 )
77 )
79 assert res.bug_id == "#11"
80 assert res.bug_url == "https://github.com/org/repo/issues/11"
83def test_bugs_with_user(db):
84 user, token = generate_user(username="testing_user")
86 with bugs_session(token) as bugs:
88 def dud_post(url, auth, json):
89 assert url == "https://api.github.com/repos/org/repo/issues"
90 assert auth == ("user", "token")
91 assert json == {
92 "title": "subject",
93 "body": (
94 "Subject: subject\nDescription:\ndescription\n\nResults:\nresults\n\nBackend version: "
95 + config["VERSION"]
96 + "\nFrontend version: frontend_version\nUser Agent: user_agent\nScreen resolution: 390x844\nPage: page\nUser: [@testing_user](http://localhost:3000/user/testing_user) (1)"
97 ),
98 "labels": ["bug tool", "bug: triage needed"],
99 }
101 class _PostReturn:
102 status_code = 201
104 def json(self):
105 return {"number": 11}
107 return _PostReturn()
109 new_config = config.copy()
110 new_config["BUG_TOOL_ENABLED"] = True
112 with patch("couchers.servicers.bugs.config", new_config):
113 with patch("couchers.servicers.bugs.requests.post", dud_post): 113 ↛ anywhereline 113 didn't jump anywhere: it always raised an exception.
114 res = bugs.ReportBug(
115 bugs_pb2.ReportBugReq(
116 subject="subject",
117 description="description",
118 results="results",
119 frontend_version="frontend_version",
120 user_agent="user_agent",
121 screen_resolution=bugs_pb2.ScreenResolution(width=390, height=844),
122 page="page",
123 )
124 )
126 assert res.bug_id == "#11"
127 assert res.bug_url == "https://github.com/org/repo/issues/11"
130def test_bugs_fails_on_network_error(db):
131 with bugs_session() as bugs:
133 def dud_post(url, auth, json):
134 class _PostReturn:
135 status_code = 400
137 return _PostReturn()
139 new_config = config.copy()
140 new_config["BUG_TOOL_ENABLED"] = True
142 with patch("couchers.servicers.bugs.config", new_config):
143 with patch("couchers.servicers.bugs.requests.post", dud_post): 143 ↛ anywhereline 143 didn't jump anywhere: it always raised an exception.
144 with pytest.raises(grpc.RpcError) as e:
145 res = bugs.ReportBug(
146 bugs_pb2.ReportBugReq(
147 subject="subject",
148 description="description",
149 results="results",
150 frontend_version="frontend_version",
151 user_agent="user_agent",
152 page="page",
153 )
154 )
155 assert e.value.code() == grpc.StatusCode.INTERNAL
158def test_version():
159 with bugs_session() as bugs:
160 res = bugs.Version(empty_pb2.Empty())
161 assert res.version == "testing_version"
164def test_status(db):
165 for _ in range(5):
166 generate_user()
168 with bugs_session() as bugs:
169 nonce = random_hex()
170 res = bugs.Status(bugs_pb2.StatusReq(nonce=nonce))
171 assert res.nonce == nonce
172 assert res.version == "testing_version"
173 assert res.coucher_count == 5
176def test_GetDescriptors():
177 with bugs_session() as bugs:
178 res = bugs.GetDescriptors(empty_pb2.Empty())
179 # test we got something roughly binary back
180 assert res.content_type == "application/octet-stream"
181 assert len(res.data) > 2**12
184def _get_events(session, event_type=None):
185 stmt = select(EventLog).order_by(EventLog.id)
186 if event_type: 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true
187 stmt = stmt.where(EventLog.event_type == event_type)
188 return session.execute(stmt).scalars().all()
191def test_report_diagnostics_anonymous(db):
192 with bugs_session() as bugs:
193 bugs.ReportDiagnostics(
194 bugs_pb2.ReportDiagnosticsReq(
195 frontend_version="1.2.3",
196 infos=[
197 bugs_pb2.DiagnosticInfo(
198 tag="page.viewed",
199 properties_json='{"path": "/"}',
200 value=1,
201 ),
202 bugs_pb2.DiagnosticInfo(
203 tag="session.started",
204 properties_json='{"referrer": "google.com"}',
205 value=1,
206 ),
207 ],
208 )
209 )
211 with session_scope() as session:
212 events = _get_events(session)
213 assert len(events) == 2
215 e0 = events[0]
216 assert e0.event_type == "page.viewed"
217 assert e0.properties == {"path": "/"}
218 assert e0.user_id is None
219 assert e0.source == EventSource.frontend
220 assert e0.value == 1
221 assert e0.version == "1.2.3"
223 e1 = events[1]
224 assert e1.event_type == "session.started"
225 assert e1.properties == {"referrer": "google.com"}
226 assert e1.source == EventSource.frontend
229def test_report_diagnostics_authenticated(db):
230 user, token = generate_user()
232 with bugs_session(token) as bugs:
233 bugs.ReportDiagnostics(
234 bugs_pb2.ReportDiagnosticsReq(
235 frontend_version="1.2.3",
236 infos=[
237 bugs_pb2.DiagnosticInfo(
238 tag="page.viewed",
239 properties_json='{"path": "/search"}',
240 value=1,
241 ),
242 ],
243 )
244 )
246 with session_scope() as session:
247 events = _get_events(session)
248 assert len(events) == 1
249 assert events[0].user_id == user.id
250 assert events[0].source == EventSource.frontend
253def test_report_diagnostics_with_value(db):
254 with bugs_session() as bugs:
255 bugs.ReportDiagnostics(
256 bugs_pb2.ReportDiagnosticsReq(
257 frontend_version="1.2.3",
258 infos=[
259 bugs_pb2.DiagnosticInfo(
260 tag="search.result_hovered",
261 properties_json='{"user_id": 5}',
262 value=1500.5,
263 ),
264 ],
265 )
266 )
268 with session_scope() as session:
269 events = _get_events(session)
270 assert len(events) == 1
271 assert events[0].value == pytest.approx(1500.5)
274def test_report_diagnostics_with_occurred(db):
275 from datetime import datetime
277 ts = timestamp_pb2.Timestamp()
278 ts.FromDatetime(datetime(2026, 1, 15, 10, 30, 0, tzinfo=UTC))
280 with bugs_session() as bugs:
281 bugs.ReportDiagnostics(
282 bugs_pb2.ReportDiagnosticsReq(
283 frontend_version="1.2.3",
284 infos=[
285 bugs_pb2.DiagnosticInfo(
286 tag="page.viewed",
287 properties_json="{}",
288 value=1,
289 occurred=ts,
290 ),
291 ],
292 )
293 )
295 with session_scope() as session:
296 events = _get_events(session)
297 assert len(events) == 1
298 assert events[0].occurred.year == 2026
299 assert events[0].occurred.month == 1
300 assert events[0].occurred.day == 15
301 assert events[0].occurred.hour == 10
302 assert events[0].occurred.minute == 30
305def test_report_diagnostics_invalid_json(db):
306 with bugs_session() as bugs, pytest.raises(grpc.RpcError) as e:
307 bugs.ReportDiagnostics(
308 bugs_pb2.ReportDiagnosticsReq(
309 frontend_version="1.2.3",
310 infos=[
311 bugs_pb2.DiagnosticInfo(
312 tag="page.viewed",
313 properties_json="not valid json{{{",
314 value=1,
315 ),
316 ],
317 )
318 )
319 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
322def test_report_diagnostics_empty_batch(db):
323 with bugs_session() as bugs:
324 bugs.ReportDiagnostics(
325 bugs_pb2.ReportDiagnosticsReq(
326 frontend_version="1.2.3",
327 infos=[],
328 )
329 )
331 with session_scope() as session:
332 events = _get_events(session)
333 assert len(events) == 0
336def test_report_diagnostics_too_many(db):
337 infos = [bugs_pb2.DiagnosticInfo(tag=f"event.{i}", properties_json="{}", value=1) for i in range(101)]
339 with bugs_session() as bugs, pytest.raises(grpc.RpcError) as e:
340 bugs.ReportDiagnostics(
341 bugs_pb2.ReportDiagnosticsReq(
342 frontend_version="1.2.3",
343 infos=infos,
344 )
345 )
346 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
349def test_report_diagnostics_frontend_version(db):
350 with bugs_session() as bugs:
351 bugs.ReportDiagnostics(
352 bugs_pb2.ReportDiagnosticsReq(
353 frontend_version="abc-def-123",
354 infos=[
355 bugs_pb2.DiagnosticInfo(
356 tag="page.viewed",
357 properties_json="{}",
358 value=1,
359 ),
360 ],
361 )
362 )
364 with session_scope() as session:
365 events = _get_events(session)
366 assert len(events) == 1
367 assert events[0].version == "abc-def-123"