Coverage for app/backend/src/tests/test_events.py: 99%
1514 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 00:30 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 00:30 +0000
1from datetime import timedelta
3import grpc
4import pytest
5from google.protobuf import empty_pb2, wrappers_pb2
6from psycopg.types.range import TimestamptzRange
7from sqlalchemy import select
8from sqlalchemy.sql.expression import update
10from couchers.db import session_scope
11from couchers.jobs.handlers import send_event_reminders
12from couchers.models import (
13 BackgroundJob,
14 BackgroundJobState,
15 Comment,
16 EventOccurrence,
17 ModerationState,
18 ModerationVisibility,
19 Notification,
20 NotificationDelivery,
21 NotificationTopicAction,
22 Reply,
23 Upload,
24 User,
25)
26from couchers.proto import editor_pb2, events_pb2, threads_pb2
27from couchers.tasks import enforce_community_memberships
28from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime
29from tests.fixtures.db import generate_user
30from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs
31from tests.fixtures.sessions import events_session, real_editor_session, threads_session
32from tests.test_communities import create_community, create_group
35@pytest.fixture(autouse=True)
36def _(testconfig):
37 pass
40def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator):
41 # test cases:
42 # can create event
43 # cannot create event with missing details
44 # can create online event
45 # can create in person event
46 # can't create event that starts in the past
47 # can create in different timezones
49 # event creator
50 user1, token1 = generate_user()
51 # community moderator
52 user2, token2 = generate_user()
53 # third party
54 user3, token3 = generate_user()
56 with session_scope() as session:
57 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
59 time_before = now()
60 start_time = now() + timedelta(hours=2)
61 end_time = start_time + timedelta(hours=3)
63 with events_session(token1) as api:
64 # in person event
65 res = api.CreateEvent(
66 events_pb2.CreateEventReq(
67 title="Dummy Title",
68 content="Dummy content.",
69 photo_key=None,
70 offline_information=events_pb2.OfflineEventInformation(
71 address="Near Null Island",
72 lat=0.1,
73 lng=0.2,
74 ),
75 start_time=Timestamp_from_datetime(start_time),
76 end_time=Timestamp_from_datetime(end_time),
77 timezone="UTC",
78 )
79 )
81 assert res.is_next
82 assert res.title == "Dummy Title"
83 assert res.slug == "dummy-title"
84 assert res.content == "Dummy content."
85 assert not res.photo_url
86 assert res.WhichOneof("mode") == "offline_information"
87 assert res.offline_information.lat == 0.1
88 assert res.offline_information.lng == 0.2
89 assert res.offline_information.address == "Near Null Island"
90 assert time_before <= to_aware_datetime(res.created) <= now()
91 assert time_before <= to_aware_datetime(res.last_edited) <= now()
92 assert res.creator_user_id == user1.id
93 assert to_aware_datetime(res.start_time) == start_time
94 assert to_aware_datetime(res.end_time) == end_time
95 # assert res.timezone == "UTC"
96 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
97 assert res.organizer
98 assert res.subscriber
99 assert res.going_count == 1
100 assert res.organizer_count == 1
101 assert res.subscriber_count == 1
102 assert res.owner_user_id == user1.id
103 assert not res.owner_community_id
104 assert not res.owner_group_id
105 assert res.thread.thread_id
106 assert res.can_edit
107 assert not res.can_moderate
109 event_id = res.event_id
111 # Approve the event so other users can see it
112 moderator.approve_event_occurrence(event_id)
114 with events_session(token2) as api:
115 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
117 assert res.is_next
118 assert res.title == "Dummy Title"
119 assert res.slug == "dummy-title"
120 assert res.content == "Dummy content."
121 assert not res.photo_url
122 assert res.WhichOneof("mode") == "offline_information"
123 assert res.offline_information.lat == 0.1
124 assert res.offline_information.lng == 0.2
125 assert res.offline_information.address == "Near Null Island"
126 assert time_before <= to_aware_datetime(res.created) <= now()
127 assert time_before <= to_aware_datetime(res.last_edited) <= now()
128 assert res.creator_user_id == user1.id
129 assert to_aware_datetime(res.start_time) == start_time
130 assert to_aware_datetime(res.end_time) == end_time
131 # assert res.timezone == "UTC"
132 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
133 assert not res.organizer
134 assert not res.subscriber
135 assert res.going_count == 1
136 assert res.organizer_count == 1
137 assert res.subscriber_count == 1
138 assert res.owner_user_id == user1.id
139 assert not res.owner_community_id
140 assert not res.owner_group_id
141 assert res.thread.thread_id
142 assert res.can_edit
143 assert res.can_moderate
145 with events_session(token3) as api:
146 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
148 assert res.is_next
149 assert res.title == "Dummy Title"
150 assert res.slug == "dummy-title"
151 assert res.content == "Dummy content."
152 assert not res.photo_url
153 assert res.WhichOneof("mode") == "offline_information"
154 assert res.offline_information.lat == 0.1
155 assert res.offline_information.lng == 0.2
156 assert res.offline_information.address == "Near Null Island"
157 assert time_before <= to_aware_datetime(res.created) <= now()
158 assert time_before <= to_aware_datetime(res.last_edited) <= now()
159 assert res.creator_user_id == user1.id
160 assert to_aware_datetime(res.start_time) == start_time
161 assert to_aware_datetime(res.end_time) == end_time
162 # assert res.timezone == "UTC"
163 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
164 assert not res.organizer
165 assert not res.subscriber
166 assert res.going_count == 1
167 assert res.organizer_count == 1
168 assert res.subscriber_count == 1
169 assert res.owner_user_id == user1.id
170 assert not res.owner_community_id
171 assert not res.owner_group_id
172 assert res.thread.thread_id
173 assert not res.can_edit
174 assert not res.can_moderate
176 with events_session(token1) as api:
177 # online only event
178 res = api.CreateEvent(
179 events_pb2.CreateEventReq(
180 title="Dummy Title",
181 content="Dummy content.",
182 photo_key=None,
183 online_information=events_pb2.OnlineEventInformation(
184 link="https://couchers.org/meet/",
185 ),
186 parent_community_id=c_id,
187 start_time=Timestamp_from_datetime(start_time),
188 end_time=Timestamp_from_datetime(end_time),
189 timezone="UTC",
190 )
191 )
193 assert res.is_next
194 assert res.title == "Dummy Title"
195 assert res.slug == "dummy-title"
196 assert res.content == "Dummy content."
197 assert not res.photo_url
198 assert res.WhichOneof("mode") == "online_information"
199 assert res.online_information.link == "https://couchers.org/meet/"
200 assert time_before <= to_aware_datetime(res.created) <= now()
201 assert time_before <= to_aware_datetime(res.last_edited) <= now()
202 assert res.creator_user_id == user1.id
203 assert to_aware_datetime(res.start_time) == start_time
204 assert to_aware_datetime(res.end_time) == end_time
205 # assert res.timezone == "UTC"
206 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
207 assert res.organizer
208 assert res.subscriber
209 assert res.going_count == 1
210 assert res.organizer_count == 1
211 assert res.subscriber_count == 1
212 assert res.owner_user_id == user1.id
213 assert not res.owner_community_id
214 assert not res.owner_group_id
215 assert res.thread.thread_id
216 assert res.can_edit
217 assert not res.can_moderate
219 event_id = res.event_id
221 # Approve the online event so other users can see it
222 moderator.approve_event_occurrence(event_id)
224 with events_session(token2) as api:
225 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
227 assert res.is_next
228 assert res.title == "Dummy Title"
229 assert res.slug == "dummy-title"
230 assert res.content == "Dummy content."
231 assert not res.photo_url
232 assert res.WhichOneof("mode") == "online_information"
233 assert res.online_information.link == "https://couchers.org/meet/"
234 assert time_before <= to_aware_datetime(res.created) <= now()
235 assert time_before <= to_aware_datetime(res.last_edited) <= now()
236 assert res.creator_user_id == user1.id
237 assert to_aware_datetime(res.start_time) == start_time
238 assert to_aware_datetime(res.end_time) == end_time
239 # assert res.timezone == "UTC"
240 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
241 assert not res.organizer
242 assert not res.subscriber
243 assert res.going_count == 1
244 assert res.organizer_count == 1
245 assert res.subscriber_count == 1
246 assert res.owner_user_id == user1.id
247 assert not res.owner_community_id
248 assert not res.owner_group_id
249 assert res.thread.thread_id
250 assert res.can_edit
251 assert res.can_moderate
253 with events_session(token3) as api:
254 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
256 assert res.is_next
257 assert res.title == "Dummy Title"
258 assert res.slug == "dummy-title"
259 assert res.content == "Dummy content."
260 assert not res.photo_url
261 assert res.WhichOneof("mode") == "online_information"
262 assert res.online_information.link == "https://couchers.org/meet/"
263 assert time_before <= to_aware_datetime(res.created) <= now()
264 assert time_before <= to_aware_datetime(res.last_edited) <= now()
265 assert res.creator_user_id == user1.id
266 assert to_aware_datetime(res.start_time) == start_time
267 assert to_aware_datetime(res.end_time) == end_time
268 # assert res.timezone == "UTC"
269 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
270 assert not res.organizer
271 assert not res.subscriber
272 assert res.going_count == 1
273 assert res.organizer_count == 1
274 assert res.subscriber_count == 1
275 assert res.owner_user_id == user1.id
276 assert not res.owner_community_id
277 assert not res.owner_group_id
278 assert res.thread.thread_id
279 assert not res.can_edit
280 assert not res.can_moderate
282 with events_session(token1) as api:
283 with pytest.raises(grpc.RpcError) as e:
284 api.CreateEvent(
285 events_pb2.CreateEventReq(
286 title="Dummy Title",
287 content="Dummy content.",
288 photo_key=None,
289 online_information=events_pb2.OnlineEventInformation(
290 link="https://couchers.org/meet/",
291 ),
292 start_time=Timestamp_from_datetime(start_time),
293 end_time=Timestamp_from_datetime(end_time),
294 timezone="UTC",
295 )
296 )
297 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
298 assert e.value.details() == "The online event is missing a parent community."
300 with pytest.raises(grpc.RpcError) as e:
301 api.CreateEvent(
302 events_pb2.CreateEventReq(
303 # title="Dummy Title",
304 content="Dummy content.",
305 photo_key=None,
306 offline_information=events_pb2.OfflineEventInformation(
307 address="Near Null Island",
308 lat=0.1,
309 lng=0.1,
310 ),
311 start_time=Timestamp_from_datetime(start_time),
312 end_time=Timestamp_from_datetime(end_time),
313 timezone="UTC",
314 )
315 )
316 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
317 assert e.value.details() == "Missing event title."
319 with pytest.raises(grpc.RpcError) as e:
320 api.CreateEvent(
321 events_pb2.CreateEventReq(
322 title="Dummy Title",
323 # content="Dummy content.",
324 photo_key=None,
325 offline_information=events_pb2.OfflineEventInformation(
326 address="Near Null Island",
327 lat=0.1,
328 lng=0.1,
329 ),
330 start_time=Timestamp_from_datetime(start_time),
331 end_time=Timestamp_from_datetime(end_time),
332 timezone="UTC",
333 )
334 )
335 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
336 assert e.value.details() == "Missing event content."
338 with pytest.raises(grpc.RpcError) as e:
339 api.CreateEvent(
340 events_pb2.CreateEventReq(
341 title="Dummy Title",
342 content="Dummy content.",
343 photo_key="nonexistent",
344 offline_information=events_pb2.OfflineEventInformation(
345 address="Near Null Island",
346 lat=0.1,
347 lng=0.1,
348 ),
349 start_time=Timestamp_from_datetime(start_time),
350 end_time=Timestamp_from_datetime(end_time),
351 timezone="UTC",
352 )
353 )
354 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
355 assert e.value.details() == "Photo not found."
357 with pytest.raises(grpc.RpcError) as e:
358 api.CreateEvent(
359 events_pb2.CreateEventReq(
360 title="Dummy Title",
361 content="Dummy content.",
362 photo_key=None,
363 offline_information=events_pb2.OfflineEventInformation(
364 address="Near Null Island",
365 ),
366 start_time=Timestamp_from_datetime(start_time),
367 end_time=Timestamp_from_datetime(end_time),
368 timezone="UTC",
369 )
370 )
371 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
372 assert e.value.details() == "Missing event address or location."
374 with pytest.raises(grpc.RpcError) as e:
375 api.CreateEvent(
376 events_pb2.CreateEventReq(
377 title="Dummy Title",
378 content="Dummy content.",
379 photo_key=None,
380 offline_information=events_pb2.OfflineEventInformation(
381 lat=0.1,
382 lng=0.1,
383 ),
384 start_time=Timestamp_from_datetime(start_time),
385 end_time=Timestamp_from_datetime(end_time),
386 timezone="UTC",
387 )
388 )
389 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
390 assert e.value.details() == "Missing event address or location."
392 with pytest.raises(grpc.RpcError) as e:
393 api.CreateEvent(
394 events_pb2.CreateEventReq(
395 title="Dummy Title",
396 content="Dummy content.",
397 photo_key=None,
398 online_information=events_pb2.OnlineEventInformation(),
399 start_time=Timestamp_from_datetime(start_time),
400 end_time=Timestamp_from_datetime(end_time),
401 timezone="UTC",
402 )
403 )
404 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
405 assert e.value.details() == "An online-only event requires a link."
407 with pytest.raises(grpc.RpcError) as e:
408 api.CreateEvent(
409 events_pb2.CreateEventReq(
410 title="Dummy Title",
411 content="Dummy content.",
412 parent_community_id=c_id,
413 online_information=events_pb2.OnlineEventInformation(
414 link="https://couchers.org/meet/",
415 ),
416 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)),
417 end_time=Timestamp_from_datetime(end_time),
418 timezone="UTC",
419 )
420 )
421 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
422 assert e.value.details() == "The event must be in the future."
424 with pytest.raises(grpc.RpcError) as e:
425 api.CreateEvent(
426 events_pb2.CreateEventReq(
427 title="Dummy Title",
428 content="Dummy content.",
429 parent_community_id=c_id,
430 online_information=events_pb2.OnlineEventInformation(
431 link="https://couchers.org/meet/",
432 ),
433 start_time=Timestamp_from_datetime(end_time),
434 end_time=Timestamp_from_datetime(start_time),
435 timezone="UTC",
436 )
437 )
438 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
439 assert e.value.details() == "The event must end after it starts."
441 with pytest.raises(grpc.RpcError) as e:
442 api.CreateEvent(
443 events_pb2.CreateEventReq(
444 title="Dummy Title",
445 content="Dummy content.",
446 parent_community_id=c_id,
447 online_information=events_pb2.OnlineEventInformation(
448 link="https://couchers.org/meet/",
449 ),
450 start_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)),
451 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)),
452 timezone="UTC",
453 )
454 )
455 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
456 assert e.value.details() == "The event needs to start within the next year."
458 with pytest.raises(grpc.RpcError) as e:
459 api.CreateEvent(
460 events_pb2.CreateEventReq(
461 title="Dummy Title",
462 content="Dummy content.",
463 parent_community_id=c_id,
464 online_information=events_pb2.OnlineEventInformation(
465 link="https://couchers.org/meet/",
466 ),
467 start_time=Timestamp_from_datetime(start_time),
468 end_time=Timestamp_from_datetime(now() + timedelta(days=100)),
469 timezone="UTC",
470 )
471 )
472 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
473 assert e.value.details() == "Events cannot last longer than 7 days."
476def test_CreateEvent_incomplete_profile(db):
477 user1, token1 = generate_user(complete_profile=False)
478 user2, token2 = generate_user()
480 with session_scope() as session:
481 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
483 start_time = now() + timedelta(hours=2)
484 end_time = start_time + timedelta(hours=3)
486 with events_session(token1) as api:
487 with pytest.raises(grpc.RpcError) as e:
488 api.CreateEvent(
489 events_pb2.CreateEventReq(
490 title="Dummy Title",
491 content="Dummy content.",
492 photo_key=None,
493 offline_information=events_pb2.OfflineEventInformation(
494 address="Near Null Island",
495 lat=0.1,
496 lng=0.2,
497 ),
498 start_time=Timestamp_from_datetime(start_time),
499 end_time=Timestamp_from_datetime(end_time),
500 timezone="UTC",
501 )
502 )
503 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
504 assert e.value.details() == "You have to complete your profile before you can create an event."
507def test_ScheduleEvent(db):
508 # test cases:
509 # can schedule a new event occurrence
511 user, token = generate_user()
513 with session_scope() as session:
514 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
516 time_before = now()
517 start_time = now() + timedelta(hours=2)
518 end_time = start_time + timedelta(hours=3)
520 with events_session(token) as api:
521 res = api.CreateEvent(
522 events_pb2.CreateEventReq(
523 title="Dummy Title",
524 content="Dummy content.",
525 parent_community_id=c_id,
526 online_information=events_pb2.OnlineEventInformation(
527 link="https://couchers.org/meet/",
528 ),
529 start_time=Timestamp_from_datetime(start_time),
530 end_time=Timestamp_from_datetime(end_time),
531 timezone="UTC",
532 )
533 )
535 new_start_time = now() + timedelta(hours=6)
536 new_end_time = new_start_time + timedelta(hours=2)
538 res = api.ScheduleEvent(
539 events_pb2.ScheduleEventReq(
540 event_id=res.event_id,
541 content="New event occurrence",
542 offline_information=events_pb2.OfflineEventInformation(
543 address="A bit further but still near Null Island",
544 lat=0.3,
545 lng=0.2,
546 ),
547 start_time=Timestamp_from_datetime(new_start_time),
548 end_time=Timestamp_from_datetime(new_end_time),
549 timezone="UTC",
550 )
551 )
553 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
555 assert not res.is_next
556 assert res.title == "Dummy Title"
557 assert res.slug == "dummy-title"
558 assert res.content == "New event occurrence"
559 assert not res.photo_url
560 assert res.WhichOneof("mode") == "offline_information"
561 assert res.offline_information.lat == 0.3
562 assert res.offline_information.lng == 0.2
563 assert res.offline_information.address == "A bit further but still near Null Island"
564 assert time_before <= to_aware_datetime(res.created) <= now()
565 assert time_before <= to_aware_datetime(res.last_edited) <= now()
566 assert res.creator_user_id == user.id
567 assert to_aware_datetime(res.start_time) == new_start_time
568 assert to_aware_datetime(res.end_time) == new_end_time
569 # assert res.timezone == "UTC"
570 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
571 assert res.organizer
572 assert res.subscriber
573 assert res.going_count == 1
574 assert res.organizer_count == 1
575 assert res.subscriber_count == 1
576 assert res.owner_user_id == user.id
577 assert not res.owner_community_id
578 assert not res.owner_group_id
579 assert res.thread.thread_id
580 assert res.can_edit
581 assert res.can_moderate
584def test_cannot_overlap_occurrences_schedule(db):
585 user, token = generate_user()
587 with session_scope() as session:
588 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
590 start = now()
592 with events_session(token) 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 online_information=events_pb2.OnlineEventInformation(
599 link="https://couchers.org/meet/",
600 ),
601 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
602 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
603 timezone="UTC",
604 )
605 )
607 with pytest.raises(grpc.RpcError) as e:
608 api.ScheduleEvent(
609 events_pb2.ScheduleEventReq(
610 event_id=res.event_id,
611 content="New event occurrence",
612 offline_information=events_pb2.OfflineEventInformation(
613 address="A bit further but still near Null Island",
614 lat=0.3,
615 lng=0.2,
616 ),
617 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
618 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
619 timezone="UTC",
620 )
621 )
622 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
623 assert e.value.details() == "An event cannot have overlapping occurrences."
626def test_cannot_overlap_occurrences_update(db):
627 user, token = generate_user()
629 with session_scope() as session:
630 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
632 start = now()
634 with events_session(token) as api:
635 res = api.CreateEvent(
636 events_pb2.CreateEventReq(
637 title="Dummy Title",
638 content="Dummy content.",
639 parent_community_id=c_id,
640 online_information=events_pb2.OnlineEventInformation(
641 link="https://couchers.org/meet/",
642 ),
643 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
644 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
645 timezone="UTC",
646 )
647 )
649 event_id = api.ScheduleEvent(
650 events_pb2.ScheduleEventReq(
651 event_id=res.event_id,
652 content="New event occurrence",
653 offline_information=events_pb2.OfflineEventInformation(
654 address="A bit further but still near Null Island",
655 lat=0.3,
656 lng=0.2,
657 ),
658 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
659 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
660 timezone="UTC",
661 )
662 ).event_id
664 # can overlap with this current existing occurrence
665 api.UpdateEvent(
666 events_pb2.UpdateEventReq(
667 event_id=event_id,
668 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
669 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
670 )
671 )
673 with pytest.raises(grpc.RpcError) as e:
674 api.UpdateEvent(
675 events_pb2.UpdateEventReq(
676 event_id=event_id,
677 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
678 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
679 )
680 )
681 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
682 assert e.value.details() == "An event cannot have overlapping occurrences."
685def test_UpdateEvent_single(db, moderator: Moderator):
686 # test cases:
687 # owner can update
688 # community owner can update
689 # can't mess up online/in person dichotomy
690 # notifies attendees
692 # event creator
693 user1, token1 = generate_user()
694 # community moderator
695 user2, token2 = generate_user()
696 # third parties
697 user3, token3 = generate_user()
698 user4, token4 = generate_user()
699 user5, token5 = generate_user()
700 user6, token6 = generate_user()
702 with session_scope() as session:
703 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
705 time_before = now()
706 start_time = now() + timedelta(hours=2)
707 end_time = start_time + timedelta(hours=3)
709 with events_session(token1) as api:
710 res = api.CreateEvent(
711 events_pb2.CreateEventReq(
712 title="Dummy Title",
713 content="Dummy content.",
714 offline_information=events_pb2.OfflineEventInformation(
715 address="Near Null Island",
716 lat=0.1,
717 lng=0.2,
718 ),
719 start_time=Timestamp_from_datetime(start_time),
720 end_time=Timestamp_from_datetime(end_time),
721 timezone="UTC",
722 )
723 )
725 event_id = res.event_id
727 moderator.approve_event_occurrence(event_id)
729 with events_session(token4) as api:
730 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
732 with events_session(token5) as api:
733 api.SetEventAttendance(
734 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
735 )
737 with events_session(token6) as api:
738 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
740 time_before_update = now()
742 with events_session(token1) as api:
743 res = api.UpdateEvent(
744 events_pb2.UpdateEventReq(
745 event_id=event_id,
746 )
747 )
749 with events_session(token1) as api:
750 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
752 assert res.is_next
753 assert res.title == "Dummy Title"
754 assert res.slug == "dummy-title"
755 assert res.content == "Dummy content."
756 assert not res.photo_url
757 assert res.WhichOneof("mode") == "offline_information"
758 assert res.offline_information.lat == 0.1
759 assert res.offline_information.lng == 0.2
760 assert res.offline_information.address == "Near Null Island"
761 assert time_before <= to_aware_datetime(res.created) <= time_before_update
762 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
763 assert res.creator_user_id == user1.id
764 assert to_aware_datetime(res.start_time) == start_time
765 assert to_aware_datetime(res.end_time) == end_time
766 # assert res.timezone == "UTC"
767 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
768 assert res.organizer
769 assert res.subscriber
770 assert res.going_count == 2
771 assert res.organizer_count == 1
772 assert res.subscriber_count == 3
773 assert res.owner_user_id == user1.id
774 assert not res.owner_community_id
775 assert not res.owner_group_id
776 assert res.thread.thread_id
777 assert res.can_edit
778 assert not res.can_moderate
780 with events_session(token2) as api:
781 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
783 assert res.is_next
784 assert res.title == "Dummy Title"
785 assert res.slug == "dummy-title"
786 assert res.content == "Dummy content."
787 assert not res.photo_url
788 assert res.WhichOneof("mode") == "offline_information"
789 assert res.offline_information.lat == 0.1
790 assert res.offline_information.lng == 0.2
791 assert res.offline_information.address == "Near Null Island"
792 assert time_before <= to_aware_datetime(res.created) <= time_before_update
793 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
794 assert res.creator_user_id == user1.id
795 assert to_aware_datetime(res.start_time) == start_time
796 assert to_aware_datetime(res.end_time) == end_time
797 # assert res.timezone == "UTC"
798 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
799 assert not res.organizer
800 assert not res.subscriber
801 assert res.going_count == 2
802 assert res.organizer_count == 1
803 assert res.subscriber_count == 3
804 assert res.owner_user_id == user1.id
805 assert not res.owner_community_id
806 assert not res.owner_group_id
807 assert res.thread.thread_id
808 assert res.can_edit
809 assert res.can_moderate
811 with events_session(token3) as api:
812 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
814 assert res.is_next
815 assert res.title == "Dummy Title"
816 assert res.slug == "dummy-title"
817 assert res.content == "Dummy content."
818 assert not res.photo_url
819 assert res.WhichOneof("mode") == "offline_information"
820 assert res.offline_information.lat == 0.1
821 assert res.offline_information.lng == 0.2
822 assert res.offline_information.address == "Near Null Island"
823 assert time_before <= to_aware_datetime(res.created) <= time_before_update
824 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
825 assert res.creator_user_id == user1.id
826 assert to_aware_datetime(res.start_time) == start_time
827 assert to_aware_datetime(res.end_time) == end_time
828 # assert res.timezone == "UTC"
829 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
830 assert not res.organizer
831 assert not res.subscriber
832 assert res.going_count == 2
833 assert res.organizer_count == 1
834 assert res.subscriber_count == 3
835 assert res.owner_user_id == user1.id
836 assert not res.owner_community_id
837 assert not res.owner_group_id
838 assert res.thread.thread_id
839 assert not res.can_edit
840 assert not res.can_moderate
842 with events_session(token1) as api:
843 res = api.UpdateEvent(
844 events_pb2.UpdateEventReq(
845 event_id=event_id,
846 title=wrappers_pb2.StringValue(value="Dummy Title"),
847 content=wrappers_pb2.StringValue(value="Dummy content."),
848 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
849 start_time=Timestamp_from_datetime(start_time),
850 end_time=Timestamp_from_datetime(end_time),
851 timezone=wrappers_pb2.StringValue(value="UTC"),
852 )
853 )
855 assert res.is_next
856 assert res.title == "Dummy Title"
857 assert res.slug == "dummy-title"
858 assert res.content == "Dummy content."
859 assert not res.photo_url
860 assert res.WhichOneof("mode") == "online_information"
861 assert res.online_information.link == "https://couchers.org/meet/"
862 assert time_before <= to_aware_datetime(res.created) <= time_before_update
863 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
864 assert res.creator_user_id == user1.id
865 assert to_aware_datetime(res.start_time) == start_time
866 assert to_aware_datetime(res.end_time) == end_time
867 # assert res.timezone == "UTC"
868 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
869 assert res.organizer
870 assert res.subscriber
871 assert res.going_count == 2
872 assert res.organizer_count == 1
873 assert res.subscriber_count == 3
874 assert res.owner_user_id == user1.id
875 assert not res.owner_community_id
876 assert not res.owner_group_id
877 assert res.thread.thread_id
878 assert res.can_edit
879 assert not res.can_moderate
881 event_id = res.event_id
883 with events_session(token2) as api:
884 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
886 assert res.is_next
887 assert res.title == "Dummy Title"
888 assert res.slug == "dummy-title"
889 assert res.content == "Dummy content."
890 assert not res.photo_url
891 assert res.WhichOneof("mode") == "online_information"
892 assert res.online_information.link == "https://couchers.org/meet/"
893 assert time_before <= to_aware_datetime(res.created) <= time_before_update
894 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
895 assert res.creator_user_id == user1.id
896 assert to_aware_datetime(res.start_time) == start_time
897 assert to_aware_datetime(res.end_time) == end_time
898 # assert res.timezone == "UTC"
899 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
900 assert not res.organizer
901 assert not res.subscriber
902 assert res.going_count == 2
903 assert res.organizer_count == 1
904 assert res.subscriber_count == 3
905 assert res.owner_user_id == user1.id
906 assert not res.owner_community_id
907 assert not res.owner_group_id
908 assert res.thread.thread_id
909 assert res.can_edit
910 assert res.can_moderate
912 with events_session(token3) as api:
913 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
915 assert res.is_next
916 assert res.title == "Dummy Title"
917 assert res.slug == "dummy-title"
918 assert res.content == "Dummy content."
919 assert not res.photo_url
920 assert res.WhichOneof("mode") == "online_information"
921 assert res.online_information.link == "https://couchers.org/meet/"
922 assert time_before <= to_aware_datetime(res.created) <= time_before_update
923 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
924 assert res.creator_user_id == user1.id
925 assert to_aware_datetime(res.start_time) == start_time
926 assert to_aware_datetime(res.end_time) == end_time
927 # assert res.timezone == "UTC"
928 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
929 assert not res.organizer
930 assert not res.subscriber
931 assert res.going_count == 2
932 assert res.organizer_count == 1
933 assert res.subscriber_count == 3
934 assert res.owner_user_id == user1.id
935 assert not res.owner_community_id
936 assert not res.owner_group_id
937 assert res.thread.thread_id
938 assert not res.can_edit
939 assert not res.can_moderate
941 with events_session(token1) as api:
942 res = api.UpdateEvent(
943 events_pb2.UpdateEventReq(
944 event_id=event_id,
945 offline_information=events_pb2.OfflineEventInformation(
946 address="Near Null Island",
947 lat=0.1,
948 lng=0.2,
949 ),
950 )
951 )
953 with events_session(token3) as api:
954 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
956 assert res.WhichOneof("mode") == "offline_information"
957 assert res.offline_information.address == "Near Null Island"
958 assert res.offline_information.lat == 0.1
959 assert res.offline_information.lng == 0.2
962def test_UpdateEvent_all(db, moderator: Moderator):
963 # event creator
964 user1, token1 = generate_user()
965 # community moderator
966 user2, token2 = generate_user()
967 # third parties
968 user3, token3 = generate_user()
969 user4, token4 = generate_user()
970 user5, token5 = generate_user()
971 user6, token6 = generate_user()
973 with session_scope() as session:
974 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
976 time_before = now()
977 start_time = now() + timedelta(hours=1)
978 end_time = start_time + timedelta(hours=1.5)
980 event_ids = []
982 with events_session(token1) as api:
983 res = api.CreateEvent(
984 events_pb2.CreateEventReq(
985 title="Dummy Title",
986 content="0th occurrence",
987 offline_information=events_pb2.OfflineEventInformation(
988 address="Near Null Island",
989 lat=0.1,
990 lng=0.2,
991 ),
992 start_time=Timestamp_from_datetime(start_time),
993 end_time=Timestamp_from_datetime(end_time),
994 timezone="UTC",
995 )
996 )
998 event_id = res.event_id
999 event_ids.append(event_id)
1001 moderator.approve_event_occurrence(event_id)
1003 with events_session(token4) as api:
1004 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1006 with events_session(token5) as api:
1007 api.SetEventAttendance(
1008 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1009 )
1011 with events_session(token6) as api:
1012 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1014 with events_session(token1) as api:
1015 for i in range(5):
1016 res = api.ScheduleEvent(
1017 events_pb2.ScheduleEventReq(
1018 event_id=event_ids[-1],
1019 content=f"{i + 1}th occurrence",
1020 online_information=events_pb2.OnlineEventInformation(
1021 link="https://couchers.org/meet/",
1022 ),
1023 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)),
1024 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)),
1025 timezone="UTC",
1026 )
1027 )
1029 event_ids.append(res.event_id)
1031 # Approve all scheduled occurrences
1032 for eid in event_ids[1:]:
1033 moderator.approve_event_occurrence(eid)
1035 updated_event_id = event_ids[3]
1037 time_before_update = now()
1039 with events_session(token1) as api:
1040 res = api.UpdateEvent(
1041 events_pb2.UpdateEventReq(
1042 event_id=updated_event_id,
1043 title=wrappers_pb2.StringValue(value="New Title"),
1044 content=wrappers_pb2.StringValue(value="New content."),
1045 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
1046 update_all_future=True,
1047 )
1048 )
1050 time_after_update = now()
1052 with events_session(token2) as api:
1053 for i in range(3):
1054 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
1055 assert res.content == f"{i}th occurrence"
1056 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
1058 for i in range(3, 6):
1059 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
1060 assert res.content == "New content."
1061 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
1064def test_GetEvent(db, moderator: Moderator):
1065 # event creator
1066 user1, token1 = generate_user()
1067 # community moderator
1068 user2, token2 = generate_user()
1069 # third parties
1070 user3, token3 = generate_user()
1071 user4, token4 = generate_user()
1072 user5, token5 = generate_user()
1073 user6, token6 = generate_user()
1075 with session_scope() as session:
1076 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1078 time_before = now()
1079 start_time = now() + timedelta(hours=2)
1080 end_time = start_time + timedelta(hours=3)
1082 with events_session(token1) as api:
1083 # in person event
1084 res = api.CreateEvent(
1085 events_pb2.CreateEventReq(
1086 title="Dummy Title",
1087 content="Dummy content.",
1088 offline_information=events_pb2.OfflineEventInformation(
1089 address="Near Null Island",
1090 lat=0.1,
1091 lng=0.2,
1092 ),
1093 start_time=Timestamp_from_datetime(start_time),
1094 end_time=Timestamp_from_datetime(end_time),
1095 timezone="UTC",
1096 )
1097 )
1099 event_id = res.event_id
1101 moderator.approve_event_occurrence(event_id)
1103 with events_session(token4) as api:
1104 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1106 with events_session(token5) as api:
1107 api.SetEventAttendance(
1108 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1109 )
1111 with events_session(token6) as api:
1112 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1114 with events_session(token1) as api:
1115 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1117 assert res.is_next
1118 assert res.title == "Dummy Title"
1119 assert res.slug == "dummy-title"
1120 assert res.content == "Dummy content."
1121 assert not res.photo_url
1122 assert res.WhichOneof("mode") == "offline_information"
1123 assert res.offline_information.lat == 0.1
1124 assert res.offline_information.lng == 0.2
1125 assert res.offline_information.address == "Near Null Island"
1126 assert time_before <= to_aware_datetime(res.created) <= now()
1127 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1128 assert res.creator_user_id == user1.id
1129 assert to_aware_datetime(res.start_time) == start_time
1130 assert to_aware_datetime(res.end_time) == end_time
1131 # assert res.timezone == "UTC"
1132 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
1133 assert res.organizer
1134 assert res.subscriber
1135 assert res.going_count == 2
1136 assert res.organizer_count == 1
1137 assert res.subscriber_count == 3
1138 assert res.owner_user_id == user1.id
1139 assert not res.owner_community_id
1140 assert not res.owner_group_id
1141 assert res.thread.thread_id
1142 assert res.can_edit
1143 assert not res.can_moderate
1145 with events_session(token2) as api:
1146 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1148 assert res.is_next
1149 assert res.title == "Dummy Title"
1150 assert res.slug == "dummy-title"
1151 assert res.content == "Dummy content."
1152 assert not res.photo_url
1153 assert res.WhichOneof("mode") == "offline_information"
1154 assert res.offline_information.lat == 0.1
1155 assert res.offline_information.lng == 0.2
1156 assert res.offline_information.address == "Near Null Island"
1157 assert time_before <= to_aware_datetime(res.created) <= now()
1158 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1159 assert res.creator_user_id == user1.id
1160 assert to_aware_datetime(res.start_time) == start_time
1161 assert to_aware_datetime(res.end_time) == end_time
1162 # assert res.timezone == "UTC"
1163 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1164 assert not res.organizer
1165 assert not res.subscriber
1166 assert res.going_count == 2
1167 assert res.organizer_count == 1
1168 assert res.subscriber_count == 3
1169 assert res.owner_user_id == user1.id
1170 assert not res.owner_community_id
1171 assert not res.owner_group_id
1172 assert res.thread.thread_id
1173 assert res.can_edit
1174 assert res.can_moderate
1176 with events_session(token3) as api:
1177 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1179 assert res.is_next
1180 assert res.title == "Dummy Title"
1181 assert res.slug == "dummy-title"
1182 assert res.content == "Dummy content."
1183 assert not res.photo_url
1184 assert res.WhichOneof("mode") == "offline_information"
1185 assert res.offline_information.lat == 0.1
1186 assert res.offline_information.lng == 0.2
1187 assert res.offline_information.address == "Near Null Island"
1188 assert time_before <= to_aware_datetime(res.created) <= now()
1189 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1190 assert res.creator_user_id == user1.id
1191 assert to_aware_datetime(res.start_time) == start_time
1192 assert to_aware_datetime(res.end_time) == end_time
1193 # assert res.timezone == "UTC"
1194 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1195 assert not res.organizer
1196 assert not res.subscriber
1197 assert res.going_count == 2
1198 assert res.organizer_count == 1
1199 assert res.subscriber_count == 3
1200 assert res.owner_user_id == user1.id
1201 assert not res.owner_community_id
1202 assert not res.owner_group_id
1203 assert res.thread.thread_id
1204 assert not res.can_edit
1205 assert not res.can_moderate
1208def test_CancelEvent(db, moderator: Moderator):
1209 # event creator
1210 user1, token1 = generate_user()
1211 # community moderator
1212 user2, token2 = generate_user()
1213 # third parties
1214 user3, token3 = generate_user()
1215 user4, token4 = generate_user()
1216 user5, token5 = generate_user()
1217 user6, token6 = generate_user()
1219 with session_scope() as session:
1220 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1222 start_time = now() + timedelta(hours=2)
1223 end_time = start_time + timedelta(hours=3)
1225 with events_session(token1) as api:
1226 res = api.CreateEvent(
1227 events_pb2.CreateEventReq(
1228 title="Dummy Title",
1229 content="Dummy content.",
1230 offline_information=events_pb2.OfflineEventInformation(
1231 address="Near Null Island",
1232 lat=0.1,
1233 lng=0.2,
1234 ),
1235 start_time=Timestamp_from_datetime(start_time),
1236 end_time=Timestamp_from_datetime(end_time),
1237 timezone="UTC",
1238 )
1239 )
1241 event_id = res.event_id
1243 moderator.approve_event_occurrence(event_id)
1245 with events_session(token4) as api:
1246 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1248 with events_session(token5) as api:
1249 api.SetEventAttendance(
1250 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1251 )
1253 with events_session(token6) as api:
1254 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1256 with events_session(token1) as api:
1257 res = api.CancelEvent(
1258 events_pb2.CancelEventReq(
1259 event_id=event_id,
1260 )
1261 )
1263 with events_session(token1) as api:
1264 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1265 assert res.is_cancelled
1267 with events_session(token1) as api:
1268 with pytest.raises(grpc.RpcError) as e:
1269 api.UpdateEvent(
1270 events_pb2.UpdateEventReq(
1271 event_id=event_id,
1272 title=wrappers_pb2.StringValue(value="New Title"),
1273 )
1274 )
1275 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1276 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1278 with pytest.raises(grpc.RpcError) as e:
1279 api.InviteEventOrganizer(
1280 events_pb2.InviteEventOrganizerReq(
1281 event_id=event_id,
1282 user_id=user3.id,
1283 )
1284 )
1285 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1286 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1288 with pytest.raises(grpc.RpcError) as e:
1289 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1290 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1291 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1293 with events_session(token3) as api:
1294 with pytest.raises(grpc.RpcError) as e:
1295 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1296 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1297 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1299 with pytest.raises(grpc.RpcError) as e:
1300 api.SetEventAttendance(
1301 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1302 )
1303 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1304 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1306 with events_session(token1) as api:
1307 for include_cancelled in [True, False]:
1308 res = api.ListEventOccurrences(
1309 events_pb2.ListEventOccurrencesReq(
1310 event_id=event_id,
1311 include_cancelled=include_cancelled,
1312 )
1313 )
1314 if include_cancelled:
1315 assert len(res.events) > 0
1316 else:
1317 assert len(res.events) == 0
1319 res = api.ListMyEvents(
1320 events_pb2.ListMyEventsReq(
1321 include_cancelled=include_cancelled,
1322 )
1323 )
1324 if include_cancelled:
1325 assert len(res.events) > 0
1326 else:
1327 assert len(res.events) == 0
1330def test_ListEventAttendees(db, moderator: Moderator):
1331 # event creator
1332 user1, token1 = generate_user()
1333 # others
1334 user2, token2 = generate_user()
1335 user3, token3 = generate_user()
1336 user4, token4 = generate_user()
1337 user5, token5 = generate_user()
1338 user6, token6 = generate_user()
1340 with session_scope() as session:
1341 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1343 with events_session(token1) as api:
1344 event_id = api.CreateEvent(
1345 events_pb2.CreateEventReq(
1346 title="Dummy Title",
1347 content="Dummy content.",
1348 offline_information=events_pb2.OfflineEventInformation(
1349 address="Near Null Island",
1350 lat=0.1,
1351 lng=0.2,
1352 ),
1353 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1354 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1355 timezone="UTC",
1356 )
1357 ).event_id
1359 moderator.approve_event_occurrence(event_id)
1361 for token in [token2, token3, token4, token5]:
1362 with events_session(token) as api:
1363 api.SetEventAttendance(
1364 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1365 )
1367 with events_session(token6) as api:
1368 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1370 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1371 assert res.attendee_user_ids == [user1.id, user2.id]
1373 res = api.ListEventAttendees(
1374 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1375 )
1376 assert res.attendee_user_ids == [user3.id, user4.id]
1378 res = api.ListEventAttendees(
1379 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1380 )
1381 assert res.attendee_user_ids == [user5.id]
1382 assert not res.next_page_token
1385def test_ListEventSubscribers(db, moderator: Moderator):
1386 # event creator
1387 user1, token1 = generate_user()
1388 # others
1389 user2, token2 = generate_user()
1390 user3, token3 = generate_user()
1391 user4, token4 = generate_user()
1392 user5, token5 = generate_user()
1393 user6, token6 = generate_user()
1395 with session_scope() as session:
1396 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1398 with events_session(token1) as api:
1399 event_id = api.CreateEvent(
1400 events_pb2.CreateEventReq(
1401 title="Dummy Title",
1402 content="Dummy content.",
1403 offline_information=events_pb2.OfflineEventInformation(
1404 address="Near Null Island",
1405 lat=0.1,
1406 lng=0.2,
1407 ),
1408 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1409 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1410 timezone="UTC",
1411 )
1412 ).event_id
1414 moderator.approve_event_occurrence(event_id)
1416 for token in [token2, token3, token4, token5]:
1417 with events_session(token) as api:
1418 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1420 with events_session(token6) as api:
1421 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1423 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1424 assert res.subscriber_user_ids == [user1.id, user2.id]
1426 res = api.ListEventSubscribers(
1427 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1428 )
1429 assert res.subscriber_user_ids == [user3.id, user4.id]
1431 res = api.ListEventSubscribers(
1432 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1433 )
1434 assert res.subscriber_user_ids == [user5.id]
1435 assert not res.next_page_token
1438def test_ListEventOrganizers(db, moderator: Moderator):
1439 # event creator
1440 user1, token1 = generate_user()
1441 # others
1442 user2, token2 = generate_user()
1443 user3, token3 = generate_user()
1444 user4, token4 = generate_user()
1445 user5, token5 = generate_user()
1446 user6, token6 = generate_user()
1448 with session_scope() as session:
1449 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1451 with events_session(token1) as api:
1452 event_id = api.CreateEvent(
1453 events_pb2.CreateEventReq(
1454 title="Dummy Title",
1455 content="Dummy content.",
1456 offline_information=events_pb2.OfflineEventInformation(
1457 address="Near Null Island",
1458 lat=0.1,
1459 lng=0.2,
1460 ),
1461 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1462 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1463 timezone="UTC",
1464 )
1465 ).event_id
1467 moderator.approve_event_occurrence(event_id)
1469 with events_session(token1) as api:
1470 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1471 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1473 with events_session(token6) as api:
1474 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1476 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1477 assert res.organizer_user_ids == [user1.id, user2.id]
1479 res = api.ListEventOrganizers(
1480 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1481 )
1482 assert res.organizer_user_ids == [user3.id, user4.id]
1484 res = api.ListEventOrganizers(
1485 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1486 )
1487 assert res.organizer_user_ids == [user5.id]
1488 assert not res.next_page_token
1491def test_TransferEvent(db):
1492 user1, token1 = generate_user()
1493 user2, token2 = generate_user()
1494 user3, token3 = generate_user()
1495 user4, token4 = generate_user()
1497 with session_scope() as session:
1498 c = create_community(session, 0, 2, "Community", [user3], [], None)
1499 h = create_group(session, "Group", [user4], [], c)
1500 c_id = c.id
1501 h_id = h.id
1503 with events_session(token1) as api:
1504 event_id = api.CreateEvent(
1505 events_pb2.CreateEventReq(
1506 title="Dummy Title",
1507 content="Dummy content.",
1508 offline_information=events_pb2.OfflineEventInformation(
1509 address="Near Null Island",
1510 lat=0.1,
1511 lng=0.2,
1512 ),
1513 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1514 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1515 timezone="UTC",
1516 )
1517 ).event_id
1519 api.TransferEvent(
1520 events_pb2.TransferEventReq(
1521 event_id=event_id,
1522 new_owner_community_id=c_id,
1523 )
1524 )
1526 # remove ourselves as organizer, otherwise we can still edit it
1527 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1529 with pytest.raises(grpc.RpcError) as e:
1530 api.TransferEvent(
1531 events_pb2.TransferEventReq(
1532 event_id=event_id,
1533 new_owner_group_id=h_id,
1534 )
1535 )
1536 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1537 assert e.value.details() == "You're not allowed to transfer that event."
1539 event_id = api.CreateEvent(
1540 events_pb2.CreateEventReq(
1541 title="Dummy Title",
1542 content="Dummy content.",
1543 offline_information=events_pb2.OfflineEventInformation(
1544 address="Near Null Island",
1545 lat=0.1,
1546 lng=0.2,
1547 ),
1548 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1549 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1550 timezone="UTC",
1551 )
1552 ).event_id
1554 api.TransferEvent(
1555 events_pb2.TransferEventReq(
1556 event_id=event_id,
1557 new_owner_group_id=h_id,
1558 )
1559 )
1561 # remove ourselves as organizer, otherwise we can still edit it
1562 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1564 with pytest.raises(grpc.RpcError) as e:
1565 api.TransferEvent(
1566 events_pb2.TransferEventReq(
1567 event_id=event_id,
1568 new_owner_community_id=c_id,
1569 )
1570 )
1571 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1572 assert e.value.details() == "You're not allowed to transfer that event."
1575def test_SetEventSubscription(db, moderator: Moderator):
1576 user1, token1 = generate_user()
1577 user2, token2 = generate_user()
1579 with session_scope() as session:
1580 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1582 with events_session(token1) as api:
1583 event_id = api.CreateEvent(
1584 events_pb2.CreateEventReq(
1585 title="Dummy Title",
1586 content="Dummy content.",
1587 offline_information=events_pb2.OfflineEventInformation(
1588 address="Near Null Island",
1589 lat=0.1,
1590 lng=0.2,
1591 ),
1592 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1593 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1594 timezone="UTC",
1595 )
1596 ).event_id
1598 moderator.approve_event_occurrence(event_id)
1600 with events_session(token2) as api:
1601 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1602 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1603 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1604 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1605 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1608def test_SetEventAttendance(db, moderator: Moderator):
1609 user1, token1 = generate_user()
1610 user2, token2 = generate_user()
1612 with session_scope() as session:
1613 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1615 with events_session(token1) as api:
1616 event_id = api.CreateEvent(
1617 events_pb2.CreateEventReq(
1618 title="Dummy Title",
1619 content="Dummy content.",
1620 offline_information=events_pb2.OfflineEventInformation(
1621 address="Near Null Island",
1622 lat=0.1,
1623 lng=0.2,
1624 ),
1625 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1626 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1627 timezone="UTC",
1628 )
1629 ).event_id
1631 moderator.approve_event_occurrence(event_id)
1633 with events_session(token2) as api:
1634 assert (
1635 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1636 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1637 )
1638 api.SetEventAttendance(
1639 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1640 )
1641 assert (
1642 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1643 == events_pb2.ATTENDANCE_STATE_GOING
1644 )
1645 api.SetEventAttendance(
1646 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1647 )
1648 assert (
1649 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1650 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1651 )
1654def test_InviteEventOrganizer(db, moderator: Moderator):
1655 user1, token1 = generate_user()
1656 user2, token2 = generate_user()
1658 with session_scope() as session:
1659 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1661 with events_session(token1) as api:
1662 event_id = api.CreateEvent(
1663 events_pb2.CreateEventReq(
1664 title="Dummy Title",
1665 content="Dummy content.",
1666 offline_information=events_pb2.OfflineEventInformation(
1667 address="Near Null Island",
1668 lat=0.1,
1669 lng=0.2,
1670 ),
1671 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1672 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1673 timezone="UTC",
1674 )
1675 ).event_id
1677 moderator.approve_event_occurrence(event_id)
1679 with events_session(token2) as api:
1680 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1682 with pytest.raises(grpc.RpcError) as e:
1683 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1684 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1685 assert e.value.details() == "You're not allowed to edit that event."
1687 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1689 with events_session(token1) as api:
1690 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1692 with events_session(token2) as api:
1693 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1696def test_ListEventOccurrences(db):
1697 user1, token1 = generate_user()
1698 user2, token2 = generate_user()
1699 user3, token3 = generate_user()
1701 with session_scope() as session:
1702 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1704 start = now()
1706 event_ids = []
1708 with events_session(token1) as api:
1709 res = api.CreateEvent(
1710 events_pb2.CreateEventReq(
1711 title="First occurrence",
1712 content="Dummy content.",
1713 parent_community_id=c_id,
1714 online_information=events_pb2.OnlineEventInformation(
1715 link="https://couchers.org/meet/",
1716 ),
1717 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
1718 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
1719 timezone="UTC",
1720 )
1721 )
1723 event_ids.append(res.event_id)
1725 for i in range(5):
1726 res = api.ScheduleEvent(
1727 events_pb2.ScheduleEventReq(
1728 event_id=event_ids[-1],
1729 content=f"{i}th occurrence",
1730 online_information=events_pb2.OnlineEventInformation(
1731 link="https://couchers.org/meet/",
1732 ),
1733 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
1734 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
1735 timezone="UTC",
1736 )
1737 )
1739 event_ids.append(res.event_id)
1741 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1742 assert [event.event_id for event in res.events] == event_ids[:2]
1744 res = api.ListEventOccurrences(
1745 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1746 )
1747 assert [event.event_id for event in res.events] == event_ids[2:4]
1749 res = api.ListEventOccurrences(
1750 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1751 )
1752 assert [event.event_id for event in res.events] == event_ids[4:6]
1753 assert not res.next_page_token
1756def test_ListMyEvents(db, moderator: Moderator):
1757 user1, token1 = generate_user()
1758 user2, token2 = generate_user()
1759 user3, token3 = generate_user()
1760 user4, token4 = generate_user()
1761 user5, token5 = generate_user()
1763 with session_scope() as session:
1764 # Create global (world) -> macroregion -> region -> subregion hierarchy
1765 # my_communities_exclude_global filters out world, macroregion, and region level communities
1766 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1767 c_id = global_community.id
1768 macroregion_community = create_community(
1769 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1770 )
1771 region_community = create_community(
1772 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1773 )
1774 subregion_community = create_community(
1775 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1776 )
1777 c2_id = subregion_community.id
1779 start = now()
1781 def new_event(hours_from_now: int, community_id: int, online: bool = True) -> events_pb2.CreateEventReq:
1782 if online:
1783 return events_pb2.CreateEventReq(
1784 title="Dummy Online Title",
1785 content="Dummy content.",
1786 online_information=events_pb2.OnlineEventInformation(
1787 link="https://couchers.org/meet/",
1788 ),
1789 parent_community_id=community_id,
1790 timezone="UTC",
1791 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1792 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1793 )
1794 else:
1795 return events_pb2.CreateEventReq(
1796 title="Dummy Offline Title",
1797 content="Dummy content.",
1798 offline_information=events_pb2.OfflineEventInformation(
1799 address="Near Null Island",
1800 lat=0.1,
1801 lng=0.2,
1802 ),
1803 parent_community_id=community_id,
1804 timezone="UTC",
1805 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1806 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1807 )
1809 with events_session(token1) as api:
1810 e2 = api.CreateEvent(new_event(2, c_id, True)).event_id
1812 moderator.approve_event_occurrence(e2)
1814 with events_session(token2) as api:
1815 e1 = api.CreateEvent(new_event(1, c_id, False)).event_id
1817 moderator.approve_event_occurrence(e1)
1819 with events_session(token1) as api:
1820 e3 = api.CreateEvent(new_event(3, c_id, False)).event_id
1822 moderator.approve_event_occurrence(e3)
1824 with events_session(token2) as api:
1825 e5 = api.CreateEvent(new_event(5, c_id, True)).event_id
1827 moderator.approve_event_occurrence(e5)
1829 with events_session(token3) as api:
1830 e4 = api.CreateEvent(new_event(4, c_id, True)).event_id
1832 moderator.approve_event_occurrence(e4)
1834 with events_session(token4) as api:
1835 e6 = api.CreateEvent(new_event(6, c2_id, True)).event_id
1837 moderator.approve_event_occurrence(e6)
1839 with events_session(token1) as api:
1840 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1842 with events_session(token1) as api:
1843 api.SetEventAttendance(
1844 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1845 )
1846 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1848 with events_session(token2) as api:
1849 api.SetEventAttendance(
1850 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1851 )
1853 with events_session(token3) as api:
1854 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1856 with events_session(token1) as api:
1857 # test pagination with token first
1858 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1859 assert [event.event_id for event in res.events] == [e1, e2]
1860 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1861 assert [event.event_id for event in res.events] == [e3, e4]
1862 assert not res.next_page_token
1864 res = api.ListMyEvents(
1865 events_pb2.ListMyEventsReq(
1866 subscribed=True,
1867 attending=True,
1868 organizing=True,
1869 )
1870 )
1871 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1873 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1874 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1876 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1877 assert [event.event_id for event in res.events] == [e2, e3, e4]
1879 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1880 assert [event.event_id for event in res.events] == [e1, e2, e3]
1882 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1883 assert [event.event_id for event in res.events] == [e2, e3]
1885 with events_session(token1) as api:
1886 # Test pagination with page_number and verify total_items
1887 res = api.ListMyEvents(
1888 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1889 )
1890 assert [event.event_id for event in res.events] == [e1, e2]
1891 assert res.total_items == 4
1893 res = api.ListMyEvents(
1894 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1895 )
1896 assert [event.event_id for event in res.events] == [e3, e4]
1897 assert res.total_items == 4
1899 # Verify no more pages
1900 res = api.ListMyEvents(
1901 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1902 )
1903 assert not res.events
1904 assert res.total_items == 4
1906 with events_session(token2) as api:
1907 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1908 assert [event.event_id for event in res.events] == [e1, e3, e5]
1910 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1911 assert [event.event_id for event in res.events] == [e1, e5]
1913 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1914 assert [event.event_id for event in res.events] == [e1, e3, e5]
1916 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1917 assert [event.event_id for event in res.events] == [e1, e5]
1919 with events_session(token3) as api:
1920 # user3 is member of both global (c_id) and child (c2_id) communities
1921 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1922 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1924 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1925 assert [event.event_id for event in res.events] == [e2, e4]
1927 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1928 assert [event.event_id for event in res.events] == [e4]
1930 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1931 assert [event.event_id for event in res.events] == [e3, e4]
1933 # my_communities returns events from both communities user3 is a member of
1934 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1935 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1937 # my_communities_exclude_global filters out events from global community (node_id=1)
1938 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1939 assert [event.event_id for event in res.events] == [e6]
1941 # my_communities_exclude_global works independently of my_communities flag
1942 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1943 assert [event.event_id for event in res.events] == [e6]
1945 # my_communities_exclude_global filters organizing results too
1946 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1947 assert [event.event_id for event in res.events] == []
1949 # my_communities_exclude_global filters subscribed results too
1950 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1951 assert [event.event_id for event in res.events] == []
1953 with events_session(token5) as api:
1954 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1955 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1958def test_RemoveEventOrganizer(db, moderator: Moderator):
1959 user1, token1 = generate_user()
1960 user2, token2 = generate_user()
1962 with session_scope() as session:
1963 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1965 with events_session(token1) as api:
1966 event_id = api.CreateEvent(
1967 events_pb2.CreateEventReq(
1968 title="Dummy Title",
1969 content="Dummy content.",
1970 offline_information=events_pb2.OfflineEventInformation(
1971 address="Near Null Island",
1972 lat=0.1,
1973 lng=0.2,
1974 ),
1975 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1976 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1977 timezone="UTC",
1978 )
1979 ).event_id
1981 moderator.approve_event_occurrence(event_id)
1983 with events_session(token2) as api:
1984 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1986 with pytest.raises(grpc.RpcError) as e:
1987 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1988 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1989 assert e.value.details() == "You're not allowed to edit that event."
1991 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1993 with events_session(token1) as api:
1994 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1996 with pytest.raises(grpc.RpcError) as e:
1997 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1998 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1999 assert e.value.details() == "You cannot remove the event owner as an organizer."
2001 with events_session(token2) as api:
2002 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2003 assert res.organizer
2004 assert res.organizer_count == 2
2005 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2006 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2008 with pytest.raises(grpc.RpcError) as e:
2009 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2010 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2011 assert e.value.details() == "You're not allowed to edit that event."
2013 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2014 assert not res.organizer
2015 assert res.organizer_count == 1
2017 # Test that event owner can remove co-organizers
2018 with events_session(token1) as api:
2019 # Add user2 back as organizer
2020 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2022 # Verify user2 is now an organizer
2023 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2024 assert res.organizer_count == 2
2026 # Event owner can remove co-organizer
2027 api.RemoveEventOrganizer(
2028 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
2029 )
2031 # Verify user2 is no longer an organizer
2032 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2033 assert res.organizer_count == 1
2035 # Test that non-organizers cannot remove other organizers
2036 with events_session(token2) as api:
2037 # User2 cannot invite themselves as organizer (not the owner)
2038 with pytest.raises(grpc.RpcError) as e:
2039 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2040 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2041 assert e.value.details() == "You're not allowed to edit that event."
2043 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
2044 with events_session(token1) as api:
2045 # Add user2 back as organizer
2046 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2049def test_ListEventAttendees_regression(db):
2050 # see issue #1617:
2051 #
2052 # 1. Create an event
2053 # 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`
2054 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
2055 # 4. Change the current user's attendance state to "going" again
2056 #
2057 # **Expected behaviour**
2058 # `ListEventAttendees` should return the current user's ID
2059 #
2060 # **Actual/current behaviour**
2061 # `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
2063 user1, token1 = generate_user()
2064 user2, token2 = generate_user()
2065 user3, token3 = generate_user()
2066 user4, token4 = generate_user()
2067 user5, token5 = generate_user()
2069 with session_scope() as session:
2070 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2072 start_time = now() + timedelta(hours=2)
2073 end_time = start_time + timedelta(hours=3)
2075 with events_session(token1) as api:
2076 res = api.CreateEvent(
2077 events_pb2.CreateEventReq(
2078 title="Dummy Title",
2079 content="Dummy content.",
2080 online_information=events_pb2.OnlineEventInformation(
2081 link="https://couchers.org",
2082 ),
2083 parent_community_id=c_id,
2084 start_time=Timestamp_from_datetime(start_time),
2085 end_time=Timestamp_from_datetime(end_time),
2086 timezone="UTC",
2087 )
2088 )
2090 res = api.TransferEvent(
2091 events_pb2.TransferEventReq(
2092 event_id=res.event_id,
2093 new_owner_community_id=c_id,
2094 )
2095 )
2097 event_id = res.event_id
2099 api.SetEventAttendance(
2100 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2101 )
2102 api.SetEventAttendance(
2103 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2104 )
2106 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2107 assert len(res.attendee_user_ids) == 1
2108 assert res.attendee_user_ids[0] == user1.id
2111def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2112 user1, token1 = generate_user()
2113 user2, token2 = generate_user()
2114 user3, token3 = generate_user()
2115 user4, token4 = generate_user()
2117 with session_scope() as session:
2118 c = create_community(session, 0, 2, "Community", [user3], [], None)
2119 h = create_group(session, "Group", [user4], [], c)
2120 c_id = c.id
2121 h_id = h.id
2122 user4_id = user4.id
2124 with events_session(token1) as api:
2125 event = api.CreateEvent(
2126 events_pb2.CreateEventReq(
2127 title="Dummy Title",
2128 content="Dummy content.",
2129 offline_information=events_pb2.OfflineEventInformation(
2130 address="Near Null Island",
2131 lat=0.1,
2132 lng=0.2,
2133 ),
2134 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2135 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2136 timezone="UTC",
2137 )
2138 )
2140 moderator.approve_event_occurrence(event.event_id)
2142 with threads_session(token2) as api:
2143 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2145 moderator.approve_thread_post(reply_id)
2147 with events_session(token3) as api:
2148 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2149 assert res.thread.num_responses == 1
2151 with threads_session(token3) as api:
2152 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2153 assert len(ret.replies) == 1
2154 assert not ret.next_page_token
2155 assert ret.replies[0].thread_id == reply_id
2156 assert ret.replies[0].content == "hi"
2157 assert ret.replies[0].author_user_id == user2.id
2158 assert ret.replies[0].num_replies == 0
2160 nested_reply_id = api.PostReply(
2161 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2162 ).thread_id
2164 moderator.approve_thread_post(nested_reply_id)
2166 process_jobs()
2168 push = push_collector.pop_for_user(user1.id, last=True)
2169 assert push.topic_action == NotificationTopicAction.event__comment.display
2170 assert push.content.title == f"{user2.name} • Dummy Title"
2171 assert push.content.ios_title == user2.name
2172 assert push.content.ios_subtitle == "Commented on Dummy Title"
2173 assert push.content.body == "hi"
2175 push = push_collector.pop_for_user(user2.id, last=True)
2176 assert push.content.title == f"{user3.name} • Dummy Title"
2178 assert push_collector.count_for_user(user4_id) == 0
2181def test_can_overlap_other_events_schedule_regression(db):
2182 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2183 user, token = generate_user()
2185 with session_scope() as session:
2186 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2188 start = now()
2190 with events_session(token) as api:
2191 # create another event, should be able to overlap with this one
2192 api.CreateEvent(
2193 events_pb2.CreateEventReq(
2194 title="Dummy Title",
2195 content="Dummy content.",
2196 parent_community_id=c_id,
2197 online_information=events_pb2.OnlineEventInformation(
2198 link="https://couchers.org/meet/",
2199 ),
2200 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2201 end_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2202 timezone="UTC",
2203 )
2204 )
2206 # this event
2207 res = api.CreateEvent(
2208 events_pb2.CreateEventReq(
2209 title="Dummy Title",
2210 content="Dummy content.",
2211 parent_community_id=c_id,
2212 online_information=events_pb2.OnlineEventInformation(
2213 link="https://couchers.org/meet/",
2214 ),
2215 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2216 end_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2217 timezone="UTC",
2218 )
2219 )
2221 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2222 api.ScheduleEvent(
2223 events_pb2.ScheduleEventReq(
2224 event_id=res.event_id,
2225 content="New event occurrence",
2226 offline_information=events_pb2.OfflineEventInformation(
2227 address="A bit further but still near Null Island",
2228 lat=0.3,
2229 lng=0.2,
2230 ),
2231 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2232 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2233 timezone="UTC",
2234 )
2235 )
2238def test_can_overlap_other_events_update_regression(db):
2239 user, token = generate_user()
2241 with session_scope() as session:
2242 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2244 start = now()
2246 with events_session(token) as api:
2247 # create another event, should be able to overlap with this one
2248 api.CreateEvent(
2249 events_pb2.CreateEventReq(
2250 title="Dummy Title",
2251 content="Dummy content.",
2252 parent_community_id=c_id,
2253 online_information=events_pb2.OnlineEventInformation(
2254 link="https://couchers.org/meet/",
2255 ),
2256 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2257 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2258 timezone="UTC",
2259 )
2260 )
2262 res = api.CreateEvent(
2263 events_pb2.CreateEventReq(
2264 title="Dummy Title",
2265 content="Dummy content.",
2266 parent_community_id=c_id,
2267 online_information=events_pb2.OnlineEventInformation(
2268 link="https://couchers.org/meet/",
2269 ),
2270 start_time=Timestamp_from_datetime(start + timedelta(hours=7)),
2271 end_time=Timestamp_from_datetime(start + timedelta(hours=8)),
2272 timezone="UTC",
2273 )
2274 )
2276 event_id = api.ScheduleEvent(
2277 events_pb2.ScheduleEventReq(
2278 event_id=res.event_id,
2279 content="New event occurrence",
2280 offline_information=events_pb2.OfflineEventInformation(
2281 address="A bit further but still near Null Island",
2282 lat=0.3,
2283 lng=0.2,
2284 ),
2285 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2286 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2287 timezone="UTC",
2288 )
2289 ).event_id
2291 # can overlap with this current existing occurrence
2292 api.UpdateEvent(
2293 events_pb2.UpdateEventReq(
2294 event_id=event_id,
2295 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2296 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2297 )
2298 )
2300 api.UpdateEvent(
2301 events_pb2.UpdateEventReq(
2302 event_id=event_id,
2303 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2304 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2305 )
2306 )
2309def test_list_past_events_regression(db):
2310 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2311 user, token = generate_user()
2313 with session_scope() as session:
2314 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2316 start = now()
2318 with events_session(token) as api:
2319 api.CreateEvent(
2320 events_pb2.CreateEventReq(
2321 title="Dummy Title",
2322 content="Dummy content.",
2323 parent_community_id=c_id,
2324 online_information=events_pb2.OnlineEventInformation(
2325 link="https://couchers.org/meet/",
2326 ),
2327 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2328 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2329 timezone="UTC",
2330 )
2331 )
2333 with session_scope() as session:
2334 session.execute(
2335 update(EventOccurrence).values(
2336 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2337 )
2338 )
2340 with events_session(token) as api:
2341 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2342 assert len(res.events) == 1
2345def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2346 user1, token1 = generate_user(complete_profile=True)
2347 user2, token2 = generate_user()
2348 user3, token3 = generate_user()
2349 user4, token4 = generate_user()
2350 user5, token5 = generate_user(is_superuser=True)
2352 with session_scope() as session:
2353 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2354 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2355 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2356 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2358 enforce_community_memberships()
2360 with events_session(token1) as api:
2361 res = api.CreateEvent(
2362 events_pb2.CreateEventReq(
2363 title="Dummy Title",
2364 content="Dummy content.",
2365 parent_community_id=c_id,
2366 online_information=events_pb2.OnlineEventInformation(
2367 link="https://couchers.org/meet/",
2368 ),
2369 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)),
2370 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
2371 timezone="UTC",
2372 )
2373 )
2374 user_url = f"http://localhost:3000/user/{user1.username}"
2375 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2377 event_id = res.event_id
2379 moderator.approve_event_occurrence(event_id)
2381 with events_session(token1) as api:
2382 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2384 email = email_collector.pop_for_mods(last=True)
2386 assert user_url in email.plain
2387 assert event_url in email.plain
2389 # can't send another req
2390 with pytest.raises(grpc.RpcError) as err:
2391 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2392 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2393 assert err.value.details() == "You have already requested a community invite for this event."
2395 # another user can send one though
2396 with events_session(token3) as api:
2397 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2399 # but not a non-admin
2400 with events_session(token2) as api:
2401 with pytest.raises(grpc.RpcError) as err:
2402 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2403 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2404 assert err.value.details() == "You're not allowed to edit that event."
2406 with real_editor_session(token5) as editor:
2407 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2408 assert len(res.requests) == 2
2409 assert res.requests[0].user_id == user1.id
2410 assert res.requests[0].approx_users_to_notify == 3
2411 assert res.requests[1].user_id == user3.id
2412 assert res.requests[1].approx_users_to_notify == 3
2414 editor.DecideEventCommunityInviteRequest(
2415 editor_pb2.DecideEventCommunityInviteRequestReq(
2416 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2417 approve=False,
2418 )
2419 )
2421 editor.DecideEventCommunityInviteRequest(
2422 editor_pb2.DecideEventCommunityInviteRequestReq(
2423 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2424 approve=True,
2425 )
2426 )
2428 # not after approve
2429 with events_session(token4) as api:
2430 with pytest.raises(grpc.RpcError) as err:
2431 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2432 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2433 assert err.value.details() == "A community invite has already been sent out for this event."
2436def test_update_event_should_notify_queues_job():
2437 user, token = generate_user()
2438 start = now()
2440 with session_scope() as session:
2441 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2443 # create an event
2444 with events_session(token) as api:
2445 create_res = api.CreateEvent(
2446 events_pb2.CreateEventReq(
2447 title="Dummy Title",
2448 content="Dummy content.",
2449 parent_community_id=c_id,
2450 offline_information=events_pb2.OfflineEventInformation(
2451 address="https://couchers.org/meet/",
2452 lat=1.0,
2453 lng=2.0,
2454 ),
2455 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2456 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2457 timezone="UTC",
2458 )
2459 )
2461 event_id = create_res.event_id
2463 # measure initial background job queue length
2464 with session_scope() as session:
2465 jobs = session.query(BackgroundJob).all()
2466 job_length_before_update = len(jobs)
2468 # update with should_notify=False, expect no change in background job queue
2469 api.UpdateEvent(
2470 events_pb2.UpdateEventReq(
2471 event_id=event_id,
2472 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2473 should_notify=False,
2474 )
2475 )
2477 with session_scope() as session:
2478 jobs = session.query(BackgroundJob).all()
2479 assert len(jobs) == job_length_before_update
2481 # update with should_notify=True, expect one new background job added
2482 api.UpdateEvent(
2483 events_pb2.UpdateEventReq(
2484 event_id=event_id,
2485 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2486 should_notify=True,
2487 )
2488 )
2490 with session_scope() as session:
2491 jobs = session.query(BackgroundJob).all()
2492 assert len(jobs) == job_length_before_update + 1
2495def test_event_photo_key(db):
2496 """Test that events return the photo_key field when a photo is set."""
2497 user, token = generate_user()
2499 start_time = now() + timedelta(hours=2)
2500 end_time = start_time + timedelta(hours=3)
2502 # Create a community and an upload for the event photo
2503 with session_scope() as session:
2504 create_community(session, 0, 2, "Community", [user], [], None)
2505 upload = Upload(
2506 key="test_event_photo_key_123",
2507 filename="test_event_photo_key_123.jpg",
2508 creator_user_id=user.id,
2509 )
2510 session.add(upload)
2512 with events_session(token) as api:
2513 # Create event without photo
2514 res = api.CreateEvent(
2515 events_pb2.CreateEventReq(
2516 title="Event Without Photo",
2517 content="No photo content.",
2518 photo_key=None,
2519 offline_information=events_pb2.OfflineEventInformation(
2520 address="Near Null Island",
2521 lat=0.1,
2522 lng=0.2,
2523 ),
2524 start_time=Timestamp_from_datetime(start_time),
2525 end_time=Timestamp_from_datetime(end_time),
2526 timezone="UTC",
2527 )
2528 )
2530 assert res.photo_key == ""
2531 assert res.photo_url == ""
2533 # Create event with photo
2534 res_with_photo = api.CreateEvent(
2535 events_pb2.CreateEventReq(
2536 title="Event With Photo",
2537 content="Has photo content.",
2538 photo_key="test_event_photo_key_123",
2539 offline_information=events_pb2.OfflineEventInformation(
2540 address="Near Null Island",
2541 lat=0.1,
2542 lng=0.2,
2543 ),
2544 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)),
2545 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)),
2546 timezone="UTC",
2547 )
2548 )
2550 assert res_with_photo.photo_key == "test_event_photo_key_123"
2551 assert "test_event_photo_key_123" in res_with_photo.photo_url
2553 event_id = res_with_photo.event_id
2555 # Verify photo_key is returned when getting the event
2556 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2557 assert get_res.photo_key == "test_event_photo_key_123"
2558 assert "test_event_photo_key_123" in get_res.photo_url
2561def test_event_created_with_shadowed_visibility(db):
2562 """Events start in SHADOWED state when created."""
2563 user, token = generate_user()
2565 with session_scope() as session:
2566 create_community(session, 0, 2, "Community", [user], [], None)
2568 start_time = now() + timedelta(hours=2)
2569 end_time = start_time + timedelta(hours=3)
2571 with events_session(token) as api:
2572 res = api.CreateEvent(
2573 events_pb2.CreateEventReq(
2574 title="Test UMS Event",
2575 content="UMS content.",
2576 offline_information=events_pb2.OfflineEventInformation(
2577 address="Near Null Island",
2578 lat=0.1,
2579 lng=0.2,
2580 ),
2581 start_time=Timestamp_from_datetime(start_time),
2582 end_time=Timestamp_from_datetime(end_time),
2583 timezone="UTC",
2584 )
2585 )
2586 event_id = res.event_id
2588 with session_scope() as session:
2589 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2590 mod_state = session.execute(
2591 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2592 ).scalar_one()
2593 assert mod_state.visibility == ModerationVisibility.shadowed
2596def test_shadowed_event_visible_to_creator_only(db):
2597 """SHADOWED events are visible to the creator but not to other users."""
2598 user1, token1 = generate_user()
2599 user2, token2 = generate_user()
2601 with session_scope() as session:
2602 create_community(session, 0, 2, "Community", [user1], [], None)
2604 start_time = now() + timedelta(hours=2)
2605 end_time = start_time + timedelta(hours=3)
2607 with events_session(token1) as api:
2608 res = api.CreateEvent(
2609 events_pb2.CreateEventReq(
2610 title="Shadowed Event",
2611 content="Content.",
2612 offline_information=events_pb2.OfflineEventInformation(
2613 address="Near Null Island",
2614 lat=0.1,
2615 lng=0.2,
2616 ),
2617 start_time=Timestamp_from_datetime(start_time),
2618 end_time=Timestamp_from_datetime(end_time),
2619 timezone="UTC",
2620 )
2621 )
2622 event_id = res.event_id
2624 # Creator can see it
2625 with events_session(token1) as api:
2626 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2627 assert res.title == "Shadowed Event"
2629 # Other user cannot
2630 with events_session(token2) as api:
2631 with pytest.raises(grpc.RpcError) as e:
2632 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2633 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2636def test_event_visible_after_approval(db, moderator: Moderator):
2637 """Events become visible to all users after moderation approval."""
2638 user1, token1 = generate_user()
2639 user2, token2 = generate_user()
2641 with session_scope() as session:
2642 create_community(session, 0, 2, "Community", [user1], [], None)
2644 start_time = now() + timedelta(hours=2)
2645 end_time = start_time + timedelta(hours=3)
2647 with events_session(token1) as api:
2648 res = api.CreateEvent(
2649 events_pb2.CreateEventReq(
2650 title="Approved Event",
2651 content="Content.",
2652 offline_information=events_pb2.OfflineEventInformation(
2653 address="Near Null Island",
2654 lat=0.1,
2655 lng=0.2,
2656 ),
2657 start_time=Timestamp_from_datetime(start_time),
2658 end_time=Timestamp_from_datetime(end_time),
2659 timezone="UTC",
2660 )
2661 )
2662 event_id = res.event_id
2664 # Other user cannot see it yet
2665 with events_session(token2) as api:
2666 with pytest.raises(grpc.RpcError) as e:
2667 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2668 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2670 # Approve the event
2671 moderator.approve_event_occurrence(event_id)
2673 # Now other user can see it
2674 with events_session(token2) as api:
2675 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2676 assert res.title == "Approved Event"
2679def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2680 """SHADOWED events appear in lists for the creator but not for other users."""
2681 user1, token1 = generate_user()
2682 user2, token2 = generate_user()
2684 with session_scope() as session:
2685 create_community(session, 0, 2, "Community", [user1], [], None)
2687 start_time = now() + timedelta(hours=2)
2688 end_time = start_time + timedelta(hours=3)
2690 with events_session(token1) as api:
2691 res = api.CreateEvent(
2692 events_pb2.CreateEventReq(
2693 title="List Test Event",
2694 content="Content.",
2695 offline_information=events_pb2.OfflineEventInformation(
2696 address="Near Null Island",
2697 lat=0.1,
2698 lng=0.2,
2699 ),
2700 start_time=Timestamp_from_datetime(start_time),
2701 end_time=Timestamp_from_datetime(end_time),
2702 timezone="UTC",
2703 )
2704 )
2705 event_id = res.event_id
2707 # Creator can see their own SHADOWED event in lists
2708 with events_session(token1) as api:
2709 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2710 event_ids = [e.event_id for e in list_res.events]
2711 assert event_id in event_ids
2713 # Other user cannot see the SHADOWED event in lists
2714 with events_session(token2) as api:
2715 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2716 event_ids = [e.event_id for e in list_res.events]
2717 assert event_id not in event_ids
2719 # After approval, other user can see it
2720 moderator.approve_event_occurrence(event_id)
2722 with events_session(token2) as api:
2723 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2724 event_ids = [e.event_id for e in list_res.events]
2725 assert event_id in event_ids
2728def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2729 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2730 user1, token1 = generate_user()
2731 user2, token2 = generate_user()
2733 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2734 with session_scope() as session:
2735 world = create_community(session, 0, 10, "World", [user1], [], None)
2736 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2737 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2738 create_community(session, 0, 2, "Child", [user2], [], region)
2740 start_time = now() + timedelta(hours=2)
2741 end_time = start_time + timedelta(hours=3)
2743 with events_session(token1) as api:
2744 res = api.CreateEvent(
2745 events_pb2.CreateEventReq(
2746 title="Deferred Event",
2747 content="Content.",
2748 offline_information=events_pb2.OfflineEventInformation(
2749 address="Near Null Island",
2750 lat=0.1,
2751 lng=0.2,
2752 ),
2753 start_time=Timestamp_from_datetime(start_time),
2754 end_time=Timestamp_from_datetime(end_time),
2755 timezone="UTC",
2756 )
2757 )
2758 event_id = res.event_id
2760 # Process all jobs — notification should be deferred (event is SHADOWED)
2761 process_jobs()
2763 with session_scope() as session:
2764 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2765 # Notification was created with moderation_state_id for deferral
2766 assert notif.moderation_state_id is not None
2767 # No delivery exists (deferred because event is SHADOWED)
2768 delivery_count = session.execute(
2769 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2770 ).scalar_one_or_none()
2771 assert delivery_count is None
2773 # Approve the event — handle_notification is re-queued for deferred notifications
2774 moderator.approve_event_occurrence(event_id)
2776 # Verify handle_notification job was queued
2777 with session_scope() as session:
2778 pending_jobs = (
2779 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2780 .scalars()
2781 .all()
2782 )
2783 assert any("handle_notification" in j.job_type for j in pending_jobs)
2786def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2787 """Event update notifications should carry the event's moderation_state_id for deferral."""
2788 user1, token1 = generate_user()
2789 user2, token2 = generate_user()
2791 with session_scope() as session:
2792 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2794 start_time = now() + timedelta(hours=2)
2795 end_time = start_time + timedelta(hours=3)
2797 with events_session(token1) as api:
2798 res = api.CreateEvent(
2799 events_pb2.CreateEventReq(
2800 title="Update Test",
2801 content="Content.",
2802 offline_information=events_pb2.OfflineEventInformation(
2803 address="Near Null Island",
2804 lat=0.1,
2805 lng=0.2,
2806 ),
2807 start_time=Timestamp_from_datetime(start_time),
2808 end_time=Timestamp_from_datetime(end_time),
2809 timezone="UTC",
2810 )
2811 )
2812 event_id = res.event_id
2814 moderator.approve_event_occurrence(event_id)
2815 process_jobs()
2816 # Clear any create notifications
2817 while push_collector.count_for_user(user2.id): 2817 ↛ 2818line 2817 didn't jump to line 2818 because the condition on line 2817 was never true
2818 push_collector.pop_for_user(user2.id)
2820 # User2 subscribes to the event
2821 with events_session(token2) as api:
2822 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2824 # User1 updates the event with should_notify=True
2825 with events_session(token1) as api:
2826 api.UpdateEvent(
2827 events_pb2.UpdateEventReq(
2828 event_id=event_id,
2829 title=wrappers_pb2.StringValue(value="Updated Title"),
2830 should_notify=True,
2831 )
2832 )
2834 process_jobs()
2836 # Verify that the update notification for user2 has moderation_state_id set
2837 with session_scope() as session:
2838 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2840 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2841 # Find the update notification (most recent one)
2842 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2843 assert len(update_notifs) == 1
2844 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2847def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2848 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2849 user1, token1 = generate_user()
2850 user2, token2 = generate_user()
2852 with session_scope() as session:
2853 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2855 start_time = now() + timedelta(hours=2)
2856 end_time = start_time + timedelta(hours=3)
2858 with events_session(token1) as api:
2859 res = api.CreateEvent(
2860 events_pb2.CreateEventReq(
2861 title="Cancel Test",
2862 content="Content.",
2863 offline_information=events_pb2.OfflineEventInformation(
2864 address="Near Null Island",
2865 lat=0.1,
2866 lng=0.2,
2867 ),
2868 start_time=Timestamp_from_datetime(start_time),
2869 end_time=Timestamp_from_datetime(end_time),
2870 timezone="UTC",
2871 )
2872 )
2873 event_id = res.event_id
2875 moderator.approve_event_occurrence(event_id)
2876 process_jobs()
2877 while push_collector.count_for_user(user2.id): 2877 ↛ 2878line 2877 didn't jump to line 2878 because the condition on line 2877 was never true
2878 push_collector.pop_for_user(user2.id)
2880 # User2 subscribes
2881 with events_session(token2) as api:
2882 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2884 # User1 cancels the event
2885 with events_session(token1) as api:
2886 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2888 process_jobs()
2890 # Verify that the cancel notification for user2 has moderation_state_id set
2891 with session_scope() as session:
2892 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2894 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2895 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2896 assert len(cancel_notifs) == 1
2897 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2900def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2901 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2902 user1, token1 = generate_user()
2903 user2, token2 = generate_user()
2905 with session_scope() as session:
2906 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2908 # Create event starting 23 hours from now (within 24h reminder window)
2909 start_time = now() + timedelta(hours=23)
2910 end_time = start_time + timedelta(hours=1)
2912 with events_session(token1) as api:
2913 res = api.CreateEvent(
2914 events_pb2.CreateEventReq(
2915 title="Reminder Test",
2916 content="Content.",
2917 offline_information=events_pb2.OfflineEventInformation(
2918 address="Near Null Island",
2919 lat=0.1,
2920 lng=0.2,
2921 ),
2922 start_time=Timestamp_from_datetime(start_time),
2923 end_time=Timestamp_from_datetime(end_time),
2924 timezone="UTC",
2925 )
2926 )
2927 event_id = res.event_id
2929 moderator.approve_event_occurrence(event_id)
2930 process_jobs()
2931 while push_collector.count_for_user(user2.id): 2931 ↛ 2932line 2931 didn't jump to line 2932 because the condition on line 2931 was never true
2932 push_collector.pop_for_user(user2.id)
2934 # User2 marks attendance
2935 with events_session(token2) as api:
2936 api.SetEventAttendance(
2937 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2938 )
2940 # Run the event reminder handler
2941 send_event_reminders(empty_pb2.Empty())
2942 process_jobs()
2944 # Verify that the reminder notification for user2 has moderation_state_id set
2945 with session_scope() as session:
2946 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2948 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2949 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2950 assert len(reminder_notifs) == 1
2951 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2954def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2955 """Event reminders should not be sent for cancelled events."""
2956 user1, token1 = generate_user()
2957 user2, token2 = generate_user()
2959 with session_scope() as session:
2960 create_community(session, 0, 2, "Community", [user2], [], None)
2962 # Create event starting 23 hours from now (within 24h reminder window)
2963 start_time = now() + timedelta(hours=23)
2964 end_time = start_time + timedelta(hours=1)
2966 with events_session(token1) as api:
2967 res = api.CreateEvent(
2968 events_pb2.CreateEventReq(
2969 title="Cancelled Reminder Test",
2970 content="Content.",
2971 offline_information=events_pb2.OfflineEventInformation(
2972 address="Near Null Island",
2973 lat=0.1,
2974 lng=0.2,
2975 ),
2976 start_time=Timestamp_from_datetime(start_time),
2977 end_time=Timestamp_from_datetime(end_time),
2978 timezone="UTC",
2979 )
2980 )
2981 event_id = res.event_id
2983 moderator.approve_event_occurrence(event_id)
2984 process_jobs()
2986 # User2 marks attendance
2987 with events_session(token2) as api:
2988 api.SetEventAttendance(
2989 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2990 )
2992 # User1 cancels the event
2993 with events_session(token1) as api:
2994 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2996 process_jobs()
2997 # Drain any cancellation-related notifications so we can cleanly assert on reminders
2998 while push_collector.count_for_user(user2.id):
2999 push_collector.pop_for_user(user2.id)
3001 # Run the event reminder handler
3002 send_event_reminders(empty_pb2.Empty())
3003 process_jobs()
3005 # Verify that no reminder notification was sent for user2
3006 with session_scope() as session:
3007 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3008 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3009 assert len(reminder_notifs) == 0
3012@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
3013def test_event_reminder_not_sent_for_invisible_attendee(
3014 db, push_collector: PushCollector, moderator: Moderator, invisible_field
3015):
3016 user1, token1 = generate_user()
3017 user2, token2 = generate_user()
3019 with session_scope() as session:
3020 create_community(session, 0, 2, "Community", [user2], [], None)
3022 start_time = now() + timedelta(hours=23)
3023 end_time = start_time + timedelta(hours=1)
3025 with events_session(token1) as api:
3026 res = api.CreateEvent(
3027 events_pb2.CreateEventReq(
3028 title="Invisible Attendee Reminder Test",
3029 content="Content.",
3030 offline_information=events_pb2.OfflineEventInformation(
3031 address="Near Null Island",
3032 lat=0.1,
3033 lng=0.2,
3034 ),
3035 start_time=Timestamp_from_datetime(start_time),
3036 end_time=Timestamp_from_datetime(end_time),
3037 timezone="UTC",
3038 )
3039 )
3040 event_id = res.event_id
3042 moderator.approve_event_occurrence(event_id)
3043 process_jobs()
3045 with events_session(token2) as api:
3046 api.SetEventAttendance(
3047 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3048 )
3050 with session_scope() as session:
3051 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3053 send_event_reminders(empty_pb2.Empty())
3054 process_jobs()
3056 with session_scope() as session:
3057 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3058 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3059 assert len(reminder_notifs) == 0
3062def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3063 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3064 user1, token1 = generate_user()
3065 user2, token2 = generate_user()
3067 with session_scope() as session:
3068 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3070 start = now()
3072 # User1 creates event A with 3 occurrences
3073 event_a_ids = []
3074 with events_session(token1) as api:
3075 res = api.CreateEvent(
3076 events_pb2.CreateEventReq(
3077 title="Event A",
3078 content="Content A.",
3079 parent_community_id=c_id,
3080 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3081 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
3082 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
3083 timezone="UTC",
3084 )
3085 )
3086 event_a_ids.append(res.event_id)
3087 for i in range(2):
3088 res = api.ScheduleEvent(
3089 events_pb2.ScheduleEventReq(
3090 event_id=event_a_ids[-1],
3091 content=f"A occurrence {i}",
3092 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3093 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
3094 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
3095 timezone="UTC",
3096 )
3097 )
3098 event_a_ids.append(res.event_id)
3100 # User2 creates event B with 2 occurrences
3101 event_b_ids = []
3102 with events_session(token2) as api:
3103 res = api.CreateEvent(
3104 events_pb2.CreateEventReq(
3105 title="Event B",
3106 content="Content B.",
3107 parent_community_id=c_id,
3108 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3109 start_time=Timestamp_from_datetime(start + timedelta(hours=10)),
3110 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)),
3111 timezone="UTC",
3112 )
3113 )
3114 event_b_ids.append(res.event_id)
3115 res = api.ScheduleEvent(
3116 events_pb2.ScheduleEventReq(
3117 event_id=event_b_ids[-1],
3118 content="B occurrence 1",
3119 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3120 start_time=Timestamp_from_datetime(start + timedelta(hours=11)),
3121 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)),
3122 timezone="UTC",
3123 )
3124 )
3125 event_b_ids.append(res.event_id)
3127 moderator.approve_event_occurrence(event_a_ids[0])
3128 moderator.approve_event_occurrence(event_b_ids[0])
3130 # List occurrences for event A — should only get event A's 3 occurrences
3131 with events_session(token1) as api:
3132 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3133 returned_ids = [e.event_id for e in res.events]
3134 assert sorted(returned_ids) == sorted(event_a_ids)
3136 # List occurrences for event B — should only get event B's 2 occurrences
3137 with events_session(token2) as api:
3138 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3139 returned_ids = [e.event_id for e in res.events]
3140 assert sorted(returned_ids) == sorted(event_b_ids)
3143def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3144 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3145 user1, token1 = generate_user()
3146 user2, token2 = generate_user()
3148 with session_scope() as session:
3149 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3151 start_time = now() + timedelta(hours=2)
3152 end_time = start_time + timedelta(hours=3)
3154 with events_session(token1) as api:
3155 res = api.CreateEvent(
3156 events_pb2.CreateEventReq(
3157 title="Comment Test",
3158 content="Content.",
3159 offline_information=events_pb2.OfflineEventInformation(
3160 address="Near Null Island",
3161 lat=0.1,
3162 lng=0.2,
3163 ),
3164 start_time=Timestamp_from_datetime(start_time),
3165 end_time=Timestamp_from_datetime(end_time),
3166 timezone="UTC",
3167 )
3168 )
3169 event_id = res.event_id
3170 thread_id = res.thread.thread_id
3172 moderator.approve_event_occurrence(event_id)
3173 process_jobs()
3174 while push_collector.count_for_user(user1.id): 3174 ↛ 3175line 3174 didn't jump to line 3175 because the condition on line 3174 was never true
3175 push_collector.pop_for_user(user1.id)
3177 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3178 with events_session(token1) as api:
3179 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3181 # User2 posts a top-level comment on the event thread
3182 with threads_session(token2) as api:
3183 comment_thread_id = api.PostReply(
3184 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3185 ).thread_id
3187 process_jobs()
3189 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3190 comment_db_id = comment_thread_id // 10
3191 with session_scope() as session:
3192 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3194 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3195 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3196 assert len(comment_notifs) == 1
3197 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3200def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3201 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3202 user1, token1 = generate_user()
3203 user2, token2 = generate_user()
3204 user3, token3 = generate_user()
3206 with session_scope() as session:
3207 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3209 start_time = now() + timedelta(hours=2)
3210 end_time = start_time + timedelta(hours=3)
3212 with events_session(token1) as api:
3213 res = api.CreateEvent(
3214 events_pb2.CreateEventReq(
3215 title="Reply Test",
3216 content="Content.",
3217 offline_information=events_pb2.OfflineEventInformation(
3218 address="Near Null Island",
3219 lat=0.1,
3220 lng=0.2,
3221 ),
3222 start_time=Timestamp_from_datetime(start_time),
3223 end_time=Timestamp_from_datetime(end_time),
3224 timezone="UTC",
3225 )
3226 )
3227 event_id = res.event_id
3228 thread_id = res.thread.thread_id
3230 moderator.approve_event_occurrence(event_id)
3231 process_jobs()
3232 while push_collector.count_for_user(user1.id): 3232 ↛ 3233line 3232 didn't jump to line 3233 because the condition on line 3232 was never true
3233 push_collector.pop_for_user(user1.id)
3235 # User2 posts a top-level comment
3236 with threads_session(token2) as api:
3237 comment_thread_id = api.PostReply(
3238 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3239 ).thread_id
3241 process_jobs()
3242 while push_collector.count_for_user(user1.id): 3242 ↛ 3243line 3242 didn't jump to line 3243 because the condition on line 3242 was never true
3243 push_collector.pop_for_user(user1.id)
3245 # User3 replies to user2's comment (depth=2 reply)
3246 with threads_session(token3) as api:
3247 nested_reply_thread_id = api.PostReply(
3248 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3249 ).thread_id
3251 process_jobs()
3253 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3254 nested_reply_db_id = nested_reply_thread_id // 10
3255 with session_scope() as session:
3256 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3258 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3259 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3260 assert len(reply_notifs) == 1
3261 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id