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

425 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-12 12:41 +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 messages_pb2, 

22 references_pb2, 

23 reporting_pb2, 

24 search_pb2, 

25) 

26from couchers.utils import create_coordinate, today 

27from tests.fixtures.api import events as events_api 

28from tests.fixtures.api import requests as requests_api 

29from tests.fixtures.db import generate_user, make_friends 

30from tests.fixtures.feature_flags import Flags 

31from tests.fixtures.sessions import ( 

32 MockGrpcContext, 

33 account_session, 

34 api_session, 

35 auth_api_session, 

36 conversations_session, 

37 references_session, 

38 reporting_session, 

39 search_session, 

40) 

41from tests.test_communities import create_community 

42 

43 

44@pytest.fixture(autouse=True) 

45def _(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 signup_initiated, signup_email_verified and signup_completed events sharing a flow id.""" 

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 flow_id = session.execute(select(SignupFlow.id).where(SignupFlow.flow_token == flow_token)).scalar_one() 

226 assert events[0].properties["signup_flow_id"] == flow_id 

227 

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

229 with session_scope() as session: 

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

231 email_token = flow.email_token 

232 

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

234 auth_api.SignupFlow( 

235 auth_pb2.SignupFlowReq( 

236 flow_token=flow_token, 

237 email_token=email_token, 

238 ) 

239 ) 

240 

241 with session_scope() as session: 

242 events = _get_events(session, "account.signup_email_verified") 

243 assert len(events) == 1 

244 assert events[0].properties == {"signup_flow_id": flow_id} 

245 

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

247 auth_api.SignupFlow( 

248 auth_pb2.SignupFlowReq( 

249 flow_token=flow_token, 

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

251 ) 

252 ) 

253 

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

255 auth_api.SignupFlow( 

256 auth_pb2.SignupFlowReq( 

257 flow_token=flow_token, 

258 account=auth_pb2.SignupAccount( 

259 username="frodo", 

260 password="a very insecure password", 

261 birthdate="1970-01-01", 

262 gender="Bot", 

263 hosting_status=api_pb2.HOSTING_STATUS_MAYBE, 

264 city="New York City", 

265 lat=40.7331, 

266 lng=-73.9778, 

267 radius=500, 

268 accept_tos=True, 

269 ), 

270 ) 

271 ) 

272 

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

274 res = auth_api.SignupFlow( 

275 auth_pb2.SignupFlowReq( 

276 flow_token=flow_token, 

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

278 ) 

279 ) 

280 

281 assert res.HasField("auth_res") 

282 user_id = res.auth_res.user_id 

283 

284 with session_scope() as session: 

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

286 assert len(events) == 1 

287 e = events[0] 

288 assert e.user_id == user_id 

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

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

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

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

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

294 assert "filled_contributor_form" in e.properties 

295 assert e.properties["signup_flow_id"] == flow_id 

296 

297 

298def test_login_creates_event(db): 

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

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

301 

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

303 auth_api.Authenticate( 

304 auth_pb2.AuthReq( 

305 user=user.username, 

306 password="password123", 

307 remember_device=True, 

308 ) 

309 ) 

310 

311 with session_scope() as session: 

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

313 assert len(events) == 1 

314 e = events[0] 

315 assert e.user_id == user.id 

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

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

318 

319 

320def test_logout_creates_event(db): 

321 """Logout creates account.logout event.""" 

322 user, token = generate_user() 

323 

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

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

326 

327 with session_scope() as session: 

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

329 assert len(events) == 1 

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

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

332 

333 

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

335 

336 

337def test_host_request_created_event(db, moderator): 

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

339 user1, token1 = generate_user() 

340 user2, token2 = generate_user( 

341 city="Berlin", 

342 geom=create_coordinate(52.5200, 13.4050), 

343 geom_radius=200, 

344 ) 

345 

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

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

348 

349 host_request_id = requests_api.create_host_request( 

350 token=token1, host_user_id=user2.id, from_date=from_date, to_date=to_date 

351 ) 

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 host_request_id = requests_api.create_host_request( 

381 token=token1, host_user_id=user2.id, from_date=from_date, to_date=to_date, approve_by=moderator 

382 ) 

383 

384 # Host accepts 

385 requests_api.respond_host_request( 

386 token=token2, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_ACCEPTED 

387 ) 

388 

389 with session_scope() as session: 

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

391 assert len(events) == 1 

392 e = events[0] 

393 assert e.user_id == user2.id 

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

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

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

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

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

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

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

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

402 

403 # Surfer confirms 

404 requests_api.respond_host_request( 

405 token=token1, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED 

406 ) 

407 

408 with session_scope() as session: 

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

410 assert len(events) == 1 

411 e = events[0] 

412 assert e.user_id == user1.id 

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

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

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

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

417 

418 

419def test_host_request_rejected_event(db, moderator): 

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

421 user1, token1 = generate_user() 

422 user2, token2 = generate_user( 

423 city="Paris", 

424 geom=create_coordinate(48.8566, 2.3522), 

425 geom_radius=200, 

426 ) 

427 

428 host_request_id = requests_api.create_host_request(token=token1, host_user_id=user2.id, approve_by=moderator) 

429 

430 requests_api.respond_host_request( 

431 token=token2, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED 

432 ) 

433 

434 with session_scope() as session: 

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

436 assert len(events) == 1 

437 e = events[0] 

438 assert e.user_id == user2.id 

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

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

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

442 

443 

444def test_host_request_cancelled_event(db, moderator): 

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

446 user1, token1 = generate_user() 

447 user2, token2 = generate_user( 

448 geom=create_coordinate(52.5200, 13.4050), 

449 geom_radius=200, 

450 ) 

451 

452 host_request_id = requests_api.create_host_request(token=token1, host_user_id=user2.id, approve_by=moderator) 

453 

454 requests_api.respond_host_request( 

455 token=token1, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED 

456 ) 

457 

458 with session_scope() as session: 

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

460 assert len(events) == 1 

461 e = events[0] 

462 assert e.user_id == user1.id 

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

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

465 

466 

467def test_host_request_message_event(db, moderator): 

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

469 user1, token1 = generate_user() 

470 user2, token2 = generate_user( 

471 geom=create_coordinate(52.5200, 13.4050), 

472 geom_radius=200, 

473 ) 

474 

475 host_request_id = requests_api.create_host_request(token=token1, host_user_id=user2.id, approve_by=moderator) 

476 

477 # Host sends a message 

478 requests_api.send_host_request_message(token=token2, host_request_id=host_request_id, text="Welcome!") 

479 

480 with session_scope() as session: 

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

482 assert len(events) == 1 

483 e = events[0] 

484 assert e.user_id == user2.id 

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

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

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

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

489 

490 # Surfer sends a message 

491 requests_api.send_host_request_message(token=token1, host_request_id=host_request_id, text="Thanks!") 

492 

493 with session_scope() as session: 

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

495 assert len(events) == 2 

496 e = events[1] 

497 assert e.user_id == user1.id 

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

499 

500 

501# ===== Integration tests: messaging events ===== 

502 

503 

504def test_send_message_creates_event(db): 

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

506 user1, token1 = generate_user() 

507 user2, token2 = generate_user() 

508 make_friends(user1, user2) 

509 

510 with conversations_session(token1) as api: 

511 res = api.SendDirectMessage( 

512 conversations_pb2.SendDirectMessageReq( 

513 recipient_user_id=user2.id, 

514 text="Hello friend!", 

515 ) 

516 ) 

517 

518 with session_scope() as session: 

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

520 assert len(events) == 1 

521 e = events[0] 

522 assert e.user_id == user1.id 

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

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

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

526 

527 

528def test_create_group_chat_event(db): 

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

530 user1, token1 = generate_user() 

531 user2, token2 = generate_user() 

532 user3, token3 = generate_user() 

533 make_friends(user1, user2) 

534 make_friends(user1, user3) 

535 

536 with conversations_session(token1) as api: 

537 res = api.CreateGroupChat( 

538 conversations_pb2.CreateGroupChatReq( 

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

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

541 ) 

542 ) 

543 

544 with session_scope() as session: 

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

546 assert len(events) == 1 

547 e = events[0] 

548 assert e.user_id == user1.id 

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

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

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

552 

553 

554# ===== Integration tests: friendship events ===== 

555 

556 

557def test_friendship_request_events(db, moderator): 

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

559 user1, token1 = generate_user() 

560 user2, token2 = generate_user() 

561 

562 # Send friend request 

563 with api_session(token1) as api: 

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

565 

566 with session_scope() as session: 

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

568 assert len(events) == 1 

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

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

571 

572 # Approve and accept friend request 

573 with session_scope() as session: 

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

575 fr_id = fr.id 

576 

577 moderator.approve_friend_request(fr_id) 

578 

579 with api_session(token2) as api: 

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

581 

582 with session_scope() as session: 

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

584 assert len(events) == 1 

585 e = events[0] 

586 assert e.user_id == user2.id 

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

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

589 

590 # Remove friend 

591 with api_session(token1) as api: 

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

593 

594 with session_scope() as session: 

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

596 assert len(events) == 1 

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

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

599 

600 

601def test_friendship_cancel_event(db, moderator): 

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

603 user1, token1 = generate_user() 

604 user2, token2 = generate_user() 

605 

606 with api_session(token1) as api: 

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

608 

609 with session_scope() as session: 

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

611 fr_id = fr.id 

612 

613 with api_session(token1) as api: 

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

615 

616 with session_scope() as session: 

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

618 assert len(events) == 1 

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

620 

621 

622# ===== Integration tests: reporting events ===== 

623 

624 

625def test_report_creates_event(db): 

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

627 user1, token1 = generate_user() 

628 user2, token2 = generate_user() 

629 

630 with reporting_session(token1) as api: 

631 api.Report( 

632 reporting_pb2.ReportReq( 

633 reason="spam", 

634 description="This is spam", 

635 content_ref="comment/456", 

636 author_user=user2.username, 

637 user_agent="TestAgent/1.0", 

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

639 ) 

640 ) 

641 

642 with session_scope() as session: 

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

644 assert len(events) == 1 

645 e = events[0] 

646 assert e.user_id == user1.id 

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

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

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

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

651 

652 

653# ===== Integration tests: search events ===== 

654 

655 

656def test_search_creates_event(db): 

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

658 user, token = generate_user() 

659 

660 with search_session(token) as api: 

661 api.UserSearchV2(search_pb2.UserSearchReq()) 

662 

663 with session_scope() as session: 

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

665 assert len(events) == 1 

666 e = events[0] 

667 assert e.user_id == user.id 

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

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

670 assert "total_items" in e.properties 

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

672 assert e.properties["ranking_offsets_variant"] == "" 

673 

674 

675def test_search_v2_creates_event(db, flags: Flags): 

676 user, token = generate_user() 

677 flags.set_string("search_ranking_offsets_variant", "a") 

678 

679 with search_session(token) as api: 

680 api.UserSearchV2( 

681 search_pb2.UserSearchReq( 

682 query=wrappers_pb2.StringValue(value="paris"), 

683 search_in_rectangle=search_pb2.RectArea(lat_min=0, lat_max=1, lng_min=0, lng_max=1), 

684 only_with_references=True, 

685 age_min=wrappers_pb2.UInt32Value(value=25), 

686 accepts_pets=wrappers_pb2.BoolValue(value=False), 

687 page_size=10, 

688 ) 

689 ) 

690 

691 with session_scope() as session: 

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

693 assert len(events) == 1 

694 e = events[0] 

695 assert e.user_id == user.id 

696 assert e.properties["has_query"] is True 

697 assert e.properties["has_filters"] is True 

698 assert e.properties["filters"] == ["accepts_pets", "age_min", "only_with_references"] 

699 assert e.properties["is_selection"] is False 

700 assert e.properties["has_page_token"] is False 

701 assert e.properties["total_items"] == 0 

702 assert e.properties["search_in"] == "search_in_rectangle" 

703 assert e.properties["ranking_offsets_variant"] == "a" 

704 

705 

706def test_search_v2_selection_event(db): 

707 user, token = generate_user() 

708 other, _ = generate_user() 

709 

710 with search_session(token) as api: 

711 api.UserSearchV2(search_pb2.UserSearchReq(exactly_user_ids=[other.id])) 

712 

713 with session_scope() as session: 

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

715 assert len(events) == 1 

716 e = events[0] 

717 assert e.properties["is_selection"] is True 

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

719 assert e.properties["filters"] == [] 

720 

721 

722# ===== Integration tests: reference events ===== 

723 

724 

725def test_friend_reference_event(db): 

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

727 user1, token1 = generate_user() 

728 user2, token2 = generate_user() 

729 make_friends(user1, user2) 

730 

731 with references_session(token1) as api: 

732 api.WriteFriendReference( 

733 references_pb2.WriteFriendReferenceReq( 

734 to_user_id=user2.id, 

735 text="Great person!", 

736 private_text="", 

737 rating=0.9, 

738 was_appropriate=True, 

739 ) 

740 ) 

741 

742 with session_scope() as session: 

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

744 assert len(events) == 1 

745 e = events[0] 

746 assert e.user_id == user1.id 

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

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

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

750 

751 

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

753 

754 

755def test_event_created_event(db): 

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

757 user, token = generate_user() 

758 

759 with session_scope() as session: 

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

761 

762 events_api.create_event(token=token) 

763 

764 with session_scope() as session: 

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

766 assert len(events) == 1 

767 e = events[0] 

768 assert e.user_id == user.id 

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

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

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

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

773 

774 

775# ===== Integration tests: password change ===== 

776 

777 

778def test_password_change_event(db): 

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

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

781 

782 with account_session(token) as api: 

783 api.ChangePasswordV2( 

784 account_pb2.ChangePasswordV2Req( 

785 old_password="oldpassword", 

786 new_password="a new very secure password", 

787 ) 

788 ) 

789 

790 with session_scope() as session: 

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

792 assert len(events) == 1 

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

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

795 

796 

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

798 

799 

800def test_no_stale_events(db): 

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

802 with session_scope() as session: 

803 events = _get_events(session) 

804 assert len(events) == 0