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

410 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 11:09 +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.logging import EventLog 

15from couchers.proto import ( 

16 api_pb2, 

17 auth_pb2, 

18 conversations_pb2, 

19 events_pb2, 

20 references_pb2, 

21 reporting_pb2, 

22 requests_pb2, 

23 search_pb2, 

24) 

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

26from tests.fixtures.db import generate_user, make_friends 

27from tests.fixtures.sessions import ( 

28 MockGrpcContext, 

29 api_session, 

30 auth_api_session, 

31 conversations_session, 

32 events_session, 

33 references_session, 

34 reporting_session, 

35 requests_session, 

36 search_session, 

37) 

38from tests.test_communities import create_community 

39 

40 

41@pytest.fixture(autouse=True) 

42def _(testconfig, fast_passwords): 

43 pass 

44 

45 

46def _get_events(session, event_type=None): 

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

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

49 if event_type: 

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

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

52 

53 

54# ===== Unit tests for log_event function ===== 

55 

56 

57def test_log_event_authenticated_context(db): 

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

59 user, token = generate_user() 

60 

61 with session_scope() as session: 

62 context = make_interactive_context( 

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

64 user_id=user.id, 

65 is_api_key=False, 

66 token=token, 

67 localization=LocalizationContext.en_utc(), 

68 sofa="test-sofa-123", 

69 ) 

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

71 

72 with session_scope() as session: 

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

74 assert len(events) == 1 

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

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

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

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

79 assert events[0].created is not None 

80 

81 

82def test_log_event_with_override_user_id(db): 

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

84 user, token = generate_user() 

85 

86 with session_scope() as session: 

87 context = make_interactive_context( 

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

89 user_id=None, 

90 is_api_key=False, 

91 token=None, 

92 localization=LocalizationContext.en_utc(), 

93 sofa="sofa-456", 

94 ) 

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

96 

97 with session_scope() as session: 

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

99 assert len(events) == 1 

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

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

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

103 

104 

105def test_log_event_anonymous(db): 

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

107 with session_scope() as session: 

108 context = make_interactive_context( 

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

110 user_id=None, 

111 is_api_key=False, 

112 token=None, 

113 localization=LocalizationContext.en_utc(), 

114 ) 

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

116 

117 with session_scope() as session: 

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

119 assert len(events) == 1 

120 assert events[0].user_id is None 

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

122 

123 

124def test_log_event_complex_properties(db): 

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

126 user, token = generate_user() 

127 

128 props = { 

129 "string_val": "hello", 

130 "int_val": 42, 

131 "float_val": 3.14, 

132 "bool_val": True, 

133 "none_val": None, 

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

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

136 } 

137 

138 with session_scope() as session: 

139 context = make_interactive_context( 

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

141 user_id=user.id, 

142 is_api_key=False, 

143 token=token, 

144 localization=LocalizationContext.en_utc(), 

145 ) 

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

147 

148 with session_scope() as session: 

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

150 assert len(events) == 1 

151 assert events[0].properties == props 

152 

153 

154def test_log_event_empty_properties(db): 

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

156 user, token = generate_user() 

157 

158 with session_scope() as session: 

159 context = make_interactive_context( 

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

161 user_id=user.id, 

162 is_api_key=False, 

163 token=token, 

164 localization=LocalizationContext.en_utc(), 

165 ) 

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

167 

168 with session_scope() as session: 

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

170 assert len(events) == 1 

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

172 

173 

174def test_log_event_multiple_events(db): 

175 """Multiple events are stored independently.""" 

176 user, token = generate_user() 

177 

178 with session_scope() as session: 

179 context = make_interactive_context( 

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

181 user_id=user.id, 

182 is_api_key=False, 

183 token=token, 

184 localization=LocalizationContext.en_utc(), 

185 ) 

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

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

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

189 

190 with session_scope() as session: 

191 all_events = _get_events(session) 

192 assert len(all_events) == 3 

193 

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

195 assert len(first_events) == 2 

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

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

198 

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

200 assert len(second_events) == 1 

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

202 

203 

204# ===== Integration tests: auth events ===== 

205 

206 

207def test_signup_flow_creates_events(db): 

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

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

210 res = auth_api.SignupFlow( 

211 auth_pb2.SignupFlowReq( 

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

213 ) 

214 ) 

215 

216 flow_token = res.flow_token 

217 

218 with session_scope() as session: 

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

220 assert len(events) == 1 

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

222 

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

224 from couchers.models import SignupFlow 

225 

226 with session_scope() as session: 

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

228 email_token = flow.email_token 

229 

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

231 auth_api.SignupFlow( 

232 auth_pb2.SignupFlowReq( 

233 flow_token=flow_token, 

234 email_token=email_token, 

235 ) 

236 ) 

237 

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

239 auth_api.SignupFlow( 

240 auth_pb2.SignupFlowReq( 

241 flow_token=flow_token, 

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

243 ) 

244 ) 

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 account=auth_pb2.SignupAccount( 

251 username="frodo", 

252 password="a very insecure password", 

253 birthdate="1970-01-01", 

254 gender="Bot", 

255 hosting_status=api_pb2.HOSTING_STATUS_MAYBE, 

256 city="New York City", 

257 lat=40.7331, 

258 lng=-73.9778, 

259 radius=500, 

260 accept_tos=True, 

261 ), 

262 ) 

263 ) 

264 

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

266 res = auth_api.SignupFlow( 

267 auth_pb2.SignupFlowReq( 

268 flow_token=flow_token, 

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

270 ) 

271 ) 

272 

273 assert res.HasField("auth_res") 

274 user_id = res.auth_res.user_id 

275 

276 with session_scope() as session: 

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

278 assert len(events) == 1 

279 e = events[0] 

280 assert e.user_id == user_id 

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

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

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

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

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

286 assert "filled_contributor_form" in e.properties 

287 

288 

289def test_login_creates_event(db): 

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

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

292 

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

294 auth_api.Authenticate( 

295 auth_pb2.AuthReq( 

296 user=user.username, 

297 password="password123", 

298 remember_device=True, 

299 ) 

300 ) 

301 

302 with session_scope() as session: 

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

304 assert len(events) == 1 

305 e = events[0] 

306 assert e.user_id == user.id 

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

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

309 

310 

311def test_logout_creates_event(db): 

312 """Logout creates account.logout event.""" 

313 user, token = generate_user() 

314 

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

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

317 

318 with session_scope() as session: 

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

320 assert len(events) == 1 

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

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

323 

324 

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

326 

327 

328def test_host_request_created_event(db, moderator): 

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

330 user1, token1 = generate_user() 

331 user2, token2 = generate_user( 

332 city="Berlin", 

333 geom=create_coordinate(52.5200, 13.4050), 

334 geom_radius=200, 

335 ) 

336 

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

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

339 

340 with requests_session(token1) as api: 

341 res = api.CreateHostRequest( 

342 requests_pb2.CreateHostRequestReq( 

343 host_user_id=user2.id, 

344 from_date=from_date.isoformat(), 

345 to_date=to_date.isoformat(), 

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

347 ) 

348 ) 

349 

350 host_request_id = res.host_request_id 

351 

352 with session_scope() as session: 

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

354 assert len(events) == 1 

355 e = events[0] 

356 assert e.user_id == user1.id 

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

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

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

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

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

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

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

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

365 

366 

367def test_host_request_status_change_events(db, moderator): 

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

369 user1, token1 = generate_user() 

370 user2, token2 = generate_user( 

371 city="Berlin", 

372 geom=create_coordinate(52.5200, 13.4050), 

373 geom_radius=200, 

374 ) 

375 

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

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

378 

379 with requests_session(token1) as api: 

380 res = api.CreateHostRequest( 

381 requests_pb2.CreateHostRequestReq( 

382 host_user_id=user2.id, 

383 from_date=from_date.isoformat(), 

384 to_date=to_date.isoformat(), 

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

386 ) 

387 ) 

388 host_request_id = res.host_request_id 

389 moderator.approve_host_request(host_request_id) 

390 

391 # Host accepts 

392 with requests_session(token2) as api: 

393 api.RespondHostRequest( 

394 requests_pb2.RespondHostRequestReq( 

395 host_request_id=host_request_id, 

396 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED, 

397 ) 

398 ) 

399 

400 with session_scope() as session: 

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

402 assert len(events) == 1 

403 e = events[0] 

404 assert e.user_id == user2.id 

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

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

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

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

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

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

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

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

413 

414 # Surfer confirms 

415 with requests_session(token1) as api: 

416 api.RespondHostRequest( 

417 requests_pb2.RespondHostRequestReq( 

418 host_request_id=host_request_id, 

419 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED, 

420 ) 

421 ) 

422 

423 with session_scope() as session: 

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

425 assert len(events) == 1 

426 e = events[0] 

427 assert e.user_id == user1.id 

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

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

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

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

432 

433 

434def test_host_request_rejected_event(db, moderator): 

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

436 user1, token1 = generate_user() 

437 user2, token2 = generate_user( 

438 city="Paris", 

439 geom=create_coordinate(48.8566, 2.3522), 

440 geom_radius=200, 

441 ) 

442 

443 with requests_session(token1) as api: 

444 res = api.CreateHostRequest( 

445 requests_pb2.CreateHostRequestReq( 

446 host_user_id=user2.id, 

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

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

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

450 ) 

451 ) 

452 moderator.approve_host_request(res.host_request_id) 

453 

454 with requests_session(token2) as api: 

455 api.RespondHostRequest( 

456 requests_pb2.RespondHostRequestReq( 

457 host_request_id=res.host_request_id, 

458 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED, 

459 ) 

460 ) 

461 

462 with session_scope() as session: 

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

464 assert len(events) == 1 

465 e = events[0] 

466 assert e.user_id == user2.id 

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

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

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

470 

471 

472def test_host_request_cancelled_event(db, moderator): 

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

474 user1, token1 = generate_user() 

475 user2, token2 = generate_user( 

476 geom=create_coordinate(52.5200, 13.4050), 

477 geom_radius=200, 

478 ) 

479 

480 with requests_session(token1) as api: 

481 res = api.CreateHostRequest( 

482 requests_pb2.CreateHostRequestReq( 

483 host_user_id=user2.id, 

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

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

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

487 ) 

488 ) 

489 moderator.approve_host_request(res.host_request_id) 

490 

491 with requests_session(token1) as api: 

492 api.RespondHostRequest( 

493 requests_pb2.RespondHostRequestReq( 

494 host_request_id=res.host_request_id, 

495 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED, 

496 ) 

497 ) 

498 

499 with session_scope() as session: 

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

501 assert len(events) == 1 

502 e = events[0] 

503 assert e.user_id == user1.id 

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

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

506 

507 

508def test_host_request_message_event(db, moderator): 

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

510 user1, token1 = generate_user() 

511 user2, token2 = generate_user( 

512 geom=create_coordinate(52.5200, 13.4050), 

513 geom_radius=200, 

514 ) 

515 

516 with requests_session(token1) as api: 

517 res = api.CreateHostRequest( 

518 requests_pb2.CreateHostRequestReq( 

519 host_user_id=user2.id, 

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

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

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

523 ) 

524 ) 

525 host_request_id = res.host_request_id 

526 moderator.approve_host_request(host_request_id) 

527 

528 # Host sends a message 

529 with requests_session(token2) as api: 

530 api.SendHostRequestMessage( 

531 requests_pb2.SendHostRequestMessageReq( 

532 host_request_id=host_request_id, 

533 text="Welcome!", 

534 ) 

535 ) 

536 

537 with session_scope() as session: 

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

539 assert len(events) == 1 

540 e = events[0] 

541 assert e.user_id == user2.id 

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

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

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

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

546 

547 # Surfer sends a message 

548 with requests_session(token1) as api: 

549 api.SendHostRequestMessage( 

550 requests_pb2.SendHostRequestMessageReq( 

551 host_request_id=host_request_id, 

552 text="Thanks!", 

553 ) 

554 ) 

555 

556 with session_scope() as session: 

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

558 assert len(events) == 2 

559 e = events[1] 

560 assert e.user_id == user1.id 

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

562 

563 

564# ===== Integration tests: messaging events ===== 

565 

566 

567def test_send_message_creates_event(db): 

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

569 user1, token1 = generate_user() 

570 user2, token2 = generate_user() 

571 make_friends(user1, user2) 

572 

573 with conversations_session(token1) as api: 

574 res = api.SendDirectMessage( 

575 conversations_pb2.SendDirectMessageReq( 

576 recipient_user_id=user2.id, 

577 text="Hello friend!", 

578 ) 

579 ) 

580 

581 with session_scope() as session: 

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

583 assert len(events) == 1 

584 e = events[0] 

585 assert e.user_id == user1.id 

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

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

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

589 

590 

591def test_create_group_chat_event(db): 

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

593 user1, token1 = generate_user() 

594 user2, token2 = generate_user() 

595 user3, token3 = generate_user() 

596 make_friends(user1, user2) 

597 make_friends(user1, user3) 

598 

599 with conversations_session(token1) as api: 

600 res = api.CreateGroupChat( 

601 conversations_pb2.CreateGroupChatReq( 

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

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

604 ) 

605 ) 

606 

607 with session_scope() as session: 

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

609 assert len(events) == 1 

610 e = events[0] 

611 assert e.user_id == user1.id 

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

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

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

615 

616 

617# ===== Integration tests: friendship events ===== 

618 

619 

620def test_friendship_request_events(db, moderator): 

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

622 user1, token1 = generate_user() 

623 user2, token2 = generate_user() 

624 

625 # Send friend request 

626 with api_session(token1) as api: 

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

628 

629 with session_scope() as session: 

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

631 assert len(events) == 1 

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

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

634 

635 # Approve and accept friend request 

636 from couchers.models import FriendRelationship 

637 

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 from couchers.models import FriendRelationship 

675 

676 with session_scope() as session: 

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

678 fr_id = fr.id 

679 

680 with api_session(token1) as api: 

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

682 

683 with session_scope() as session: 

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

685 assert len(events) == 1 

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

687 

688 

689# ===== Integration tests: reporting events ===== 

690 

691 

692def test_report_creates_event(db): 

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

694 user1, token1 = generate_user() 

695 user2, token2 = generate_user() 

696 

697 with reporting_session(token1) as api: 

698 api.Report( 

699 reporting_pb2.ReportReq( 

700 reason="spam", 

701 description="This is spam", 

702 content_ref="comment/456", 

703 author_user=user2.username, 

704 user_agent="TestAgent/1.0", 

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

706 ) 

707 ) 

708 

709 with session_scope() as session: 

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

711 assert len(events) == 1 

712 e = events[0] 

713 assert e.user_id == user1.id 

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

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

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

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

718 

719 

720# ===== Integration tests: search events ===== 

721 

722 

723def test_search_creates_event(db): 

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

725 user, token = generate_user() 

726 

727 with search_session(token) as api: 

728 api.UserSearch(search_pb2.UserSearchReq()) 

729 

730 with session_scope() as session: 

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

732 assert len(events) == 1 

733 e = events[0] 

734 assert e.user_id == user.id 

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

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

737 assert "total_items" in e.properties 

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

739 

740 

741# ===== Integration tests: reference events ===== 

742 

743 

744def test_friend_reference_event(db): 

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

746 user1, token1 = generate_user() 

747 user2, token2 = generate_user() 

748 make_friends(user1, user2) 

749 

750 with references_session(token1) as api: 

751 api.WriteFriendReference( 

752 references_pb2.WriteFriendReferenceReq( 

753 to_user_id=user2.id, 

754 text="Great person!", 

755 private_text="", 

756 rating=0.9, 

757 was_appropriate=True, 

758 ) 

759 ) 

760 

761 with session_scope() as session: 

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

763 assert len(events) == 1 

764 e = events[0] 

765 assert e.user_id == user1.id 

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

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

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

769 

770 

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

772 

773 

774def test_event_created_event(db): 

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

776 user, token = generate_user() 

777 

778 with session_scope() as session: 

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

780 

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

782 end_time = start_time + timedelta(hours=2) 

783 

784 with events_session(token) as api: 

785 res = api.CreateEvent( 

786 events_pb2.CreateEventReq( 

787 title="Test Meetup", 

788 content="Let's hang out", 

789 offline_information=events_pb2.OfflineEventInformation( 

790 address="123 Main St", 

791 lat=0.1, 

792 lng=0.2, 

793 ), 

794 start_time=Timestamp_from_datetime(start_time), 

795 end_time=Timestamp_from_datetime(end_time), 

796 timezone="UTC", 

797 ) 

798 ) 

799 

800 with session_scope() as session: 

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

802 assert len(events) == 1 

803 e = events[0] 

804 assert e.user_id == user.id 

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

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

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

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

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

810 

811 

812# ===== Integration tests: password change ===== 

813 

814 

815def test_password_change_event(db): 

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

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

818 

819 from couchers.proto import account_pb2 

820 from tests.fixtures.sessions import account_session 

821 

822 with account_session(token) as api: 

823 api.ChangePasswordV2( 

824 account_pb2.ChangePasswordV2Req( 

825 old_password="oldpassword", 

826 new_password="a new very secure password", 

827 ) 

828 ) 

829 

830 with session_scope() as session: 

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

832 assert len(events) == 1 

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

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

835 

836 

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

838 

839 

840def test_no_stale_events(db): 

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

842 with session_scope() as session: 

843 events = _get_events(session) 

844 assert len(events) == 0