Coverage for app / backend / src / tests / test_event_log.py: 100%
406 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 02:44 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-13 02:44 +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 events_pb2,
22 references_pb2,
23 reporting_pb2,
24 requests_pb2,
25 search_pb2,
26)
27from couchers.utils import Timestamp_from_datetime, create_coordinate, now, today
28from tests.fixtures.db import generate_user, make_friends
29from tests.fixtures.sessions import (
30 MockGrpcContext,
31 account_session,
32 api_session,
33 auth_api_session,
34 conversations_session,
35 events_session,
36 references_session,
37 reporting_session,
38 requests_session,
39 search_session,
40)
41from tests.test_communities import create_community
44@pytest.fixture(autouse=True)
45def _(testconfig, 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 account.signup_initiated and account.signup_completed events."""
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
226 # complete signup: get email token, verify, fill account, etc.
227 with session_scope() as session:
228 flow = session.execute(select(SignupFlow).where(SignupFlow.flow_token == flow_token)).scalar_one()
229 email_token = flow.email_token
231 with auth_api_session() as (auth_api, metadata_interceptor):
232 auth_api.SignupFlow(
233 auth_pb2.SignupFlowReq(
234 flow_token=flow_token,
235 email_token=email_token,
236 )
237 )
239 with auth_api_session() as (auth_api, metadata_interceptor):
240 auth_api.SignupFlow(
241 auth_pb2.SignupFlowReq(
242 flow_token=flow_token,
243 accept_community_guidelines=wrappers_pb2.BoolValue(value=True),
244 )
245 )
247 with auth_api_session() as (auth_api, metadata_interceptor):
248 auth_api.SignupFlow(
249 auth_pb2.SignupFlowReq(
250 flow_token=flow_token,
251 account=auth_pb2.SignupAccount(
252 username="frodo",
253 password="a very insecure password",
254 birthdate="1970-01-01",
255 gender="Bot",
256 hosting_status=api_pb2.HOSTING_STATUS_MAYBE,
257 city="New York City",
258 lat=40.7331,
259 lng=-73.9778,
260 radius=500,
261 accept_tos=True,
262 ),
263 )
264 )
266 with auth_api_session() as (auth_api, metadata_interceptor):
267 res = auth_api.SignupFlow(
268 auth_pb2.SignupFlowReq(
269 flow_token=flow_token,
270 motivations=auth_pb2.SignupMotivations(motivations=["surfing"]),
271 )
272 )
274 assert res.HasField("auth_res")
275 user_id = res.auth_res.user_id
277 with session_scope() as session:
278 events = _get_events(session, "account.signup_completed")
279 assert len(events) == 1
280 e = events[0]
281 assert e.user_id == user_id
282 assert e.properties["gender"] == "Bot"
283 assert e.properties["hosting_status"] is not None
284 assert e.properties["city"] == "New York City"
285 assert e.properties["has_invite_code"] is False
286 assert isinstance(e.properties["signup_duration_s"], (int, float))
287 assert "filled_contributor_form" in e.properties
290def test_login_creates_event(db):
291 """Login creates account.login event with gender and remember_device."""
292 user, token = generate_user(hashed_password=hash_password("password123"))
294 with auth_api_session() as (auth_api, metadata_interceptor):
295 auth_api.Authenticate(
296 auth_pb2.AuthReq(
297 user=user.username,
298 password="password123",
299 remember_device=True,
300 )
301 )
303 with session_scope() as session:
304 events = _get_events(session, "account.login")
305 assert len(events) == 1
306 e = events[0]
307 assert e.user_id == user.id
308 assert e.properties["gender"] == user.gender
309 assert e.properties["remember_device"] is True
312def test_logout_creates_event(db):
313 """Logout creates account.logout event."""
314 user, token = generate_user()
316 with auth_api_session() as (auth_api, metadata_interceptor):
317 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),))
319 with session_scope() as session:
320 events = _get_events(session, "account.logout")
321 assert len(events) == 1
322 assert events[0].user_id == user.id
323 assert events[0].properties == {}
326# ===== Integration tests: host request events =====
329def test_host_request_created_event(db, moderator):
330 """Creating a host request logs host_request.created with full context."""
331 user1, token1 = generate_user()
332 user2, token2 = generate_user(
333 city="Berlin",
334 geom=create_coordinate(52.5200, 13.4050),
335 geom_radius=200,
336 )
338 from_date = today() + timedelta(days=2)
339 to_date = today() + timedelta(days=5)
341 with requests_session(token1) as api:
342 res = api.CreateHostRequest(
343 requests_pb2.CreateHostRequestReq(
344 host_user_id=user2.id,
345 from_date=from_date.isoformat(),
346 to_date=to_date.isoformat(),
347 text="a]" * 200 + "Hello! I would love to stay with you.",
348 )
349 )
351 host_request_id = res.host_request_id
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 with requests_session(token1) as api:
381 res = api.CreateHostRequest(
382 requests_pb2.CreateHostRequestReq(
383 host_user_id=user2.id,
384 from_date=from_date.isoformat(),
385 to_date=to_date.isoformat(),
386 text="a]" * 200 + "Hello! I would love to stay with you.",
387 )
388 )
389 host_request_id = res.host_request_id
390 moderator.approve_host_request(host_request_id)
392 # Host accepts
393 with requests_session(token2) as api:
394 api.RespondHostRequest(
395 requests_pb2.RespondHostRequestReq(
396 host_request_id=host_request_id,
397 status=conversations_pb2.HOST_REQUEST_STATUS_ACCEPTED,
398 )
399 )
401 with session_scope() as session:
402 events = _get_events(session, "host_request.accepted")
403 assert len(events) == 1
404 e = events[0]
405 assert e.user_id == user2.id
406 assert e.properties["host_request_id"] == host_request_id
407 assert e.properties["surfer_id"] == user1.id
408 assert e.properties["host_id"] == user2.id
409 assert e.properties["surfer_gender"] == user1.gender
410 assert e.properties["host_gender"] == user2.gender
411 assert e.properties["from_date"] == str(from_date)
412 assert e.properties["to_date"] == str(to_date)
413 assert e.properties["host_city"] == "Berlin"
415 # Surfer confirms
416 with requests_session(token1) as api:
417 api.RespondHostRequest(
418 requests_pb2.RespondHostRequestReq(
419 host_request_id=host_request_id,
420 status=conversations_pb2.HOST_REQUEST_STATUS_CONFIRMED,
421 )
422 )
424 with session_scope() as session:
425 events = _get_events(session, "host_request.confirmed")
426 assert len(events) == 1
427 e = events[0]
428 assert e.user_id == user1.id
429 assert e.properties["surfer_id"] == user1.id
430 assert e.properties["host_id"] == user2.id
431 assert e.properties["surfer_gender"] == user1.gender
432 assert e.properties["host_gender"] == user2.gender
435def test_host_request_rejected_event(db, moderator):
436 """Rejecting a host request logs event."""
437 user1, token1 = generate_user()
438 user2, token2 = generate_user(
439 city="Paris",
440 geom=create_coordinate(48.8566, 2.3522),
441 geom_radius=200,
442 )
444 with requests_session(token1) as api:
445 res = api.CreateHostRequest(
446 requests_pb2.CreateHostRequestReq(
447 host_user_id=user2.id,
448 from_date=(today() + timedelta(days=2)).isoformat(),
449 to_date=(today() + timedelta(days=4)).isoformat(),
450 text="a]" * 200 + "Would love to visit!",
451 )
452 )
453 moderator.approve_host_request(res.host_request_id)
455 with requests_session(token2) as api:
456 api.RespondHostRequest(
457 requests_pb2.RespondHostRequestReq(
458 host_request_id=res.host_request_id,
459 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED,
460 )
461 )
463 with session_scope() as session:
464 events = _get_events(session, "host_request.rejected")
465 assert len(events) == 1
466 e = events[0]
467 assert e.user_id == user2.id
468 assert e.properties["surfer_id"] == user1.id
469 assert e.properties["host_id"] == user2.id
470 assert e.properties["host_city"] == "Paris"
473def test_host_request_cancelled_event(db, moderator):
474 """Cancelling a host request logs event."""
475 user1, token1 = generate_user()
476 user2, token2 = generate_user(
477 geom=create_coordinate(52.5200, 13.4050),
478 geom_radius=200,
479 )
481 with requests_session(token1) as api:
482 res = api.CreateHostRequest(
483 requests_pb2.CreateHostRequestReq(
484 host_user_id=user2.id,
485 from_date=(today() + timedelta(days=2)).isoformat(),
486 to_date=(today() + timedelta(days=4)).isoformat(),
487 text="a]" * 200 + "Would love to visit!",
488 )
489 )
490 moderator.approve_host_request(res.host_request_id)
492 with requests_session(token1) as api:
493 api.RespondHostRequest(
494 requests_pb2.RespondHostRequestReq(
495 host_request_id=res.host_request_id,
496 status=conversations_pb2.HOST_REQUEST_STATUS_CANCELLED,
497 )
498 )
500 with session_scope() as session:
501 events = _get_events(session, "host_request.cancelled")
502 assert len(events) == 1
503 e = events[0]
504 assert e.user_id == user1.id
505 assert e.properties["surfer_id"] == user1.id
506 assert e.properties["host_id"] == user2.id
509def test_host_request_message_event(db, moderator):
510 """Sending a message in a host request logs event with role."""
511 user1, token1 = generate_user()
512 user2, token2 = generate_user(
513 geom=create_coordinate(52.5200, 13.4050),
514 geom_radius=200,
515 )
517 with requests_session(token1) as api:
518 res = api.CreateHostRequest(
519 requests_pb2.CreateHostRequestReq(
520 host_user_id=user2.id,
521 from_date=(today() + timedelta(days=2)).isoformat(),
522 to_date=(today() + timedelta(days=4)).isoformat(),
523 text="a]" * 200 + "Hello!",
524 )
525 )
526 host_request_id = res.host_request_id
527 moderator.approve_host_request(host_request_id)
529 # Host sends a message
530 with requests_session(token2) as api:
531 api.SendHostRequestMessage(
532 requests_pb2.SendHostRequestMessageReq(
533 host_request_id=host_request_id,
534 text="Welcome!",
535 )
536 )
538 with session_scope() as session:
539 events = _get_events(session, "host_request.message_sent")
540 assert len(events) == 1
541 e = events[0]
542 assert e.user_id == user2.id
543 assert e.properties["host_request_id"] == host_request_id
544 assert e.properties["role"] == "host"
545 assert e.properties["surfer_id"] == user1.id
546 assert e.properties["host_id"] == user2.id
548 # Surfer sends a message
549 with requests_session(token1) as api:
550 api.SendHostRequestMessage(
551 requests_pb2.SendHostRequestMessageReq(
552 host_request_id=host_request_id,
553 text="Thanks!",
554 )
555 )
557 with session_scope() as session:
558 events = _get_events(session, "host_request.message_sent")
559 assert len(events) == 2
560 e = events[1]
561 assert e.user_id == user1.id
562 assert e.properties["role"] == "surfer"
565# ===== Integration tests: messaging events =====
568def test_send_message_creates_event(db):
569 """Sending a direct message creates a message.sent event."""
570 user1, token1 = generate_user()
571 user2, token2 = generate_user()
572 make_friends(user1, user2)
574 with conversations_session(token1) as api:
575 res = api.SendDirectMessage(
576 conversations_pb2.SendDirectMessageReq(
577 recipient_user_id=user2.id,
578 text="Hello friend!",
579 )
580 )
582 with session_scope() as session:
583 events = _get_events(session, "message.sent")
584 assert len(events) == 1
585 e = events[0]
586 assert e.user_id == user1.id
587 assert e.properties["group_chat_id"] == res.group_chat_id
588 assert e.properties["is_dm"] is True
589 assert e.properties["recipient_id"] == user2.id
592def test_create_group_chat_event(db):
593 """Creating a group chat creates a group_chat.created event."""
594 user1, token1 = generate_user()
595 user2, token2 = generate_user()
596 user3, token3 = generate_user()
597 make_friends(user1, user2)
598 make_friends(user1, user3)
600 with conversations_session(token1) as api:
601 res = api.CreateGroupChat(
602 conversations_pb2.CreateGroupChatReq(
603 recipient_user_ids=[user2.id, user3.id],
604 title=wrappers_pb2.StringValue(value="Test Group"),
605 )
606 )
608 with session_scope() as session:
609 events = _get_events(session, "group_chat.created")
610 assert len(events) == 1
611 e = events[0]
612 assert e.user_id == user1.id
613 assert e.properties["group_chat_id"] == res.group_chat_id
614 assert e.properties["is_dm"] is False
615 assert e.properties["recipient_count"] == 2
618# ===== Integration tests: friendship events =====
621def test_friendship_request_events(db, moderator):
622 """Friend request lifecycle creates appropriate events."""
623 user1, token1 = generate_user()
624 user2, token2 = generate_user()
626 # Send friend request
627 with api_session(token1) as api:
628 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
630 with session_scope() as session:
631 events = _get_events(session, "friendship.request_sent")
632 assert len(events) == 1
633 assert events[0].user_id == user1.id
634 assert events[0].properties["to_user_id"] == user2.id
636 # Approve and accept friend request
637 with session_scope() as session:
638 fr = session.execute(select(FriendRelationship)).scalar_one()
639 fr_id = fr.id
641 moderator.approve_friend_request(fr_id)
643 with api_session(token2) as api:
644 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
646 with session_scope() as session:
647 events = _get_events(session, "friendship.request_responded")
648 assert len(events) == 1
649 e = events[0]
650 assert e.user_id == user2.id
651 assert e.properties["from_user_id"] == user1.id
652 assert e.properties["accepted"] is True
654 # Remove friend
655 with api_session(token1) as api:
656 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id))
658 with session_scope() as session:
659 events = _get_events(session, "friendship.removed")
660 assert len(events) == 1
661 assert events[0].user_id == user1.id
662 assert events[0].properties["other_user_id"] == user2.id
665def test_friendship_cancel_event(db, moderator):
666 """Cancelling a friend request creates event."""
667 user1, token1 = generate_user()
668 user2, token2 = generate_user()
670 with api_session(token1) as api:
671 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
673 with session_scope() as session:
674 fr = session.execute(select(FriendRelationship)).scalar_one()
675 fr_id = fr.id
677 with api_session(token1) as api:
678 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id))
680 with session_scope() as session:
681 events = _get_events(session, "friendship.request_cancelled")
682 assert len(events) == 1
683 assert events[0].properties["to_user_id"] == user2.id
686# ===== Integration tests: reporting events =====
689def test_report_creates_event(db):
690 """Reporting content creates content.reported event with full context."""
691 user1, token1 = generate_user()
692 user2, token2 = generate_user()
694 with reporting_session(token1) as api:
695 api.Report(
696 reporting_pb2.ReportReq(
697 reason="spam",
698 description="This is spam",
699 content_ref="comment/456",
700 author_user=user2.username,
701 user_agent="TestAgent/1.0",
702 page="https://couchers.org/profile/123",
703 )
704 )
706 with session_scope() as session:
707 events = _get_events(session, "content.reported")
708 assert len(events) == 1
709 e = events[0]
710 assert e.user_id == user1.id
711 assert e.properties["author_user_id"] == user2.id
712 assert e.properties["reason"] == "spam"
713 assert e.properties["content_ref"] == "comment/456"
714 assert e.properties["page"] == "https://couchers.org/profile/123"
717# ===== Integration tests: search events =====
720def test_search_creates_event(db):
721 """User search creates search.performed event with search parameters."""
722 user, token = generate_user()
724 with search_session(token) as api:
725 api.UserSearch(search_pb2.UserSearchReq())
727 with session_scope() as session:
728 events = _get_events(session, "search.performed")
729 assert len(events) == 1
730 e = events[0]
731 assert e.user_id == user.id
732 assert e.properties["has_query"] is False
733 assert e.properties["has_filters"] is False
734 assert "total_items" in e.properties
735 assert e.properties["search_in"] is None
738# ===== Integration tests: reference events =====
741def test_friend_reference_event(db):
742 """Writing a friend reference creates reference.friend_written event."""
743 user1, token1 = generate_user()
744 user2, token2 = generate_user()
745 make_friends(user1, user2)
747 with references_session(token1) as api:
748 api.WriteFriendReference(
749 references_pb2.WriteFriendReferenceReq(
750 to_user_id=user2.id,
751 text="Great person!",
752 private_text="",
753 rating=0.9,
754 was_appropriate=True,
755 )
756 )
758 with session_scope() as session:
759 events = _get_events(session, "reference.friend_written")
760 assert len(events) == 1
761 e = events[0]
762 assert e.user_id == user1.id
763 assert e.properties["to_user_id"] == user2.id
764 assert e.properties["rating"] == pytest.approx(0.9)
765 assert e.properties["was_appropriate"] is True
768# ===== Integration tests: event (calendar) events =====
771def test_event_created_event(db):
772 """Creating an event logs event.created with community info and online status."""
773 user, token = generate_user()
775 with session_scope() as session:
776 create_community(session, 0, 2, "Community", [user], [], None)
778 start_time = now() + timedelta(days=1)
779 end_time = start_time + timedelta(hours=2)
781 with events_session(token) as api:
782 res = api.CreateEvent(
783 events_pb2.CreateEventReq(
784 title="Test Meetup",
785 content="Let's hang out",
786 offline_information=events_pb2.OfflineEventInformation(
787 address="123 Main St",
788 lat=0.1,
789 lng=0.2,
790 ),
791 start_time=Timestamp_from_datetime(start_time),
792 end_time=Timestamp_from_datetime(end_time),
793 timezone="UTC",
794 )
795 )
797 with session_scope() as session:
798 events = _get_events(session, "event.created")
799 assert len(events) == 1
800 e = events[0]
801 assert e.user_id == user.id
802 assert e.properties["event_id"] is not None
803 assert e.properties["occurrence_id"] is not None
804 assert e.properties["parent_community_id"] is not None
805 assert e.properties["parent_community_name"] is not None
806 assert e.properties["online"] is False
809# ===== Integration tests: password change =====
812def test_password_change_event(db):
813 """Changing password creates account.password_changed event."""
814 user, token = generate_user(hashed_password=hash_password("oldpassword"))
816 with account_session(token) as api:
817 api.ChangePasswordV2(
818 account_pb2.ChangePasswordV2Req(
819 old_password="oldpassword",
820 new_password="a new very secure password",
821 )
822 )
824 with session_scope() as session:
825 events = _get_events(session, "account.password_changed")
826 assert len(events) == 1
827 assert events[0].user_id == user.id
828 assert events[0].properties == {}
831# ===== Test that events don't leak across tests =====
834def test_no_stale_events(db):
835 """Verify the database is clean - no events from previous tests."""
836 with session_scope() as session:
837 events = _get_events(session)
838 assert len(events) == 0