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

406 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-13 02:44 +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 references_pb2, 

23 reporting_pb2, 

24 requests_pb2, 

25 search_pb2, 

26) 

27from couchers.utils import Timestamp_from_datetime, create_coordinate, now, today 

28from tests.fixtures.db import generate_user, make_friends 

29from tests.fixtures.sessions import ( 

30 MockGrpcContext, 

31 account_session, 

32 api_session, 

33 auth_api_session, 

34 conversations_session, 

35 events_session, 

36 references_session, 

37 reporting_session, 

38 requests_session, 

39 search_session, 

40) 

41from tests.test_communities import create_community 

42 

43 

44@pytest.fixture(autouse=True) 

45def _(testconfig, fast_passwords): 

46 pass 

47 

48 

49def _get_events(session, event_type=None): 

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

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

52 if event_type: 

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

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

55 

56 

57# ===== Unit tests for log_event function ===== 

58 

59 

60def test_log_event_authenticated_context(db): 

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

62 user, token = generate_user() 

63 

64 with session_scope() as session: 

65 context = make_interactive_context( 

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

67 user_id=user.id, 

68 is_api_key=False, 

69 token=token, 

70 localization=LocalizationContext.en_utc(), 

71 sofa="test-sofa-123", 

72 ) 

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

74 

75 with session_scope() as session: 

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

77 assert len(events) == 1 

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

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

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

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

82 assert events[0].created is not None 

83 

84 

85def test_log_event_with_override_user_id(db): 

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

87 user, token = generate_user() 

88 

89 with session_scope() as session: 

90 context = make_interactive_context( 

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

92 user_id=None, 

93 is_api_key=False, 

94 token=None, 

95 localization=LocalizationContext.en_utc(), 

96 sofa="sofa-456", 

97 ) 

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

99 

100 with session_scope() as session: 

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

102 assert len(events) == 1 

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

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

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

106 

107 

108def test_log_event_anonymous(db): 

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

110 with session_scope() as session: 

111 context = make_interactive_context( 

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

113 user_id=None, 

114 is_api_key=False, 

115 token=None, 

116 localization=LocalizationContext.en_utc(), 

117 ) 

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

119 

120 with session_scope() as session: 

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

122 assert len(events) == 1 

123 assert events[0].user_id is None 

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

125 

126 

127def test_log_event_complex_properties(db): 

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

129 user, token = generate_user() 

130 

131 props = { 

132 "string_val": "hello", 

133 "int_val": 42, 

134 "float_val": 3.14, 

135 "bool_val": True, 

136 "none_val": None, 

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

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

139 } 

140 

141 with session_scope() as session: 

142 context = make_interactive_context( 

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

144 user_id=user.id, 

145 is_api_key=False, 

146 token=token, 

147 localization=LocalizationContext.en_utc(), 

148 ) 

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

150 

151 with session_scope() as session: 

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

153 assert len(events) == 1 

154 assert events[0].properties == props 

155 

156 

157def test_log_event_empty_properties(db): 

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

159 user, token = generate_user() 

160 

161 with session_scope() as session: 

162 context = make_interactive_context( 

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

164 user_id=user.id, 

165 is_api_key=False, 

166 token=token, 

167 localization=LocalizationContext.en_utc(), 

168 ) 

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

170 

171 with session_scope() as session: 

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

173 assert len(events) == 1 

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

175 

176 

177def test_log_event_multiple_events(db): 

178 """Multiple events are stored independently.""" 

179 user, token = generate_user() 

180 

181 with session_scope() as session: 

182 context = make_interactive_context( 

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

184 user_id=user.id, 

185 is_api_key=False, 

186 token=token, 

187 localization=LocalizationContext.en_utc(), 

188 ) 

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

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

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

192 

193 with session_scope() as session: 

194 all_events = _get_events(session) 

195 assert len(all_events) == 3 

196 

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

198 assert len(first_events) == 2 

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

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

201 

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

203 assert len(second_events) == 1 

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

205 

206 

207# ===== Integration tests: auth events ===== 

208 

209 

210def test_signup_flow_creates_events(db): 

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

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

213 res = auth_api.SignupFlow( 

214 auth_pb2.SignupFlowReq( 

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

216 ) 

217 ) 

218 

219 flow_token = res.flow_token 

220 

221 with session_scope() as session: 

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

223 assert len(events) == 1 

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

225 

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

227 with session_scope() as session: 

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

229 email_token = flow.email_token 

230 

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

232 auth_api.SignupFlow( 

233 auth_pb2.SignupFlowReq( 

234 flow_token=flow_token, 

235 email_token=email_token, 

236 ) 

237 ) 

238 

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

240 auth_api.SignupFlow( 

241 auth_pb2.SignupFlowReq( 

242 flow_token=flow_token, 

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

244 ) 

245 ) 

246 

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

248 auth_api.SignupFlow( 

249 auth_pb2.SignupFlowReq( 

250 flow_token=flow_token, 

251 account=auth_pb2.SignupAccount( 

252 username="frodo", 

253 password="a very insecure password", 

254 birthdate="1970-01-01", 

255 gender="Bot", 

256 hosting_status=api_pb2.HOSTING_STATUS_MAYBE, 

257 city="New York City", 

258 lat=40.7331, 

259 lng=-73.9778, 

260 radius=500, 

261 accept_tos=True, 

262 ), 

263 ) 

264 ) 

265 

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

267 res = auth_api.SignupFlow( 

268 auth_pb2.SignupFlowReq( 

269 flow_token=flow_token, 

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

271 ) 

272 ) 

273 

274 assert res.HasField("auth_res") 

275 user_id = res.auth_res.user_id 

276 

277 with session_scope() as session: 

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

279 assert len(events) == 1 

280 e = events[0] 

281 assert e.user_id == user_id 

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

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

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

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

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

287 assert "filled_contributor_form" in e.properties 

288 

289 

290def test_login_creates_event(db): 

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

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

293 

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

295 auth_api.Authenticate( 

296 auth_pb2.AuthReq( 

297 user=user.username, 

298 password="password123", 

299 remember_device=True, 

300 ) 

301 ) 

302 

303 with session_scope() as session: 

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

305 assert len(events) == 1 

306 e = events[0] 

307 assert e.user_id == user.id 

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

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

310 

311 

312def test_logout_creates_event(db): 

313 """Logout creates account.logout event.""" 

314 user, token = generate_user() 

315 

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

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

318 

319 with session_scope() as session: 

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

321 assert len(events) == 1 

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

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

324 

325 

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

327 

328 

329def test_host_request_created_event(db, moderator): 

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

331 user1, token1 = generate_user() 

332 user2, token2 = generate_user( 

333 city="Berlin", 

334 geom=create_coordinate(52.5200, 13.4050), 

335 geom_radius=200, 

336 ) 

337 

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

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

340 

341 with requests_session(token1) as api: 

342 res = api.CreateHostRequest( 

343 requests_pb2.CreateHostRequestReq( 

344 host_user_id=user2.id, 

345 from_date=from_date.isoformat(), 

346 to_date=to_date.isoformat(), 

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

348 ) 

349 ) 

350 

351 host_request_id = res.host_request_id 

352 

353 with session_scope() as session: 

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

355 assert len(events) == 1 

356 e = events[0] 

357 assert e.user_id == user1.id 

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

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

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

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

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

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

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

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

366 

367 

368def test_host_request_status_change_events(db, moderator): 

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

370 user1, token1 = generate_user() 

371 user2, token2 = generate_user( 

372 city="Berlin", 

373 geom=create_coordinate(52.5200, 13.4050), 

374 geom_radius=200, 

375 ) 

376 

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

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

379 

380 with requests_session(token1) as api: 

381 res = api.CreateHostRequest( 

382 requests_pb2.CreateHostRequestReq( 

383 host_user_id=user2.id, 

384 from_date=from_date.isoformat(), 

385 to_date=to_date.isoformat(), 

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

387 ) 

388 ) 

389 host_request_id = res.host_request_id 

390 moderator.approve_host_request(host_request_id) 

391 

392 # Host accepts 

393 with requests_session(token2) as api: 

394 api.RespondHostRequest( 

395 requests_pb2.RespondHostRequestReq( 

396 host_request_id=host_request_id, 

397 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

398 ) 

399 ) 

400 

401 with session_scope() as session: 

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

403 assert len(events) == 1 

404 e = events[0] 

405 assert e.user_id == user2.id 

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

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

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

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

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

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

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

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

414 

415 # Surfer confirms 

416 with requests_session(token1) as api: 

417 api.RespondHostRequest( 

418 requests_pb2.RespondHostRequestReq( 

419 host_request_id=host_request_id, 

420 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

421 ) 

422 ) 

423 

424 with session_scope() as session: 

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

426 assert len(events) == 1 

427 e = events[0] 

428 assert e.user_id == user1.id 

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

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

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

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

433 

434 

435def test_host_request_rejected_event(db, moderator): 

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

437 user1, token1 = generate_user() 

438 user2, token2 = generate_user( 

439 city="Paris", 

440 geom=create_coordinate(48.8566, 2.3522), 

441 geom_radius=200, 

442 ) 

443 

444 with requests_session(token1) as api: 

445 res = api.CreateHostRequest( 

446 requests_pb2.CreateHostRequestReq( 

447 host_user_id=user2.id, 

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

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

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

451 ) 

452 ) 

453 moderator.approve_host_request(res.host_request_id) 

454 

455 with requests_session(token2) as api: 

456 api.RespondHostRequest( 

457 requests_pb2.RespondHostRequestReq( 

458 host_request_id=res.host_request_id, 

459 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED, 

460 ) 

461 ) 

462 

463 with session_scope() as session: 

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

465 assert len(events) == 1 

466 e = events[0] 

467 assert e.user_id == user2.id 

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

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

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

471 

472 

473def test_host_request_cancelled_event(db, moderator): 

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

475 user1, token1 = generate_user() 

476 user2, token2 = generate_user( 

477 geom=create_coordinate(52.5200, 13.4050), 

478 geom_radius=200, 

479 ) 

480 

481 with requests_session(token1) as api: 

482 res = api.CreateHostRequest( 

483 requests_pb2.CreateHostRequestReq( 

484 host_user_id=user2.id, 

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

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

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

488 ) 

489 ) 

490 moderator.approve_host_request(res.host_request_id) 

491 

492 with requests_session(token1) as api: 

493 api.RespondHostRequest( 

494 requests_pb2.RespondHostRequestReq( 

495 host_request_id=res.host_request_id, 

496 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

497 ) 

498 ) 

499 

500 with session_scope() as session: 

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

502 assert len(events) == 1 

503 e = events[0] 

504 assert e.user_id == user1.id 

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

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

507 

508 

509def test_host_request_message_event(db, moderator): 

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

511 user1, token1 = generate_user() 

512 user2, token2 = generate_user( 

513 geom=create_coordinate(52.5200, 13.4050), 

514 geom_radius=200, 

515 ) 

516 

517 with requests_session(token1) as api: 

518 res = api.CreateHostRequest( 

519 requests_pb2.CreateHostRequestReq( 

520 host_user_id=user2.id, 

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

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

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

524 ) 

525 ) 

526 host_request_id = res.host_request_id 

527 moderator.approve_host_request(host_request_id) 

528 

529 # Host sends a message 

530 with requests_session(token2) as api: 

531 api.SendHostRequestMessage( 

532 requests_pb2.SendHostRequestMessageReq( 

533 host_request_id=host_request_id, 

534 text="Welcome!", 

535 ) 

536 ) 

537 

538 with session_scope() as session: 

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

540 assert len(events) == 1 

541 e = events[0] 

542 assert e.user_id == user2.id 

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

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

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

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

547 

548 # Surfer sends a message 

549 with requests_session(token1) as api: 

550 api.SendHostRequestMessage( 

551 requests_pb2.SendHostRequestMessageReq( 

552 host_request_id=host_request_id, 

553 text="Thanks!", 

554 ) 

555 ) 

556 

557 with session_scope() as session: 

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

559 assert len(events) == 2 

560 e = events[1] 

561 assert e.user_id == user1.id 

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

563 

564 

565# ===== Integration tests: messaging events ===== 

566 

567 

568def test_send_message_creates_event(db): 

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

570 user1, token1 = generate_user() 

571 user2, token2 = generate_user() 

572 make_friends(user1, user2) 

573 

574 with conversations_session(token1) as api: 

575 res = api.SendDirectMessage( 

576 conversations_pb2.SendDirectMessageReq( 

577 recipient_user_id=user2.id, 

578 text="Hello friend!", 

579 ) 

580 ) 

581 

582 with session_scope() as session: 

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

584 assert len(events) == 1 

585 e = events[0] 

586 assert e.user_id == user1.id 

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

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

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

590 

591 

592def test_create_group_chat_event(db): 

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

594 user1, token1 = generate_user() 

595 user2, token2 = generate_user() 

596 user3, token3 = generate_user() 

597 make_friends(user1, user2) 

598 make_friends(user1, user3) 

599 

600 with conversations_session(token1) as api: 

601 res = api.CreateGroupChat( 

602 conversations_pb2.CreateGroupChatReq( 

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

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

605 ) 

606 ) 

607 

608 with session_scope() as session: 

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

610 assert len(events) == 1 

611 e = events[0] 

612 assert e.user_id == user1.id 

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

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

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

616 

617 

618# ===== Integration tests: friendship events ===== 

619 

620 

621def test_friendship_request_events(db, moderator): 

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

623 user1, token1 = generate_user() 

624 user2, token2 = generate_user() 

625 

626 # Send friend request 

627 with api_session(token1) as api: 

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

629 

630 with session_scope() as session: 

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

632 assert len(events) == 1 

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

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

635 

636 # Approve and accept friend request 

637 with session_scope() as session: 

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

639 fr_id = fr.id 

640 

641 moderator.approve_friend_request(fr_id) 

642 

643 with api_session(token2) as api: 

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

645 

646 with session_scope() as session: 

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

648 assert len(events) == 1 

649 e = events[0] 

650 assert e.user_id == user2.id 

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

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

653 

654 # Remove friend 

655 with api_session(token1) as api: 

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

657 

658 with session_scope() as session: 

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

660 assert len(events) == 1 

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

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

663 

664 

665def test_friendship_cancel_event(db, moderator): 

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

667 user1, token1 = generate_user() 

668 user2, token2 = generate_user() 

669 

670 with api_session(token1) as api: 

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

672 

673 with session_scope() as session: 

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

675 fr_id = fr.id 

676 

677 with api_session(token1) as api: 

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

679 

680 with session_scope() as session: 

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

682 assert len(events) == 1 

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

684 

685 

686# ===== Integration tests: reporting events ===== 

687 

688 

689def test_report_creates_event(db): 

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

691 user1, token1 = generate_user() 

692 user2, token2 = generate_user() 

693 

694 with reporting_session(token1) as api: 

695 api.Report( 

696 reporting_pb2.ReportReq( 

697 reason="spam", 

698 description="This is spam", 

699 content_ref="comment/456", 

700 author_user=user2.username, 

701 user_agent="TestAgent/1.0", 

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

703 ) 

704 ) 

705 

706 with session_scope() as session: 

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

708 assert len(events) == 1 

709 e = events[0] 

710 assert e.user_id == user1.id 

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

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

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

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

715 

716 

717# ===== Integration tests: search events ===== 

718 

719 

720def test_search_creates_event(db): 

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

722 user, token = generate_user() 

723 

724 with search_session(token) as api: 

725 api.UserSearch(search_pb2.UserSearchReq()) 

726 

727 with session_scope() as session: 

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

729 assert len(events) == 1 

730 e = events[0] 

731 assert e.user_id == user.id 

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

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

734 assert "total_items" in e.properties 

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

736 

737 

738# ===== Integration tests: reference events ===== 

739 

740 

741def test_friend_reference_event(db): 

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

743 user1, token1 = generate_user() 

744 user2, token2 = generate_user() 

745 make_friends(user1, user2) 

746 

747 with references_session(token1) as api: 

748 api.WriteFriendReference( 

749 references_pb2.WriteFriendReferenceReq( 

750 to_user_id=user2.id, 

751 text="Great person!", 

752 private_text="", 

753 rating=0.9, 

754 was_appropriate=True, 

755 ) 

756 ) 

757 

758 with session_scope() as session: 

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

760 assert len(events) == 1 

761 e = events[0] 

762 assert e.user_id == user1.id 

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

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

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

766 

767 

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

769 

770 

771def test_event_created_event(db): 

772 """Creating an event logs event.created with community info and online status.""" 

773 user, token = generate_user() 

774 

775 with session_scope() as session: 

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

777 

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

779 end_time = start_time + timedelta(hours=2) 

780 

781 with events_session(token) as api: 

782 res = api.CreateEvent( 

783 events_pb2.CreateEventReq( 

784 title="Test Meetup", 

785 content="Let's hang out", 

786 offline_information=events_pb2.OfflineEventInformation( 

787 address="123 Main St", 

788 lat=0.1, 

789 lng=0.2, 

790 ), 

791 start_time=Timestamp_from_datetime(start_time), 

792 end_time=Timestamp_from_datetime(end_time), 

793 timezone="UTC", 

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 assert e.properties["online"] is False 

807 

808 

809# ===== Integration tests: password change ===== 

810 

811 

812def test_password_change_event(db): 

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

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

815 

816 with account_session(token) as api: 

817 api.ChangePasswordV2( 

818 account_pb2.ChangePasswordV2Req( 

819 old_password="oldpassword", 

820 new_password="a new very secure password", 

821 ) 

822 ) 

823 

824 with session_scope() as session: 

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

826 assert len(events) == 1 

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

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

829 

830 

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

832 

833 

834def test_no_stale_events(db): 

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

836 with session_scope() as session: 

837 events = _get_events(session) 

838 assert len(events) == 0