Coverage for app/backend/src/tests/test_events.py: 99%
1549 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +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_list_my_events_exclude_attending(db, moderator: Moderator):
1959 user1, token1 = generate_user()
1960 user2, token2 = generate_user()
1962 with session_scope() as session:
1963 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1964 c_id = c.id
1966 start = now()
1968 def make_event(hours):
1969 return events_pb2.CreateEventReq(
1970 title="Test Event",
1971 content="Test content.",
1972 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
1973 parent_community_id=c_id,
1974 timezone="UTC",
1975 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)),
1976 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)),
1977 )
1979 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1980 with events_session(token1) as api:
1981 e_own = api.CreateEvent(make_event(1)).event_id
1983 with events_session(token2) as api:
1984 e_attending = api.CreateEvent(make_event(2)).event_id
1985 e_community_only = api.CreateEvent(make_event(3)).event_id
1986 # e_both: user1 will be both organizer and attendee
1987 e_both = api.CreateEvent(make_event(4)).event_id
1989 moderator.approve_event_occurrence(e_own)
1990 moderator.approve_event_occurrence(e_attending)
1991 moderator.approve_event_occurrence(e_community_only)
1992 moderator.approve_event_occurrence(e_both)
1994 # invite user1 as organizer of e_both
1995 with events_session(token2) as api:
1996 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
1998 # user1 RSVPs to e_attending and e_both
1999 with events_session(token1) as api:
2000 api.SetEventAttendance(
2001 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2002 )
2003 api.SetEventAttendance(
2004 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2005 )
2007 with events_session(token1) as api:
2008 # baseline: all four community events visible
2009 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
2010 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
2012 # exclude_attending removes events user1 is attending (e_attending, e_both)
2013 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
2014 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
2015 assert [e.event_id for e in res.events] == [e_community_only]
2017 # exclude_attending with attending=True: invalid combination
2018 with pytest.raises(grpc.RpcError) as e:
2019 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
2020 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2022 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
2023 with events_session(token2) as api:
2024 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
2025 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
2026 assert [e.event_id for e in res.events] == [e_own]
2029def test_RemoveEventOrganizer(db, moderator: Moderator):
2030 user1, token1 = generate_user()
2031 user2, token2 = generate_user()
2033 with session_scope() as session:
2034 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2036 with events_session(token1) as api:
2037 event_id = api.CreateEvent(
2038 events_pb2.CreateEventReq(
2039 title="Dummy Title",
2040 content="Dummy content.",
2041 offline_information=events_pb2.OfflineEventInformation(
2042 address="Near Null Island",
2043 lat=0.1,
2044 lng=0.2,
2045 ),
2046 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2047 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2048 timezone="UTC",
2049 )
2050 ).event_id
2052 moderator.approve_event_occurrence(event_id)
2054 with events_session(token2) as api:
2055 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2057 with pytest.raises(grpc.RpcError) as e:
2058 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2059 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2060 assert e.value.details() == "You're not allowed to edit that event."
2062 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2064 with events_session(token1) as api:
2065 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2067 with pytest.raises(grpc.RpcError) as e:
2068 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2069 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2070 assert e.value.details() == "You cannot remove the event owner as an organizer."
2072 with events_session(token2) as api:
2073 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2074 assert res.organizer
2075 assert res.organizer_count == 2
2076 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2077 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2079 with pytest.raises(grpc.RpcError) as e:
2080 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2081 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2082 assert e.value.details() == "You're not allowed to edit that event."
2084 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2085 assert not res.organizer
2086 assert res.organizer_count == 1
2088 # Test that event owner can remove co-organizers
2089 with events_session(token1) as api:
2090 # Add user2 back as organizer
2091 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2093 # Verify user2 is now an organizer
2094 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2095 assert res.organizer_count == 2
2097 # Event owner can remove co-organizer
2098 api.RemoveEventOrganizer(
2099 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
2100 )
2102 # Verify user2 is no longer an organizer
2103 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2104 assert res.organizer_count == 1
2106 # Test that non-organizers cannot remove other organizers
2107 with events_session(token2) as api:
2108 # User2 cannot invite themselves as organizer (not the owner)
2109 with pytest.raises(grpc.RpcError) as e:
2110 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2111 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2112 assert e.value.details() == "You're not allowed to edit that event."
2114 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
2115 with events_session(token1) as api:
2116 # Add user2 back as organizer
2117 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2120def test_ListEventAttendees_regression(db):
2121 # see issue #1617:
2122 #
2123 # 1. Create an event
2124 # 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`
2125 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
2126 # 4. Change the current user's attendance state to "going" again
2127 #
2128 # **Expected behaviour**
2129 # `ListEventAttendees` should return the current user's ID
2130 #
2131 # **Actual/current behaviour**
2132 # `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
2134 user1, token1 = generate_user()
2135 user2, token2 = generate_user()
2136 user3, token3 = generate_user()
2137 user4, token4 = generate_user()
2138 user5, token5 = generate_user()
2140 with session_scope() as session:
2141 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2143 start_time = now() + timedelta(hours=2)
2144 end_time = start_time + timedelta(hours=3)
2146 with events_session(token1) as api:
2147 res = api.CreateEvent(
2148 events_pb2.CreateEventReq(
2149 title="Dummy Title",
2150 content="Dummy content.",
2151 online_information=events_pb2.OnlineEventInformation(
2152 link="https://couchers.org",
2153 ),
2154 parent_community_id=c_id,
2155 start_time=Timestamp_from_datetime(start_time),
2156 end_time=Timestamp_from_datetime(end_time),
2157 timezone="UTC",
2158 )
2159 )
2161 res = api.TransferEvent(
2162 events_pb2.TransferEventReq(
2163 event_id=res.event_id,
2164 new_owner_community_id=c_id,
2165 )
2166 )
2168 event_id = res.event_id
2170 api.SetEventAttendance(
2171 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2172 )
2173 api.SetEventAttendance(
2174 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2175 )
2177 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2178 assert len(res.attendee_user_ids) == 1
2179 assert res.attendee_user_ids[0] == user1.id
2182def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2183 user1, token1 = generate_user()
2184 user2, token2 = generate_user()
2185 user3, token3 = generate_user()
2186 user4, token4 = generate_user()
2188 with session_scope() as session:
2189 c = create_community(session, 0, 2, "Community", [user3], [], None)
2190 h = create_group(session, "Group", [user4], [], c)
2191 c_id = c.id
2192 h_id = h.id
2193 user4_id = user4.id
2195 with events_session(token1) as api:
2196 event = api.CreateEvent(
2197 events_pb2.CreateEventReq(
2198 title="Dummy Title",
2199 content="Dummy content.",
2200 offline_information=events_pb2.OfflineEventInformation(
2201 address="Near Null Island",
2202 lat=0.1,
2203 lng=0.2,
2204 ),
2205 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2206 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2207 timezone="UTC",
2208 )
2209 )
2211 moderator.approve_event_occurrence(event.event_id)
2213 with threads_session(token2) as api:
2214 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2216 moderator.approve_thread_post(reply_id)
2218 with events_session(token3) as api:
2219 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2220 assert res.thread.num_responses == 1
2222 with threads_session(token3) as api:
2223 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2224 assert len(ret.replies) == 1
2225 assert not ret.next_page_token
2226 assert ret.replies[0].thread_id == reply_id
2227 assert ret.replies[0].content == "hi"
2228 assert ret.replies[0].author_user_id == user2.id
2229 assert ret.replies[0].num_replies == 0
2231 nested_reply_id = api.PostReply(
2232 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2233 ).thread_id
2235 moderator.approve_thread_post(nested_reply_id)
2237 process_jobs()
2239 push = push_collector.pop_for_user(user1.id, last=True)
2240 assert push.topic_action == NotificationTopicAction.event__comment.display
2241 assert push.content.title == f"{user2.name} • Dummy Title"
2242 assert push.content.ios_title == user2.name
2243 assert push.content.ios_subtitle == "Commented on Dummy Title"
2244 assert push.content.body == "hi"
2246 push = push_collector.pop_for_user(user2.id, last=True)
2247 assert push.content.title == f"{user3.name} • Dummy Title"
2249 assert push_collector.count_for_user(user4_id) == 0
2252def test_can_overlap_other_events_schedule_regression(db):
2253 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2254 user, token = generate_user()
2256 with session_scope() as session:
2257 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2259 start = now()
2261 with events_session(token) as api:
2262 # create another event, should be able to overlap with this one
2263 api.CreateEvent(
2264 events_pb2.CreateEventReq(
2265 title="Dummy Title",
2266 content="Dummy content.",
2267 parent_community_id=c_id,
2268 online_information=events_pb2.OnlineEventInformation(
2269 link="https://couchers.org/meet/",
2270 ),
2271 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2272 end_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2273 timezone="UTC",
2274 )
2275 )
2277 # this event
2278 res = api.CreateEvent(
2279 events_pb2.CreateEventReq(
2280 title="Dummy Title",
2281 content="Dummy content.",
2282 parent_community_id=c_id,
2283 online_information=events_pb2.OnlineEventInformation(
2284 link="https://couchers.org/meet/",
2285 ),
2286 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2287 end_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2288 timezone="UTC",
2289 )
2290 )
2292 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2293 api.ScheduleEvent(
2294 events_pb2.ScheduleEventReq(
2295 event_id=res.event_id,
2296 content="New event occurrence",
2297 offline_information=events_pb2.OfflineEventInformation(
2298 address="A bit further but still near Null Island",
2299 lat=0.3,
2300 lng=0.2,
2301 ),
2302 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2303 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2304 timezone="UTC",
2305 )
2306 )
2309def test_can_overlap_other_events_update_regression(db):
2310 user, token = generate_user()
2312 with session_scope() as session:
2313 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2315 start = now()
2317 with events_session(token) as api:
2318 # create another event, should be able to overlap with this one
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=1)),
2328 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2329 timezone="UTC",
2330 )
2331 )
2333 res = api.CreateEvent(
2334 events_pb2.CreateEventReq(
2335 title="Dummy Title",
2336 content="Dummy content.",
2337 parent_community_id=c_id,
2338 online_information=events_pb2.OnlineEventInformation(
2339 link="https://couchers.org/meet/",
2340 ),
2341 start_time=Timestamp_from_datetime(start + timedelta(hours=7)),
2342 end_time=Timestamp_from_datetime(start + timedelta(hours=8)),
2343 timezone="UTC",
2344 )
2345 )
2347 event_id = api.ScheduleEvent(
2348 events_pb2.ScheduleEventReq(
2349 event_id=res.event_id,
2350 content="New event occurrence",
2351 offline_information=events_pb2.OfflineEventInformation(
2352 address="A bit further but still near Null Island",
2353 lat=0.3,
2354 lng=0.2,
2355 ),
2356 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2357 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2358 timezone="UTC",
2359 )
2360 ).event_id
2362 # can overlap with this current existing occurrence
2363 api.UpdateEvent(
2364 events_pb2.UpdateEventReq(
2365 event_id=event_id,
2366 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2367 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2368 )
2369 )
2371 api.UpdateEvent(
2372 events_pb2.UpdateEventReq(
2373 event_id=event_id,
2374 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2375 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2376 )
2377 )
2380def test_list_past_events_regression(db):
2381 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2382 user, token = generate_user()
2384 with session_scope() as session:
2385 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2387 start = now()
2389 with events_session(token) as api:
2390 api.CreateEvent(
2391 events_pb2.CreateEventReq(
2392 title="Dummy Title",
2393 content="Dummy content.",
2394 parent_community_id=c_id,
2395 online_information=events_pb2.OnlineEventInformation(
2396 link="https://couchers.org/meet/",
2397 ),
2398 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2399 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2400 timezone="UTC",
2401 )
2402 )
2404 with session_scope() as session:
2405 session.execute(
2406 update(EventOccurrence).values(
2407 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2408 )
2409 )
2411 with events_session(token) as api:
2412 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2413 assert len(res.events) == 1
2416def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2417 user1, token1 = generate_user(complete_profile=True)
2418 user2, token2 = generate_user()
2419 user3, token3 = generate_user()
2420 user4, token4 = generate_user()
2421 user5, token5 = generate_user(is_superuser=True)
2423 with session_scope() as session:
2424 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2425 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2426 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2427 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2429 enforce_community_memberships()
2431 with events_session(token1) as api:
2432 res = api.CreateEvent(
2433 events_pb2.CreateEventReq(
2434 title="Dummy Title",
2435 content="Dummy content.",
2436 parent_community_id=c_id,
2437 online_information=events_pb2.OnlineEventInformation(
2438 link="https://couchers.org/meet/",
2439 ),
2440 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)),
2441 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
2442 timezone="UTC",
2443 )
2444 )
2445 user_url = f"http://localhost:3000/user/{user1.username}"
2446 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2448 event_id = res.event_id
2450 moderator.approve_event_occurrence(event_id)
2452 with events_session(token1) as api:
2453 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2455 email = email_collector.pop_for_mods(last=True)
2457 assert user_url in email.plain
2458 assert event_url in email.plain
2460 # can't send another req
2461 with pytest.raises(grpc.RpcError) as err:
2462 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2463 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2464 assert err.value.details() == "You have already requested a community invite for this event."
2466 # another user can send one though
2467 with events_session(token3) as api:
2468 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2470 # but not a non-admin
2471 with events_session(token2) as api:
2472 with pytest.raises(grpc.RpcError) as err:
2473 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2474 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2475 assert err.value.details() == "You're not allowed to edit that event."
2477 with real_editor_session(token5) as editor:
2478 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2479 assert len(res.requests) == 2
2480 assert res.requests[0].user_id == user1.id
2481 assert res.requests[0].approx_users_to_notify == 3
2482 assert res.requests[1].user_id == user3.id
2483 assert res.requests[1].approx_users_to_notify == 3
2485 editor.DecideEventCommunityInviteRequest(
2486 editor_pb2.DecideEventCommunityInviteRequestReq(
2487 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2488 approve=False,
2489 )
2490 )
2492 editor.DecideEventCommunityInviteRequest(
2493 editor_pb2.DecideEventCommunityInviteRequestReq(
2494 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2495 approve=True,
2496 )
2497 )
2499 # not after approve
2500 with events_session(token4) as api:
2501 with pytest.raises(grpc.RpcError) as err:
2502 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2503 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2504 assert err.value.details() == "A community invite has already been sent out for this event."
2507def test_update_event_should_notify_queues_job():
2508 user, token = generate_user()
2509 start = now()
2511 with session_scope() as session:
2512 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2514 # create an event
2515 with events_session(token) as api:
2516 create_res = api.CreateEvent(
2517 events_pb2.CreateEventReq(
2518 title="Dummy Title",
2519 content="Dummy content.",
2520 parent_community_id=c_id,
2521 offline_information=events_pb2.OfflineEventInformation(
2522 address="https://couchers.org/meet/",
2523 lat=1.0,
2524 lng=2.0,
2525 ),
2526 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2527 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2528 timezone="UTC",
2529 )
2530 )
2532 event_id = create_res.event_id
2534 # measure initial background job queue length
2535 with session_scope() as session:
2536 jobs = session.query(BackgroundJob).all()
2537 job_length_before_update = len(jobs)
2539 # update with should_notify=False, expect no change in background job queue
2540 api.UpdateEvent(
2541 events_pb2.UpdateEventReq(
2542 event_id=event_id,
2543 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2544 should_notify=False,
2545 )
2546 )
2548 with session_scope() as session:
2549 jobs = session.query(BackgroundJob).all()
2550 assert len(jobs) == job_length_before_update
2552 # update with should_notify=True, expect one new background job added
2553 api.UpdateEvent(
2554 events_pb2.UpdateEventReq(
2555 event_id=event_id,
2556 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2557 should_notify=True,
2558 )
2559 )
2561 with session_scope() as session:
2562 jobs = session.query(BackgroundJob).all()
2563 assert len(jobs) == job_length_before_update + 1
2566def test_event_photo_key(db):
2567 """Test that events return the photo_key field when a photo is set."""
2568 user, token = generate_user()
2570 start_time = now() + timedelta(hours=2)
2571 end_time = start_time + timedelta(hours=3)
2573 # Create a community and an upload for the event photo
2574 with session_scope() as session:
2575 create_community(session, 0, 2, "Community", [user], [], None)
2576 upload = Upload(
2577 key="test_event_photo_key_123",
2578 filename="test_event_photo_key_123.jpg",
2579 creator_user_id=user.id,
2580 )
2581 session.add(upload)
2583 with events_session(token) as api:
2584 # Create event without photo
2585 res = api.CreateEvent(
2586 events_pb2.CreateEventReq(
2587 title="Event Without Photo",
2588 content="No photo content.",
2589 photo_key=None,
2590 offline_information=events_pb2.OfflineEventInformation(
2591 address="Near Null Island",
2592 lat=0.1,
2593 lng=0.2,
2594 ),
2595 start_time=Timestamp_from_datetime(start_time),
2596 end_time=Timestamp_from_datetime(end_time),
2597 timezone="UTC",
2598 )
2599 )
2601 assert res.photo_key == ""
2602 assert res.photo_url == ""
2604 # Create event with photo
2605 res_with_photo = api.CreateEvent(
2606 events_pb2.CreateEventReq(
2607 title="Event With Photo",
2608 content="Has photo content.",
2609 photo_key="test_event_photo_key_123",
2610 offline_information=events_pb2.OfflineEventInformation(
2611 address="Near Null Island",
2612 lat=0.1,
2613 lng=0.2,
2614 ),
2615 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)),
2616 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)),
2617 timezone="UTC",
2618 )
2619 )
2621 assert res_with_photo.photo_key == "test_event_photo_key_123"
2622 assert "test_event_photo_key_123" in res_with_photo.photo_url
2624 event_id = res_with_photo.event_id
2626 # Verify photo_key is returned when getting the event
2627 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2628 assert get_res.photo_key == "test_event_photo_key_123"
2629 assert "test_event_photo_key_123" in get_res.photo_url
2632def test_event_created_with_shadowed_visibility(db):
2633 """Events start in SHADOWED state when created."""
2634 user, token = generate_user()
2636 with session_scope() as session:
2637 create_community(session, 0, 2, "Community", [user], [], None)
2639 start_time = now() + timedelta(hours=2)
2640 end_time = start_time + timedelta(hours=3)
2642 with events_session(token) as api:
2643 res = api.CreateEvent(
2644 events_pb2.CreateEventReq(
2645 title="Test UMS Event",
2646 content="UMS content.",
2647 offline_information=events_pb2.OfflineEventInformation(
2648 address="Near Null Island",
2649 lat=0.1,
2650 lng=0.2,
2651 ),
2652 start_time=Timestamp_from_datetime(start_time),
2653 end_time=Timestamp_from_datetime(end_time),
2654 timezone="UTC",
2655 )
2656 )
2657 event_id = res.event_id
2659 with session_scope() as session:
2660 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2661 mod_state = session.execute(
2662 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2663 ).scalar_one()
2664 assert mod_state.visibility == ModerationVisibility.shadowed
2667def test_shadowed_event_visible_to_creator_only(db):
2668 """SHADOWED events are visible to the creator but not to other users."""
2669 user1, token1 = generate_user()
2670 user2, token2 = generate_user()
2672 with session_scope() as session:
2673 create_community(session, 0, 2, "Community", [user1], [], None)
2675 start_time = now() + timedelta(hours=2)
2676 end_time = start_time + timedelta(hours=3)
2678 with events_session(token1) as api:
2679 res = api.CreateEvent(
2680 events_pb2.CreateEventReq(
2681 title="Shadowed Event",
2682 content="Content.",
2683 offline_information=events_pb2.OfflineEventInformation(
2684 address="Near Null Island",
2685 lat=0.1,
2686 lng=0.2,
2687 ),
2688 start_time=Timestamp_from_datetime(start_time),
2689 end_time=Timestamp_from_datetime(end_time),
2690 timezone="UTC",
2691 )
2692 )
2693 event_id = res.event_id
2695 # Creator can see it
2696 with events_session(token1) as api:
2697 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2698 assert res.title == "Shadowed Event"
2700 # Other user cannot
2701 with events_session(token2) as api:
2702 with pytest.raises(grpc.RpcError) as e:
2703 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2704 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2707def test_event_visible_after_approval(db, moderator: Moderator):
2708 """Events become visible to all users after moderation approval."""
2709 user1, token1 = generate_user()
2710 user2, token2 = generate_user()
2712 with session_scope() as session:
2713 create_community(session, 0, 2, "Community", [user1], [], None)
2715 start_time = now() + timedelta(hours=2)
2716 end_time = start_time + timedelta(hours=3)
2718 with events_session(token1) as api:
2719 res = api.CreateEvent(
2720 events_pb2.CreateEventReq(
2721 title="Approved Event",
2722 content="Content.",
2723 offline_information=events_pb2.OfflineEventInformation(
2724 address="Near Null Island",
2725 lat=0.1,
2726 lng=0.2,
2727 ),
2728 start_time=Timestamp_from_datetime(start_time),
2729 end_time=Timestamp_from_datetime(end_time),
2730 timezone="UTC",
2731 )
2732 )
2733 event_id = res.event_id
2735 # Other user cannot see it yet
2736 with events_session(token2) as api:
2737 with pytest.raises(grpc.RpcError) as e:
2738 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2739 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2741 # Approve the event
2742 moderator.approve_event_occurrence(event_id)
2744 # Now other user can see it
2745 with events_session(token2) as api:
2746 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2747 assert res.title == "Approved Event"
2750def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2751 """SHADOWED events appear in lists for the creator but not for other users."""
2752 user1, token1 = generate_user()
2753 user2, token2 = generate_user()
2755 with session_scope() as session:
2756 create_community(session, 0, 2, "Community", [user1], [], None)
2758 start_time = now() + timedelta(hours=2)
2759 end_time = start_time + timedelta(hours=3)
2761 with events_session(token1) as api:
2762 res = api.CreateEvent(
2763 events_pb2.CreateEventReq(
2764 title="List Test Event",
2765 content="Content.",
2766 offline_information=events_pb2.OfflineEventInformation(
2767 address="Near Null Island",
2768 lat=0.1,
2769 lng=0.2,
2770 ),
2771 start_time=Timestamp_from_datetime(start_time),
2772 end_time=Timestamp_from_datetime(end_time),
2773 timezone="UTC",
2774 )
2775 )
2776 event_id = res.event_id
2778 # Creator can see their own SHADOWED event in lists
2779 with events_session(token1) as api:
2780 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2781 event_ids = [e.event_id for e in list_res.events]
2782 assert event_id in event_ids
2784 # Other user cannot see the SHADOWED event in lists
2785 with events_session(token2) as api:
2786 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2787 event_ids = [e.event_id for e in list_res.events]
2788 assert event_id not in event_ids
2790 # After approval, other user can see it
2791 moderator.approve_event_occurrence(event_id)
2793 with events_session(token2) as api:
2794 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2795 event_ids = [e.event_id for e in list_res.events]
2796 assert event_id in event_ids
2799def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2800 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2801 user1, token1 = generate_user()
2802 user2, token2 = generate_user()
2804 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2805 with session_scope() as session:
2806 world = create_community(session, 0, 10, "World", [user1], [], None)
2807 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2808 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2809 create_community(session, 0, 2, "Child", [user2], [], region)
2811 start_time = now() + timedelta(hours=2)
2812 end_time = start_time + timedelta(hours=3)
2814 with events_session(token1) as api:
2815 res = api.CreateEvent(
2816 events_pb2.CreateEventReq(
2817 title="Deferred Event",
2818 content="Content.",
2819 offline_information=events_pb2.OfflineEventInformation(
2820 address="Near Null Island",
2821 lat=0.1,
2822 lng=0.2,
2823 ),
2824 start_time=Timestamp_from_datetime(start_time),
2825 end_time=Timestamp_from_datetime(end_time),
2826 timezone="UTC",
2827 )
2828 )
2829 event_id = res.event_id
2831 # Process all jobs — notification should be deferred (event is SHADOWED)
2832 process_jobs()
2834 with session_scope() as session:
2835 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2836 # Notification was created with moderation_state_id for deferral
2837 assert notif.moderation_state_id is not None
2838 # No delivery exists (deferred because event is SHADOWED)
2839 delivery_count = session.execute(
2840 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2841 ).scalar_one_or_none()
2842 assert delivery_count is None
2844 # Approve the event — handle_notification is re-queued for deferred notifications
2845 moderator.approve_event_occurrence(event_id)
2847 # Verify handle_notification job was queued
2848 with session_scope() as session:
2849 pending_jobs = (
2850 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2851 .scalars()
2852 .all()
2853 )
2854 assert any("handle_notification" in j.job_type for j in pending_jobs)
2857def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2858 """Event update notifications should carry the event's moderation_state_id for deferral."""
2859 user1, token1 = generate_user()
2860 user2, token2 = generate_user()
2862 with session_scope() as session:
2863 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2865 start_time = now() + timedelta(hours=2)
2866 end_time = start_time + timedelta(hours=3)
2868 with events_session(token1) as api:
2869 res = api.CreateEvent(
2870 events_pb2.CreateEventReq(
2871 title="Update Test",
2872 content="Content.",
2873 offline_information=events_pb2.OfflineEventInformation(
2874 address="Near Null Island",
2875 lat=0.1,
2876 lng=0.2,
2877 ),
2878 start_time=Timestamp_from_datetime(start_time),
2879 end_time=Timestamp_from_datetime(end_time),
2880 timezone="UTC",
2881 )
2882 )
2883 event_id = res.event_id
2885 moderator.approve_event_occurrence(event_id)
2886 process_jobs()
2887 # Clear any create notifications
2888 while push_collector.count_for_user(user2.id): 2888 ↛ 2889line 2888 didn't jump to line 2889 because the condition on line 2888 was never true
2889 push_collector.pop_for_user(user2.id)
2891 # User2 subscribes to the event
2892 with events_session(token2) as api:
2893 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2895 # User1 updates the event with should_notify=True
2896 with events_session(token1) as api:
2897 api.UpdateEvent(
2898 events_pb2.UpdateEventReq(
2899 event_id=event_id,
2900 title=wrappers_pb2.StringValue(value="Updated Title"),
2901 should_notify=True,
2902 )
2903 )
2905 process_jobs()
2907 # Verify that the update notification for user2 has moderation_state_id set
2908 with session_scope() as session:
2909 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2911 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2912 # Find the update notification (most recent one)
2913 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2914 assert len(update_notifs) == 1
2915 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2918def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2919 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2920 user1, token1 = generate_user()
2921 user2, token2 = generate_user()
2923 with session_scope() as session:
2924 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2926 start_time = now() + timedelta(hours=2)
2927 end_time = start_time + timedelta(hours=3)
2929 with events_session(token1) as api:
2930 res = api.CreateEvent(
2931 events_pb2.CreateEventReq(
2932 title="Cancel Test",
2933 content="Content.",
2934 offline_information=events_pb2.OfflineEventInformation(
2935 address="Near Null Island",
2936 lat=0.1,
2937 lng=0.2,
2938 ),
2939 start_time=Timestamp_from_datetime(start_time),
2940 end_time=Timestamp_from_datetime(end_time),
2941 timezone="UTC",
2942 )
2943 )
2944 event_id = res.event_id
2946 moderator.approve_event_occurrence(event_id)
2947 process_jobs()
2948 while push_collector.count_for_user(user2.id): 2948 ↛ 2949line 2948 didn't jump to line 2949 because the condition on line 2948 was never true
2949 push_collector.pop_for_user(user2.id)
2951 # User2 subscribes
2952 with events_session(token2) as api:
2953 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2955 # User1 cancels the event
2956 with events_session(token1) as api:
2957 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2959 process_jobs()
2961 # Verify that the cancel notification for user2 has moderation_state_id set
2962 with session_scope() as session:
2963 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2965 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2966 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2967 assert len(cancel_notifs) == 1
2968 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2971def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2972 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2973 user1, token1 = generate_user()
2974 user2, token2 = generate_user()
2976 with session_scope() as session:
2977 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2979 # Create event starting 23 hours from now (within 24h reminder window)
2980 start_time = now() + timedelta(hours=23)
2981 end_time = start_time + timedelta(hours=1)
2983 with events_session(token1) as api:
2984 res = api.CreateEvent(
2985 events_pb2.CreateEventReq(
2986 title="Reminder Test",
2987 content="Content.",
2988 offline_information=events_pb2.OfflineEventInformation(
2989 address="Near Null Island",
2990 lat=0.1,
2991 lng=0.2,
2992 ),
2993 start_time=Timestamp_from_datetime(start_time),
2994 end_time=Timestamp_from_datetime(end_time),
2995 timezone="UTC",
2996 )
2997 )
2998 event_id = res.event_id
3000 moderator.approve_event_occurrence(event_id)
3001 process_jobs()
3002 while push_collector.count_for_user(user2.id): 3002 ↛ 3003line 3002 didn't jump to line 3003 because the condition on line 3002 was never true
3003 push_collector.pop_for_user(user2.id)
3005 # User2 marks attendance
3006 with events_session(token2) as api:
3007 api.SetEventAttendance(
3008 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3009 )
3011 # Run the event reminder handler
3012 send_event_reminders(empty_pb2.Empty())
3013 process_jobs()
3015 # Verify that the reminder notification for user2 has moderation_state_id set
3016 with session_scope() as session:
3017 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3019 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3020 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
3021 assert len(reminder_notifs) == 1
3022 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
3025def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
3026 """Event reminders should not be sent for cancelled events."""
3027 user1, token1 = generate_user()
3028 user2, token2 = generate_user()
3030 with session_scope() as session:
3031 create_community(session, 0, 2, "Community", [user2], [], None)
3033 # Create event starting 23 hours from now (within 24h reminder window)
3034 start_time = now() + timedelta(hours=23)
3035 end_time = start_time + timedelta(hours=1)
3037 with events_session(token1) as api:
3038 res = api.CreateEvent(
3039 events_pb2.CreateEventReq(
3040 title="Cancelled Reminder Test",
3041 content="Content.",
3042 offline_information=events_pb2.OfflineEventInformation(
3043 address="Near Null Island",
3044 lat=0.1,
3045 lng=0.2,
3046 ),
3047 start_time=Timestamp_from_datetime(start_time),
3048 end_time=Timestamp_from_datetime(end_time),
3049 timezone="UTC",
3050 )
3051 )
3052 event_id = res.event_id
3054 moderator.approve_event_occurrence(event_id)
3055 process_jobs()
3057 # User2 marks attendance
3058 with events_session(token2) as api:
3059 api.SetEventAttendance(
3060 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3061 )
3063 # User1 cancels the event
3064 with events_session(token1) as api:
3065 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3067 process_jobs()
3068 # Drain any cancellation-related notifications so we can cleanly assert on reminders
3069 while push_collector.count_for_user(user2.id):
3070 push_collector.pop_for_user(user2.id)
3072 # Run the event reminder handler
3073 send_event_reminders(empty_pb2.Empty())
3074 process_jobs()
3076 # Verify that no reminder notification was sent for user2
3077 with session_scope() as session:
3078 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3079 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3080 assert len(reminder_notifs) == 0
3083@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
3084def test_event_reminder_not_sent_for_invisible_attendee(
3085 db, push_collector: PushCollector, moderator: Moderator, invisible_field
3086):
3087 user1, token1 = generate_user()
3088 user2, token2 = generate_user()
3090 with session_scope() as session:
3091 create_community(session, 0, 2, "Community", [user2], [], None)
3093 start_time = now() + timedelta(hours=23)
3094 end_time = start_time + timedelta(hours=1)
3096 with events_session(token1) as api:
3097 res = api.CreateEvent(
3098 events_pb2.CreateEventReq(
3099 title="Invisible Attendee Reminder Test",
3100 content="Content.",
3101 offline_information=events_pb2.OfflineEventInformation(
3102 address="Near Null Island",
3103 lat=0.1,
3104 lng=0.2,
3105 ),
3106 start_time=Timestamp_from_datetime(start_time),
3107 end_time=Timestamp_from_datetime(end_time),
3108 timezone="UTC",
3109 )
3110 )
3111 event_id = res.event_id
3113 moderator.approve_event_occurrence(event_id)
3114 process_jobs()
3116 with events_session(token2) as api:
3117 api.SetEventAttendance(
3118 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3119 )
3121 with session_scope() as session:
3122 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3124 send_event_reminders(empty_pb2.Empty())
3125 process_jobs()
3127 with session_scope() as session:
3128 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3129 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3130 assert len(reminder_notifs) == 0
3133def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3134 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3135 user1, token1 = generate_user()
3136 user2, token2 = generate_user()
3138 with session_scope() as session:
3139 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3141 start = now()
3143 # User1 creates event A with 3 occurrences
3144 event_a_ids = []
3145 with events_session(token1) as api:
3146 res = api.CreateEvent(
3147 events_pb2.CreateEventReq(
3148 title="Event A",
3149 content="Content A.",
3150 parent_community_id=c_id,
3151 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3152 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
3153 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
3154 timezone="UTC",
3155 )
3156 )
3157 event_a_ids.append(res.event_id)
3158 for i in range(2):
3159 res = api.ScheduleEvent(
3160 events_pb2.ScheduleEventReq(
3161 event_id=event_a_ids[-1],
3162 content=f"A occurrence {i}",
3163 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3164 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
3165 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
3166 timezone="UTC",
3167 )
3168 )
3169 event_a_ids.append(res.event_id)
3171 # User2 creates event B with 2 occurrences
3172 event_b_ids = []
3173 with events_session(token2) as api:
3174 res = api.CreateEvent(
3175 events_pb2.CreateEventReq(
3176 title="Event B",
3177 content="Content B.",
3178 parent_community_id=c_id,
3179 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3180 start_time=Timestamp_from_datetime(start + timedelta(hours=10)),
3181 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)),
3182 timezone="UTC",
3183 )
3184 )
3185 event_b_ids.append(res.event_id)
3186 res = api.ScheduleEvent(
3187 events_pb2.ScheduleEventReq(
3188 event_id=event_b_ids[-1],
3189 content="B occurrence 1",
3190 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3191 start_time=Timestamp_from_datetime(start + timedelta(hours=11)),
3192 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)),
3193 timezone="UTC",
3194 )
3195 )
3196 event_b_ids.append(res.event_id)
3198 moderator.approve_event_occurrence(event_a_ids[0])
3199 moderator.approve_event_occurrence(event_b_ids[0])
3201 # List occurrences for event A — should only get event A's 3 occurrences
3202 with events_session(token1) as api:
3203 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3204 returned_ids = [e.event_id for e in res.events]
3205 assert sorted(returned_ids) == sorted(event_a_ids)
3207 # List occurrences for event B — should only get event B's 2 occurrences
3208 with events_session(token2) as api:
3209 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3210 returned_ids = [e.event_id for e in res.events]
3211 assert sorted(returned_ids) == sorted(event_b_ids)
3214def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3215 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3216 user1, token1 = generate_user()
3217 user2, token2 = generate_user()
3219 with session_scope() as session:
3220 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3222 start_time = now() + timedelta(hours=2)
3223 end_time = start_time + timedelta(hours=3)
3225 with events_session(token1) as api:
3226 res = api.CreateEvent(
3227 events_pb2.CreateEventReq(
3228 title="Comment Test",
3229 content="Content.",
3230 offline_information=events_pb2.OfflineEventInformation(
3231 address="Near Null Island",
3232 lat=0.1,
3233 lng=0.2,
3234 ),
3235 start_time=Timestamp_from_datetime(start_time),
3236 end_time=Timestamp_from_datetime(end_time),
3237 timezone="UTC",
3238 )
3239 )
3240 event_id = res.event_id
3241 thread_id = res.thread.thread_id
3243 moderator.approve_event_occurrence(event_id)
3244 process_jobs()
3245 while push_collector.count_for_user(user1.id): 3245 ↛ 3246line 3245 didn't jump to line 3246 because the condition on line 3245 was never true
3246 push_collector.pop_for_user(user1.id)
3248 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3249 with events_session(token1) as api:
3250 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3252 # User2 posts a top-level comment on the event thread
3253 with threads_session(token2) as api:
3254 comment_thread_id = api.PostReply(
3255 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3256 ).thread_id
3258 process_jobs()
3260 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3261 comment_db_id = comment_thread_id // 10
3262 with session_scope() as session:
3263 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3265 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3266 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3267 assert len(comment_notifs) == 1
3268 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3271def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3272 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3273 user1, token1 = generate_user()
3274 user2, token2 = generate_user()
3275 user3, token3 = generate_user()
3277 with session_scope() as session:
3278 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3280 start_time = now() + timedelta(hours=2)
3281 end_time = start_time + timedelta(hours=3)
3283 with events_session(token1) as api:
3284 res = api.CreateEvent(
3285 events_pb2.CreateEventReq(
3286 title="Reply Test",
3287 content="Content.",
3288 offline_information=events_pb2.OfflineEventInformation(
3289 address="Near Null Island",
3290 lat=0.1,
3291 lng=0.2,
3292 ),
3293 start_time=Timestamp_from_datetime(start_time),
3294 end_time=Timestamp_from_datetime(end_time),
3295 timezone="UTC",
3296 )
3297 )
3298 event_id = res.event_id
3299 thread_id = res.thread.thread_id
3301 moderator.approve_event_occurrence(event_id)
3302 process_jobs()
3303 while push_collector.count_for_user(user1.id): 3303 ↛ 3304line 3303 didn't jump to line 3304 because the condition on line 3303 was never true
3304 push_collector.pop_for_user(user1.id)
3306 # User2 posts a top-level comment
3307 with threads_session(token2) as api:
3308 comment_thread_id = api.PostReply(
3309 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3310 ).thread_id
3312 process_jobs()
3313 while push_collector.count_for_user(user1.id): 3313 ↛ 3314line 3313 didn't jump to line 3314 because the condition on line 3313 was never true
3314 push_collector.pop_for_user(user1.id)
3316 # User3 replies to user2's comment (depth=2 reply)
3317 with threads_session(token3) as api:
3318 nested_reply_thread_id = api.PostReply(
3319 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3320 ).thread_id
3322 process_jobs()
3324 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3325 nested_reply_db_id = nested_reply_thread_id // 10
3326 with session_scope() as session:
3327 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3329 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3330 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3331 assert len(reply_notifs) == 1
3332 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id