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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 11:09 +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.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
41@pytest.fixture(autouse=True)
42def _(testconfig, fast_passwords):
43 pass
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()
54# ===== Unit tests for log_event function =====
57def test_log_event_authenticated_context(db):
58 """log_event stores event with user_id from context."""
59 user, token = generate_user()
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"})
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
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()
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)
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"
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})
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}
124def test_log_event_complex_properties(db):
125 """Properties dict with various types is stored as JSONB correctly."""
126 user, token = generate_user()
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 }
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)
148 with session_scope() as session:
149 events = _get_events(session, "test.complex")
150 assert len(events) == 1
151 assert events[0].properties == props
154def test_log_event_empty_properties(db):
155 """Empty properties dict is stored correctly."""
156 user, token = generate_user()
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", {})
168 with session_scope() as session:
169 events = _get_events(session, "account.logout")
170 assert len(events) == 1
171 assert events[0].properties == {}
174def test_log_event_multiple_events(db):
175 """Multiple events are stored independently."""
176 user, token = generate_user()
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})
190 with session_scope() as session:
191 all_events = _get_events(session)
192 assert len(all_events) == 3
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}
199 second_events = _get_events(session, "test.second")
200 assert len(second_events) == 1
201 assert second_events[0].properties == {"n": 2}
204# ===== Integration tests: auth events =====
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 )
216 flow_token = res.flow_token
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
223 # complete signup: get email token, verify, fill account, etc.
224 from couchers.models import SignupFlow
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
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 )
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 )
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 )
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 )
273 assert res.HasField("auth_res")
274 user_id = res.auth_res.user_id
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
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"))
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 )
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
311def test_logout_creates_event(db):
312 """Logout creates account.logout event."""
313 user, token = generate_user()
315 with auth_api_session() as (auth_api, metadata_interceptor):
316 auth_api.Deauthenticate(empty_pb2.Empty(), metadata=(("cookie", f"couchers-sesh={token}"),))
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 == {}
325# ===== Integration tests: host request events =====
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 )
337 from_date = today() + timedelta(days=2)
338 to_date = today() + timedelta(days=5)
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 )
350 host_request_id = res.host_request_id
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
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 )
376 from_date = today() + timedelta(days=2)
377 to_date = today() + timedelta(days=5)
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)
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 )
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"
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 )
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
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 )
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)
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 )
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"
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 )
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)
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 )
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
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 )
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)
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 )
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
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 )
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"
564# ===== Integration tests: messaging events =====
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)
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 )
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
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)
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 )
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
617# ===== Integration tests: friendship events =====
620def test_friendship_request_events(db, moderator):
621 """Friend request lifecycle creates appropriate events."""
622 user1, token1 = generate_user()
623 user2, token2 = generate_user()
625 # Send friend request
626 with api_session(token1) as api:
627 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
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
635 # Approve and accept friend request
636 from couchers.models import FriendRelationship
638 with session_scope() as session:
639 fr = session.execute(select(FriendRelationship)).scalar_one()
640 fr_id = fr.id
642 moderator.approve_friend_request(fr_id)
644 with api_session(token2) as api:
645 api.RespondFriendRequest(api_pb2.RespondFriendRequestReq(friend_request_id=fr_id, accept=True))
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
655 # Remove friend
656 with api_session(token1) as api:
657 api.RemoveFriend(api_pb2.RemoveFriendReq(user_id=user2.id))
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
666def test_friendship_cancel_event(db, moderator):
667 """Cancelling a friend request creates event."""
668 user1, token1 = generate_user()
669 user2, token2 = generate_user()
671 with api_session(token1) as api:
672 api.SendFriendRequest(api_pb2.SendFriendRequestReq(user_id=user2.id))
674 from couchers.models import FriendRelationship
676 with session_scope() as session:
677 fr = session.execute(select(FriendRelationship)).scalar_one()
678 fr_id = fr.id
680 with api_session(token1) as api:
681 api.CancelFriendRequest(api_pb2.CancelFriendRequestReq(friend_request_id=fr_id))
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
689# ===== Integration tests: reporting events =====
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()
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 )
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"
720# ===== Integration tests: search events =====
723def test_search_creates_event(db):
724 """User search creates search.performed event with search parameters."""
725 user, token = generate_user()
727 with search_session(token) as api:
728 api.UserSearch(search_pb2.UserSearchReq())
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
741# ===== Integration tests: reference events =====
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)
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 )
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
771# ===== Integration tests: event (calendar) events =====
774def test_event_created_event(db):
775 """Creating an event logs event.created with community info and online status."""
776 user, token = generate_user()
778 with session_scope() as session:
779 create_community(session, 0, 2, "Community", [user], [], None)
781 start_time = now() + timedelta(days=1)
782 end_time = start_time + timedelta(hours=2)
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 )
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
812# ===== Integration tests: password change =====
815def test_password_change_event(db):
816 """Changing password creates account.password_changed event."""
817 user, token = generate_user(hashed_password=hash_password("oldpassword"))
819 from couchers.proto import account_pb2
820 from tests.fixtures.sessions import account_session
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 )
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 == {}
837# ===== Test that events don't leak across tests =====
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