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
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-12 12:41 +0000
1from datetime import timedelta
2from typing import cast
4import grpc
5import pytest
6from google.protobuf import empty_pb2, wrappers_pb2
7from sqlalchemy import select
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
44@pytest.fixture(autouse=True)
45def _(fast_passwords):
46 pass
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()
57# ===== Unit tests for log_event function =====
60def test_log_event_authenticated_context(db):
61 """log_event stores event with user_id from context."""
62 user, token = generate_user()
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"})
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
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()
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)
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"
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})
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}
127def test_log_event_complex_properties(db):
128 """Properties dict with various types is stored as JSONB correctly."""
129 user, token = generate_user()
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 }
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)
151 with session_scope() as session:
152 events = _get_events(session, "test.complex")
153 assert len(events) == 1
154 assert events[0].properties == props
157def test_log_event_empty_properties(db):
158 """Empty properties dict is stored correctly."""
159 user, token = generate_user()
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", {})
171 with session_scope() as session:
172 events = _get_events(session, "account.logout")
173 assert len(events) == 1
174 assert events[0].properties == {}
177def test_log_event_multiple_events(db):
178 """Multiple events are stored independently."""
179 user, token = generate_user()
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})
193 with session_scope() as session:
194 all_events = _get_events(session)
195 assert len(all_events) == 3
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}
202 second_events = _get_events(session, "test.second")
203 assert len(second_events) == 1
204 assert second_events[0].properties == {"n": 2}
207# ===== Integration tests: auth events =====
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 )
219 flow_token = res.flow_token
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
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
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 )
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}
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 )
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 )
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 )
281 assert res.HasField("auth_res")
282 user_id = res.auth_res.user_id
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
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"))
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 )
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
320def test_logout_creates_event(db):
321 """Logout creates account.logout event."""
322 user, token = generate_user()
324 with auth_api_session() as (auth_api, metadata_interceptor):
325 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),))
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 == {}
334# ===== Integration tests: host request events =====
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 )
346 from_date = today() + timedelta(days=2)
347 to_date = today() + timedelta(days=5)
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 )
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
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 )
377 from_date = today() + timedelta(days=2)
378 to_date = today() + timedelta(days=5)
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 )
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 )
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 requests_api.respond_host_request(
405 token=token1, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CONFIRMED
406 )
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
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 )
428 host_request_id = requests_api.create_host_request(token=token1, host_user_id=user2.id, approve_by=moderator)
430 requests_api.respond_host_request(
431 token=token2, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_REJECTED
432 )
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"
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 )
452 host_request_id = requests_api.create_host_request(token=token1, host_user_id=user2.id, approve_by=moderator)
454 requests_api.respond_host_request(
455 token=token1, host_request_id=host_request_id, status=messages_pb2.HOST_REQUEST_STATUS_CANCELLED
456 )
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
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 )
475 host_request_id = requests_api.create_host_request(token=token1, host_user_id=user2.id, approve_by=moderator)
477 # Host sends a message
478 requests_api.send_host_request_message(token=token2, host_request_id=host_request_id, text="Welcome!")
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
490 # Surfer sends a message
491 requests_api.send_host_request_message(token=token1, host_request_id=host_request_id, text="Thanks!")
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"
501# ===== Integration tests: messaging events =====
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)
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 )
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
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)
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 )
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
554# ===== Integration tests: friendship events =====
557def test_friendship_request_events(db, moderator):
558 """Friend request lifecycle creates appropriate events."""
559 user1, token1 = generate_user()
560 user2, token2 = generate_user()
562 # Send friend request
563 with api_session(token1) as api:
564 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
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
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
577 moderator.approve_friend_request(fr_id)
579 with api_session(token2) as api:
580 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
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
590 # Remove friend
591 with api_session(token1) as api:
592 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id))
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
601def test_friendship_cancel_event(db, moderator):
602 """Cancelling a friend request creates event."""
603 user1, token1 = generate_user()
604 user2, token2 = generate_user()
606 with api_session(token1) as api:
607 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
609 with session_scope() as session:
610 fr = session.execute(select(FriendRelationship)).scalar_one()
611 fr_id = fr.id
613 with api_session(token1) as api:
614 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id))
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
622# ===== Integration tests: reporting events =====
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()
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 )
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"
653# ===== Integration tests: search events =====
656def test_search_creates_event(db):
657 """User search creates search.performed event with search parameters."""
658 user, token = generate_user()
660 with search_session(token) as api:
661 api.UserSearchV2(search_pb2.UserSearchReq())
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"] == ""
675def test_search_v2_creates_event(db, flags: Flags):
676 user, token = generate_user()
677 flags.set_string("search_ranking_offsets_variant", "a")
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 )
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"
706def test_search_v2_selection_event(db):
707 user, token = generate_user()
708 other, _ = generate_user()
710 with search_session(token) as api:
711 api.UserSearchV2(search_pb2.UserSearchReq(exactly_user_ids=[other.id]))
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"] == []
722# ===== Integration tests: reference events =====
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)
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 )
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
752# ===== Integration tests: event (calendar) events =====
755def test_event_created_event(db):
756 """Creating an event logs event.created with community info."""
757 user, token = generate_user()
759 with session_scope() as session:
760 create_community(session, 0, 2, "Community", [user], [], None)
762 events_api.create_event(token=token)
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
775# ===== Integration tests: password change =====
778def test_password_change_event(db):
779 """Changing password creates account.password_changed event."""
780 user, token = generate_user(hashed_password=hash_password("oldpassword"))
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 )
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 == {}
797# ===== Test that events don't leak across tests =====
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