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
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 19:22 +0000
1from datetime import timedelta
3import pytest
4from google.protobuf import empty_pb2, wrappers_pb2
5from sqlalchemy import select
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
38@pytest.fixture(autouse=True)
39def _(testconfig, fast_passwords):
40 pass
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()
51# ===== Unit tests for log_event function =====
54def test_log_event_authenticated_context(db):
55 """log_event stores event with user_id from context."""
56 user, token = generate_user()
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"})
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
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()
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)
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"
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})
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}
121def test_log_event_complex_properties(db):
122 """Properties dict with various types is stored as JSONB correctly."""
123 user, token = generate_user()
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 }
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)
145 with session_scope() as session:
146 events = _get_events(session, "test.complex")
147 assert len(events) == 1
148 assert events[0].properties == props
151def test_log_event_empty_properties(db):
152 """Empty properties dict is stored correctly."""
153 user, token = generate_user()
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", {})
165 with session_scope() as session:
166 events = _get_events(session, "account.logout")
167 assert len(events) == 1
168 assert events[0].properties == {}
171def test_log_event_multiple_events(db):
172 """Multiple events are stored independently."""
173 user, token = generate_user()
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})
187 with session_scope() as session:
188 all_events = _get_events(session)
189 assert len(all_events) == 3
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}
196 second_events = _get_events(session, "test.second")
197 assert len(second_events) == 1
198 assert second_events[0].properties == {"n": 2}
201# ===== Integration tests: auth events =====
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 )
213 flow_token = res.flow_token
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
220 # complete signup: get email token, verify, fill account, etc.
221 from couchers.models import SignupFlow
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
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 )
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 )
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 )
262 assert res.HasField("auth_res")
263 user_id = res.auth_res.user_id
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
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"))
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 )
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
300def test_logout_creates_event(db):
301 """Logout creates account.logout event."""
302 user, token = generate_user()
304 with auth_api_session() as (auth_api, metadata_interceptor):
305 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),))
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 == {}
314# ===== Integration tests: host request events =====
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 )
326 from_date = today() + timedelta(days=2)
327 to_date = today() + timedelta(days=5)
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 )
339 host_request_id = res.host_request_id
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
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 )
365 from_date = today() + timedelta(days=2)
366 to_date = today() + timedelta(days=5)
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)
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 )
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"
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 )
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
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 )
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)
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 )
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"
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 )
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)
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 )
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
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 )
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)
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 )
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
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 )
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"
553# ===== Integration tests: messaging events =====
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)
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 )
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
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)
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 )
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
606# ===== Integration tests: friendship events =====
609def test_friendship_request_events(db, moderator):
610 """Friend request lifecycle creates appropriate events."""
611 user1, token1 = generate_user()
612 user2, token2 = generate_user()
614 # Send friend request
615 with api_session(token1) as api:
616 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
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
624 # Approve and accept friend request
625 from couchers.models import FriendRelationship
627 with session_scope() as session:
628 fr = session.execute(select(FriendRelationship)).scalar_one()
629 fr_id = fr.id
631 moderator.approve_friend_request(fr_id)
633 with api_session(token2) as api:
634 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
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
644 # Remove friend
645 with api_session(token1) as api:
646 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id))
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
655def test_friendship_cancel_event(db, moderator):
656 """Cancelling a friend request creates event."""
657 user1, token1 = generate_user()
658 user2, token2 = generate_user()
660 with api_session(token1) as api:
661 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
663 from couchers.models import FriendRelationship
665 with session_scope() as session:
666 fr = session.execute(select(FriendRelationship)).scalar_one()
667 fr_id = fr.id
669 with api_session(token1) as api:
670 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id))
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
678# ===== Integration tests: reporting events =====
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()
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 )
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"
709# ===== Integration tests: search events =====
712def test_search_creates_event(db):
713 """User search creates search.performed event with search parameters."""
714 user, token = generate_user()
716 with search_session(token) as api:
717 api.UserSearch(search_pb2.UserSearchReq())
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
730# ===== Integration tests: reference events =====
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)
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 )
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
760# ===== Integration tests: event (calendar) events =====
763def test_event_created_event(db):
764 """Creating an event logs event.created with community info and online status."""
765 user, token = generate_user()
767 with session_scope() as session:
768 create_community(session, 0, 2, "Community", [user], [], None)
770 start_time = now() + timedelta(days=1)
771 end_time = start_time + timedelta(hours=2)
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 )
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
801# ===== Integration tests: password change =====
804def test_password_change_event(db):
805 """Changing password creates account.password_changed event."""
806 user, token = generate_user(hashed_password=hash_password("oldpassword"))
808 from couchers.proto import account_pb2
809 from tests.fixtures.sessions import account_session
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 )
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 == {}
826# ===== Test that events don't leak across tests =====
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