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

405 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 19:22 +0000

1from datetime import timedelta 

2 

3import pytest 

4from google.protobuf import empty_pb2, wrappers_pb2 

5from sqlalchemy import select 

6 

7from couchers.context import make_interactive_context 

8from couchers.crypto import hash_password 

9from couchers.db import session_scope 

10from couchers.event_log import log_event 

11from couchers.models.logging import EventLog 

12from couchers.proto import ( 

13 api_pb2, 

14 auth_pb2, 

15 conversations_pb2, 

16 events_pb2, 

17 references_pb2, 

18 reporting_pb2, 

19 requests_pb2, 

20 search_pb2, 

21) 

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

23from tests.fixtures.db import generate_user, make_friends 

24from tests.fixtures.sessions import ( 

25 MockGrpcContext, 

26 api_session, 

27 auth_api_session, 

28 conversations_session, 

29 events_session, 

30 references_session, 

31 reporting_session, 

32 requests_session, 

33 search_session, 

34) 

35from tests.test_communities import create_community 

36 

37 

38@pytest.fixture(autouse=True) 

39def _(testconfig, fast_passwords): 

40 pass 

41 

42 

43def _get_events(session, event_type=None): 

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

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

46 if event_type: 

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

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

49 

50 

51# ===== Unit tests for log_event function ===== 

52 

53 

54def test_log_event_authenticated_context(db): 

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

56 user, token = generate_user() 

57 

58 with session_scope() as session: 

59 context = make_interactive_context( 

60 grpc_context=MockGrpcContext(), 

61 user_id=user.id, 

62 is_api_key=False, 

63 token=token, 

64 ui_language_preference=None, 

65 sofa="test-sofa-123", 

66 ) 

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

68 

69 with session_scope() as session: 

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

71 assert len(events) == 1 

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

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

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

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

76 assert events[0].created is not None 

77 

78 

79def test_log_event_with_override_user_id(db): 

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

81 user, token = generate_user() 

82 

83 with session_scope() as session: 

84 context = make_interactive_context( 

85 grpc_context=MockGrpcContext(), 

86 user_id=None, 

87 is_api_key=False, 

88 token=None, 

89 ui_language_preference=None, 

90 sofa="sofa-456", 

91 ) 

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

93 

94 with session_scope() as session: 

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

96 assert len(events) == 1 

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

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

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

100 

101 

102def test_log_event_anonymous(db): 

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

104 with session_scope() as session: 

105 context = make_interactive_context( 

106 grpc_context=MockGrpcContext(), 

107 user_id=None, 

108 is_api_key=False, 

109 token=None, 

110 ui_language_preference=None, 

111 ) 

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

113 

114 with session_scope() as session: 

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

116 assert len(events) == 1 

117 assert events[0].user_id is None 

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

119 

120 

121def test_log_event_complex_properties(db): 

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

123 user, token = generate_user() 

124 

125 props = { 

126 "string_val": "hello", 

127 "int_val": 42, 

128 "float_val": 3.14, 

129 "bool_val": True, 

130 "none_val": None, 

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

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

133 } 

134 

135 with session_scope() as session: 

136 context = make_interactive_context( 

137 grpc_context=MockGrpcContext(), 

138 user_id=user.id, 

139 is_api_key=False, 

140 token=token, 

141 ui_language_preference=None, 

142 ) 

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

144 

145 with session_scope() as session: 

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

147 assert len(events) == 1 

148 assert events[0].properties == props 

149 

150 

151def test_log_event_empty_properties(db): 

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

153 user, token = generate_user() 

154 

155 with session_scope() as session: 

156 context = make_interactive_context( 

157 grpc_context=MockGrpcContext(), 

158 user_id=user.id, 

159 is_api_key=False, 

160 token=token, 

161 ui_language_preference=None, 

162 ) 

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

164 

165 with session_scope() as session: 

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

167 assert len(events) == 1 

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

169 

170 

171def test_log_event_multiple_events(db): 

172 """Multiple events are stored independently.""" 

173 user, token = generate_user() 

174 

175 with session_scope() as session: 

176 context = make_interactive_context( 

177 grpc_context=MockGrpcContext(), 

178 user_id=user.id, 

179 is_api_key=False, 

180 token=token, 

181 ui_language_preference=None, 

182 ) 

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

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

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

186 

187 with session_scope() as session: 

188 all_events = _get_events(session) 

189 assert len(all_events) == 3 

190 

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

192 assert len(first_events) == 2 

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

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

195 

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

197 assert len(second_events) == 1 

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

199 

200 

201# ===== Integration tests: auth events ===== 

202 

203 

204def test_signup_flow_creates_events(db): 

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

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

207 res = auth_api.SignupFlow( 

208 auth_pb2.SignupFlowReq( 

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

210 ) 

211 ) 

212 

213 flow_token = res.flow_token 

214 

215 with session_scope() as session: 

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

217 assert len(events) == 1 

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

219 

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

221 from couchers.models import SignupFlow 

222 

223 with session_scope() as session: 

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

225 email_token = flow.email_token 

226 

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

228 auth_api.SignupFlow( 

229 auth_pb2.SignupFlowReq( 

230 flow_token=flow_token, 

231 email_token=email_token, 

232 ) 

233 ) 

234 

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

236 auth_api.SignupFlow( 

237 auth_pb2.SignupFlowReq( 

238 flow_token=flow_token, 

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

240 ) 

241 ) 

242 

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

244 res = auth_api.SignupFlow( 

245 auth_pb2.SignupFlowReq( 

246 flow_token=flow_token, 

247 account=auth_pb2.SignupAccount( 

248 username="frodo", 

249 password="a very insecure password", 

250 birthdate="1970-01-01", 

251 gender="Bot", 

252 hosting_status=api_pb2.HOSTING_STATUS_MAYBE, 

253 city="New York City", 

254 lat=40.7331, 

255 lng=-73.9778, 

256 radius=500, 

257 accept_tos=True, 

258 ), 

259 ) 

260 ) 

261 

262 assert res.HasField("auth_res") 

263 user_id = res.auth_res.user_id 

264 

265 with session_scope() as session: 

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

267 assert len(events) == 1 

268 e = events[0] 

269 assert e.user_id == user_id 

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

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

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

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

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

275 assert "filled_contributor_form" in e.properties 

276 

277 

278def test_login_creates_event(db): 

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

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

281 

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

283 auth_api.Authenticate( 

284 auth_pb2.AuthReq( 

285 user=user.username, 

286 password="password123", 

287 remember_device=True, 

288 ) 

289 ) 

290 

291 with session_scope() as session: 

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

293 assert len(events) == 1 

294 e = events[0] 

295 assert e.user_id == user.id 

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

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

298 

299 

300def test_logout_creates_event(db): 

301 """Logout creates account.logout event.""" 

302 user, token = generate_user() 

303 

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

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

306 

307 with session_scope() as session: 

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

309 assert len(events) == 1 

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

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

312 

313 

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

315 

316 

317def test_host_request_created_event(db, moderator): 

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

319 user1, token1 = generate_user() 

320 user2, token2 = generate_user( 

321 city="Berlin", 

322 geom=create_coordinate(52.5200, 13.4050), 

323 geom_radius=200, 

324 ) 

325 

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

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

328 

329 with requests_session(token1) as api: 

330 res = api.CreateHostRequest( 

331 requests_pb2.CreateHostRequestReq( 

332 host_user_id=user2.id, 

333 from_date=from_date.isoformat(), 

334 to_date=to_date.isoformat(), 

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

336 ) 

337 ) 

338 

339 host_request_id = res.host_request_id 

340 

341 with session_scope() as session: 

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

343 assert len(events) == 1 

344 e = events[0] 

345 assert e.user_id == user1.id 

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

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

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

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

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

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

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

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

354 

355 

356def test_host_request_status_change_events(db, moderator): 

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

358 user1, token1 = generate_user() 

359 user2, token2 = generate_user( 

360 city="Berlin", 

361 geom=create_coordinate(52.5200, 13.4050), 

362 geom_radius=200, 

363 ) 

364 

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

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

367 

368 with requests_session(token1) as api: 

369 res = api.CreateHostRequest( 

370 requests_pb2.CreateHostRequestReq( 

371 host_user_id=user2.id, 

372 from_date=from_date.isoformat(), 

373 to_date=to_date.isoformat(), 

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

375 ) 

376 ) 

377 host_request_id = res.host_request_id 

378 moderator.approve_host_request(host_request_id) 

379 

380 # Host accepts 

381 with requests_session(token2) as api: 

382 api.RespondHostRequest( 

383 requests_pb2.RespondHostRequestReq( 

384 host_request_id=host_request_id, 

385 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

386 ) 

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 with requests_session(token1) as api: 

405 api.RespondHostRequest( 

406 requests_pb2.RespondHostRequestReq( 

407 host_request_id=host_request_id, 

408 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

409 ) 

410 ) 

411 

412 with session_scope() as session: 

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

414 assert len(events) == 1 

415 e = events[0] 

416 assert e.user_id == user1.id 

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

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

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

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

421 

422 

423def test_host_request_rejected_event(db, moderator): 

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

425 user1, token1 = generate_user() 

426 user2, token2 = generate_user( 

427 city="Paris", 

428 geom=create_coordinate(48.8566, 2.3522), 

429 geom_radius=200, 

430 ) 

431 

432 with requests_session(token1) as api: 

433 res = api.CreateHostRequest( 

434 requests_pb2.CreateHostRequestReq( 

435 host_user_id=user2.id, 

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

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

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

439 ) 

440 ) 

441 moderator.approve_host_request(res.host_request_id) 

442 

443 with requests_session(token2) as api: 

444 api.RespondHostRequest( 

445 requests_pb2.RespondHostRequestReq( 

446 host_request_id=res.host_request_id, 

447 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED, 

448 ) 

449 ) 

450 

451 with session_scope() as session: 

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

453 assert len(events) == 1 

454 e = events[0] 

455 assert e.user_id == user2.id 

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

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

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

459 

460 

461def test_host_request_cancelled_event(db, moderator): 

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

463 user1, token1 = generate_user() 

464 user2, token2 = generate_user( 

465 geom=create_coordinate(52.5200, 13.4050), 

466 geom_radius=200, 

467 ) 

468 

469 with requests_session(token1) as api: 

470 res = api.CreateHostRequest( 

471 requests_pb2.CreateHostRequestReq( 

472 host_user_id=user2.id, 

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

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

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

476 ) 

477 ) 

478 moderator.approve_host_request(res.host_request_id) 

479 

480 with requests_session(token1) as api: 

481 api.RespondHostRequest( 

482 requests_pb2.RespondHostRequestReq( 

483 host_request_id=res.host_request_id, 

484 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

485 ) 

486 ) 

487 

488 with session_scope() as session: 

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

490 assert len(events) == 1 

491 e = events[0] 

492 assert e.user_id == user1.id 

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

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

495 

496 

497def test_host_request_message_event(db, moderator): 

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

499 user1, token1 = generate_user() 

500 user2, token2 = generate_user( 

501 geom=create_coordinate(52.5200, 13.4050), 

502 geom_radius=200, 

503 ) 

504 

505 with requests_session(token1) as api: 

506 res = api.CreateHostRequest( 

507 requests_pb2.CreateHostRequestReq( 

508 host_user_id=user2.id, 

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

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

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

512 ) 

513 ) 

514 host_request_id = res.host_request_id 

515 moderator.approve_host_request(host_request_id) 

516 

517 # Host sends a message 

518 with requests_session(token2) as api: 

519 api.SendHostRequestMessage( 

520 requests_pb2.SendHostRequestMessageReq( 

521 host_request_id=host_request_id, 

522 text="Welcome!", 

523 ) 

524 ) 

525 

526 with session_scope() as session: 

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

528 assert len(events) == 1 

529 e = events[0] 

530 assert e.user_id == user2.id 

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

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

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

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

535 

536 # Surfer sends a message 

537 with requests_session(token1) as api: 

538 api.SendHostRequestMessage( 

539 requests_pb2.SendHostRequestMessageReq( 

540 host_request_id=host_request_id, 

541 text="Thanks!", 

542 ) 

543 ) 

544 

545 with session_scope() as session: 

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

547 assert len(events) == 2 

548 e = events[1] 

549 assert e.user_id == user1.id 

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

551 

552 

553# ===== Integration tests: messaging events ===== 

554 

555 

556def test_send_message_creates_event(db): 

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

558 user1, token1 = generate_user() 

559 user2, token2 = generate_user() 

560 make_friends(user1, user2) 

561 

562 with conversations_session(token1) as api: 

563 res = api.SendDirectMessage( 

564 conversations_pb2.SendDirectMessageReq( 

565 recipient_user_id=user2.id, 

566 text="Hello friend!", 

567 ) 

568 ) 

569 

570 with session_scope() as session: 

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

572 assert len(events) == 1 

573 e = events[0] 

574 assert e.user_id == user1.id 

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

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

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

578 

579 

580def test_create_group_chat_event(db): 

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

582 user1, token1 = generate_user() 

583 user2, token2 = generate_user() 

584 user3, token3 = generate_user() 

585 make_friends(user1, user2) 

586 make_friends(user1, user3) 

587 

588 with conversations_session(token1) as api: 

589 res = api.CreateGroupChat( 

590 conversations_pb2.CreateGroupChatReq( 

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

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

593 ) 

594 ) 

595 

596 with session_scope() as session: 

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

598 assert len(events) == 1 

599 e = events[0] 

600 assert e.user_id == user1.id 

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

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

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

604 

605 

606# ===== Integration tests: friendship events ===== 

607 

608 

609def test_friendship_request_events(db, moderator): 

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

611 user1, token1 = generate_user() 

612 user2, token2 = generate_user() 

613 

614 # Send friend request 

615 with api_session(token1) as api: 

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

617 

618 with session_scope() as session: 

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

620 assert len(events) == 1 

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

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

623 

624 # Approve and accept friend request 

625 from couchers.models import FriendRelationship 

626 

627 with session_scope() as session: 

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

629 fr_id = fr.id 

630 

631 moderator.approve_friend_request(fr_id) 

632 

633 with api_session(token2) as api: 

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

635 

636 with session_scope() as session: 

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

638 assert len(events) == 1 

639 e = events[0] 

640 assert e.user_id == user2.id 

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

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

643 

644 # Remove friend 

645 with api_session(token1) as api: 

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

647 

648 with session_scope() as session: 

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

650 assert len(events) == 1 

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

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

653 

654 

655def test_friendship_cancel_event(db, moderator): 

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

657 user1, token1 = generate_user() 

658 user2, token2 = generate_user() 

659 

660 with api_session(token1) as api: 

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

662 

663 from couchers.models import FriendRelationship 

664 

665 with session_scope() as session: 

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

667 fr_id = fr.id 

668 

669 with api_session(token1) as api: 

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

671 

672 with session_scope() as session: 

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

674 assert len(events) == 1 

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

676 

677 

678# ===== Integration tests: reporting events ===== 

679 

680 

681def test_report_creates_event(db): 

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

683 user1, token1 = generate_user() 

684 user2, token2 = generate_user() 

685 

686 with reporting_session(token1) as api: 

687 api.Report( 

688 reporting_pb2.ReportReq( 

689 reason="spam", 

690 description="This is spam", 

691 content_ref="comment/456", 

692 author_user=user2.username, 

693 user_agent="TestAgent/1.0", 

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

695 ) 

696 ) 

697 

698 with session_scope() as session: 

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

700 assert len(events) == 1 

701 e = events[0] 

702 assert e.user_id == user1.id 

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

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

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

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

707 

708 

709# ===== Integration tests: search events ===== 

710 

711 

712def test_search_creates_event(db): 

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

714 user, token = generate_user() 

715 

716 with search_session(token) as api: 

717 api.UserSearch(search_pb2.UserSearchReq()) 

718 

719 with session_scope() as session: 

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

721 assert len(events) == 1 

722 e = events[0] 

723 assert e.user_id == user.id 

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

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

726 assert "total_items" in e.properties 

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

728 

729 

730# ===== Integration tests: reference events ===== 

731 

732 

733def test_friend_reference_event(db): 

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

735 user1, token1 = generate_user() 

736 user2, token2 = generate_user() 

737 make_friends(user1, user2) 

738 

739 with references_session(token1) as api: 

740 api.WriteFriendReference( 

741 references_pb2.WriteFriendReferenceReq( 

742 to_user_id=user2.id, 

743 text="Great person!", 

744 private_text="", 

745 rating=0.9, 

746 was_appropriate=True, 

747 ) 

748 ) 

749 

750 with session_scope() as session: 

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

752 assert len(events) == 1 

753 e = events[0] 

754 assert e.user_id == user1.id 

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

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

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

758 

759 

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

761 

762 

763def test_event_created_event(db): 

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

765 user, token = generate_user() 

766 

767 with session_scope() as session: 

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

769 

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

771 end_time = start_time + timedelta(hours=2) 

772 

773 with events_session(token) as api: 

774 res = api.CreateEvent( 

775 events_pb2.CreateEventReq( 

776 title="Test Meetup", 

777 content="Let's hang out", 

778 offline_information=events_pb2.OfflineEventInformation( 

779 address="123 Main St", 

780 lat=0.1, 

781 lng=0.2, 

782 ), 

783 start_time=Timestamp_from_datetime(start_time), 

784 end_time=Timestamp_from_datetime(end_time), 

785 timezone="UTC", 

786 ) 

787 ) 

788 

789 with session_scope() as session: 

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

791 assert len(events) == 1 

792 e = events[0] 

793 assert e.user_id == user.id 

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

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

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

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

798 assert e.properties["online"] is False 

799 

800 

801# ===== Integration tests: password change ===== 

802 

803 

804def test_password_change_event(db): 

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

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

807 

808 from couchers.proto import account_pb2 

809 from tests.fixtures.sessions import account_session 

810 

811 with account_session(token) as api: 

812 api.ChangePasswordV2( 

813 account_pb2.ChangePasswordV2Req( 

814 old_password="oldpassword", 

815 new_password="a new very secure password", 

816 ) 

817 ) 

818 

819 with session_scope() as session: 

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

821 assert len(events) == 1 

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

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

824 

825 

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

827 

828 

829def test_no_stale_events(db): 

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

831 with session_scope() as session: 

832 events = _get_events(session) 

833 assert len(events) == 0