Coverage for app/backend/src/tests/test_event_log.py: 100%

405 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-25 22:54 +0000

1from datetime import timedelta 

2from typing import cast 

3 

4import grpc 

5import pytest 

6from google.protobuf import empty_pb2, wrappers_pb2 

7from sqlalchemy import select 

8 

9from couchers.context import make_interactive_context 

10from couchers.crypto import hash_password 

11from couchers.db import session_scope 

12from couchers.event_log import log_event 

13from couchers.i18n import LocalizationContext 

14from couchers.models import FriendRelationship, SignupFlow 

15from couchers.models.logging import EventLog 

16from couchers.proto import ( 

17 account_pb2, 

18 api_pb2, 

19 auth_pb2, 

20 conversations_pb2, 

21 events_pb2, 

22 messages_pb2, 

23 references_pb2, 

24 reporting_pb2, 

25 requests_pb2, 

26 search_pb2, 

27) 

28from couchers.utils import create_coordinate, datetime_to_iso8601_local, now, today 

29from tests.fixtures.db import generate_user, make_friends 

30from tests.fixtures.sessions import ( 

31 MockGrpcContext, 

32 account_session, 

33 api_session, 

34 auth_api_session, 

35 conversations_session, 

36 events_session, 

37 references_session, 

38 reporting_session, 

39 requests_session, 

40 search_session, 

41) 

42from tests.test_communities import create_community 

43 

44 

45@pytest.fixture(autouse=True) 

46def _(testconfig, fast_passwords): 

47 pass 

48 

49 

50def _get_events(session, event_type=None): 

51 """Helper to query EventLog entries, optionally filtered by event_type.""" 

52 stmt = select(EventLog).order_by(EventLog.id) 

53 if event_type: 

54 stmt = stmt.where(EventLog.event_type == event_type) 

55 return session.execute(stmt).scalars().all() 

56 

57 

58# ===== Unit tests for log_event function ===== 

59 

60 

61def test_log_event_authenticated_context(db): 

62 """log_event stores event with user_id from context.""" 

63 user, token = generate_user() 

64 

65 with session_scope() as session: 

66 context = make_interactive_context( 

67 grpc_context=cast(grpc.ServicerContext, MockGrpcContext()), 

68 user_id=user.id, 

69 is_api_key=False, 

70 token=token, 

71 localization=LocalizationContext.en_utc(), 

72 sofa="test-sofa-123", 

73 ) 

74 log_event(context, session, "test.event", {"key": "value"}) 

75 

76 with session_scope() as session: 

77 events = _get_events(session, "test.event") 

78 assert len(events) == 1 

79 assert events[0].user_id == user.id 

80 assert events[0].event_type == "test.event" 

81 assert events[0].properties == {"key": "value"} 

82 assert events[0].sofa == "test-sofa-123" 

83 assert events[0].created is not None 

84 

85 

86def test_log_event_with_override_user_id(db): 

87 """log_event uses _override_user_id to set user_id.""" 

88 user, token = generate_user() 

89 

90 with session_scope() as session: 

91 context = make_interactive_context( 

92 grpc_context=cast(grpc.ServicerContext, MockGrpcContext()), 

93 user_id=None, 

94 is_api_key=False, 

95 token=None, 

96 localization=LocalizationContext.en_utc(), 

97 sofa="sofa-456", 

98 ) 

99 log_event(context, session, "account.signup_completed", {"gender": "Woman"}, _override_user_id=user.id) 

100 

101 with session_scope() as session: 

102 events = _get_events(session, "account.signup_completed") 

103 assert len(events) == 1 

104 assert events[0].user_id == user.id 

105 assert events[0].properties == {"gender": "Woman"} 

106 assert events[0].sofa == "sofa-456" 

107 

108 

109def test_log_event_anonymous(db): 

110 """log_event stores event with user_id=None when context has no user.""" 

111 with session_scope() as session: 

112 context = make_interactive_context( 

113 grpc_context=cast(grpc.ServicerContext, MockGrpcContext()), 

114 user_id=None, 

115 is_api_key=False, 

116 token=None, 

117 localization=LocalizationContext.en_utc(), 

118 ) 

119 log_event(context, session, "account.signup_initiated", {"has_invite_code": False}) 

120 

121 with session_scope() as session: 

122 events = _get_events(session, "account.signup_initiated") 

123 assert len(events) == 1 

124 assert events[0].user_id is None 

125 assert events[0].properties == {"has_invite_code": False} 

126 

127 

128def test_log_event_complex_properties(db): 

129 """Properties dict with various types is stored as JSONB correctly.""" 

130 user, token = generate_user() 

131 

132 props = { 

133 "string_val": "hello", 

134 "int_val": 42, 

135 "float_val": 3.14, 

136 "bool_val": True, 

137 "none_val": None, 

138 "list_val": [1, 2, 3], 

139 "nested": {"a": 1, "b": "two"}, 

140 } 

141 

142 with session_scope() as session: 

143 context = make_interactive_context( 

144 grpc_context=cast(grpc.ServicerContext, MockGrpcContext()), 

145 user_id=user.id, 

146 is_api_key=False, 

147 token=token, 

148 localization=LocalizationContext.en_utc(), 

149 ) 

150 log_event(context, session, "test.complex", props) 

151 

152 with session_scope() as session: 

153 events = _get_events(session, "test.complex") 

154 assert len(events) == 1 

155 assert events[0].properties == props 

156 

157 

158def test_log_event_empty_properties(db): 

159 """Empty properties dict is stored correctly.""" 

160 user, token = generate_user() 

161 

162 with session_scope() as session: 

163 context = make_interactive_context( 

164 grpc_context=cast(grpc.ServicerContext, MockGrpcContext()), 

165 user_id=user.id, 

166 is_api_key=False, 

167 token=token, 

168 localization=LocalizationContext.en_utc(), 

169 ) 

170 log_event(context, session, "account.logout", {}) 

171 

172 with session_scope() as session: 

173 events = _get_events(session, "account.logout") 

174 assert len(events) == 1 

175 assert events[0].properties == {} 

176 

177 

178def test_log_event_multiple_events(db): 

179 """Multiple events are stored independently.""" 

180 user, token = generate_user() 

181 

182 with session_scope() as session: 

183 context = make_interactive_context( 

184 grpc_context=cast(grpc.ServicerContext, MockGrpcContext()), 

185 user_id=user.id, 

186 is_api_key=False, 

187 token=token, 

188 localization=LocalizationContext.en_utc(), 

189 ) 

190 log_event(context, session, "test.first", {"n": 1}) 

191 log_event(context, session, "test.second", {"n": 2}) 

192 log_event(context, session, "test.first", {"n": 3}) 

193 

194 with session_scope() as session: 

195 all_events = _get_events(session) 

196 assert len(all_events) == 3 

197 

198 first_events = _get_events(session, "test.first") 

199 assert len(first_events) == 2 

200 assert first_events[0].properties == {"n": 1} 

201 assert first_events[1].properties == {"n": 3} 

202 

203 second_events = _get_events(session, "test.second") 

204 assert len(second_events) == 1 

205 assert second_events[0].properties == {"n": 2} 

206 

207 

208# ===== Integration tests: auth events ===== 

209 

210 

211def test_signup_flow_creates_events(db): 

212 """Full signup flow creates account.signup_initiated and account.signup_completed events.""" 

213 with auth_api_session() as (auth_api, metadata_interceptor): 

214 res = auth_api.SignupFlow( 

215 auth_pb2.SignupFlowReq( 

216 basic=auth_pb2.SignupBasic(name="testing", email="email@couchers.org.invalid"), 

217 ) 

218 ) 

219 

220 flow_token = res.flow_token 

221 

222 with session_scope() as session: 

223 events = _get_events(session, "account.signup_initiated") 

224 assert len(events) == 1 

225 assert events[0].properties["has_invite_code"] is False 

226 

227 # complete signup: get email token, verify, fill account, etc. 

228 with session_scope() as session: 

229 flow = session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one() 

230 email_token = flow.email_token 

231 

232 with auth_api_session() as (auth_api, metadata_interceptor): 

233 auth_api.SignupFlow( 

234 auth_pb2.SignupFlowReq( 

235 flow_token=flow_token, 

236 email_token=email_token, 

237 ) 

238 ) 

239 

240 with auth_api_session() as (auth_api, metadata_interceptor): 

241 auth_api.SignupFlow( 

242 auth_pb2.SignupFlowReq( 

243 flow_token=flow_token, 

244 accept_community_guidelines=wrappers_pb2.BoolValue(value=True), 

245 ) 

246 ) 

247 

248 with auth_api_session() as (auth_api, metadata_interceptor): 

249 auth_api.SignupFlow( 

250 auth_pb2.SignupFlowReq( 

251 flow_token=flow_token, 

252 account=auth_pb2.SignupAccount( 

253 username="frodo", 

254 password="a very insecure password", 

255 birthdate="1970-01-01", 

256 gender="Bot", 

257 hosting_status=api_pb2.HOSTING_STATUS_MAYBE, 

258 city="New York City", 

259 lat=40.7331, 

260 lng=-73.9778, 

261 radius=500, 

262 accept_tos=True, 

263 ), 

264 ) 

265 ) 

266 

267 with auth_api_session() as (auth_api, metadata_interceptor): 

268 res = auth_api.SignupFlow( 

269 auth_pb2.SignupFlowReq( 

270 flow_token=flow_token, 

271 motivations=auth_pb2.SignupMotivations(motivations=["surfing"]), 

272 ) 

273 ) 

274 

275 assert res.HasField("auth_res") 

276 user_id = res.auth_res.user_id 

277 

278 with session_scope() as session: 

279 events = _get_events(session, "account.signup_completed") 

280 assert len(events) == 1 

281 e = events[0] 

282 assert e.user_id == user_id 

283 assert e.properties["gender"] == "Bot" 

284 assert e.properties["hosting_status"] is not None 

285 assert e.properties["city"] == "New York City" 

286 assert e.properties["has_invite_code"] is False 

287 assert isinstance(e.properties["signup_duration_s"], (int, float)) 

288 assert "filled_contributor_form" in e.properties 

289 

290 

291def test_login_creates_event(db): 

292 """Login creates account.login event with gender and remember_device.""" 

293 user, token = generate_user(hashed_password=hash_password("password123")) 

294 

295 with auth_api_session() as (auth_api, metadata_interceptor): 

296 auth_api.Authenticate( 

297 auth_pb2.AuthReq( 

298 user=user.username, 

299 password="password123", 

300 remember_device=True, 

301 ) 

302 ) 

303 

304 with session_scope() as session: 

305 events = _get_events(session, "account.login") 

306 assert len(events) == 1 

307 e = events[0] 

308 assert e.user_id == user.id 

309 assert e.properties["gender"] == user.gender 

310 assert e.properties["remember_device"] is True 

311 

312 

313def test_logout_creates_event(db): 

314 """Logout creates account.logout event.""" 

315 user, token = generate_user() 

316 

317 with auth_api_session() as (auth_api, metadata_interceptor): 

318 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),)) 

319 

320 with session_scope() as session: 

321 events = _get_events(session, "account.logout") 

322 assert len(events) == 1 

323 assert events[0].user_id == user.id 

324 assert events[0].properties == {} 

325 

326 

327# ===== Integration tests: host request events ===== 

328 

329 

330def test_host_request_created_event(db, moderator): 

331 """Creating a host request logs host_request.created with full context.""" 

332 user1, token1 = generate_user() 

333 user2, token2 = generate_user( 

334 city="Berlin", 

335 geom=create_coordinate(52.5200, 13.4050), 

336 geom_radius=200, 

337 ) 

338 

339 from_date = today() + timedelta(days=2) 

340 to_date = today() + timedelta(days=5) 

341 

342 with requests_session(token1) as api: 

343 res = api.CreateHostRequest( 

344 requests_pb2.CreateHostRequestReq( 

345 host_user_id=user2.id, 

346 from_date=from_date.isoformat(), 

347 to_date=to_date.isoformat(), 

348 text="a]" * 200 + "Hello! I would love to stay with you.", 

349 ) 

350 ) 

351 

352 host_request_id = res.host_request_id 

353 

354 with session_scope() as session: 

355 events = _get_events(session, "host_request.created") 

356 assert len(events) == 1 

357 e = events[0] 

358 assert e.user_id == user1.id 

359 assert e.properties["host_request_id"] == host_request_id 

360 assert e.properties["host_id"] == user2.id 

361 assert e.properties["surfer_gender"] == user1.gender 

362 assert e.properties["host_gender"] == user2.gender 

363 assert e.properties["city"] == "Berlin" 

364 assert e.properties["from_date"] == str(from_date) 

365 assert e.properties["to_date"] == str(to_date) 

366 assert e.properties["nights"] == 3 

367 

368 

369def test_host_request_status_change_events(db, moderator): 

370 """Accepting a host request logs event with both parties' info.""" 

371 user1, token1 = generate_user() 

372 user2, token2 = generate_user( 

373 city="Berlin", 

374 geom=create_coordinate(52.5200, 13.4050), 

375 geom_radius=200, 

376 ) 

377 

378 from_date = today() + timedelta(days=2) 

379 to_date = today() + timedelta(days=5) 

380 

381 with requests_session(token1) as api: 

382 res = api.CreateHostRequest( 

383 requests_pb2.CreateHostRequestReq( 

384 host_user_id=user2.id, 

385 from_date=from_date.isoformat(), 

386 to_date=to_date.isoformat(), 

387 text="a]" * 200 + "Hello! I would love to stay with you.", 

388 ) 

389 ) 

390 host_request_id = res.host_request_id 

391 moderator.approve_host_request(host_request_id) 

392 

393 # Host accepts 

394 with requests_session(token2) as api: 

395 api.RespondHostRequest( 

396 requests_pb2.RespondHostRequestReq( 

397 host_request_id=host_request_id, 

398 status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

399 ) 

400 ) 

401 

402 with session_scope() as session: 

403 events = _get_events(session, "host_request.accepted") 

404 assert len(events) == 1 

405 e = events[0] 

406 assert e.user_id == user2.id 

407 assert e.properties["host_request_id"] == host_request_id 

408 assert e.properties["surfer_id"] == user1.id 

409 assert e.properties["host_id"] == user2.id 

410 assert e.properties["surfer_gender"] == user1.gender 

411 assert e.properties["host_gender"] == user2.gender 

412 assert e.properties["from_date"] == str(from_date) 

413 assert e.properties["to_date"] == str(to_date) 

414 assert e.properties["host_city"] == "Berlin" 

415 

416 # Surfer confirms 

417 with requests_session(token1) as api: 

418 api.RespondHostRequest( 

419 requests_pb2.RespondHostRequestReq( 

420 host_request_id=host_request_id, 

421 status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

422 ) 

423 ) 

424 

425 with session_scope() as session: 

426 events = _get_events(session, "host_request.confirmed") 

427 assert len(events) == 1 

428 e = events[0] 

429 assert e.user_id == user1.id 

430 assert e.properties["surfer_id"] == user1.id 

431 assert e.properties["host_id"] == user2.id 

432 assert e.properties["surfer_gender"] == user1.gender 

433 assert e.properties["host_gender"] == user2.gender 

434 

435 

436def test_host_request_rejected_event(db, moderator): 

437 """Rejecting a host request logs event.""" 

438 user1, token1 = generate_user() 

439 user2, token2 = generate_user( 

440 city="Paris", 

441 geom=create_coordinate(48.8566, 2.3522), 

442 geom_radius=200, 

443 ) 

444 

445 with requests_session(token1) as api: 

446 res = api.CreateHostRequest( 

447 requests_pb2.CreateHostRequestReq( 

448 host_user_id=user2.id, 

449 from_date=(today() + timedelta(days=2)).isoformat(), 

450 to_date=(today() + timedelta(days=4)).isoformat(), 

451 text="a]" * 200 + "Would love to visit!", 

452 ) 

453 ) 

454 moderator.approve_host_request(res.host_request_id) 

455 

456 with requests_session(token2) as api: 

457 api.RespondHostRequest( 

458 requests_pb2.RespondHostRequestReq( 

459 host_request_id=res.host_request_id, 

460 status=messages_pb2.HOST_REQUEST_STATUS_REJECTED, 

461 ) 

462 ) 

463 

464 with session_scope() as session: 

465 events = _get_events(session, "host_request.rejected") 

466 assert len(events) == 1 

467 e = events[0] 

468 assert e.user_id == user2.id 

469 assert e.properties["surfer_id"] == user1.id 

470 assert e.properties["host_id"] == user2.id 

471 assert e.properties["host_city"] == "Paris" 

472 

473 

474def test_host_request_cancelled_event(db, moderator): 

475 """Cancelling a host request logs event.""" 

476 user1, token1 = generate_user() 

477 user2, token2 = generate_user( 

478 geom=create_coordinate(52.5200, 13.4050), 

479 geom_radius=200, 

480 ) 

481 

482 with requests_session(token1) as api: 

483 res = api.CreateHostRequest( 

484 requests_pb2.CreateHostRequestReq( 

485 host_user_id=user2.id, 

486 from_date=(today() + timedelta(days=2)).isoformat(), 

487 to_date=(today() + timedelta(days=4)).isoformat(), 

488 text="a]" * 200 + "Would love to visit!", 

489 ) 

490 ) 

491 moderator.approve_host_request(res.host_request_id) 

492 

493 with requests_session(token1) as api: 

494 api.RespondHostRequest( 

495 requests_pb2.RespondHostRequestReq( 

496 host_request_id=res.host_request_id, 

497 status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED, 

498 ) 

499 ) 

500 

501 with session_scope() as session: 

502 events = _get_events(session, "host_request.cancelled") 

503 assert len(events) == 1 

504 e = events[0] 

505 assert e.user_id == user1.id 

506 assert e.properties["surfer_id"] == user1.id 

507 assert e.properties["host_id"] == user2.id 

508 

509 

510def test_host_request_message_event(db, moderator): 

511 """Sending a message in a host request logs event with role.""" 

512 user1, token1 = generate_user() 

513 user2, token2 = generate_user( 

514 geom=create_coordinate(52.5200, 13.4050), 

515 geom_radius=200, 

516 ) 

517 

518 with requests_session(token1) as api: 

519 res = api.CreateHostRequest( 

520 requests_pb2.CreateHostRequestReq( 

521 host_user_id=user2.id, 

522 from_date=(today() + timedelta(days=2)).isoformat(), 

523 to_date=(today() + timedelta(days=4)).isoformat(), 

524 text="a]" * 200 + "Hello!", 

525 ) 

526 ) 

527 host_request_id = res.host_request_id 

528 moderator.approve_host_request(host_request_id) 

529 

530 # Host sends a message 

531 with requests_session(token2) as api: 

532 api.SendHostRequestMessage( 

533 requests_pb2.SendHostRequestMessageReq( 

534 host_request_id=host_request_id, 

535 text="Welcome!", 

536 ) 

537 ) 

538 

539 with session_scope() as session: 

540 events = _get_events(session, "host_request.message_sent") 

541 assert len(events) == 1 

542 e = events[0] 

543 assert e.user_id == user2.id 

544 assert e.properties["host_request_id"] == host_request_id 

545 assert e.properties["role"] == "host" 

546 assert e.properties["surfer_id"] == user1.id 

547 assert e.properties["host_id"] == user2.id 

548 

549 # Surfer sends a message 

550 with requests_session(token1) as api: 

551 api.SendHostRequestMessage( 

552 requests_pb2.SendHostRequestMessageReq( 

553 host_request_id=host_request_id, 

554 text="Thanks!", 

555 ) 

556 ) 

557 

558 with session_scope() as session: 

559 events = _get_events(session, "host_request.message_sent") 

560 assert len(events) == 2 

561 e = events[1] 

562 assert e.user_id == user1.id 

563 assert e.properties["role"] == "surfer" 

564 

565 

566# ===== Integration tests: messaging events ===== 

567 

568 

569def test_send_message_creates_event(db): 

570 """Sending a direct message creates a message.sent event.""" 

571 user1, token1 = generate_user() 

572 user2, token2 = generate_user() 

573 make_friends(user1, user2) 

574 

575 with conversations_session(token1) as api: 

576 res = api.SendDirectMessage( 

577 conversations_pb2.SendDirectMessageReq( 

578 recipient_user_id=user2.id, 

579 text="Hello friend!", 

580 ) 

581 ) 

582 

583 with session_scope() as session: 

584 events = _get_events(session, "message.sent") 

585 assert len(events) == 1 

586 e = events[0] 

587 assert e.user_id == user1.id 

588 assert e.properties["group_chat_id"] == res.group_chat_id 

589 assert e.properties["is_dm"] is True 

590 assert e.properties["recipient_id"] == user2.id 

591 

592 

593def test_create_group_chat_event(db): 

594 """Creating a group chat creates a group_chat.created event.""" 

595 user1, token1 = generate_user() 

596 user2, token2 = generate_user() 

597 user3, token3 = generate_user() 

598 make_friends(user1, user2) 

599 make_friends(user1, user3) 

600 

601 with conversations_session(token1) as api: 

602 res = api.CreateGroupChat( 

603 conversations_pb2.CreateGroupChatReq( 

604 recipient_user_ids=[user2.id, user3.id], 

605 title=wrappers_pb2.StringValue(value="Test Group"), 

606 ) 

607 ) 

608 

609 with session_scope() as session: 

610 events = _get_events(session, "group_chat.created") 

611 assert len(events) == 1 

612 e = events[0] 

613 assert e.user_id == user1.id 

614 assert e.properties["group_chat_id"] == res.group_chat_id 

615 assert e.properties["is_dm"] is False 

616 assert e.properties["recipient_count"] == 2 

617 

618 

619# ===== Integration tests: friendship events ===== 

620 

621 

622def test_friendship_request_events(db, moderator): 

623 """Friend request lifecycle creates appropriate events.""" 

624 user1, token1 = generate_user() 

625 user2, token2 = generate_user() 

626 

627 # Send friend request 

628 with api_session(token1) as api: 

629 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id)) 

630 

631 with session_scope() as session: 

632 events = _get_events(session, "friendship.request_sent") 

633 assert len(events) == 1 

634 assert events[0].user_id == user1.id 

635 assert events[0].properties["to_user_id"] == user2.id 

636 

637 # Approve and accept friend request 

638 with session_scope() as session: 

639 fr = session.execute(select(FriendRelationship)).scalar_one() 

640 fr_id = fr.id 

641 

642 moderator.approve_friend_request(fr_id) 

643 

644 with api_session(token2) as api: 

645 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True)) 

646 

647 with session_scope() as session: 

648 events = _get_events(session, "friendship.request_responded") 

649 assert len(events) == 1 

650 e = events[0] 

651 assert e.user_id == user2.id 

652 assert e.properties["from_user_id"] == user1.id 

653 assert e.properties["accepted"] is True 

654 

655 # Remove friend 

656 with api_session(token1) as api: 

657 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id)) 

658 

659 with session_scope() as session: 

660 events = _get_events(session, "friendship.removed") 

661 assert len(events) == 1 

662 assert events[0].user_id == user1.id 

663 assert events[0].properties["other_user_id"] == user2.id 

664 

665 

666def test_friendship_cancel_event(db, moderator): 

667 """Cancelling a friend request creates event.""" 

668 user1, token1 = generate_user() 

669 user2, token2 = generate_user() 

670 

671 with api_session(token1) as api: 

672 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id)) 

673 

674 with session_scope() as session: 

675 fr = session.execute(select(FriendRelationship)).scalar_one() 

676 fr_id = fr.id 

677 

678 with api_session(token1) as api: 

679 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id)) 

680 

681 with session_scope() as session: 

682 events = _get_events(session, "friendship.request_cancelled") 

683 assert len(events) == 1 

684 assert events[0].properties["to_user_id"] == user2.id 

685 

686 

687# ===== Integration tests: reporting events ===== 

688 

689 

690def test_report_creates_event(db): 

691 """Reporting content creates content.reported event with full context.""" 

692 user1, token1 = generate_user() 

693 user2, token2 = generate_user() 

694 

695 with reporting_session(token1) as api: 

696 api.Report( 

697 reporting_pb2.ReportReq( 

698 reason="spam", 

699 description="This is spam", 

700 content_ref="comment/456", 

701 author_user=user2.username, 

702 user_agent="TestAgent/1.0", 

703 page="https://couchers.org/profile/123", 

704 ) 

705 ) 

706 

707 with session_scope() as session: 

708 events = _get_events(session, "content.reported") 

709 assert len(events) == 1 

710 e = events[0] 

711 assert e.user_id == user1.id 

712 assert e.properties["author_user_id"] == user2.id 

713 assert e.properties["reason"] == "spam" 

714 assert e.properties["content_ref"] == "comment/456" 

715 assert e.properties["page"] == "https://couchers.org/profile/123" 

716 

717 

718# ===== Integration tests: search events ===== 

719 

720 

721def test_search_creates_event(db): 

722 """User search creates search.performed event with search parameters.""" 

723 user, token = generate_user() 

724 

725 with search_session(token) as api: 

726 api.UserSearch(search_pb2.UserSearchReq()) 

727 

728 with session_scope() as session: 

729 events = _get_events(session, "search.performed") 

730 assert len(events) == 1 

731 e = events[0] 

732 assert e.user_id == user.id 

733 assert e.properties["has_query"] is False 

734 assert e.properties["has_filters"] is False 

735 assert "total_items" in e.properties 

736 assert e.properties["search_in"] is None 

737 

738 

739# ===== Integration tests: reference events ===== 

740 

741 

742def test_friend_reference_event(db): 

743 """Writing a friend reference creates reference.friend_written event.""" 

744 user1, token1 = generate_user() 

745 user2, token2 = generate_user() 

746 make_friends(user1, user2) 

747 

748 with references_session(token1) as api: 

749 api.WriteFriendReference( 

750 references_pb2.WriteFriendReferenceReq( 

751 to_user_id=user2.id, 

752 text="Great person!", 

753 private_text="", 

754 rating=0.9, 

755 was_appropriate=True, 

756 ) 

757 ) 

758 

759 with session_scope() as session: 

760 events = _get_events(session, "reference.friend_written") 

761 assert len(events) == 1 

762 e = events[0] 

763 assert e.user_id == user1.id 

764 assert e.properties["to_user_id"] == user2.id 

765 assert e.properties["rating"] == pytest.approx(0.9) 

766 assert e.properties["was_appropriate"] is True 

767 

768 

769# ===== Integration tests: event (calendar) events ===== 

770 

771 

772def test_event_created_event(db): 

773 """Creating an event logs event.created with community info.""" 

774 user, token = generate_user() 

775 

776 with session_scope() as session: 

777 create_community(session, 0, 2, "Community", [user], [], None) 

778 

779 start_time = now() + timedelta(days=1) 

780 end_time = start_time + timedelta(hours=2) 

781 

782 with events_session(token) as api: 

783 res = api.CreateEvent( 

784 events_pb2.CreateEventReq( 

785 title="Test Meetup", 

786 content="Let's hang out", 

787 location=events_pb2.EventLocation( 

788 address="123 Main St", 

789 lat=0.1, 

790 lng=0.2, 

791 ), 

792 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

793 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

794 ) 

795 ) 

796 

797 with session_scope() as session: 

798 events = _get_events(session, "event.created") 

799 assert len(events) == 1 

800 e = events[0] 

801 assert e.user_id == user.id 

802 assert e.properties["event_id"] is not None 

803 assert e.properties["occurrence_id"] is not None 

804 assert e.properties["parent_community_id"] is not None 

805 assert e.properties["parent_community_name"] is not None 

806 

807 

808# ===== Integration tests: password change ===== 

809 

810 

811def test_password_change_event(db): 

812 """Changing password creates account.password_changed event.""" 

813 user, token = generate_user(hashed_password=hash_password("oldpassword")) 

814 

815 with account_session(token) as api: 

816 api.ChangePasswordV2( 

817 account_pb2.ChangePasswordV2Req( 

818 old_password="oldpassword", 

819 new_password="a new very secure password", 

820 ) 

821 ) 

822 

823 with session_scope() as session: 

824 events = _get_events(session, "account.password_changed") 

825 assert len(events) == 1 

826 assert events[0].user_id == user.id 

827 assert events[0].properties == {} 

828 

829 

830# ===== Test that events don't leak across tests ===== 

831 

832 

833def test_no_stale_events(db): 

834 """Verify the database is clean - no events from previous tests.""" 

835 with session_scope() as session: 

836 events = _get_events(session) 

837 assert len(events) == 0