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

1from datetime import UTC 

2from unittest.mock import patch 

3 

4import grpc 

5import pytest 

6from google.protobuf import empty_pb2, timestamp_pb2 

7from sqlalchemy import select 

8 

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 

16 

17 

18@pytest.fixture(autouse=True) 

19def _(testconfig): 

20 pass 

21 

22 

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 

36 

37 

38def test_bugs(db): 

39 with bugs_session() as bugs: 

40 

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 } 

53 

54 class _PostReturn: 

55 status_code = 201 

56 

57 def json(self): 

58 return {"number": 11} 

59 

60 return _PostReturn() 

61 

62 new_config = config.copy() 

63 new_config["BUG_TOOL_ENABLED"] = True 

64 

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 ) 

78 

79 assert res.bug_id == "#11" 

80 assert res.bug_url == "https://github.com/org/repo/issues/11" 

81 

82 

83def test_bugs_with_user(db): 

84 user, token = generate_user(username="testing_user") 

85 

86 with bugs_session(token) as bugs: 

87 

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 } 

100 

101 class _PostReturn: 

102 status_code = 201 

103 

104 def json(self): 

105 return {"number": 11} 

106 

107 return _PostReturn() 

108 

109 new_config = config.copy() 

110 new_config["BUG_TOOL_ENABLED"] = True 

111 

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 ) 

125 

126 assert res.bug_id == "#11" 

127 assert res.bug_url == "https://github.com/org/repo/issues/11" 

128 

129 

130def test_bugs_fails_on_network_error(db): 

131 with bugs_session() as bugs: 

132 

133 def dud_post(url, auth, json): 

134 class _PostReturn: 

135 status_code = 400 

136 

137 return _PostReturn() 

138 

139 new_config = config.copy() 

140 new_config["BUG_TOOL_ENABLED"] = True 

141 

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 

156 

157 

158def test_version(): 

159 with bugs_session() as bugs: 

160 res = bugs.Version(empty_pb2.Empty()) 

161 assert res.version == "testing_version" 

162 

163 

164def test_status(db): 

165 for _ in range(5): 

166 generate_user() 

167 

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 

174 

175 

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 

182 

183 

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() 

189 

190 

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 ) 

210 

211 with session_scope() as session: 

212 events = _get_events(session) 

213 assert len(events) == 2 

214 

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" 

222 

223 e1 = events[1] 

224 assert e1.event_type == "session.started" 

225 assert e1.properties == {"referrer": "google.com"} 

226 assert e1.source == EventSource.frontend 

227 

228 

229def test_report_diagnostics_authenticated(db): 

230 user, token = generate_user() 

231 

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 ) 

245 

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 

251 

252 

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 ) 

267 

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) 

272 

273 

274def test_report_diagnostics_with_occurred(db): 

275 from datetime import datetime 

276 

277 ts = timestamp_pb2.Timestamp() 

278 ts.FromDatetime(datetime(2026, 1, 15, 10, 30, 0, tzinfo=UTC)) 

279 

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 ) 

294 

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 

303 

304 

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 

320 

321 

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 ) 

330 

331 with session_scope() as session: 

332 events = _get_events(session) 

333 assert len(events) == 0 

334 

335 

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)] 

338 

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 

347 

348 

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 ) 

363 

364 with session_scope() as session: 

365 events = _get_events(session) 

366 assert len(events) == 1 

367 assert events[0].version == "abc-def-123"