Coverage for app/backend/src/tests/test_events.py: 99%
1447 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 22:54 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 22:54 +0000
1from datetime import datetime, timedelta
2from zoneinfo import ZoneInfo
4import grpc
5import pytest
6from google.protobuf import empty_pb2, wrappers_pb2
7from psycopg.types.range import TimestamptzRange
8from sqlalchemy import select
9from sqlalchemy.sql.expression import update
11from couchers.db import session_scope
12from couchers.jobs.handlers import send_event_reminders
13from couchers.models import (
14 BackgroundJob,
15 BackgroundJobState,
16 Comment,
17 EventOccurrence,
18 ModerationState,
19 ModerationVisibility,
20 Notification,
21 NotificationDelivery,
22 NotificationTopicAction,
23 Reply,
24 Upload,
25 User,
26)
27from couchers.proto import editor_pb2, events_pb2, threads_pb2
28from couchers.tasks import enforce_community_memberships
29from couchers.utils import datetime_to_iso8601_local, now, to_aware_datetime
30from tests.fixtures.db import generate_user
31from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs
32from tests.fixtures.sessions import events_session, real_editor_session, threads_session
33from tests.test_communities import create_community, create_group
36@pytest.fixture(autouse=True)
37def _(testconfig):
38 pass
41def to_event_time_granularity(value: datetime) -> datetime:
42 """Events are scheduled at the minute granularity."""
43 return value.replace(second=0, microsecond=0)
46def is_utc_or_gmt(timezone: str) -> bool:
47 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT.
48 # Tests should be agnostic to which one we're using.
49 return timezone in ("Etc/UTC", "Etc/GMT")
52def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator):
53 # test cases:
54 # can create event
55 # cannot create event with missing details
56 # can't create event that starts in the past
57 # can create in different timezones
59 # event creator
60 user1, token1 = generate_user()
61 # community moderator
62 user2, token2 = generate_user()
63 # third party
64 user3, token3 = generate_user()
66 with session_scope() as session:
67 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
69 time_before = now()
70 start_time = now() + timedelta(hours=2)
71 end_time = start_time + timedelta(hours=3)
73 # Can create an event
74 with events_session(token1) as api:
75 res = api.CreateEvent(
76 events_pb2.CreateEventReq(
77 title="Dummy Title",
78 content="Dummy content.",
79 photo_key=None,
80 location=events_pb2.EventLocation(
81 address="Near Null Island",
82 lat=0.1,
83 lng=0.2,
84 ),
85 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
86 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
87 )
88 )
90 assert res.is_next
91 assert res.title == "Dummy Title"
92 assert res.slug == "dummy-title"
93 assert res.content == "Dummy content."
94 assert not res.photo_url
95 assert res.HasField("location")
96 assert res.location.lat == 0.1
97 assert res.location.lng == 0.2
98 assert res.location.address == "Near Null Island"
99 assert time_before <= to_aware_datetime(res.created) <= now()
100 assert time_before <= to_aware_datetime(res.last_edited) <= now()
101 assert res.creator_user_id == user1.id
102 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
103 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
104 assert is_utc_or_gmt(res.timezone)
105 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
106 assert res.organizer
107 assert res.subscriber
108 assert res.going_count == 1
109 assert res.organizer_count == 1
110 assert res.subscriber_count == 1
111 assert res.owner_user_id == user1.id
112 assert not res.owner_community_id
113 assert not res.owner_group_id
114 assert res.thread.thread_id
115 assert res.can_edit
116 assert not res.can_moderate
118 event_id = res.event_id
120 # Approve the event so other users can see it
121 moderator.approve_event_occurrence(event_id)
123 with events_session(token2) as api:
124 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
126 assert res.is_next
127 assert res.title == "Dummy Title"
128 assert res.slug == "dummy-title"
129 assert res.content == "Dummy content."
130 assert not res.photo_url
131 assert res.HasField("location")
132 assert res.location.lat == 0.1
133 assert res.location.lng == 0.2
134 assert res.location.address == "Near Null Island"
135 assert time_before <= to_aware_datetime(res.created) <= now()
136 assert time_before <= to_aware_datetime(res.last_edited) <= now()
137 assert res.creator_user_id == user1.id
138 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
139 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
140 assert is_utc_or_gmt(res.timezone)
141 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
142 assert not res.organizer
143 assert not res.subscriber
144 assert res.going_count == 1
145 assert res.organizer_count == 1
146 assert res.subscriber_count == 1
147 assert res.owner_user_id == user1.id
148 assert not res.owner_community_id
149 assert not res.owner_group_id
150 assert res.thread.thread_id
151 assert res.can_edit
152 assert res.can_moderate
154 with events_session(token3) as api:
155 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
157 assert res.is_next
158 assert res.title == "Dummy Title"
159 assert res.slug == "dummy-title"
160 assert res.content == "Dummy content."
161 assert not res.photo_url
162 assert res.HasField("location")
163 assert res.location.lat == 0.1
164 assert res.location.lng == 0.2
165 assert res.location.address == "Near Null Island"
166 assert time_before <= to_aware_datetime(res.created) <= now()
167 assert time_before <= to_aware_datetime(res.last_edited) <= now()
168 assert res.creator_user_id == user1.id
169 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
170 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
171 assert is_utc_or_gmt(res.timezone)
172 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
173 assert not res.organizer
174 assert not res.subscriber
175 assert res.going_count == 1
176 assert res.organizer_count == 1
177 assert res.subscriber_count == 1
178 assert res.owner_user_id == user1.id
179 assert not res.owner_community_id
180 assert not res.owner_group_id
181 assert res.thread.thread_id
182 assert not res.can_edit
183 assert not res.can_moderate
185 # Failure cases
186 with events_session(token1) as api:
187 with pytest.raises(grpc.RpcError) as e:
188 api.CreateEvent(
189 events_pb2.CreateEventReq(
190 # title="Dummy Title",
191 content="Dummy content.",
192 photo_key=None,
193 location=events_pb2.EventLocation(
194 address="Near Null Island",
195 lat=0.1,
196 lng=0.2,
197 ),
198 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
199 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
200 )
201 )
202 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
203 assert e.value.details() == "Missing event title."
205 with pytest.raises(grpc.RpcError) as e:
206 api.CreateEvent(
207 events_pb2.CreateEventReq(
208 title="Dummy Title",
209 # content="Dummy content.",
210 photo_key=None,
211 location=events_pb2.EventLocation(
212 address="Near Null Island",
213 lat=0.1,
214 lng=0.2,
215 ),
216 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
217 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
218 )
219 )
220 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
221 assert e.value.details() == "Missing event content."
223 with pytest.raises(grpc.RpcError) as e:
224 api.CreateEvent(
225 events_pb2.CreateEventReq(
226 title="Dummy Title",
227 content="Dummy content.",
228 photo_key="nonexistent",
229 location=events_pb2.EventLocation(
230 address="Near Null Island",
231 lat=0.1,
232 lng=0.2,
233 ),
234 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
235 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
236 )
237 )
238 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
239 assert e.value.details() == "Photo not found."
241 with pytest.raises(grpc.RpcError) as e:
242 api.CreateEvent(
243 events_pb2.CreateEventReq(
244 title="Dummy Title",
245 content="Dummy content.",
246 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
247 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
248 )
249 )
250 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
251 assert e.value.details() == "Missing event address or location."
253 with pytest.raises(grpc.RpcError) as e:
254 api.CreateEvent(
255 events_pb2.CreateEventReq(
256 title="Dummy Title",
257 content="Dummy content.",
258 location=events_pb2.EventLocation(
259 address="Near Null Island",
260 ),
261 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
262 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
263 )
264 )
265 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
266 assert e.value.details() == "Invalid coordinate."
268 with pytest.raises(grpc.RpcError) as e:
269 api.CreateEvent(
270 events_pb2.CreateEventReq(
271 title="Dummy Title",
272 content="Dummy content.",
273 location=events_pb2.EventLocation(
274 lat=0.1,
275 lng=0.1,
276 ),
277 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
278 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
279 )
280 )
281 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
282 assert e.value.details() == "Missing event address or location."
284 with pytest.raises(grpc.RpcError) as e:
285 api.CreateEvent(
286 events_pb2.CreateEventReq(
287 title="Dummy Title",
288 content="Dummy content.",
289 location=events_pb2.EventLocation(
290 address="Near Null Island",
291 lat=0.1,
292 lng=0.2,
293 ),
294 start_datetime_iso8601_local=datetime_to_iso8601_local(now() - timedelta(hours=2)),
295 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
296 )
297 )
298 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
299 assert e.value.details() == "The event must be in the future."
301 with pytest.raises(grpc.RpcError) as e:
302 api.CreateEvent(
303 events_pb2.CreateEventReq(
304 title="Dummy Title",
305 content="Dummy content.",
306 location=events_pb2.EventLocation(
307 address="Near Null Island",
308 lat=0.1,
309 lng=0.2,
310 ),
311 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
312 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
313 )
314 )
315 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
316 assert e.value.details() == "The event must end after it starts."
318 with pytest.raises(grpc.RpcError) as e:
319 api.CreateEvent(
320 events_pb2.CreateEventReq(
321 title="Dummy Title",
322 content="Dummy content.",
323 location=events_pb2.EventLocation(
324 address="Near Null Island",
325 lat=0.1,
326 lng=0.2,
327 ),
328 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=2)),
329 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=5)),
330 )
331 )
332 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
333 assert e.value.details() == "The event needs to start within the next year."
335 with pytest.raises(grpc.RpcError) as e:
336 api.CreateEvent(
337 events_pb2.CreateEventReq(
338 title="Dummy Title",
339 content="Dummy content.",
340 location=events_pb2.EventLocation(
341 address="Near Null Island",
342 lat=0.1,
343 lng=0.2,
344 ),
345 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
346 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=100)),
347 )
348 )
349 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
350 assert e.value.details() == "Events cannot last longer than 7 days."
353def test_CreateEvent_incomplete_profile(db):
354 user1, token1 = generate_user(complete_profile=False)
355 user2, token2 = generate_user()
357 with session_scope() as session:
358 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
360 start_time = now() + timedelta(hours=2)
361 end_time = start_time + timedelta(hours=3)
363 with events_session(token1) as api:
364 with pytest.raises(grpc.RpcError) as e:
365 api.CreateEvent(
366 events_pb2.CreateEventReq(
367 title="Dummy Title",
368 content="Dummy content.",
369 photo_key=None,
370 location=events_pb2.EventLocation(
371 address="Near Null Island",
372 lat=0.1,
373 lng=0.2,
374 ),
375 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
376 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
377 )
378 )
379 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
380 assert e.value.details() == "You have to complete your profile before you can create an event."
383def test_ScheduleEvent(db):
384 # test cases:
385 # can schedule a new event occurrence
387 user, token = generate_user()
389 with session_scope() as session:
390 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
392 time_before = now()
393 start_time = now() + timedelta(hours=2)
394 end_time = start_time + timedelta(hours=3)
396 with events_session(token) as api:
397 res = api.CreateEvent(
398 events_pb2.CreateEventReq(
399 title="Dummy Title",
400 content="Dummy content.",
401 parent_community_id=c_id,
402 location=events_pb2.EventLocation(
403 address="Near Null Island",
404 lat=0.1,
405 lng=0.2,
406 ),
407 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
408 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
409 )
410 )
412 new_start_time = now() + timedelta(hours=6)
413 new_end_time = new_start_time + timedelta(hours=2)
415 res = api.ScheduleEvent(
416 events_pb2.ScheduleEventReq(
417 event_id=res.event_id,
418 content="New event occurrence",
419 location=events_pb2.EventLocation(
420 address="A bit further but still near Null Island",
421 lat=0.3,
422 lng=0.2,
423 ),
424 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time),
425 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time),
426 )
427 )
429 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
431 assert not res.is_next
432 assert res.title == "Dummy Title"
433 assert res.slug == "dummy-title"
434 assert res.content == "New event occurrence"
435 assert not res.photo_url
436 assert res.HasField("location")
437 assert res.location.lat == 0.3
438 assert res.location.lng == 0.2
439 assert res.location.address == "A bit further but still near Null Island"
440 assert time_before <= to_aware_datetime(res.created) <= now()
441 assert time_before <= to_aware_datetime(res.last_edited) <= now()
442 assert res.creator_user_id == user.id
443 assert to_aware_datetime(res.start_time) == to_event_time_granularity(new_start_time)
444 assert to_aware_datetime(res.end_time) == to_event_time_granularity(new_end_time)
445 assert is_utc_or_gmt(res.timezone)
446 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
447 assert res.organizer
448 assert res.subscriber
449 assert res.going_count == 1
450 assert res.organizer_count == 1
451 assert res.subscriber_count == 1
452 assert res.owner_user_id == user.id
453 assert not res.owner_community_id
454 assert not res.owner_group_id
455 assert res.thread.thread_id
456 assert res.can_edit
457 assert res.can_moderate
460def test_cannot_overlap_occurrences_schedule(db):
461 user, token = generate_user()
463 with session_scope() as session:
464 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
466 start = now()
468 with events_session(token) as api:
469 res = api.CreateEvent(
470 events_pb2.CreateEventReq(
471 title="Dummy Title",
472 content="Dummy content.",
473 parent_community_id=c_id,
474 location=events_pb2.EventLocation(
475 address="Near Null Island",
476 lat=0.1,
477 lng=0.2,
478 ),
479 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
480 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
481 )
482 )
484 with pytest.raises(grpc.RpcError) as e:
485 api.ScheduleEvent(
486 events_pb2.ScheduleEventReq(
487 event_id=res.event_id,
488 content="New event occurrence",
489 location=events_pb2.EventLocation(
490 address="A bit further but still near Null Island",
491 lat=0.3,
492 lng=0.2,
493 ),
494 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
495 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
496 )
497 )
498 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
499 assert e.value.details() == "An event cannot have overlapping occurrences."
502def test_cannot_overlap_occurrences_update(db):
503 user, token = generate_user()
505 with session_scope() as session:
506 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
508 start = now()
510 with events_session(token) as api:
511 res = api.CreateEvent(
512 events_pb2.CreateEventReq(
513 title="Dummy Title",
514 content="Dummy content.",
515 parent_community_id=c_id,
516 location=events_pb2.EventLocation(
517 address="Near Null Island",
518 lat=0.1,
519 lng=0.2,
520 ),
521 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
522 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
523 )
524 )
526 event_id = api.ScheduleEvent(
527 events_pb2.ScheduleEventReq(
528 event_id=res.event_id,
529 content="New event occurrence",
530 location=events_pb2.EventLocation(
531 address="A bit further but still near Null Island",
532 lat=0.3,
533 lng=0.2,
534 ),
535 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
536 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
537 )
538 ).event_id
540 # can overlap with this current existing occurrence
541 api.UpdateEvent(
542 events_pb2.UpdateEventReq(
543 event_id=event_id,
544 start_datetime_iso8601_local=wrappers_pb2.StringValue(
545 value=datetime_to_iso8601_local(start + timedelta(hours=5))
546 ),
547 end_datetime_iso8601_local=wrappers_pb2.StringValue(
548 value=datetime_to_iso8601_local(start + timedelta(hours=6))
549 ),
550 )
551 )
553 with pytest.raises(grpc.RpcError) as e:
554 api.UpdateEvent(
555 events_pb2.UpdateEventReq(
556 event_id=event_id,
557 start_datetime_iso8601_local=wrappers_pb2.StringValue(
558 value=datetime_to_iso8601_local(start + timedelta(hours=2))
559 ),
560 end_datetime_iso8601_local=wrappers_pb2.StringValue(
561 value=datetime_to_iso8601_local(start + timedelta(hours=4))
562 ),
563 )
564 )
565 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
566 assert e.value.details() == "An event cannot have overlapping occurrences."
569def test_UpdateEvent_single(db, moderator: Moderator):
570 # test cases:
571 # owner can update
572 # community owner can update
573 # notifies attendees
575 # event creator
576 user1, token1 = generate_user()
577 # community moderator
578 user2, token2 = generate_user()
579 # third parties
580 user3, token3 = generate_user()
581 user4, token4 = generate_user()
582 user5, token5 = generate_user()
583 user6, token6 = generate_user()
585 with session_scope() as session:
586 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
588 time_before = now()
589 start_time = now() + timedelta(hours=2)
590 end_time = start_time + timedelta(hours=3)
592 with events_session(token1) as api:
593 res = api.CreateEvent(
594 events_pb2.CreateEventReq(
595 title="Dummy Title",
596 content="Dummy content.",
597 parent_community_id=c_id,
598 location=events_pb2.EventLocation(
599 address="Near Null Island",
600 lat=0.1,
601 lng=0.2,
602 ),
603 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
604 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
605 )
606 )
608 event_id = res.event_id
610 moderator.approve_event_occurrence(event_id)
612 with events_session(token4) as api:
613 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
615 with events_session(token5) as api:
616 api.SetEventAttendance(
617 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
618 )
620 with events_session(token6) as api:
621 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
623 time_before_update = now()
625 with events_session(token1) as api:
626 res = api.UpdateEvent(
627 events_pb2.UpdateEventReq(
628 event_id=event_id,
629 )
630 )
632 with events_session(token1) as api:
633 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
635 assert res.is_next
636 assert res.title == "Dummy Title"
637 assert res.slug == "dummy-title"
638 assert res.content == "Dummy content."
639 assert not res.photo_url
640 assert res.HasField("location")
641 assert res.location.lat == 0.1
642 assert res.location.lng == 0.2
643 assert res.location.address == "Near Null Island"
644 assert time_before <= to_aware_datetime(res.created) <= time_before_update
645 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
646 assert res.creator_user_id == user1.id
647 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
648 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
649 assert is_utc_or_gmt(res.timezone)
650 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
651 assert res.organizer
652 assert res.subscriber
653 assert res.going_count == 2
654 assert res.organizer_count == 1
655 assert res.subscriber_count == 3
656 assert res.owner_user_id == user1.id
657 assert not res.owner_community_id
658 assert not res.owner_group_id
659 assert res.thread.thread_id
660 assert res.can_edit
661 assert not res.can_moderate
663 with events_session(token2) as api:
664 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
666 assert res.is_next
667 assert res.title == "Dummy Title"
668 assert res.slug == "dummy-title"
669 assert res.content == "Dummy content."
670 assert not res.photo_url
671 assert res.HasField("location")
672 assert res.location.lat == 0.1
673 assert res.location.lng == 0.2
674 assert res.location.address == "Near Null Island"
675 assert time_before <= to_aware_datetime(res.created) <= time_before_update
676 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
677 assert res.creator_user_id == user1.id
678 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
679 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
680 assert is_utc_or_gmt(res.timezone)
681 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
682 assert not res.organizer
683 assert not res.subscriber
684 assert res.going_count == 2
685 assert res.organizer_count == 1
686 assert res.subscriber_count == 3
687 assert res.owner_user_id == user1.id
688 assert not res.owner_community_id
689 assert not res.owner_group_id
690 assert res.thread.thread_id
691 assert res.can_edit
692 assert res.can_moderate
694 with events_session(token3) as api:
695 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
697 assert res.is_next
698 assert res.title == "Dummy Title"
699 assert res.slug == "dummy-title"
700 assert res.content == "Dummy content."
701 assert not res.photo_url
702 assert res.HasField("location")
703 assert res.location.lat == 0.1
704 assert res.location.lng == 0.2
705 assert res.location.address == "Near Null Island"
706 assert time_before <= to_aware_datetime(res.created) <= time_before_update
707 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
708 assert res.creator_user_id == user1.id
709 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
710 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
711 assert is_utc_or_gmt(res.timezone)
712 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
713 assert not res.organizer
714 assert not res.subscriber
715 assert res.going_count == 2
716 assert res.organizer_count == 1
717 assert res.subscriber_count == 3
718 assert res.owner_user_id == user1.id
719 assert not res.owner_community_id
720 assert not res.owner_group_id
721 assert res.thread.thread_id
722 assert not res.can_edit
723 assert not res.can_moderate
725 with events_session(token1) as api:
726 res = api.UpdateEvent(
727 events_pb2.UpdateEventReq(
728 event_id=event_id,
729 location=events_pb2.EventLocation(
730 address="Nearer Null Island",
731 lat=0.01,
732 lng=0.02,
733 ),
734 )
735 )
737 with events_session(token3) as api:
738 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
740 assert res.HasField("location")
741 assert res.location.address == "Nearer Null Island"
742 assert res.location.lat == 0.01
743 assert res.location.lng == 0.02
746def test_UpdateEvent_all(db, moderator: Moderator):
747 # event creator
748 user1, token1 = generate_user()
749 # community moderator
750 user2, token2 = generate_user()
751 # third parties
752 user3, token3 = generate_user()
753 user4, token4 = generate_user()
754 user5, token5 = generate_user()
755 user6, token6 = generate_user()
757 with session_scope() as session:
758 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
760 time_before = now()
761 start_time = now() + timedelta(hours=1)
762 end_time = start_time + timedelta(hours=1.5)
764 event_ids = []
766 with events_session(token1) as api:
767 res = api.CreateEvent(
768 events_pb2.CreateEventReq(
769 title="Dummy Title",
770 content="0th occurrence",
771 location=events_pb2.EventLocation(
772 address="Near Null Island",
773 lat=0.1,
774 lng=0.2,
775 ),
776 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
777 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
778 )
779 )
781 event_id = res.event_id
782 event_ids.append(event_id)
784 moderator.approve_event_occurrence(event_id)
786 with events_session(token4) as api:
787 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
789 with events_session(token5) as api:
790 api.SetEventAttendance(
791 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
792 )
794 with events_session(token6) as api:
795 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
797 with events_session(token1) as api:
798 for i in range(5):
799 res = api.ScheduleEvent(
800 events_pb2.ScheduleEventReq(
801 event_id=event_ids[-1],
802 content=f"{i + 1}th occurrence",
803 location=events_pb2.EventLocation(
804 address="Near Null Island",
805 lat=0.1,
806 lng=0.2,
807 ),
808 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2 + i)),
809 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2.5 + i)),
810 )
811 )
813 event_ids.append(res.event_id)
815 # Approve all scheduled occurrences
816 for eid in event_ids[1:]:
817 moderator.approve_event_occurrence(eid)
819 updated_event_id = event_ids[3]
821 time_before_update = now()
823 with events_session(token1) as api:
824 res = api.UpdateEvent(
825 events_pb2.UpdateEventReq(
826 event_id=updated_event_id,
827 title=wrappers_pb2.StringValue(value="New Title"),
828 content=wrappers_pb2.StringValue(value="New content."),
829 location=events_pb2.EventLocation(
830 address="Not so near Null Island",
831 lat=0.2,
832 lng=0.2,
833 ),
834 update_all_future=True,
835 )
836 )
838 time_after_update = now()
840 with events_session(token2) as api:
841 for i in range(3):
842 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
843 assert res.content == f"{i}th occurrence"
844 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
846 for i in range(3, 6):
847 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
848 assert res.content == "New content."
849 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
852def test_GetEvent(db, moderator: Moderator):
853 # event creator
854 user1, token1 = generate_user()
855 # community moderator
856 user2, token2 = generate_user()
857 # third parties
858 user3, token3 = generate_user()
859 user4, token4 = generate_user()
860 user5, token5 = generate_user()
861 user6, token6 = generate_user()
863 with session_scope() as session:
864 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
866 time_before = now()
867 start_time = now() + timedelta(hours=2)
868 end_time = start_time + timedelta(hours=3)
870 with events_session(token1) as api:
871 # in person event
872 res = api.CreateEvent(
873 events_pb2.CreateEventReq(
874 title="Dummy Title",
875 content="Dummy content.",
876 location=events_pb2.EventLocation(
877 address="Near Null Island",
878 lat=0.1,
879 lng=0.2,
880 ),
881 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
882 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
883 )
884 )
886 event_id = res.event_id
888 moderator.approve_event_occurrence(event_id)
890 with events_session(token4) as api:
891 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
893 with events_session(token5) as api:
894 api.SetEventAttendance(
895 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
896 )
898 with events_session(token6) as api:
899 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
901 with events_session(token1) as api:
902 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
904 assert res.is_next
905 assert res.title == "Dummy Title"
906 assert res.slug == "dummy-title"
907 assert res.content == "Dummy content."
908 assert not res.photo_url
909 assert res.HasField("location")
910 assert res.location.lat == 0.1
911 assert res.location.lng == 0.2
912 assert res.location.address == "Near Null Island"
913 assert time_before <= to_aware_datetime(res.created) <= now()
914 assert time_before <= to_aware_datetime(res.last_edited) <= now()
915 assert res.creator_user_id == user1.id
916 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
917 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
918 assert is_utc_or_gmt(res.timezone)
919 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
920 assert res.organizer
921 assert res.subscriber
922 assert res.going_count == 2
923 assert res.organizer_count == 1
924 assert res.subscriber_count == 3
925 assert res.owner_user_id == user1.id
926 assert not res.owner_community_id
927 assert not res.owner_group_id
928 assert res.thread.thread_id
929 assert res.can_edit
930 assert not res.can_moderate
932 with events_session(token2) as api:
933 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
935 assert res.is_next
936 assert res.title == "Dummy Title"
937 assert res.slug == "dummy-title"
938 assert res.content == "Dummy content."
939 assert not res.photo_url
940 assert res.HasField("location")
941 assert res.location.lat == 0.1
942 assert res.location.lng == 0.2
943 assert res.location.address == "Near Null Island"
944 assert time_before <= to_aware_datetime(res.created) <= now()
945 assert time_before <= to_aware_datetime(res.last_edited) <= now()
946 assert res.creator_user_id == user1.id
947 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
948 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
949 assert is_utc_or_gmt(res.timezone)
950 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
951 assert not res.organizer
952 assert not res.subscriber
953 assert res.going_count == 2
954 assert res.organizer_count == 1
955 assert res.subscriber_count == 3
956 assert res.owner_user_id == user1.id
957 assert not res.owner_community_id
958 assert not res.owner_group_id
959 assert res.thread.thread_id
960 assert res.can_edit
961 assert res.can_moderate
963 with events_session(token3) as api:
964 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
966 assert res.is_next
967 assert res.title == "Dummy Title"
968 assert res.slug == "dummy-title"
969 assert res.content == "Dummy content."
970 assert not res.photo_url
971 assert res.HasField("location")
972 assert res.location.lat == 0.1
973 assert res.location.lng == 0.2
974 assert res.location.address == "Near Null Island"
975 assert time_before <= to_aware_datetime(res.created) <= now()
976 assert time_before <= to_aware_datetime(res.last_edited) <= now()
977 assert res.creator_user_id == user1.id
978 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
979 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
980 assert is_utc_or_gmt(res.timezone)
981 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
982 assert not res.organizer
983 assert not res.subscriber
984 assert res.going_count == 2
985 assert res.organizer_count == 1
986 assert res.subscriber_count == 3
987 assert res.owner_user_id == user1.id
988 assert not res.owner_community_id
989 assert not res.owner_group_id
990 assert res.thread.thread_id
991 assert not res.can_edit
992 assert not res.can_moderate
995def test_CancelEvent(db, moderator: Moderator):
996 # event creator
997 user1, token1 = generate_user()
998 # community moderator
999 user2, token2 = generate_user()
1000 # third parties
1001 user3, token3 = generate_user()
1002 user4, token4 = generate_user()
1003 user5, token5 = generate_user()
1004 user6, token6 = generate_user()
1006 with session_scope() as session:
1007 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1009 start_time = now() + timedelta(hours=2)
1010 end_time = start_time + timedelta(hours=3)
1012 with events_session(token1) as api:
1013 res = api.CreateEvent(
1014 events_pb2.CreateEventReq(
1015 title="Dummy Title",
1016 content="Dummy content.",
1017 location=events_pb2.EventLocation(
1018 address="Near Null Island",
1019 lat=0.1,
1020 lng=0.2,
1021 ),
1022 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1023 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1024 )
1025 )
1027 event_id = res.event_id
1029 moderator.approve_event_occurrence(event_id)
1031 with events_session(token4) as api:
1032 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1034 with events_session(token5) as api:
1035 api.SetEventAttendance(
1036 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1037 )
1039 with events_session(token6) as api:
1040 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1042 with events_session(token1) as api:
1043 res = api.CancelEvent(
1044 events_pb2.CancelEventReq(
1045 event_id=event_id,
1046 )
1047 )
1049 with events_session(token1) as api:
1050 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1051 assert res.is_cancelled
1053 with events_session(token1) as api:
1054 with pytest.raises(grpc.RpcError) as e:
1055 api.UpdateEvent(
1056 events_pb2.UpdateEventReq(
1057 event_id=event_id,
1058 title=wrappers_pb2.StringValue(value="New Title"),
1059 )
1060 )
1061 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1062 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1064 with pytest.raises(grpc.RpcError) as e:
1065 api.InviteEventOrganizer(
1066 events_pb2.InviteEventOrganizerReq(
1067 event_id=event_id,
1068 user_id=user3.id,
1069 )
1070 )
1071 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1072 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1074 with pytest.raises(grpc.RpcError) as e:
1075 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1076 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1077 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1079 with events_session(token3) as api:
1080 with pytest.raises(grpc.RpcError) as e:
1081 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1082 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1083 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1085 with pytest.raises(grpc.RpcError) as e:
1086 api.SetEventAttendance(
1087 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1088 )
1089 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1090 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1092 with events_session(token1) as api:
1093 for include_cancelled in [True, False]:
1094 res = api.ListEventOccurrences(
1095 events_pb2.ListEventOccurrencesReq(
1096 event_id=event_id,
1097 include_cancelled=include_cancelled,
1098 )
1099 )
1100 if include_cancelled:
1101 assert len(res.events) > 0
1102 else:
1103 assert len(res.events) == 0
1105 res = api.ListMyEvents(
1106 events_pb2.ListMyEventsReq(
1107 include_cancelled=include_cancelled,
1108 )
1109 )
1110 if include_cancelled:
1111 assert len(res.events) > 0
1112 else:
1113 assert len(res.events) == 0
1116def test_ListEventAttendees(db, moderator: Moderator):
1117 # event creator
1118 user1, token1 = generate_user()
1119 # others
1120 user2, token2 = generate_user()
1121 user3, token3 = generate_user()
1122 user4, token4 = generate_user()
1123 user5, token5 = generate_user()
1124 user6, token6 = generate_user()
1126 with session_scope() as session:
1127 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1129 with events_session(token1) as api:
1130 event_id = api.CreateEvent(
1131 events_pb2.CreateEventReq(
1132 title="Dummy Title",
1133 content="Dummy content.",
1134 location=events_pb2.EventLocation(
1135 address="Near Null Island",
1136 lat=0.1,
1137 lng=0.2,
1138 ),
1139 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1140 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1141 )
1142 ).event_id
1144 moderator.approve_event_occurrence(event_id)
1146 for token in [token2, token3, token4, token5]:
1147 with events_session(token) as api:
1148 api.SetEventAttendance(
1149 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1150 )
1152 with events_session(token6) as api:
1153 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1155 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1156 assert res.attendee_user_ids == [user1.id, user2.id]
1158 res = api.ListEventAttendees(
1159 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1160 )
1161 assert res.attendee_user_ids == [user3.id, user4.id]
1163 res = api.ListEventAttendees(
1164 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1165 )
1166 assert res.attendee_user_ids == [user5.id]
1167 assert not res.next_page_token
1170def test_ListEventSubscribers(db, moderator: Moderator):
1171 # event creator
1172 user1, token1 = generate_user()
1173 # others
1174 user2, token2 = generate_user()
1175 user3, token3 = generate_user()
1176 user4, token4 = generate_user()
1177 user5, token5 = generate_user()
1178 user6, token6 = generate_user()
1180 with session_scope() as session:
1181 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1183 with events_session(token1) as api:
1184 event_id = api.CreateEvent(
1185 events_pb2.CreateEventReq(
1186 title="Dummy Title",
1187 content="Dummy content.",
1188 location=events_pb2.EventLocation(
1189 address="Near Null Island",
1190 lat=0.1,
1191 lng=0.2,
1192 ),
1193 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1194 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1195 )
1196 ).event_id
1198 moderator.approve_event_occurrence(event_id)
1200 for token in [token2, token3, token4, token5]:
1201 with events_session(token) as api:
1202 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1204 with events_session(token6) as api:
1205 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1207 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1208 assert res.subscriber_user_ids == [user1.id, user2.id]
1210 res = api.ListEventSubscribers(
1211 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1212 )
1213 assert res.subscriber_user_ids == [user3.id, user4.id]
1215 res = api.ListEventSubscribers(
1216 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1217 )
1218 assert res.subscriber_user_ids == [user5.id]
1219 assert not res.next_page_token
1222def test_ListEventOrganizers(db, moderator: Moderator):
1223 # event creator
1224 user1, token1 = generate_user()
1225 # others
1226 user2, token2 = generate_user()
1227 user3, token3 = generate_user()
1228 user4, token4 = generate_user()
1229 user5, token5 = generate_user()
1230 user6, token6 = generate_user()
1232 with session_scope() as session:
1233 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1235 with events_session(token1) as api:
1236 event_id = api.CreateEvent(
1237 events_pb2.CreateEventReq(
1238 title="Dummy Title",
1239 content="Dummy content.",
1240 location=events_pb2.EventLocation(
1241 address="Near Null Island",
1242 lat=0.1,
1243 lng=0.2,
1244 ),
1245 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1246 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1247 )
1248 ).event_id
1250 moderator.approve_event_occurrence(event_id)
1252 with events_session(token1) as api:
1253 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1254 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1256 with events_session(token6) as api:
1257 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1259 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1260 assert res.organizer_user_ids == [user1.id, user2.id]
1262 res = api.ListEventOrganizers(
1263 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1264 )
1265 assert res.organizer_user_ids == [user3.id, user4.id]
1267 res = api.ListEventOrganizers(
1268 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1269 )
1270 assert res.organizer_user_ids == [user5.id]
1271 assert not res.next_page_token
1274def test_TransferEvent(db):
1275 user1, token1 = generate_user()
1276 user2, token2 = generate_user()
1277 user3, token3 = generate_user()
1278 user4, token4 = generate_user()
1280 with session_scope() as session:
1281 c = create_community(session, 0, 2, "Community", [user3], [], None)
1282 h = create_group(session, "Group", [user4], [], c)
1283 c_id = c.id
1284 h_id = h.id
1286 with events_session(token1) as api:
1287 event_id = api.CreateEvent(
1288 events_pb2.CreateEventReq(
1289 title="Dummy Title",
1290 content="Dummy content.",
1291 location=events_pb2.EventLocation(
1292 address="Near Null Island",
1293 lat=0.1,
1294 lng=0.2,
1295 ),
1296 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1297 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1298 )
1299 ).event_id
1301 api.TransferEvent(
1302 events_pb2.TransferEventReq(
1303 event_id=event_id,
1304 new_owner_community_id=c_id,
1305 )
1306 )
1308 # remove ourselves as organizer, otherwise we can still edit it
1309 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1311 with pytest.raises(grpc.RpcError) as e:
1312 api.TransferEvent(
1313 events_pb2.TransferEventReq(
1314 event_id=event_id,
1315 new_owner_group_id=h_id,
1316 )
1317 )
1318 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1319 assert e.value.details() == "You're not allowed to transfer that event."
1321 event_id = api.CreateEvent(
1322 events_pb2.CreateEventReq(
1323 title="Dummy Title",
1324 content="Dummy content.",
1325 location=events_pb2.EventLocation(
1326 address="Near Null Island",
1327 lat=0.1,
1328 lng=0.2,
1329 ),
1330 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1331 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1332 )
1333 ).event_id
1335 api.TransferEvent(
1336 events_pb2.TransferEventReq(
1337 event_id=event_id,
1338 new_owner_group_id=h_id,
1339 )
1340 )
1342 # remove ourselves as organizer, otherwise we can still edit it
1343 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1345 with pytest.raises(grpc.RpcError) as e:
1346 api.TransferEvent(
1347 events_pb2.TransferEventReq(
1348 event_id=event_id,
1349 new_owner_community_id=c_id,
1350 )
1351 )
1352 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1353 assert e.value.details() == "You're not allowed to transfer that event."
1356def test_SetEventSubscription(db, moderator: Moderator):
1357 user1, token1 = generate_user()
1358 user2, token2 = generate_user()
1360 with session_scope() as session:
1361 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1363 with events_session(token1) as api:
1364 event_id = api.CreateEvent(
1365 events_pb2.CreateEventReq(
1366 title="Dummy Title",
1367 content="Dummy content.",
1368 location=events_pb2.EventLocation(
1369 address="Near Null Island",
1370 lat=0.1,
1371 lng=0.2,
1372 ),
1373 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1374 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1375 )
1376 ).event_id
1378 moderator.approve_event_occurrence(event_id)
1380 with events_session(token2) as api:
1381 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1382 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1383 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1384 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1385 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1388def test_SetEventAttendance(db, moderator: Moderator):
1389 user1, token1 = generate_user()
1390 user2, token2 = generate_user()
1392 with session_scope() as session:
1393 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1395 with events_session(token1) as api:
1396 event_id = api.CreateEvent(
1397 events_pb2.CreateEventReq(
1398 title="Dummy Title",
1399 content="Dummy content.",
1400 location=events_pb2.EventLocation(
1401 address="Near Null Island",
1402 lat=0.1,
1403 lng=0.2,
1404 ),
1405 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1406 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1407 )
1408 ).event_id
1410 moderator.approve_event_occurrence(event_id)
1412 with events_session(token2) as api:
1413 assert (
1414 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1415 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1416 )
1417 api.SetEventAttendance(
1418 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1419 )
1420 assert (
1421 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1422 == events_pb2.ATTENDANCE_STATE_GOING
1423 )
1424 api.SetEventAttendance(
1425 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1426 )
1427 assert (
1428 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1429 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1430 )
1433def test_InviteEventOrganizer(db, moderator: Moderator):
1434 user1, token1 = generate_user()
1435 user2, token2 = generate_user()
1437 with session_scope() as session:
1438 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1440 with events_session(token1) as api:
1441 event_id = api.CreateEvent(
1442 events_pb2.CreateEventReq(
1443 title="Dummy Title",
1444 content="Dummy content.",
1445 location=events_pb2.EventLocation(
1446 address="Near Null Island",
1447 lat=0.1,
1448 lng=0.2,
1449 ),
1450 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1451 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1452 )
1453 ).event_id
1455 moderator.approve_event_occurrence(event_id)
1457 with events_session(token2) as api:
1458 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1460 with pytest.raises(grpc.RpcError) as e:
1461 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1462 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1463 assert e.value.details() == "You're not allowed to edit that event."
1465 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1467 with events_session(token1) as api:
1468 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1470 with events_session(token2) as api:
1471 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1474def test_ListEventOccurrences(db):
1475 user1, token1 = generate_user()
1476 user2, token2 = generate_user()
1477 user3, token3 = generate_user()
1479 with session_scope() as session:
1480 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1482 start = now()
1484 event_ids = []
1486 with events_session(token1) as api:
1487 res = api.CreateEvent(
1488 events_pb2.CreateEventReq(
1489 title="First occurrence",
1490 content="Dummy content.",
1491 parent_community_id=c_id,
1492 location=events_pb2.EventLocation(
1493 address="Near Null Island",
1494 lat=0.1,
1495 lng=0.2,
1496 ),
1497 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1498 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
1499 )
1500 )
1502 event_ids.append(res.event_id)
1504 for i in range(5):
1505 res = api.ScheduleEvent(
1506 events_pb2.ScheduleEventReq(
1507 event_id=event_ids[-1],
1508 content=f"{i}th occurrence",
1509 location=events_pb2.EventLocation(
1510 address="Near Null Island",
1511 lat=0.1,
1512 lng=0.2,
1513 ),
1514 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
1515 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
1516 )
1517 )
1519 event_ids.append(res.event_id)
1521 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1522 assert [event.event_id for event in res.events] == event_ids[:2]
1524 res = api.ListEventOccurrences(
1525 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1526 )
1527 assert [event.event_id for event in res.events] == event_ids[2:4]
1529 res = api.ListEventOccurrences(
1530 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1531 )
1532 assert [event.event_id for event in res.events] == event_ids[4:6]
1533 assert not res.next_page_token
1536def test_ListMyEvents(db, moderator: Moderator):
1537 user1, token1 = generate_user()
1538 user2, token2 = generate_user()
1539 user3, token3 = generate_user()
1540 user4, token4 = generate_user()
1541 user5, token5 = generate_user()
1543 with session_scope() as session:
1544 # Create global (world) -> macroregion -> region -> subregion hierarchy
1545 # my_communities_exclude_global filters out world, macroregion, and region level communities
1546 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1547 c_id = global_community.id
1548 macroregion_community = create_community(
1549 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1550 )
1551 region_community = create_community(
1552 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1553 )
1554 subregion_community = create_community(
1555 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1556 )
1557 c2_id = subregion_community.id
1559 start = now()
1561 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1562 return events_pb2.CreateEventReq(
1563 title="Dummy Title",
1564 content="Dummy content.",
1565 location=events_pb2.EventLocation(
1566 address="Near Null Island",
1567 lat=0.1,
1568 lng=0.2,
1569 ),
1570 parent_community_id=community_id,
1571 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now)),
1572 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now + 0.5)),
1573 )
1575 with events_session(token1) as api:
1576 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1578 moderator.approve_event_occurrence(e2)
1580 with events_session(token2) as api:
1581 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1583 moderator.approve_event_occurrence(e1)
1585 with events_session(token1) as api:
1586 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1588 moderator.approve_event_occurrence(e3)
1590 with events_session(token2) as api:
1591 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1593 moderator.approve_event_occurrence(e5)
1595 with events_session(token3) as api:
1596 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1598 moderator.approve_event_occurrence(e4)
1600 with events_session(token4) as api:
1601 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1603 moderator.approve_event_occurrence(e6)
1605 with events_session(token1) as api:
1606 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1608 with events_session(token1) as api:
1609 api.SetEventAttendance(
1610 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1611 )
1612 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1614 with events_session(token2) as api:
1615 api.SetEventAttendance(
1616 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1617 )
1619 with events_session(token3) as api:
1620 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1622 with events_session(token1) as api:
1623 # test pagination with token first
1624 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1625 assert [event.event_id for event in res.events] == [e1, e2]
1626 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1627 assert [event.event_id for event in res.events] == [e3, e4]
1628 assert not res.next_page_token
1630 res = api.ListMyEvents(
1631 events_pb2.ListMyEventsReq(
1632 subscribed=True,
1633 attending=True,
1634 organizing=True,
1635 )
1636 )
1637 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1639 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1640 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1642 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1643 assert [event.event_id for event in res.events] == [e2, e3, e4]
1645 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1646 assert [event.event_id for event in res.events] == [e1, e2, e3]
1648 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1649 assert [event.event_id for event in res.events] == [e2, e3]
1651 with events_session(token1) as api:
1652 # Test pagination with page_number and verify total_items
1653 res = api.ListMyEvents(
1654 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1655 )
1656 assert [event.event_id for event in res.events] == [e1, e2]
1657 assert res.total_items == 4
1659 res = api.ListMyEvents(
1660 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1661 )
1662 assert [event.event_id for event in res.events] == [e3, e4]
1663 assert res.total_items == 4
1665 # Verify no more pages
1666 res = api.ListMyEvents(
1667 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1668 )
1669 assert not res.events
1670 assert res.total_items == 4
1672 with events_session(token2) as api:
1673 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1674 assert [event.event_id for event in res.events] == [e1, e3, e5]
1676 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1677 assert [event.event_id for event in res.events] == [e1, e5]
1679 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1680 assert [event.event_id for event in res.events] == [e1, e3, e5]
1682 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1683 assert [event.event_id for event in res.events] == [e1, e5]
1685 with events_session(token3) as api:
1686 # user3 is member of both global (c_id) and child (c2_id) communities
1687 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1688 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1690 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1691 assert [event.event_id for event in res.events] == [e2, e4]
1693 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1694 assert [event.event_id for event in res.events] == [e4]
1696 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1697 assert [event.event_id for event in res.events] == [e3, e4]
1699 # my_communities returns events from both communities user3 is a member of
1700 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1701 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1703 # my_communities_exclude_global filters out events from global community (node_id=1)
1704 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1705 assert [event.event_id for event in res.events] == [e6]
1707 # my_communities_exclude_global works independently of my_communities flag
1708 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1709 assert [event.event_id for event in res.events] == [e6]
1711 # my_communities_exclude_global filters organizing results too
1712 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1713 assert [event.event_id for event in res.events] == []
1715 # my_communities_exclude_global filters subscribed results too
1716 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1717 assert [event.event_id for event in res.events] == []
1719 with events_session(token5) as api:
1720 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1721 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1724def test_list_my_events_exclude_attending(db, moderator: Moderator):
1725 user1, token1 = generate_user()
1726 user2, token2 = generate_user()
1728 with session_scope() as session:
1729 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1730 c_id = c.id
1732 start = now()
1734 def make_event(hours):
1735 return events_pb2.CreateEventReq(
1736 title="Test Event",
1737 content="Test content.",
1738 location=events_pb2.EventLocation(
1739 address="Near Null Island",
1740 lat=0.1,
1741 lng=0.2,
1742 ),
1743 parent_community_id=c_id,
1744 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours)),
1745 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours + 1)),
1746 )
1748 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1749 with events_session(token1) as api:
1750 e_own = api.CreateEvent(make_event(1)).event_id
1752 with events_session(token2) as api:
1753 e_attending = api.CreateEvent(make_event(2)).event_id
1754 e_community_only = api.CreateEvent(make_event(3)).event_id
1755 # e_both: user1 will be both organizer and attendee
1756 e_both = api.CreateEvent(make_event(4)).event_id
1758 moderator.approve_event_occurrence(e_own)
1759 moderator.approve_event_occurrence(e_attending)
1760 moderator.approve_event_occurrence(e_community_only)
1761 moderator.approve_event_occurrence(e_both)
1763 # invite user1 as organizer of e_both
1764 with events_session(token2) as api:
1765 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
1767 # user1 RSVPs to e_attending and e_both
1768 with events_session(token1) as api:
1769 api.SetEventAttendance(
1770 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1771 )
1772 api.SetEventAttendance(
1773 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1774 )
1776 with events_session(token1) as api:
1777 # baseline: all four community events visible
1778 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1779 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
1781 # exclude_attending removes events user1 is attending (e_attending, e_both)
1782 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
1783 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1784 assert [e.event_id for e in res.events] == [e_community_only]
1786 # exclude_attending with attending=True: invalid combination
1787 with pytest.raises(grpc.RpcError) as e:
1788 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
1789 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1791 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
1792 with events_session(token2) as api:
1793 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1794 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
1795 assert [e.event_id for e in res.events] == [e_own]
1798def test_RemoveEventOrganizer(db, moderator: Moderator):
1799 user1, token1 = generate_user()
1800 user2, token2 = generate_user()
1802 with session_scope() as session:
1803 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1805 with events_session(token1) as api:
1806 event_id = api.CreateEvent(
1807 events_pb2.CreateEventReq(
1808 title="Dummy Title",
1809 content="Dummy content.",
1810 location=events_pb2.EventLocation(
1811 address="Near Null Island",
1812 lat=0.1,
1813 lng=0.2,
1814 ),
1815 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1816 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1817 )
1818 ).event_id
1820 moderator.approve_event_occurrence(event_id)
1822 with events_session(token2) as api:
1823 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1825 with pytest.raises(grpc.RpcError) as e:
1826 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1827 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1828 assert e.value.details() == "You're not allowed to edit that event."
1830 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1832 with events_session(token1) as api:
1833 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1835 with pytest.raises(grpc.RpcError) as e:
1836 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1837 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1838 assert e.value.details() == "You cannot remove the event owner as an organizer."
1840 with events_session(token2) as api:
1841 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1842 assert res.organizer
1843 assert res.organizer_count == 2
1844 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1845 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1847 with pytest.raises(grpc.RpcError) as e:
1848 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1849 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1850 assert e.value.details() == "You're not allowed to edit that event."
1852 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1853 assert not res.organizer
1854 assert res.organizer_count == 1
1856 # Test that event owner can remove co-organizers
1857 with events_session(token1) as api:
1858 # Add user2 back as organizer
1859 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1861 # Verify user2 is now an organizer
1862 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1863 assert res.organizer_count == 2
1865 # Event owner can remove co-organizer
1866 api.RemoveEventOrganizer(
1867 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
1868 )
1870 # Verify user2 is no longer an organizer
1871 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1872 assert res.organizer_count == 1
1874 # Test that non-organizers cannot remove other organizers
1875 with events_session(token2) as api:
1876 # User2 cannot invite themselves as organizer (not the owner)
1877 with pytest.raises(grpc.RpcError) as e:
1878 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1879 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1880 assert e.value.details() == "You're not allowed to edit that event."
1882 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
1883 with events_session(token1) as api:
1884 # Add user2 back as organizer
1885 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1888def test_ListEventAttendees_regression(db):
1889 # see issue #1617:
1890 #
1891 # 1. Create an event
1892 # 2. Transfer the event to a community (although this step probably not necessarily, only needed for it to show up in UI/`ListEvents` from `communities.proto`
1893 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
1894 # 4. Change the current user's attendance state to "going" again
1895 #
1896 # **Expected behaviour**
1897 # `ListEventAttendees` should return the current user's ID
1898 #
1899 # **Actual/current behaviour**
1900 # `ListEventAttendees` returns another user's ID. This ID seems to be determined from the row's auto increment ID in `event_occurrence_attendees` in the database
1902 user1, token1 = generate_user()
1903 user2, token2 = generate_user()
1904 user3, token3 = generate_user()
1905 user4, token4 = generate_user()
1906 user5, token5 = generate_user()
1908 with session_scope() as session:
1909 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1911 start_time = now() + timedelta(hours=2)
1912 end_time = start_time + timedelta(hours=3)
1914 with events_session(token1) as api:
1915 res = api.CreateEvent(
1916 events_pb2.CreateEventReq(
1917 title="Dummy Title",
1918 content="Dummy content.",
1919 location=events_pb2.EventLocation(
1920 address="Near Null Island",
1921 lat=0.1,
1922 lng=0.2,
1923 ),
1924 parent_community_id=c_id,
1925 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1926 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1927 )
1928 )
1930 res = api.TransferEvent(
1931 events_pb2.TransferEventReq(
1932 event_id=res.event_id,
1933 new_owner_community_id=c_id,
1934 )
1935 )
1937 event_id = res.event_id
1939 api.SetEventAttendance(
1940 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1941 )
1942 api.SetEventAttendance(
1943 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1944 )
1946 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
1947 assert len(res.attendee_user_ids) == 1
1948 assert res.attendee_user_ids[0] == user1.id
1951def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
1952 user1, token1 = generate_user()
1953 user2, token2 = generate_user()
1954 user3, token3 = generate_user()
1955 user4, token4 = generate_user()
1957 with session_scope() as session:
1958 c = create_community(session, 0, 2, "Community", [user3], [], None)
1959 h = create_group(session, "Group", [user4], [], c)
1960 c_id = c.id
1961 h_id = h.id
1962 user4_id = user4.id
1964 with events_session(token1) as api:
1965 event = api.CreateEvent(
1966 events_pb2.CreateEventReq(
1967 title="Dummy Title",
1968 content="Dummy content.",
1969 location=events_pb2.EventLocation(
1970 address="Near Null Island",
1971 lat=0.1,
1972 lng=0.2,
1973 ),
1974 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1975 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1976 )
1977 )
1979 moderator.approve_event_occurrence(event.event_id)
1981 with threads_session(token2) as api:
1982 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
1984 moderator.approve_thread_post(reply_id)
1986 with events_session(token3) as api:
1987 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
1988 assert res.thread.num_responses == 1
1990 with threads_session(token3) as api:
1991 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
1992 assert len(ret.replies) == 1
1993 assert not ret.next_page_token
1994 assert ret.replies[0].thread_id == reply_id
1995 assert ret.replies[0].content == "hi"
1996 assert ret.replies[0].author_user_id == user2.id
1997 assert ret.replies[0].num_replies == 0
1999 nested_reply_id = api.PostReply(
2000 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2001 ).thread_id
2003 moderator.approve_thread_post(nested_reply_id)
2005 process_jobs()
2007 push = push_collector.pop_for_user(user1.id, last=True)
2008 assert push.topic_action == NotificationTopicAction.event__comment.display
2009 assert push.content.title == f"{user2.name} • Dummy Title"
2010 assert push.content.ios_title == user2.name
2011 assert push.content.ios_subtitle == "Commented on Dummy Title"
2012 assert push.content.body == "hi"
2014 push = push_collector.pop_for_user(user2.id, last=True)
2015 assert push.content.title == f"{user3.name} • Dummy Title"
2017 assert push_collector.count_for_user(user4_id) == 0
2020def test_can_overlap_other_events_schedule_regression(db):
2021 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2022 user, token = generate_user()
2024 with session_scope() as session:
2025 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2027 start = now()
2029 with events_session(token) as api:
2030 # create another event, should be able to overlap with this one
2031 api.CreateEvent(
2032 events_pb2.CreateEventReq(
2033 title="Dummy Title",
2034 content="Dummy content.",
2035 parent_community_id=c_id,
2036 location=events_pb2.EventLocation(
2037 address="Near Null Island",
2038 lat=0.1,
2039 lng=0.2,
2040 ),
2041 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2042 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)),
2043 )
2044 )
2046 # this event
2047 res = api.CreateEvent(
2048 events_pb2.CreateEventReq(
2049 title="Dummy Title",
2050 content="Dummy content.",
2051 parent_community_id=c_id,
2052 location=events_pb2.EventLocation(
2053 address="Near Null Island",
2054 lat=0.1,
2055 lng=0.2,
2056 ),
2057 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2058 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
2059 )
2060 )
2062 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2063 api.ScheduleEvent(
2064 events_pb2.ScheduleEventReq(
2065 event_id=res.event_id,
2066 content="New event occurrence",
2067 location=events_pb2.EventLocation(
2068 address="A bit further but still near Null Island",
2069 lat=0.3,
2070 lng=0.2,
2071 ),
2072 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2073 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2074 )
2075 )
2078def test_can_overlap_other_events_update_regression(db):
2079 user, token = generate_user()
2081 with session_scope() as session:
2082 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2084 start = now()
2086 with events_session(token) as api:
2087 # create another event, should be able to overlap with this one
2088 api.CreateEvent(
2089 events_pb2.CreateEventReq(
2090 title="Dummy Title",
2091 content="Dummy content.",
2092 parent_community_id=c_id,
2093 location=events_pb2.EventLocation(
2094 address="Near Null Island",
2095 lat=0.1,
2096 lng=0.2,
2097 ),
2098 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2099 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2100 )
2101 )
2103 res = api.CreateEvent(
2104 events_pb2.CreateEventReq(
2105 title="Dummy Title",
2106 content="Dummy content.",
2107 parent_community_id=c_id,
2108 location=events_pb2.EventLocation(
2109 address="Near Null Island",
2110 lat=0.1,
2111 lng=0.2,
2112 ),
2113 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)),
2114 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)),
2115 )
2116 )
2118 event_id = api.ScheduleEvent(
2119 events_pb2.ScheduleEventReq(
2120 event_id=res.event_id,
2121 content="New event occurrence",
2122 location=events_pb2.EventLocation(
2123 address="A bit further but still near Null Island",
2124 lat=0.3,
2125 lng=0.2,
2126 ),
2127 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2128 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2129 )
2130 ).event_id
2132 # can overlap with this current existing occurrence
2133 api.UpdateEvent(
2134 events_pb2.UpdateEventReq(
2135 event_id=event_id,
2136 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2137 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2138 ),
2139 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2140 value=datetime_to_iso8601_local(start + timedelta(hours=6))
2141 ),
2142 )
2143 )
2145 api.UpdateEvent(
2146 events_pb2.UpdateEventReq(
2147 event_id=event_id,
2148 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2149 value=datetime_to_iso8601_local(start + timedelta(hours=2))
2150 ),
2151 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2152 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2153 ),
2154 )
2155 )
2158def test_list_past_events_regression(db):
2159 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2160 user, token = generate_user()
2162 with session_scope() as session:
2163 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2165 start = now()
2167 with events_session(token) as api:
2168 api.CreateEvent(
2169 events_pb2.CreateEventReq(
2170 title="Dummy Title",
2171 content="Dummy content.",
2172 parent_community_id=c_id,
2173 location=events_pb2.EventLocation(
2174 address="Near Null Island",
2175 lat=0.1,
2176 lng=0.2,
2177 ),
2178 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2179 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2180 )
2181 )
2183 with session_scope() as session:
2184 session.execute(
2185 update(EventOccurrence).values(
2186 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2187 )
2188 )
2190 with events_session(token) as api:
2191 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2192 assert len(res.events) == 1
2195def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2196 user1, token1 = generate_user(complete_profile=True)
2197 user2, token2 = generate_user()
2198 user3, token3 = generate_user()
2199 user4, token4 = generate_user()
2200 user5, token5 = generate_user(is_superuser=True)
2202 with session_scope() as session:
2203 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2204 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2205 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2206 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2208 enforce_community_memberships()
2210 with events_session(token1) as api:
2211 res = api.CreateEvent(
2212 events_pb2.CreateEventReq(
2213 title="Dummy Title",
2214 content="Dummy content.",
2215 parent_community_id=c_id,
2216 location=events_pb2.EventLocation(
2217 address="Near Null Island",
2218 lat=0.1,
2219 lng=0.2,
2220 ),
2221 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2222 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2223 )
2224 )
2225 user_url = f"http://localhost:3000/user/{user1.username}"
2226 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2228 event_id = res.event_id
2230 moderator.approve_event_occurrence(event_id)
2232 with events_session(token1) as api:
2233 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2235 email = email_collector.pop_for_mods(last=True)
2237 assert user_url in email.plain
2238 assert event_url in email.plain
2240 # can't send another req
2241 with pytest.raises(grpc.RpcError) as err:
2242 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2243 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2244 assert err.value.details() == "You have already requested a community invite for this event."
2246 # another user can send one though
2247 with events_session(token3) as api:
2248 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2250 # but not a non-admin
2251 with events_session(token2) as api:
2252 with pytest.raises(grpc.RpcError) as err:
2253 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2254 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2255 assert err.value.details() == "You're not allowed to edit that event."
2257 with real_editor_session(token5) as editor:
2258 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2259 assert len(res.requests) == 2
2260 assert res.requests[0].user_id == user1.id
2261 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain)
2262 assert res.requests[0].approx_users_to_notify == 2
2263 assert res.requests[1].user_id == user3.id
2264 assert res.requests[1].approx_users_to_notify == 2
2266 editor.DecideEventCommunityInviteRequest(
2267 editor_pb2.DecideEventCommunityInviteRequestReq(
2268 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2269 approve=False,
2270 )
2271 )
2273 editor.DecideEventCommunityInviteRequest(
2274 editor_pb2.DecideEventCommunityInviteRequestReq(
2275 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2276 approve=True,
2277 )
2278 )
2280 # not after approve
2281 with events_session(token4) as api:
2282 with pytest.raises(grpc.RpcError) as err:
2283 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2284 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2285 assert err.value.details() == "A community invite has already been sent out for this event."
2288def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator):
2289 # Regression: users who already RSVP'd (or organize the event) must not get the
2290 # community invite notification when it is approved.
2291 organizer, organizer_token = generate_user()
2292 attendee, attendee_token = generate_user()
2293 member, _ = generate_user()
2294 superuser, superuser_token = generate_user(is_superuser=True)
2296 with session_scope() as session:
2297 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2298 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2299 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2300 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id
2302 enforce_community_memberships()
2304 with events_session(organizer_token) as api:
2305 event_id = api.CreateEvent(
2306 events_pb2.CreateEventReq(
2307 title="Dummy Title",
2308 content="Dummy content.",
2309 parent_community_id=c_id,
2310 location=events_pb2.EventLocation(
2311 address="Near Null Island",
2312 lat=0.1,
2313 lng=0.2,
2314 ),
2315 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2316 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2317 )
2318 ).event_id
2320 moderator.approve_event_occurrence(event_id)
2322 # the attendee RSVPs before the community invite is approved
2323 with events_session(attendee_token) as api:
2324 api.SetEventAttendance(
2325 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2326 )
2328 with events_session(organizer_token) as api:
2329 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2331 with real_editor_session(superuser_token) as editor:
2332 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2333 editor.DecideEventCommunityInviteRequest(
2334 editor_pb2.DecideEventCommunityInviteRequestReq(
2335 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2336 approve=True,
2337 )
2338 )
2340 process_jobs()
2342 with session_scope() as session:
2344 def invite_notification_count(user_id: int) -> int:
2345 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all()
2346 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved])
2348 # a plain community member gets the invite...
2349 assert invite_notification_count(member.id) == 1
2350 # ...but the attendee and the organizer don't
2351 assert invite_notification_count(attendee.id) == 0
2352 assert invite_notification_count(organizer.id) == 0
2355def test_update_event_should_notify_queues_job():
2356 user, token = generate_user()
2357 start = now()
2359 with session_scope() as session:
2360 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2362 # create an event
2363 with events_session(token) as api:
2364 create_res = api.CreateEvent(
2365 events_pb2.CreateEventReq(
2366 title="Dummy Title",
2367 content="Dummy content.",
2368 parent_community_id=c_id,
2369 location=events_pb2.EventLocation(
2370 address="Near Null Island",
2371 lat=1.0,
2372 lng=2.0,
2373 ),
2374 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2375 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2376 )
2377 )
2379 event_id = create_res.event_id
2381 # measure initial background job queue length
2382 with session_scope() as session:
2383 jobs = session.query(BackgroundJob).all()
2384 job_length_before_update = len(jobs)
2386 # update with should_notify=False, expect no change in background job queue
2387 api.UpdateEvent(
2388 events_pb2.UpdateEventReq(
2389 event_id=event_id,
2390 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2391 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2392 ),
2393 should_notify=False,
2394 )
2395 )
2397 with session_scope() as session:
2398 jobs = session.query(BackgroundJob).all()
2399 assert len(jobs) == job_length_before_update
2401 # update with should_notify=True, expect one new background job added
2402 api.UpdateEvent(
2403 events_pb2.UpdateEventReq(
2404 event_id=event_id,
2405 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2406 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2407 ),
2408 should_notify=True,
2409 )
2410 )
2412 with session_scope() as session:
2413 jobs = session.query(BackgroundJob).all()
2414 assert len(jobs) == job_length_before_update + 1
2417def test_event_photo_key(db):
2418 """Test that events return the photo_key field when a photo is set."""
2419 user, token = generate_user()
2421 start_time = now() + timedelta(hours=2)
2422 end_time = start_time + timedelta(hours=3)
2424 # Create a community and an upload for the event photo
2425 with session_scope() as session:
2426 create_community(session, 0, 2, "Community", [user], [], None)
2427 upload = Upload(
2428 key="test_event_photo_key_123",
2429 filename="test_event_photo_key_123.jpg",
2430 creator_user_id=user.id,
2431 )
2432 session.add(upload)
2434 with events_session(token) as api:
2435 # Create event without photo
2436 res = api.CreateEvent(
2437 events_pb2.CreateEventReq(
2438 title="Event Without Photo",
2439 content="No photo content.",
2440 photo_key=None,
2441 location=events_pb2.EventLocation(
2442 address="Near Null Island",
2443 lat=0.1,
2444 lng=0.2,
2445 ),
2446 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2447 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2448 )
2449 )
2451 assert res.photo_key == ""
2452 assert res.photo_url == ""
2454 # Create event with photo
2455 res_with_photo = api.CreateEvent(
2456 events_pb2.CreateEventReq(
2457 title="Event With Photo",
2458 content="Has photo content.",
2459 photo_key="test_event_photo_key_123",
2460 location=events_pb2.EventLocation(
2461 address="Near Null Island",
2462 lat=0.1,
2463 lng=0.2,
2464 ),
2465 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)),
2466 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)),
2467 )
2468 )
2470 assert res_with_photo.photo_key == "test_event_photo_key_123"
2471 assert "test_event_photo_key_123" in res_with_photo.photo_url
2473 event_id = res_with_photo.event_id
2475 # Verify photo_key is returned when getting the event
2476 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2477 assert get_res.photo_key == "test_event_photo_key_123"
2478 assert "test_event_photo_key_123" in get_res.photo_url
2481def test_event_timezone(db):
2482 user, token = generate_user()
2484 with session_scope() as session:
2485 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2487 # Midnight future day, UTC timezone
2488 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2489 end_time = start_time + timedelta(days=1)
2491 with events_session(token) as api:
2492 create_res: events_pb2.Event = api.CreateEvent(
2493 events_pb2.CreateEventReq(
2494 title="Dummy Title",
2495 content="Dummy content.",
2496 photo_key=None,
2497 parent_community_id=c_id,
2498 # timezone_areas.sql-fake has a region for Europe/Helsinki
2499 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831),
2500 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2501 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2502 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2503 )
2504 )
2506 # Backend should have deduced the helsinki timezone when creating the event,
2507 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2508 assert create_res.timezone == "Europe/Helsinki"
2509 assert to_aware_datetime(create_res.start_time).hour != 0
2510 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2512 # Now update its location such that it gets a new timezone
2513 update_res: events_pb2.Event = api.UpdateEvent(
2514 events_pb2.UpdateEventReq(
2515 event_id=create_res.event_id,
2516 # timezone_areas.sql-fake has a region for America/New_York
2517 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974),
2518 )
2519 )
2521 # The user didn't touch the datetime components on the frontend,
2522 # so they expect the event to be at the same local time (midnight),
2523 # but now in the New York timezone.
2524 assert update_res.timezone == "America/New_York"
2525 assert update_res.start_time != create_res.start_time
2526 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2527 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2529 # Also validate GetEvent
2530 get_res: events_pb2.Event = api.GetEvent(
2531 events_pb2.GetEventReq(
2532 event_id=create_res.event_id,
2533 )
2534 )
2536 assert get_res.timezone == update_res.timezone
2537 assert get_res.start_time == update_res.start_time
2540def test_event_created_with_shadowed_visibility(db):
2541 """Events start in SHADOWED state when created."""
2542 user, token = generate_user()
2544 with session_scope() as session:
2545 create_community(session, 0, 2, "Community", [user], [], None)
2547 start_time = now() + timedelta(hours=2)
2548 end_time = start_time + timedelta(hours=3)
2550 with events_session(token) as api:
2551 res = api.CreateEvent(
2552 events_pb2.CreateEventReq(
2553 title="Test UMS Event",
2554 content="UMS content.",
2555 location=events_pb2.EventLocation(
2556 address="Near Null Island",
2557 lat=0.1,
2558 lng=0.2,
2559 ),
2560 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2561 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2562 )
2563 )
2564 event_id = res.event_id
2566 with session_scope() as session:
2567 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2568 mod_state = session.execute(
2569 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2570 ).scalar_one()
2571 assert mod_state.visibility == ModerationVisibility.shadowed
2574def test_shadowed_event_visible_to_creator_only(db):
2575 """SHADOWED events are visible to the creator but not to other users."""
2576 user1, token1 = generate_user()
2577 user2, token2 = generate_user()
2579 with session_scope() as session:
2580 create_community(session, 0, 2, "Community", [user1], [], None)
2582 start_time = now() + timedelta(hours=2)
2583 end_time = start_time + timedelta(hours=3)
2585 with events_session(token1) as api:
2586 res = api.CreateEvent(
2587 events_pb2.CreateEventReq(
2588 title="Shadowed Event",
2589 content="Content.",
2590 location=events_pb2.EventLocation(
2591 address="Near Null Island",
2592 lat=0.1,
2593 lng=0.2,
2594 ),
2595 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2596 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2597 )
2598 )
2599 event_id = res.event_id
2601 # Creator can see it
2602 with events_session(token1) as api:
2603 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2604 assert res.title == "Shadowed Event"
2606 # Other user cannot
2607 with events_session(token2) as api:
2608 with pytest.raises(grpc.RpcError) as e:
2609 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2610 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2613def test_event_visible_after_approval(db, moderator: Moderator):
2614 """Events become visible to all users after moderation approval."""
2615 user1, token1 = generate_user()
2616 user2, token2 = generate_user()
2618 with session_scope() as session:
2619 create_community(session, 0, 2, "Community", [user1], [], None)
2621 start_time = now() + timedelta(hours=2)
2622 end_time = start_time + timedelta(hours=3)
2624 with events_session(token1) as api:
2625 res = api.CreateEvent(
2626 events_pb2.CreateEventReq(
2627 title="Approved Event",
2628 content="Content.",
2629 location=events_pb2.EventLocation(
2630 address="Near Null Island",
2631 lat=0.1,
2632 lng=0.2,
2633 ),
2634 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2635 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2636 )
2637 )
2638 event_id = res.event_id
2640 # Other user cannot see it yet
2641 with events_session(token2) as api:
2642 with pytest.raises(grpc.RpcError) as e:
2643 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2644 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2646 # Approve the event
2647 moderator.approve_event_occurrence(event_id)
2649 # Now other user can see it
2650 with events_session(token2) as api:
2651 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2652 assert res.title == "Approved Event"
2655def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2656 """SHADOWED events appear in lists for the creator but not for other users."""
2657 user1, token1 = generate_user()
2658 user2, token2 = generate_user()
2660 with session_scope() as session:
2661 create_community(session, 0, 2, "Community", [user1], [], None)
2663 start_time = now() + timedelta(hours=2)
2664 end_time = start_time + timedelta(hours=3)
2666 with events_session(token1) as api:
2667 res = api.CreateEvent(
2668 events_pb2.CreateEventReq(
2669 title="List Test Event",
2670 content="Content.",
2671 location=events_pb2.EventLocation(
2672 address="Near Null Island",
2673 lat=0.1,
2674 lng=0.2,
2675 ),
2676 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2677 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2678 )
2679 )
2680 event_id = res.event_id
2682 # Creator can see their own SHADOWED event in lists
2683 with events_session(token1) as api:
2684 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2685 event_ids = [e.event_id for e in list_res.events]
2686 assert event_id in event_ids
2688 # Other user cannot see the SHADOWED event in lists
2689 with events_session(token2) as api:
2690 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2691 event_ids = [e.event_id for e in list_res.events]
2692 assert event_id not in event_ids
2694 # After approval, other user can see it
2695 moderator.approve_event_occurrence(event_id)
2697 with events_session(token2) as api:
2698 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2699 event_ids = [e.event_id for e in list_res.events]
2700 assert event_id in event_ids
2703def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2704 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2705 user1, token1 = generate_user()
2706 user2, token2 = generate_user()
2708 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2709 with session_scope() as session:
2710 world = create_community(session, 0, 10, "World", [user1], [], None)
2711 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2712 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2713 create_community(session, 0, 2, "Child", [user2], [], region)
2715 start_time = now() + timedelta(hours=2)
2716 end_time = start_time + timedelta(hours=3)
2718 with events_session(token1) as api:
2719 res = api.CreateEvent(
2720 events_pb2.CreateEventReq(
2721 title="Deferred Event",
2722 content="Content.",
2723 location=events_pb2.EventLocation(
2724 address="Near Null Island",
2725 lat=0.1,
2726 lng=0.2,
2727 ),
2728 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2729 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2730 )
2731 )
2732 event_id = res.event_id
2734 # Process all jobs — notification should be deferred (event is SHADOWED)
2735 process_jobs()
2737 with session_scope() as session:
2738 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2739 # Notification was created with moderation_state_id for deferral
2740 assert notif.moderation_state_id is not None
2741 # No delivery exists (deferred because event is SHADOWED)
2742 delivery_count = session.execute(
2743 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2744 ).scalar_one_or_none()
2745 assert delivery_count is None
2747 # Approve the event — handle_notification is re-queued for deferred notifications
2748 moderator.approve_event_occurrence(event_id)
2750 # Verify handle_notification job was queued
2751 with session_scope() as session:
2752 pending_jobs = (
2753 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2754 .scalars()
2755 .all()
2756 )
2757 assert any("handle_notification" in j.job_type for j in pending_jobs)
2760def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2761 """Event update notifications should carry the event's moderation_state_id for deferral."""
2762 user1, token1 = generate_user()
2763 user2, token2 = generate_user()
2765 with session_scope() as session:
2766 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2768 start_time = now() + timedelta(hours=2)
2769 end_time = start_time + timedelta(hours=3)
2771 with events_session(token1) as api:
2772 res = api.CreateEvent(
2773 events_pb2.CreateEventReq(
2774 title="Update Test",
2775 content="Content.",
2776 location=events_pb2.EventLocation(
2777 address="Near Null Island",
2778 lat=0.1,
2779 lng=0.2,
2780 ),
2781 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2782 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2783 )
2784 )
2785 event_id = res.event_id
2787 moderator.approve_event_occurrence(event_id)
2788 process_jobs()
2789 # Clear any create notifications
2790 while push_collector.count_for_user(user2.id): 2790 ↛ 2791line 2790 didn't jump to line 2791 because the condition on line 2790 was never true
2791 push_collector.pop_for_user(user2.id)
2793 # User2 subscribes to the event
2794 with events_session(token2) as api:
2795 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2797 # User1 updates the event with should_notify=True
2798 with events_session(token1) as api:
2799 api.UpdateEvent(
2800 events_pb2.UpdateEventReq(
2801 event_id=event_id,
2802 title=wrappers_pb2.StringValue(value="Updated Title"),
2803 should_notify=True,
2804 )
2805 )
2807 process_jobs()
2809 # Verify that the update notification for user2 has moderation_state_id set
2810 with session_scope() as session:
2811 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2813 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2814 # Find the update notification (most recent one)
2815 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2816 assert len(update_notifs) == 1
2817 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2820def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2821 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2822 user1, token1 = generate_user()
2823 user2, token2 = generate_user()
2825 with session_scope() as session:
2826 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2828 start_time = now() + timedelta(hours=2)
2829 end_time = start_time + timedelta(hours=3)
2831 with events_session(token1) as api:
2832 res = api.CreateEvent(
2833 events_pb2.CreateEventReq(
2834 title="Cancel Test",
2835 content="Content.",
2836 location=events_pb2.EventLocation(
2837 address="Near Null Island",
2838 lat=0.1,
2839 lng=0.2,
2840 ),
2841 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2842 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2843 )
2844 )
2845 event_id = res.event_id
2847 moderator.approve_event_occurrence(event_id)
2848 process_jobs()
2849 while push_collector.count_for_user(user2.id): 2849 ↛ 2850line 2849 didn't jump to line 2850 because the condition on line 2849 was never true
2850 push_collector.pop_for_user(user2.id)
2852 # User2 subscribes
2853 with events_session(token2) as api:
2854 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2856 # User1 cancels the event
2857 with events_session(token1) as api:
2858 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2860 process_jobs()
2862 # Verify that the cancel notification for user2 has moderation_state_id set
2863 with session_scope() as session:
2864 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2866 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2867 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2868 assert len(cancel_notifs) == 1
2869 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2872def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2873 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2874 user1, token1 = generate_user()
2875 user2, token2 = generate_user()
2877 with session_scope() as session:
2878 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2880 # Create event starting 23 hours from now (within 24h reminder window)
2881 start_time = now() + timedelta(hours=23)
2882 end_time = start_time + timedelta(hours=1)
2884 with events_session(token1) as api:
2885 res = api.CreateEvent(
2886 events_pb2.CreateEventReq(
2887 title="Reminder Test",
2888 content="Content.",
2889 location=events_pb2.EventLocation(
2890 address="Near Null Island",
2891 lat=0.1,
2892 lng=0.2,
2893 ),
2894 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2895 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2896 )
2897 )
2898 event_id = res.event_id
2900 moderator.approve_event_occurrence(event_id)
2901 process_jobs()
2902 while push_collector.count_for_user(user2.id): 2902 ↛ 2903line 2902 didn't jump to line 2903 because the condition on line 2902 was never true
2903 push_collector.pop_for_user(user2.id)
2905 # User2 marks attendance
2906 with events_session(token2) as api:
2907 api.SetEventAttendance(
2908 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2909 )
2911 # Run the event reminder handler
2912 send_event_reminders(empty_pb2.Empty())
2913 process_jobs()
2915 # Verify that the reminder notification for user2 has moderation_state_id set
2916 with session_scope() as session:
2917 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2919 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2920 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2921 assert len(reminder_notifs) == 1
2922 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2925def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2926 """Event reminders should not be sent for cancelled events."""
2927 user1, token1 = generate_user()
2928 user2, token2 = generate_user()
2930 with session_scope() as session:
2931 create_community(session, 0, 2, "Community", [user2], [], None)
2933 # Create event starting 23 hours from now (within 24h reminder window)
2934 start_time = now() + timedelta(hours=23)
2935 end_time = start_time + timedelta(hours=1)
2937 with events_session(token1) as api:
2938 res = api.CreateEvent(
2939 events_pb2.CreateEventReq(
2940 title="Cancelled Reminder Test",
2941 content="Content.",
2942 location=events_pb2.EventLocation(
2943 address="Near Null Island",
2944 lat=0.1,
2945 lng=0.2,
2946 ),
2947 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2948 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2949 )
2950 )
2951 event_id = res.event_id
2953 moderator.approve_event_occurrence(event_id)
2954 process_jobs()
2956 # User2 marks attendance
2957 with events_session(token2) as api:
2958 api.SetEventAttendance(
2959 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2960 )
2962 # User1 cancels the event
2963 with events_session(token1) as api:
2964 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2966 process_jobs()
2967 # Drain any cancellation-related notifications so we can cleanly assert on reminders
2968 while push_collector.count_for_user(user2.id):
2969 push_collector.pop_for_user(user2.id)
2971 # Run the event reminder handler
2972 send_event_reminders(empty_pb2.Empty())
2973 process_jobs()
2975 # Verify that no reminder notification was sent for user2
2976 with session_scope() as session:
2977 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2978 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2979 assert len(reminder_notifs) == 0
2982@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
2983def test_event_reminder_not_sent_for_invisible_attendee(
2984 db, push_collector: PushCollector, moderator: Moderator, invisible_field
2985):
2986 user1, token1 = generate_user()
2987 user2, token2 = generate_user()
2989 with session_scope() as session:
2990 create_community(session, 0, 2, "Community", [user2], [], None)
2992 start_time = now() + timedelta(hours=23)
2993 end_time = start_time + timedelta(hours=1)
2995 with events_session(token1) as api:
2996 res = api.CreateEvent(
2997 events_pb2.CreateEventReq(
2998 title="Invisible Attendee Reminder Test",
2999 content="Content.",
3000 location=events_pb2.EventLocation(
3001 address="Near Null Island",
3002 lat=0.1,
3003 lng=0.2,
3004 ),
3005 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3006 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3007 )
3008 )
3009 event_id = res.event_id
3011 moderator.approve_event_occurrence(event_id)
3012 process_jobs()
3014 with events_session(token2) as api:
3015 api.SetEventAttendance(
3016 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3017 )
3019 with session_scope() as session:
3020 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3022 send_event_reminders(empty_pb2.Empty())
3023 process_jobs()
3025 with session_scope() as session:
3026 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3027 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3028 assert len(reminder_notifs) == 0
3031def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3032 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3033 user1, token1 = generate_user()
3034 user2, token2 = generate_user()
3036 with session_scope() as session:
3037 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3039 start = now()
3041 # User1 creates event A with 3 occurrences
3042 event_a_ids = []
3043 with events_session(token1) as api:
3044 res = api.CreateEvent(
3045 events_pb2.CreateEventReq(
3046 title="Event A",
3047 content="Content A.",
3048 parent_community_id=c_id,
3049 location=events_pb2.EventLocation(
3050 address="Near Null Island",
3051 lat=0.1,
3052 lng=0.2,
3053 ),
3054 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
3055 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
3056 )
3057 )
3058 event_a_ids.append(res.event_id)
3059 for i in range(2):
3060 res = api.ScheduleEvent(
3061 events_pb2.ScheduleEventReq(
3062 event_id=event_a_ids[-1],
3063 content=f"A occurrence {i}",
3064 location=events_pb2.EventLocation(
3065 address="Near Null Island",
3066 lat=0.1,
3067 lng=0.2,
3068 ),
3069 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
3070 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
3071 )
3072 )
3073 event_a_ids.append(res.event_id)
3075 # User2 creates event B with 2 occurrences
3076 event_b_ids = []
3077 with events_session(token2) as api:
3078 res = api.CreateEvent(
3079 events_pb2.CreateEventReq(
3080 title="Event B",
3081 content="Content B.",
3082 parent_community_id=c_id,
3083 location=events_pb2.EventLocation(
3084 address="Near Null Island",
3085 lat=0.1,
3086 lng=0.2,
3087 ),
3088 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)),
3089 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)),
3090 )
3091 )
3092 event_b_ids.append(res.event_id)
3093 res = api.ScheduleEvent(
3094 events_pb2.ScheduleEventReq(
3095 event_id=event_b_ids[-1],
3096 content="B occurrence 1",
3097 location=events_pb2.EventLocation(
3098 address="Near Null Island",
3099 lat=0.1,
3100 lng=0.2,
3101 ),
3102 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)),
3103 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)),
3104 )
3105 )
3106 event_b_ids.append(res.event_id)
3108 moderator.approve_event_occurrence(event_a_ids[0])
3109 moderator.approve_event_occurrence(event_b_ids[0])
3111 # List occurrences for event A — should only get event A's 3 occurrences
3112 with events_session(token1) as api:
3113 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3114 returned_ids = [e.event_id for e in res.events]
3115 assert sorted(returned_ids) == sorted(event_a_ids)
3117 # List occurrences for event B — should only get event B's 2 occurrences
3118 with events_session(token2) as api:
3119 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3120 returned_ids = [e.event_id for e in res.events]
3121 assert sorted(returned_ids) == sorted(event_b_ids)
3124def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3125 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3126 user1, token1 = generate_user()
3127 user2, token2 = generate_user()
3129 with session_scope() as session:
3130 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3132 start_time = now() + timedelta(hours=2)
3133 end_time = start_time + timedelta(hours=3)
3135 with events_session(token1) as api:
3136 res = api.CreateEvent(
3137 events_pb2.CreateEventReq(
3138 title="Comment Test",
3139 content="Content.",
3140 parent_community_id=c_id,
3141 location=events_pb2.EventLocation(
3142 address="Near Null Island",
3143 lat=0.1,
3144 lng=0.2,
3145 ),
3146 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3147 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3148 )
3149 )
3150 event_id = res.event_id
3151 thread_id = res.thread.thread_id
3153 moderator.approve_event_occurrence(event_id)
3154 process_jobs()
3155 while push_collector.count_for_user(user1.id): 3155 ↛ 3156line 3155 didn't jump to line 3156 because the condition on line 3155 was never true
3156 push_collector.pop_for_user(user1.id)
3158 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3159 with events_session(token1) as api:
3160 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3162 # User2 posts a top-level comment on the event thread
3163 with threads_session(token2) as api:
3164 comment_thread_id = api.PostReply(
3165 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3166 ).thread_id
3168 process_jobs()
3170 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3171 comment_db_id = comment_thread_id // 10
3172 with session_scope() as session:
3173 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3175 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3176 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3177 assert len(comment_notifs) == 1
3178 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3181def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3182 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3183 user1, token1 = generate_user()
3184 user2, token2 = generate_user()
3185 user3, token3 = generate_user()
3187 with session_scope() as session:
3188 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3190 start_time = now() + timedelta(hours=2)
3191 end_time = start_time + timedelta(hours=3)
3193 with events_session(token1) as api:
3194 res = api.CreateEvent(
3195 events_pb2.CreateEventReq(
3196 title="Reply Test",
3197 content="Content.",
3198 location=events_pb2.EventLocation(
3199 address="Near Null Island",
3200 lat=0.1,
3201 lng=0.2,
3202 ),
3203 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3204 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3205 )
3206 )
3207 event_id = res.event_id
3208 thread_id = res.thread.thread_id
3210 moderator.approve_event_occurrence(event_id)
3211 process_jobs()
3212 while push_collector.count_for_user(user1.id): 3212 ↛ 3213line 3212 didn't jump to line 3213 because the condition on line 3212 was never true
3213 push_collector.pop_for_user(user1.id)
3215 # User2 posts a top-level comment
3216 with threads_session(token2) as api:
3217 comment_thread_id = api.PostReply(
3218 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3219 ).thread_id
3221 process_jobs()
3222 while push_collector.count_for_user(user1.id): 3222 ↛ 3223line 3222 didn't jump to line 3223 because the condition on line 3222 was never true
3223 push_collector.pop_for_user(user1.id)
3225 # User3 replies to user2's comment (depth=2 reply)
3226 with threads_session(token3) as api:
3227 nested_reply_thread_id = api.PostReply(
3228 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3229 ).thread_id
3231 process_jobs()
3233 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3234 nested_reply_db_id = nested_reply_thread_id // 10
3235 with session_scope() as session:
3236 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3238 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3239 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3240 assert len(reply_notifs) == 1
3241 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id