Coverage for app/backend/src/tests/test_events.py: 99%
1380 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 04:10 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 04:10 +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't create event that starts in the past
45 # can create in different timezones
47 # event creator
48 user1, token1 = generate_user()
49 # community moderator
50 user2, token2 = generate_user()
51 # third party
52 user3, token3 = generate_user()
54 with session_scope() as session:
55 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
57 time_before = now()
58 start_time = now() + timedelta(hours=2)
59 end_time = start_time + timedelta(hours=3)
61 # Can create an event
62 with events_session(token1) as api:
63 res = api.CreateEvent(
64 events_pb2.CreateEventReq(
65 title="Dummy Title",
66 content="Dummy content.",
67 photo_key=None,
68 location=events_pb2.EventLocation(
69 address="Near Null Island",
70 lat=0.1,
71 lng=0.2,
72 ),
73 start_time=Timestamp_from_datetime(start_time),
74 end_time=Timestamp_from_datetime(end_time),
75 timezone="UTC",
76 )
77 )
79 assert res.is_next
80 assert res.title == "Dummy Title"
81 assert res.slug == "dummy-title"
82 assert res.content == "Dummy content."
83 assert not res.photo_url
84 assert res.HasField("location")
85 assert res.location.lat == 0.1
86 assert res.location.lng == 0.2
87 assert res.location.address == "Near Null Island"
88 assert time_before <= to_aware_datetime(res.created) <= now()
89 assert time_before <= to_aware_datetime(res.last_edited) <= now()
90 assert res.creator_user_id == user1.id
91 assert to_aware_datetime(res.start_time) == start_time
92 assert to_aware_datetime(res.end_time) == end_time
93 # assert res.timezone == "UTC"
94 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
95 assert res.organizer
96 assert res.subscriber
97 assert res.going_count == 1
98 assert res.organizer_count == 1
99 assert res.subscriber_count == 1
100 assert res.owner_user_id == user1.id
101 assert not res.owner_community_id
102 assert not res.owner_group_id
103 assert res.thread.thread_id
104 assert res.can_edit
105 assert not res.can_moderate
107 event_id = res.event_id
109 # Approve the event so other users can see it
110 moderator.approve_event_occurrence(event_id)
112 with events_session(token2) as api:
113 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
115 assert res.is_next
116 assert res.title == "Dummy Title"
117 assert res.slug == "dummy-title"
118 assert res.content == "Dummy content."
119 assert not res.photo_url
120 assert res.HasField("location")
121 assert res.location.lat == 0.1
122 assert res.location.lng == 0.2
123 assert res.location.address == "Near Null Island"
124 assert time_before <= to_aware_datetime(res.created) <= now()
125 assert time_before <= to_aware_datetime(res.last_edited) <= now()
126 assert res.creator_user_id == user1.id
127 assert to_aware_datetime(res.start_time) == start_time
128 assert to_aware_datetime(res.end_time) == end_time
129 # assert res.timezone == "UTC"
130 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
131 assert not res.organizer
132 assert not res.subscriber
133 assert res.going_count == 1
134 assert res.organizer_count == 1
135 assert res.subscriber_count == 1
136 assert res.owner_user_id == user1.id
137 assert not res.owner_community_id
138 assert not res.owner_group_id
139 assert res.thread.thread_id
140 assert res.can_edit
141 assert res.can_moderate
143 with events_session(token3) as api:
144 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
146 assert res.is_next
147 assert res.title == "Dummy Title"
148 assert res.slug == "dummy-title"
149 assert res.content == "Dummy content."
150 assert not res.photo_url
151 assert res.HasField("location")
152 assert res.location.lat == 0.1
153 assert res.location.lng == 0.2
154 assert res.location.address == "Near Null Island"
155 assert time_before <= to_aware_datetime(res.created) <= now()
156 assert time_before <= to_aware_datetime(res.last_edited) <= now()
157 assert res.creator_user_id == user1.id
158 assert to_aware_datetime(res.start_time) == start_time
159 assert to_aware_datetime(res.end_time) == end_time
160 # assert res.timezone == "UTC"
161 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
162 assert not res.organizer
163 assert not res.subscriber
164 assert res.going_count == 1
165 assert res.organizer_count == 1
166 assert res.subscriber_count == 1
167 assert res.owner_user_id == user1.id
168 assert not res.owner_community_id
169 assert not res.owner_group_id
170 assert res.thread.thread_id
171 assert not res.can_edit
172 assert not res.can_moderate
174 # Failure cases
175 with events_session(token1) as api:
176 with pytest.raises(grpc.RpcError) as e:
177 api.CreateEvent(
178 events_pb2.CreateEventReq(
179 # title="Dummy Title",
180 content="Dummy content.",
181 photo_key=None,
182 location=events_pb2.EventLocation(
183 address="Near Null Island",
184 lat=0.1,
185 lng=0.2,
186 ),
187 start_time=Timestamp_from_datetime(start_time),
188 end_time=Timestamp_from_datetime(end_time),
189 timezone="UTC",
190 )
191 )
192 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
193 assert e.value.details() == "Missing event title."
195 with pytest.raises(grpc.RpcError) as e:
196 api.CreateEvent(
197 events_pb2.CreateEventReq(
198 title="Dummy Title",
199 # content="Dummy content.",
200 photo_key=None,
201 location=events_pb2.EventLocation(
202 address="Near Null Island",
203 lat=0.1,
204 lng=0.2,
205 ),
206 start_time=Timestamp_from_datetime(start_time),
207 end_time=Timestamp_from_datetime(end_time),
208 timezone="UTC",
209 )
210 )
211 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
212 assert e.value.details() == "Missing event content."
214 with pytest.raises(grpc.RpcError) as e:
215 api.CreateEvent(
216 events_pb2.CreateEventReq(
217 title="Dummy Title",
218 content="Dummy content.",
219 photo_key="nonexistent",
220 location=events_pb2.EventLocation(
221 address="Near Null Island",
222 lat=0.1,
223 lng=0.2,
224 ),
225 start_time=Timestamp_from_datetime(start_time),
226 end_time=Timestamp_from_datetime(end_time),
227 timezone="UTC",
228 )
229 )
230 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
231 assert e.value.details() == "Photo not found."
233 with pytest.raises(grpc.RpcError) as e:
234 api.CreateEvent(
235 events_pb2.CreateEventReq(
236 title="Dummy Title",
237 content="Dummy content.",
238 location=events_pb2.EventLocation(
239 address="Near Null Island",
240 ),
241 start_time=Timestamp_from_datetime(start_time),
242 end_time=Timestamp_from_datetime(end_time),
243 timezone="UTC",
244 )
245 )
246 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
247 assert e.value.details() == "Missing event address or location."
249 with pytest.raises(grpc.RpcError) as e:
250 api.CreateEvent(
251 events_pb2.CreateEventReq(
252 title="Dummy Title",
253 content="Dummy content.",
254 location=events_pb2.EventLocation(
255 lat=0.1,
256 lng=0.1,
257 ),
258 start_time=Timestamp_from_datetime(start_time),
259 end_time=Timestamp_from_datetime(end_time),
260 timezone="UTC",
261 )
262 )
263 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
264 assert e.value.details() == "Missing event address or location."
266 with pytest.raises(grpc.RpcError) as e:
267 api.CreateEvent(
268 events_pb2.CreateEventReq(
269 title="Dummy Title",
270 content="Dummy content.",
271 location=events_pb2.EventLocation(
272 address="Near Null Island",
273 lat=0.1,
274 lng=0.2,
275 ),
276 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)),
277 end_time=Timestamp_from_datetime(end_time),
278 timezone="UTC",
279 )
280 )
281 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
282 assert e.value.details() == "The event must be in the future."
284 with pytest.raises(grpc.RpcError) as e:
285 api.CreateEvent(
286 events_pb2.CreateEventReq(
287 title="Dummy Title",
288 content="Dummy content.",
289 location=events_pb2.EventLocation(
290 address="Near Null Island",
291 lat=0.1,
292 lng=0.2,
293 ),
294 start_time=Timestamp_from_datetime(end_time),
295 end_time=Timestamp_from_datetime(start_time),
296 timezone="UTC",
297 )
298 )
299 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
300 assert e.value.details() == "The event must end after it starts."
302 with pytest.raises(grpc.RpcError) as e:
303 api.CreateEvent(
304 events_pb2.CreateEventReq(
305 title="Dummy Title",
306 content="Dummy content.",
307 location=events_pb2.EventLocation(
308 address="Near Null Island",
309 lat=0.1,
310 lng=0.2,
311 ),
312 start_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)),
313 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)),
314 timezone="UTC",
315 )
316 )
317 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
318 assert e.value.details() == "The event needs to start within the next year."
320 with pytest.raises(grpc.RpcError) as e:
321 api.CreateEvent(
322 events_pb2.CreateEventReq(
323 title="Dummy Title",
324 content="Dummy content.",
325 location=events_pb2.EventLocation(
326 address="Near Null Island",
327 lat=0.1,
328 lng=0.2,
329 ),
330 start_time=Timestamp_from_datetime(start_time),
331 end_time=Timestamp_from_datetime(now() + timedelta(days=100)),
332 timezone="UTC",
333 )
334 )
335 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
336 assert e.value.details() == "Events cannot last longer than 7 days."
339def test_CreateEvent_incomplete_profile(db):
340 user1, token1 = generate_user(complete_profile=False)
341 user2, token2 = generate_user()
343 with session_scope() as session:
344 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
346 start_time = now() + timedelta(hours=2)
347 end_time = start_time + timedelta(hours=3)
349 with events_session(token1) as api:
350 with pytest.raises(grpc.RpcError) as e:
351 api.CreateEvent(
352 events_pb2.CreateEventReq(
353 title="Dummy Title",
354 content="Dummy content.",
355 photo_key=None,
356 location=events_pb2.EventLocation(
357 address="Near Null Island",
358 lat=0.1,
359 lng=0.2,
360 ),
361 start_time=Timestamp_from_datetime(start_time),
362 end_time=Timestamp_from_datetime(end_time),
363 timezone="UTC",
364 )
365 )
366 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
367 assert e.value.details() == "You have to complete your profile before you can create an event."
370def test_ScheduleEvent(db):
371 # test cases:
372 # can schedule a new event occurrence
374 user, token = generate_user()
376 with session_scope() as session:
377 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
379 time_before = now()
380 start_time = now() + timedelta(hours=2)
381 end_time = start_time + timedelta(hours=3)
383 with events_session(token) as api:
384 res = api.CreateEvent(
385 events_pb2.CreateEventReq(
386 title="Dummy Title",
387 content="Dummy content.",
388 parent_community_id=c_id,
389 location=events_pb2.EventLocation(
390 address="Near Null Island",
391 lat=0.1,
392 lng=0.2,
393 ),
394 start_time=Timestamp_from_datetime(start_time),
395 end_time=Timestamp_from_datetime(end_time),
396 timezone="UTC",
397 )
398 )
400 new_start_time = now() + timedelta(hours=6)
401 new_end_time = new_start_time + timedelta(hours=2)
403 res = api.ScheduleEvent(
404 events_pb2.ScheduleEventReq(
405 event_id=res.event_id,
406 content="New event occurrence",
407 location=events_pb2.EventLocation(
408 address="A bit further but still near Null Island",
409 lat=0.3,
410 lng=0.2,
411 ),
412 start_time=Timestamp_from_datetime(new_start_time),
413 end_time=Timestamp_from_datetime(new_end_time),
414 timezone="UTC",
415 )
416 )
418 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
420 assert not res.is_next
421 assert res.title == "Dummy Title"
422 assert res.slug == "dummy-title"
423 assert res.content == "New event occurrence"
424 assert not res.photo_url
425 assert res.HasField("location")
426 assert res.location.lat == 0.3
427 assert res.location.lng == 0.2
428 assert res.location.address == "A bit further but still near Null Island"
429 assert time_before <= to_aware_datetime(res.created) <= now()
430 assert time_before <= to_aware_datetime(res.last_edited) <= now()
431 assert res.creator_user_id == user.id
432 assert to_aware_datetime(res.start_time) == new_start_time
433 assert to_aware_datetime(res.end_time) == new_end_time
434 # assert res.timezone == "UTC"
435 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
436 assert res.organizer
437 assert res.subscriber
438 assert res.going_count == 1
439 assert res.organizer_count == 1
440 assert res.subscriber_count == 1
441 assert res.owner_user_id == user.id
442 assert not res.owner_community_id
443 assert not res.owner_group_id
444 assert res.thread.thread_id
445 assert res.can_edit
446 assert res.can_moderate
449def test_cannot_overlap_occurrences_schedule(db):
450 user, token = generate_user()
452 with session_scope() as session:
453 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
455 start = now()
457 with events_session(token) as api:
458 res = api.CreateEvent(
459 events_pb2.CreateEventReq(
460 title="Dummy Title",
461 content="Dummy content.",
462 parent_community_id=c_id,
463 location=events_pb2.EventLocation(
464 address="Near Null Island",
465 lat=0.1,
466 lng=0.2,
467 ),
468 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
469 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
470 timezone="UTC",
471 )
472 )
474 with pytest.raises(grpc.RpcError) as e:
475 api.ScheduleEvent(
476 events_pb2.ScheduleEventReq(
477 event_id=res.event_id,
478 content="New event occurrence",
479 location=events_pb2.EventLocation(
480 address="A bit further but still near Null Island",
481 lat=0.3,
482 lng=0.2,
483 ),
484 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
485 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
486 timezone="UTC",
487 )
488 )
489 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
490 assert e.value.details() == "An event cannot have overlapping occurrences."
493def test_cannot_overlap_occurrences_update(db):
494 user, token = generate_user()
496 with session_scope() as session:
497 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
499 start = now()
501 with events_session(token) as api:
502 res = api.CreateEvent(
503 events_pb2.CreateEventReq(
504 title="Dummy Title",
505 content="Dummy content.",
506 parent_community_id=c_id,
507 location=events_pb2.EventLocation(
508 address="Near Null Island",
509 lat=0.1,
510 lng=0.2,
511 ),
512 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
513 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
514 timezone="UTC",
515 )
516 )
518 event_id = api.ScheduleEvent(
519 events_pb2.ScheduleEventReq(
520 event_id=res.event_id,
521 content="New event occurrence",
522 location=events_pb2.EventLocation(
523 address="A bit further but still near Null Island",
524 lat=0.3,
525 lng=0.2,
526 ),
527 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
528 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
529 timezone="UTC",
530 )
531 ).event_id
533 # can overlap with this current existing occurrence
534 api.UpdateEvent(
535 events_pb2.UpdateEventReq(
536 event_id=event_id,
537 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
538 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
539 )
540 )
542 with pytest.raises(grpc.RpcError) as e:
543 api.UpdateEvent(
544 events_pb2.UpdateEventReq(
545 event_id=event_id,
546 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
547 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
548 )
549 )
550 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
551 assert e.value.details() == "An event cannot have overlapping occurrences."
554def test_UpdateEvent_single(db, moderator: Moderator):
555 # test cases:
556 # owner can update
557 # community owner can update
558 # notifies attendees
560 # event creator
561 user1, token1 = generate_user()
562 # community moderator
563 user2, token2 = generate_user()
564 # third parties
565 user3, token3 = generate_user()
566 user4, token4 = generate_user()
567 user5, token5 = generate_user()
568 user6, token6 = generate_user()
570 with session_scope() as session:
571 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
573 time_before = now()
574 start_time = now() + timedelta(hours=2)
575 end_time = start_time + timedelta(hours=3)
577 with events_session(token1) as api:
578 res = api.CreateEvent(
579 events_pb2.CreateEventReq(
580 title="Dummy Title",
581 content="Dummy content.",
582 parent_community_id=c_id,
583 location=events_pb2.EventLocation(
584 address="Near Null Island",
585 lat=0.1,
586 lng=0.2,
587 ),
588 start_time=Timestamp_from_datetime(start_time),
589 end_time=Timestamp_from_datetime(end_time),
590 timezone="UTC",
591 )
592 )
594 event_id = res.event_id
596 moderator.approve_event_occurrence(event_id)
598 with events_session(token4) as api:
599 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
601 with events_session(token5) as api:
602 api.SetEventAttendance(
603 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
604 )
606 with events_session(token6) as api:
607 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
609 time_before_update = now()
611 with events_session(token1) as api:
612 res = api.UpdateEvent(
613 events_pb2.UpdateEventReq(
614 event_id=event_id,
615 )
616 )
618 with events_session(token1) as api:
619 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
621 assert res.is_next
622 assert res.title == "Dummy Title"
623 assert res.slug == "dummy-title"
624 assert res.content == "Dummy content."
625 assert not res.photo_url
626 assert res.HasField("location")
627 assert res.location.lat == 0.1
628 assert res.location.lng == 0.2
629 assert res.location.address == "Near Null Island"
630 assert time_before <= to_aware_datetime(res.created) <= time_before_update
631 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
632 assert res.creator_user_id == user1.id
633 assert to_aware_datetime(res.start_time) == start_time
634 assert to_aware_datetime(res.end_time) == end_time
635 # assert res.timezone == "UTC"
636 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
637 assert res.organizer
638 assert res.subscriber
639 assert res.going_count == 2
640 assert res.organizer_count == 1
641 assert res.subscriber_count == 3
642 assert res.owner_user_id == user1.id
643 assert not res.owner_community_id
644 assert not res.owner_group_id
645 assert res.thread.thread_id
646 assert res.can_edit
647 assert not res.can_moderate
649 with events_session(token2) as api:
650 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
652 assert res.is_next
653 assert res.title == "Dummy Title"
654 assert res.slug == "dummy-title"
655 assert res.content == "Dummy content."
656 assert not res.photo_url
657 assert res.HasField("location")
658 assert res.location.lat == 0.1
659 assert res.location.lng == 0.2
660 assert res.location.address == "Near Null Island"
661 assert time_before <= to_aware_datetime(res.created) <= time_before_update
662 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
663 assert res.creator_user_id == user1.id
664 assert to_aware_datetime(res.start_time) == start_time
665 assert to_aware_datetime(res.end_time) == end_time
666 # assert res.timezone == "UTC"
667 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
668 assert not res.organizer
669 assert not res.subscriber
670 assert res.going_count == 2
671 assert res.organizer_count == 1
672 assert res.subscriber_count == 3
673 assert res.owner_user_id == user1.id
674 assert not res.owner_community_id
675 assert not res.owner_group_id
676 assert res.thread.thread_id
677 assert res.can_edit
678 assert res.can_moderate
680 with events_session(token3) as api:
681 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
683 assert res.is_next
684 assert res.title == "Dummy Title"
685 assert res.slug == "dummy-title"
686 assert res.content == "Dummy content."
687 assert not res.photo_url
688 assert res.HasField("location")
689 assert res.location.lat == 0.1
690 assert res.location.lng == 0.2
691 assert res.location.address == "Near Null Island"
692 assert time_before <= to_aware_datetime(res.created) <= time_before_update
693 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
694 assert res.creator_user_id == user1.id
695 assert to_aware_datetime(res.start_time) == start_time
696 assert to_aware_datetime(res.end_time) == end_time
697 # assert res.timezone == "UTC"
698 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
699 assert not res.organizer
700 assert not res.subscriber
701 assert res.going_count == 2
702 assert res.organizer_count == 1
703 assert res.subscriber_count == 3
704 assert res.owner_user_id == user1.id
705 assert not res.owner_community_id
706 assert not res.owner_group_id
707 assert res.thread.thread_id
708 assert not res.can_edit
709 assert not res.can_moderate
711 with events_session(token1) as api:
712 res = api.UpdateEvent(
713 events_pb2.UpdateEventReq(
714 event_id=event_id,
715 location=events_pb2.EventLocation(
716 address="Nearer Null Island",
717 lat=0.01,
718 lng=0.02,
719 ),
720 )
721 )
723 with events_session(token3) as api:
724 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
726 assert res.HasField("location")
727 assert res.location.address == "Nearer Null Island"
728 assert res.location.lat == 0.01
729 assert res.location.lng == 0.02
732def test_UpdateEvent_all(db, moderator: Moderator):
733 # event creator
734 user1, token1 = generate_user()
735 # community moderator
736 user2, token2 = generate_user()
737 # third parties
738 user3, token3 = generate_user()
739 user4, token4 = generate_user()
740 user5, token5 = generate_user()
741 user6, token6 = generate_user()
743 with session_scope() as session:
744 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
746 time_before = now()
747 start_time = now() + timedelta(hours=1)
748 end_time = start_time + timedelta(hours=1.5)
750 event_ids = []
752 with events_session(token1) as api:
753 res = api.CreateEvent(
754 events_pb2.CreateEventReq(
755 title="Dummy Title",
756 content="0th occurrence",
757 location=events_pb2.EventLocation(
758 address="Near Null Island",
759 lat=0.1,
760 lng=0.2,
761 ),
762 start_time=Timestamp_from_datetime(start_time),
763 end_time=Timestamp_from_datetime(end_time),
764 timezone="UTC",
765 )
766 )
768 event_id = res.event_id
769 event_ids.append(event_id)
771 moderator.approve_event_occurrence(event_id)
773 with events_session(token4) as api:
774 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
776 with events_session(token5) as api:
777 api.SetEventAttendance(
778 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
779 )
781 with events_session(token6) as api:
782 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
784 with events_session(token1) as api:
785 for i in range(5):
786 res = api.ScheduleEvent(
787 events_pb2.ScheduleEventReq(
788 event_id=event_ids[-1],
789 content=f"{i + 1}th occurrence",
790 location=events_pb2.EventLocation(
791 address="Near Null Island",
792 lat=0.1,
793 lng=0.2,
794 ),
795 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)),
796 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)),
797 timezone="UTC",
798 )
799 )
801 event_ids.append(res.event_id)
803 # Approve all scheduled occurrences
804 for eid in event_ids[1:]:
805 moderator.approve_event_occurrence(eid)
807 updated_event_id = event_ids[3]
809 time_before_update = now()
811 with events_session(token1) as api:
812 res = api.UpdateEvent(
813 events_pb2.UpdateEventReq(
814 event_id=updated_event_id,
815 title=wrappers_pb2.StringValue(value="New Title"),
816 content=wrappers_pb2.StringValue(value="New content."),
817 location=events_pb2.EventLocation(
818 lat=0.2,
819 lng=0.2,
820 ),
821 update_all_future=True,
822 )
823 )
825 time_after_update = now()
827 with events_session(token2) as api:
828 for i in range(3):
829 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
830 assert res.content == f"{i}th occurrence"
831 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
833 for i in range(3, 6):
834 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
835 assert res.content == "New content."
836 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
839def test_GetEvent(db, moderator: Moderator):
840 # event creator
841 user1, token1 = generate_user()
842 # community moderator
843 user2, token2 = generate_user()
844 # third parties
845 user3, token3 = generate_user()
846 user4, token4 = generate_user()
847 user5, token5 = generate_user()
848 user6, token6 = generate_user()
850 with session_scope() as session:
851 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
853 time_before = now()
854 start_time = now() + timedelta(hours=2)
855 end_time = start_time + timedelta(hours=3)
857 with events_session(token1) as api:
858 # in person event
859 res = api.CreateEvent(
860 events_pb2.CreateEventReq(
861 title="Dummy Title",
862 content="Dummy content.",
863 location=events_pb2.EventLocation(
864 address="Near Null Island",
865 lat=0.1,
866 lng=0.2,
867 ),
868 start_time=Timestamp_from_datetime(start_time),
869 end_time=Timestamp_from_datetime(end_time),
870 timezone="UTC",
871 )
872 )
874 event_id = res.event_id
876 moderator.approve_event_occurrence(event_id)
878 with events_session(token4) as api:
879 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
881 with events_session(token5) as api:
882 api.SetEventAttendance(
883 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
884 )
886 with events_session(token6) as api:
887 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
889 with events_session(token1) as api:
890 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
892 assert res.is_next
893 assert res.title == "Dummy Title"
894 assert res.slug == "dummy-title"
895 assert res.content == "Dummy content."
896 assert not res.photo_url
897 assert res.HasField("location")
898 assert res.location.lat == 0.1
899 assert res.location.lng == 0.2
900 assert res.location.address == "Near Null Island"
901 assert time_before <= to_aware_datetime(res.created) <= now()
902 assert time_before <= to_aware_datetime(res.last_edited) <= now()
903 assert res.creator_user_id == user1.id
904 assert to_aware_datetime(res.start_time) == start_time
905 assert to_aware_datetime(res.end_time) == end_time
906 # assert res.timezone == "UTC"
907 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
908 assert res.organizer
909 assert res.subscriber
910 assert res.going_count == 2
911 assert res.organizer_count == 1
912 assert res.subscriber_count == 3
913 assert res.owner_user_id == user1.id
914 assert not res.owner_community_id
915 assert not res.owner_group_id
916 assert res.thread.thread_id
917 assert res.can_edit
918 assert not res.can_moderate
920 with events_session(token2) as api:
921 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
923 assert res.is_next
924 assert res.title == "Dummy Title"
925 assert res.slug == "dummy-title"
926 assert res.content == "Dummy content."
927 assert not res.photo_url
928 assert res.HasField("location")
929 assert res.location.lat == 0.1
930 assert res.location.lng == 0.2
931 assert res.location.address == "Near Null Island"
932 assert time_before <= to_aware_datetime(res.created) <= now()
933 assert time_before <= to_aware_datetime(res.last_edited) <= now()
934 assert res.creator_user_id == user1.id
935 assert to_aware_datetime(res.start_time) == start_time
936 assert to_aware_datetime(res.end_time) == end_time
937 # assert res.timezone == "UTC"
938 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
939 assert not res.organizer
940 assert not res.subscriber
941 assert res.going_count == 2
942 assert res.organizer_count == 1
943 assert res.subscriber_count == 3
944 assert res.owner_user_id == user1.id
945 assert not res.owner_community_id
946 assert not res.owner_group_id
947 assert res.thread.thread_id
948 assert res.can_edit
949 assert res.can_moderate
951 with events_session(token3) as api:
952 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
954 assert res.is_next
955 assert res.title == "Dummy Title"
956 assert res.slug == "dummy-title"
957 assert res.content == "Dummy content."
958 assert not res.photo_url
959 assert res.HasField("location")
960 assert res.location.lat == 0.1
961 assert res.location.lng == 0.2
962 assert res.location.address == "Near Null Island"
963 assert time_before <= to_aware_datetime(res.created) <= now()
964 assert time_before <= to_aware_datetime(res.last_edited) <= now()
965 assert res.creator_user_id == user1.id
966 assert to_aware_datetime(res.start_time) == start_time
967 assert to_aware_datetime(res.end_time) == end_time
968 # assert res.timezone == "UTC"
969 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
970 assert not res.organizer
971 assert not res.subscriber
972 assert res.going_count == 2
973 assert res.organizer_count == 1
974 assert res.subscriber_count == 3
975 assert res.owner_user_id == user1.id
976 assert not res.owner_community_id
977 assert not res.owner_group_id
978 assert res.thread.thread_id
979 assert not res.can_edit
980 assert not res.can_moderate
983def test_CancelEvent(db, moderator: Moderator):
984 # event creator
985 user1, token1 = generate_user()
986 # community moderator
987 user2, token2 = generate_user()
988 # third parties
989 user3, token3 = generate_user()
990 user4, token4 = generate_user()
991 user5, token5 = generate_user()
992 user6, token6 = generate_user()
994 with session_scope() as session:
995 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
997 start_time = now() + timedelta(hours=2)
998 end_time = start_time + timedelta(hours=3)
1000 with events_session(token1) as api:
1001 res = api.CreateEvent(
1002 events_pb2.CreateEventReq(
1003 title="Dummy Title",
1004 content="Dummy content.",
1005 location=events_pb2.EventLocation(
1006 address="Near Null Island",
1007 lat=0.1,
1008 lng=0.2,
1009 ),
1010 start_time=Timestamp_from_datetime(start_time),
1011 end_time=Timestamp_from_datetime(end_time),
1012 timezone="UTC",
1013 )
1014 )
1016 event_id = res.event_id
1018 moderator.approve_event_occurrence(event_id)
1020 with events_session(token4) as api:
1021 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1023 with events_session(token5) as api:
1024 api.SetEventAttendance(
1025 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1026 )
1028 with events_session(token6) as api:
1029 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1031 with events_session(token1) as api:
1032 res = api.CancelEvent(
1033 events_pb2.CancelEventReq(
1034 event_id=event_id,
1035 )
1036 )
1038 with events_session(token1) as api:
1039 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1040 assert res.is_cancelled
1042 with events_session(token1) as api:
1043 with pytest.raises(grpc.RpcError) as e:
1044 api.UpdateEvent(
1045 events_pb2.UpdateEventReq(
1046 event_id=event_id,
1047 title=wrappers_pb2.StringValue(value="New Title"),
1048 )
1049 )
1050 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1051 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1053 with pytest.raises(grpc.RpcError) as e:
1054 api.InviteEventOrganizer(
1055 events_pb2.InviteEventOrganizerReq(
1056 event_id=event_id,
1057 user_id=user3.id,
1058 )
1059 )
1060 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1061 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1063 with pytest.raises(grpc.RpcError) as e:
1064 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1065 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1066 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1068 with events_session(token3) as api:
1069 with pytest.raises(grpc.RpcError) as e:
1070 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1071 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1072 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1074 with pytest.raises(grpc.RpcError) as e:
1075 api.SetEventAttendance(
1076 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1077 )
1078 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1079 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1081 with events_session(token1) as api:
1082 for include_cancelled in [True, False]:
1083 res = api.ListEventOccurrences(
1084 events_pb2.ListEventOccurrencesReq(
1085 event_id=event_id,
1086 include_cancelled=include_cancelled,
1087 )
1088 )
1089 if include_cancelled:
1090 assert len(res.events) > 0
1091 else:
1092 assert len(res.events) == 0
1094 res = api.ListMyEvents(
1095 events_pb2.ListMyEventsReq(
1096 include_cancelled=include_cancelled,
1097 )
1098 )
1099 if include_cancelled:
1100 assert len(res.events) > 0
1101 else:
1102 assert len(res.events) == 0
1105def test_ListEventAttendees(db, moderator: Moderator):
1106 # event creator
1107 user1, token1 = generate_user()
1108 # others
1109 user2, token2 = generate_user()
1110 user3, token3 = generate_user()
1111 user4, token4 = generate_user()
1112 user5, token5 = generate_user()
1113 user6, token6 = generate_user()
1115 with session_scope() as session:
1116 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1118 with events_session(token1) as api:
1119 event_id = api.CreateEvent(
1120 events_pb2.CreateEventReq(
1121 title="Dummy Title",
1122 content="Dummy content.",
1123 location=events_pb2.EventLocation(
1124 address="Near Null Island",
1125 lat=0.1,
1126 lng=0.2,
1127 ),
1128 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1129 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1130 timezone="UTC",
1131 )
1132 ).event_id
1134 moderator.approve_event_occurrence(event_id)
1136 for token in [token2, token3, token4, token5]:
1137 with events_session(token) as api:
1138 api.SetEventAttendance(
1139 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1140 )
1142 with events_session(token6) as api:
1143 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1145 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1146 assert res.attendee_user_ids == [user1.id, user2.id]
1148 res = api.ListEventAttendees(
1149 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1150 )
1151 assert res.attendee_user_ids == [user3.id, user4.id]
1153 res = api.ListEventAttendees(
1154 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1155 )
1156 assert res.attendee_user_ids == [user5.id]
1157 assert not res.next_page_token
1160def test_ListEventSubscribers(db, moderator: Moderator):
1161 # event creator
1162 user1, token1 = generate_user()
1163 # others
1164 user2, token2 = generate_user()
1165 user3, token3 = generate_user()
1166 user4, token4 = generate_user()
1167 user5, token5 = generate_user()
1168 user6, token6 = generate_user()
1170 with session_scope() as session:
1171 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1173 with events_session(token1) as api:
1174 event_id = api.CreateEvent(
1175 events_pb2.CreateEventReq(
1176 title="Dummy Title",
1177 content="Dummy content.",
1178 location=events_pb2.EventLocation(
1179 address="Near Null Island",
1180 lat=0.1,
1181 lng=0.2,
1182 ),
1183 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1184 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1185 timezone="UTC",
1186 )
1187 ).event_id
1189 moderator.approve_event_occurrence(event_id)
1191 for token in [token2, token3, token4, token5]:
1192 with events_session(token) as api:
1193 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1195 with events_session(token6) as api:
1196 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1198 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1199 assert res.subscriber_user_ids == [user1.id, user2.id]
1201 res = api.ListEventSubscribers(
1202 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1203 )
1204 assert res.subscriber_user_ids == [user3.id, user4.id]
1206 res = api.ListEventSubscribers(
1207 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1208 )
1209 assert res.subscriber_user_ids == [user5.id]
1210 assert not res.next_page_token
1213def test_ListEventOrganizers(db, moderator: Moderator):
1214 # event creator
1215 user1, token1 = generate_user()
1216 # others
1217 user2, token2 = generate_user()
1218 user3, token3 = generate_user()
1219 user4, token4 = generate_user()
1220 user5, token5 = generate_user()
1221 user6, token6 = generate_user()
1223 with session_scope() as session:
1224 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1226 with events_session(token1) as api:
1227 event_id = api.CreateEvent(
1228 events_pb2.CreateEventReq(
1229 title="Dummy Title",
1230 content="Dummy content.",
1231 location=events_pb2.EventLocation(
1232 address="Near Null Island",
1233 lat=0.1,
1234 lng=0.2,
1235 ),
1236 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1237 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1238 timezone="UTC",
1239 )
1240 ).event_id
1242 moderator.approve_event_occurrence(event_id)
1244 with events_session(token1) as api:
1245 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1246 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1248 with events_session(token6) as api:
1249 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1251 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1252 assert res.organizer_user_ids == [user1.id, user2.id]
1254 res = api.ListEventOrganizers(
1255 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1256 )
1257 assert res.organizer_user_ids == [user3.id, user4.id]
1259 res = api.ListEventOrganizers(
1260 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1261 )
1262 assert res.organizer_user_ids == [user5.id]
1263 assert not res.next_page_token
1266def test_TransferEvent(db):
1267 user1, token1 = generate_user()
1268 user2, token2 = generate_user()
1269 user3, token3 = generate_user()
1270 user4, token4 = generate_user()
1272 with session_scope() as session:
1273 c = create_community(session, 0, 2, "Community", [user3], [], None)
1274 h = create_group(session, "Group", [user4], [], c)
1275 c_id = c.id
1276 h_id = h.id
1278 with events_session(token1) as api:
1279 event_id = api.CreateEvent(
1280 events_pb2.CreateEventReq(
1281 title="Dummy Title",
1282 content="Dummy content.",
1283 location=events_pb2.EventLocation(
1284 address="Near Null Island",
1285 lat=0.1,
1286 lng=0.2,
1287 ),
1288 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1289 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1290 timezone="UTC",
1291 )
1292 ).event_id
1294 api.TransferEvent(
1295 events_pb2.TransferEventReq(
1296 event_id=event_id,
1297 new_owner_community_id=c_id,
1298 )
1299 )
1301 # remove ourselves as organizer, otherwise we can still edit it
1302 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1304 with pytest.raises(grpc.RpcError) as e:
1305 api.TransferEvent(
1306 events_pb2.TransferEventReq(
1307 event_id=event_id,
1308 new_owner_group_id=h_id,
1309 )
1310 )
1311 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1312 assert e.value.details() == "You're not allowed to transfer that event."
1314 event_id = api.CreateEvent(
1315 events_pb2.CreateEventReq(
1316 title="Dummy Title",
1317 content="Dummy content.",
1318 location=events_pb2.EventLocation(
1319 address="Near Null Island",
1320 lat=0.1,
1321 lng=0.2,
1322 ),
1323 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1324 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1325 timezone="UTC",
1326 )
1327 ).event_id
1329 api.TransferEvent(
1330 events_pb2.TransferEventReq(
1331 event_id=event_id,
1332 new_owner_group_id=h_id,
1333 )
1334 )
1336 # remove ourselves as organizer, otherwise we can still edit it
1337 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1339 with pytest.raises(grpc.RpcError) as e:
1340 api.TransferEvent(
1341 events_pb2.TransferEventReq(
1342 event_id=event_id,
1343 new_owner_community_id=c_id,
1344 )
1345 )
1346 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1347 assert e.value.details() == "You're not allowed to transfer that event."
1350def test_SetEventSubscription(db, moderator: Moderator):
1351 user1, token1 = generate_user()
1352 user2, token2 = generate_user()
1354 with session_scope() as session:
1355 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1357 with events_session(token1) as api:
1358 event_id = api.CreateEvent(
1359 events_pb2.CreateEventReq(
1360 title="Dummy Title",
1361 content="Dummy content.",
1362 location=events_pb2.EventLocation(
1363 address="Near Null Island",
1364 lat=0.1,
1365 lng=0.2,
1366 ),
1367 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1368 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1369 timezone="UTC",
1370 )
1371 ).event_id
1373 moderator.approve_event_occurrence(event_id)
1375 with events_session(token2) as api:
1376 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1377 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1378 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1379 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1380 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1383def test_SetEventAttendance(db, moderator: Moderator):
1384 user1, token1 = generate_user()
1385 user2, token2 = generate_user()
1387 with session_scope() as session:
1388 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1390 with events_session(token1) as api:
1391 event_id = api.CreateEvent(
1392 events_pb2.CreateEventReq(
1393 title="Dummy Title",
1394 content="Dummy content.",
1395 location=events_pb2.EventLocation(
1396 address="Near Null Island",
1397 lat=0.1,
1398 lng=0.2,
1399 ),
1400 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1401 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1402 timezone="UTC",
1403 )
1404 ).event_id
1406 moderator.approve_event_occurrence(event_id)
1408 with events_session(token2) as api:
1409 assert (
1410 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1411 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1412 )
1413 api.SetEventAttendance(
1414 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1415 )
1416 assert (
1417 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1418 == events_pb2.ATTENDANCE_STATE_GOING
1419 )
1420 api.SetEventAttendance(
1421 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1422 )
1423 assert (
1424 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1425 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1426 )
1429def test_InviteEventOrganizer(db, moderator: Moderator):
1430 user1, token1 = generate_user()
1431 user2, token2 = generate_user()
1433 with session_scope() as session:
1434 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1436 with events_session(token1) as api:
1437 event_id = api.CreateEvent(
1438 events_pb2.CreateEventReq(
1439 title="Dummy Title",
1440 content="Dummy content.",
1441 location=events_pb2.EventLocation(
1442 address="Near Null Island",
1443 lat=0.1,
1444 lng=0.2,
1445 ),
1446 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1447 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1448 timezone="UTC",
1449 )
1450 ).event_id
1452 moderator.approve_event_occurrence(event_id)
1454 with events_session(token2) as api:
1455 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1457 with pytest.raises(grpc.RpcError) as e:
1458 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1459 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1460 assert e.value.details() == "You're not allowed to edit that event."
1462 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1464 with events_session(token1) as api:
1465 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1467 with events_session(token2) as api:
1468 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1471def test_ListEventOccurrences(db):
1472 user1, token1 = generate_user()
1473 user2, token2 = generate_user()
1474 user3, token3 = generate_user()
1476 with session_scope() as session:
1477 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1479 start = now()
1481 event_ids = []
1483 with events_session(token1) as api:
1484 res = api.CreateEvent(
1485 events_pb2.CreateEventReq(
1486 title="First occurrence",
1487 content="Dummy content.",
1488 parent_community_id=c_id,
1489 location=events_pb2.EventLocation(
1490 address="Near Null Island",
1491 lat=0.1,
1492 lng=0.2,
1493 ),
1494 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
1495 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
1496 timezone="UTC",
1497 )
1498 )
1500 event_ids.append(res.event_id)
1502 for i in range(5):
1503 res = api.ScheduleEvent(
1504 events_pb2.ScheduleEventReq(
1505 event_id=event_ids[-1],
1506 content=f"{i}th occurrence",
1507 location=events_pb2.EventLocation(
1508 address="Near Null Island",
1509 lat=0.1,
1510 lng=0.2,
1511 ),
1512 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
1513 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
1514 timezone="UTC",
1515 )
1516 )
1518 event_ids.append(res.event_id)
1520 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1521 assert [event.event_id for event in res.events] == event_ids[:2]
1523 res = api.ListEventOccurrences(
1524 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1525 )
1526 assert [event.event_id for event in res.events] == event_ids[2:4]
1528 res = api.ListEventOccurrences(
1529 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1530 )
1531 assert [event.event_id for event in res.events] == event_ids[4:6]
1532 assert not res.next_page_token
1535def test_ListMyEvents(db, moderator: Moderator):
1536 user1, token1 = generate_user()
1537 user2, token2 = generate_user()
1538 user3, token3 = generate_user()
1539 user4, token4 = generate_user()
1540 user5, token5 = generate_user()
1542 with session_scope() as session:
1543 # Create global (world) -> macroregion -> region -> subregion hierarchy
1544 # my_communities_exclude_global filters out world, macroregion, and region level communities
1545 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1546 c_id = global_community.id
1547 macroregion_community = create_community(
1548 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1549 )
1550 region_community = create_community(
1551 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1552 )
1553 subregion_community = create_community(
1554 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1555 )
1556 c2_id = subregion_community.id
1558 start = now()
1560 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1561 return events_pb2.CreateEventReq(
1562 title="Dummy Title",
1563 content="Dummy content.",
1564 location=events_pb2.EventLocation(
1565 address="Near Null Island",
1566 lat=0.1,
1567 lng=0.2,
1568 ),
1569 parent_community_id=community_id,
1570 timezone="UTC",
1571 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1572 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1573 )
1575 with events_session(token1) as api:
1576 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1578 moderator.approve_event_occurrence(e2)
1580 with events_session(token2) as api:
1581 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1583 moderator.approve_event_occurrence(e1)
1585 with events_session(token1) as api:
1586 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1588 moderator.approve_event_occurrence(e3)
1590 with events_session(token2) as api:
1591 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1593 moderator.approve_event_occurrence(e5)
1595 with events_session(token3) as api:
1596 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1598 moderator.approve_event_occurrence(e4)
1600 with events_session(token4) as api:
1601 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1603 moderator.approve_event_occurrence(e6)
1605 with events_session(token1) as api:
1606 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1608 with events_session(token1) as api:
1609 api.SetEventAttendance(
1610 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1611 )
1612 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1614 with events_session(token2) as api:
1615 api.SetEventAttendance(
1616 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1617 )
1619 with events_session(token3) as api:
1620 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1622 with events_session(token1) as api:
1623 # test pagination with token first
1624 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1625 assert [event.event_id for event in res.events] == [e1, e2]
1626 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1627 assert [event.event_id for event in res.events] == [e3, e4]
1628 assert not res.next_page_token
1630 res = api.ListMyEvents(
1631 events_pb2.ListMyEventsReq(
1632 subscribed=True,
1633 attending=True,
1634 organizing=True,
1635 )
1636 )
1637 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1639 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1640 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1642 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1643 assert [event.event_id for event in res.events] == [e2, e3, e4]
1645 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1646 assert [event.event_id for event in res.events] == [e1, e2, e3]
1648 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1649 assert [event.event_id for event in res.events] == [e2, e3]
1651 with events_session(token1) as api:
1652 # Test pagination with page_number and verify total_items
1653 res = api.ListMyEvents(
1654 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1655 )
1656 assert [event.event_id for event in res.events] == [e1, e2]
1657 assert res.total_items == 4
1659 res = api.ListMyEvents(
1660 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1661 )
1662 assert [event.event_id for event in res.events] == [e3, e4]
1663 assert res.total_items == 4
1665 # Verify no more pages
1666 res = api.ListMyEvents(
1667 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1668 )
1669 assert not res.events
1670 assert res.total_items == 4
1672 with events_session(token2) as api:
1673 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1674 assert [event.event_id for event in res.events] == [e1, e3, e5]
1676 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1677 assert [event.event_id for event in res.events] == [e1, e5]
1679 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1680 assert [event.event_id for event in res.events] == [e1, e3, e5]
1682 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1683 assert [event.event_id for event in res.events] == [e1, e5]
1685 with events_session(token3) as api:
1686 # user3 is member of both global (c_id) and child (c2_id) communities
1687 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1688 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1690 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1691 assert [event.event_id for event in res.events] == [e2, e4]
1693 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1694 assert [event.event_id for event in res.events] == [e4]
1696 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1697 assert [event.event_id for event in res.events] == [e3, e4]
1699 # my_communities returns events from both communities user3 is a member of
1700 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1701 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1703 # my_communities_exclude_global filters out events from global community (node_id=1)
1704 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1705 assert [event.event_id for event in res.events] == [e6]
1707 # my_communities_exclude_global works independently of my_communities flag
1708 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1709 assert [event.event_id for event in res.events] == [e6]
1711 # my_communities_exclude_global filters organizing results too
1712 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1713 assert [event.event_id for event in res.events] == []
1715 # my_communities_exclude_global filters subscribed results too
1716 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1717 assert [event.event_id for event in res.events] == []
1719 with events_session(token5) as api:
1720 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1721 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1724def test_list_my_events_exclude_attending(db, moderator: Moderator):
1725 user1, token1 = generate_user()
1726 user2, token2 = generate_user()
1728 with session_scope() as session:
1729 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1730 c_id = c.id
1732 start = now()
1734 def make_event(hours):
1735 return events_pb2.CreateEventReq(
1736 title="Test Event",
1737 content="Test content.",
1738 location=events_pb2.EventLocation(
1739 address="Near Null Island",
1740 lat=0.1,
1741 lng=0.2,
1742 ),
1743 parent_community_id=c_id,
1744 timezone="UTC",
1745 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)),
1746 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)),
1747 )
1749 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1750 with events_session(token1) as api:
1751 e_own = api.CreateEvent(make_event(1)).event_id
1753 with events_session(token2) as api:
1754 e_attending = api.CreateEvent(make_event(2)).event_id
1755 e_community_only = api.CreateEvent(make_event(3)).event_id
1756 # e_both: user1 will be both organizer and attendee
1757 e_both = api.CreateEvent(make_event(4)).event_id
1759 moderator.approve_event_occurrence(e_own)
1760 moderator.approve_event_occurrence(e_attending)
1761 moderator.approve_event_occurrence(e_community_only)
1762 moderator.approve_event_occurrence(e_both)
1764 # invite user1 as organizer of e_both
1765 with events_session(token2) as api:
1766 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
1768 # user1 RSVPs to e_attending and e_both
1769 with events_session(token1) as api:
1770 api.SetEventAttendance(
1771 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1772 )
1773 api.SetEventAttendance(
1774 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1775 )
1777 with events_session(token1) as api:
1778 # baseline: all four community events visible
1779 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1780 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
1782 # exclude_attending removes events user1 is attending (e_attending, e_both)
1783 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
1784 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1785 assert [e.event_id for e in res.events] == [e_community_only]
1787 # exclude_attending with attending=True: invalid combination
1788 with pytest.raises(grpc.RpcError) as e:
1789 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
1790 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1792 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
1793 with events_session(token2) as api:
1794 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1795 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
1796 assert [e.event_id for e in res.events] == [e_own]
1799def test_RemoveEventOrganizer(db, moderator: Moderator):
1800 user1, token1 = generate_user()
1801 user2, token2 = generate_user()
1803 with session_scope() as session:
1804 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1806 with events_session(token1) as api:
1807 event_id = api.CreateEvent(
1808 events_pb2.CreateEventReq(
1809 title="Dummy Title",
1810 content="Dummy content.",
1811 location=events_pb2.EventLocation(
1812 address="Near Null Island",
1813 lat=0.1,
1814 lng=0.2,
1815 ),
1816 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1817 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1818 timezone="UTC",
1819 )
1820 ).event_id
1822 moderator.approve_event_occurrence(event_id)
1824 with events_session(token2) as api:
1825 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1827 with pytest.raises(grpc.RpcError) as e:
1828 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1829 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1830 assert e.value.details() == "You're not allowed to edit that event."
1832 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1834 with events_session(token1) as api:
1835 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1837 with pytest.raises(grpc.RpcError) as e:
1838 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1839 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1840 assert e.value.details() == "You cannot remove the event owner as an organizer."
1842 with events_session(token2) as api:
1843 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1844 assert res.organizer
1845 assert res.organizer_count == 2
1846 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1847 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1849 with pytest.raises(grpc.RpcError) as e:
1850 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1851 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1852 assert e.value.details() == "You're not allowed to edit that event."
1854 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1855 assert not res.organizer
1856 assert res.organizer_count == 1
1858 # Test that event owner can remove co-organizers
1859 with events_session(token1) as api:
1860 # Add user2 back as organizer
1861 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1863 # Verify user2 is now an organizer
1864 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1865 assert res.organizer_count == 2
1867 # Event owner can remove co-organizer
1868 api.RemoveEventOrganizer(
1869 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
1870 )
1872 # Verify user2 is no longer an organizer
1873 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1874 assert res.organizer_count == 1
1876 # Test that non-organizers cannot remove other organizers
1877 with events_session(token2) as api:
1878 # User2 cannot invite themselves as organizer (not the owner)
1879 with pytest.raises(grpc.RpcError) as e:
1880 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1881 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1882 assert e.value.details() == "You're not allowed to edit that event."
1884 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
1885 with events_session(token1) as api:
1886 # Add user2 back as organizer
1887 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1890def test_ListEventAttendees_regression(db):
1891 # see issue #1617:
1892 #
1893 # 1. Create an event
1894 # 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`
1895 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
1896 # 4. Change the current user's attendance state to "going" again
1897 #
1898 # **Expected behaviour**
1899 # `ListEventAttendees` should return the current user's ID
1900 #
1901 # **Actual/current behaviour**
1902 # `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
1904 user1, token1 = generate_user()
1905 user2, token2 = generate_user()
1906 user3, token3 = generate_user()
1907 user4, token4 = generate_user()
1908 user5, token5 = generate_user()
1910 with session_scope() as session:
1911 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1913 start_time = now() + timedelta(hours=2)
1914 end_time = start_time + timedelta(hours=3)
1916 with events_session(token1) as api:
1917 res = api.CreateEvent(
1918 events_pb2.CreateEventReq(
1919 title="Dummy Title",
1920 content="Dummy content.",
1921 location=events_pb2.EventLocation(
1922 address="Near Null Island",
1923 lat=0.1,
1924 lng=0.2,
1925 ),
1926 parent_community_id=c_id,
1927 start_time=Timestamp_from_datetime(start_time),
1928 end_time=Timestamp_from_datetime(end_time),
1929 timezone="UTC",
1930 )
1931 )
1933 res = api.TransferEvent(
1934 events_pb2.TransferEventReq(
1935 event_id=res.event_id,
1936 new_owner_community_id=c_id,
1937 )
1938 )
1940 event_id = res.event_id
1942 api.SetEventAttendance(
1943 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1944 )
1945 api.SetEventAttendance(
1946 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1947 )
1949 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
1950 assert len(res.attendee_user_ids) == 1
1951 assert res.attendee_user_ids[0] == user1.id
1954def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
1955 user1, token1 = generate_user()
1956 user2, token2 = generate_user()
1957 user3, token3 = generate_user()
1958 user4, token4 = generate_user()
1960 with session_scope() as session:
1961 c = create_community(session, 0, 2, "Community", [user3], [], None)
1962 h = create_group(session, "Group", [user4], [], c)
1963 c_id = c.id
1964 h_id = h.id
1965 user4_id = user4.id
1967 with events_session(token1) as api:
1968 event = api.CreateEvent(
1969 events_pb2.CreateEventReq(
1970 title="Dummy Title",
1971 content="Dummy content.",
1972 location=events_pb2.EventLocation(
1973 address="Near Null Island",
1974 lat=0.1,
1975 lng=0.2,
1976 ),
1977 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1978 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1979 timezone="UTC",
1980 )
1981 )
1983 moderator.approve_event_occurrence(event.event_id)
1985 with threads_session(token2) as api:
1986 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
1988 moderator.approve_thread_post(reply_id)
1990 with events_session(token3) as api:
1991 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
1992 assert res.thread.num_responses == 1
1994 with threads_session(token3) as api:
1995 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
1996 assert len(ret.replies) == 1
1997 assert not ret.next_page_token
1998 assert ret.replies[0].thread_id == reply_id
1999 assert ret.replies[0].content == "hi"
2000 assert ret.replies[0].author_user_id == user2.id
2001 assert ret.replies[0].num_replies == 0
2003 nested_reply_id = api.PostReply(
2004 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2005 ).thread_id
2007 moderator.approve_thread_post(nested_reply_id)
2009 process_jobs()
2011 push = push_collector.pop_for_user(user1.id, last=True)
2012 assert push.topic_action == NotificationTopicAction.event__comment.display
2013 assert push.content.title == f"{user2.name} • Dummy Title"
2014 assert push.content.ios_title == user2.name
2015 assert push.content.ios_subtitle == "Commented on Dummy Title"
2016 assert push.content.body == "hi"
2018 push = push_collector.pop_for_user(user2.id, last=True)
2019 assert push.content.title == f"{user3.name} • Dummy Title"
2021 assert push_collector.count_for_user(user4_id) == 0
2024def test_can_overlap_other_events_schedule_regression(db):
2025 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2026 user, token = generate_user()
2028 with session_scope() as session:
2029 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2031 start = now()
2033 with events_session(token) as api:
2034 # create another event, should be able to overlap with this one
2035 api.CreateEvent(
2036 events_pb2.CreateEventReq(
2037 title="Dummy Title",
2038 content="Dummy content.",
2039 parent_community_id=c_id,
2040 location=events_pb2.EventLocation(
2041 address="Near Null Island",
2042 lat=0.1,
2043 lng=0.2,
2044 ),
2045 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2046 end_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2047 timezone="UTC",
2048 )
2049 )
2051 # this event
2052 res = api.CreateEvent(
2053 events_pb2.CreateEventReq(
2054 title="Dummy Title",
2055 content="Dummy content.",
2056 parent_community_id=c_id,
2057 location=events_pb2.EventLocation(
2058 address="Near Null Island",
2059 lat=0.1,
2060 lng=0.2,
2061 ),
2062 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2063 end_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2064 timezone="UTC",
2065 )
2066 )
2068 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2069 api.ScheduleEvent(
2070 events_pb2.ScheduleEventReq(
2071 event_id=res.event_id,
2072 content="New event occurrence",
2073 location=events_pb2.EventLocation(
2074 address="A bit further but still near Null Island",
2075 lat=0.3,
2076 lng=0.2,
2077 ),
2078 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2079 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2080 timezone="UTC",
2081 )
2082 )
2085def test_can_overlap_other_events_update_regression(db):
2086 user, token = generate_user()
2088 with session_scope() as session:
2089 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2091 start = now()
2093 with events_session(token) as api:
2094 # create another event, should be able to overlap with this one
2095 api.CreateEvent(
2096 events_pb2.CreateEventReq(
2097 title="Dummy Title",
2098 content="Dummy content.",
2099 parent_community_id=c_id,
2100 location=events_pb2.EventLocation(
2101 address="Near Null Island",
2102 lat=0.1,
2103 lng=0.2,
2104 ),
2105 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2106 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2107 timezone="UTC",
2108 )
2109 )
2111 res = api.CreateEvent(
2112 events_pb2.CreateEventReq(
2113 title="Dummy Title",
2114 content="Dummy content.",
2115 parent_community_id=c_id,
2116 location=events_pb2.EventLocation(
2117 address="Near Null Island",
2118 lat=0.1,
2119 lng=0.2,
2120 ),
2121 start_time=Timestamp_from_datetime(start + timedelta(hours=7)),
2122 end_time=Timestamp_from_datetime(start + timedelta(hours=8)),
2123 timezone="UTC",
2124 )
2125 )
2127 event_id = api.ScheduleEvent(
2128 events_pb2.ScheduleEventReq(
2129 event_id=res.event_id,
2130 content="New event occurrence",
2131 location=events_pb2.EventLocation(
2132 address="A bit further but still near Null Island",
2133 lat=0.3,
2134 lng=0.2,
2135 ),
2136 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2137 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2138 timezone="UTC",
2139 )
2140 ).event_id
2142 # can overlap with this current existing occurrence
2143 api.UpdateEvent(
2144 events_pb2.UpdateEventReq(
2145 event_id=event_id,
2146 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2147 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2148 )
2149 )
2151 api.UpdateEvent(
2152 events_pb2.UpdateEventReq(
2153 event_id=event_id,
2154 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2155 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2156 )
2157 )
2160def test_list_past_events_regression(db):
2161 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2162 user, token = generate_user()
2164 with session_scope() as session:
2165 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2167 start = now()
2169 with events_session(token) as api:
2170 api.CreateEvent(
2171 events_pb2.CreateEventReq(
2172 title="Dummy Title",
2173 content="Dummy content.",
2174 parent_community_id=c_id,
2175 location=events_pb2.EventLocation(
2176 address="Near Null Island",
2177 lat=0.1,
2178 lng=0.2,
2179 ),
2180 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2181 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2182 timezone="UTC",
2183 )
2184 )
2186 with session_scope() as session:
2187 session.execute(
2188 update(EventOccurrence).values(
2189 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2190 )
2191 )
2193 with events_session(token) as api:
2194 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2195 assert len(res.events) == 1
2198def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2199 user1, token1 = generate_user(complete_profile=True)
2200 user2, token2 = generate_user()
2201 user3, token3 = generate_user()
2202 user4, token4 = generate_user()
2203 user5, token5 = generate_user(is_superuser=True)
2205 with session_scope() as session:
2206 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2207 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2208 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2209 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2211 enforce_community_memberships()
2213 with events_session(token1) as api:
2214 res = api.CreateEvent(
2215 events_pb2.CreateEventReq(
2216 title="Dummy Title",
2217 content="Dummy content.",
2218 parent_community_id=c_id,
2219 location=events_pb2.EventLocation(
2220 address="Near Null Island",
2221 lat=0.1,
2222 lng=0.2,
2223 ),
2224 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)),
2225 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
2226 timezone="UTC",
2227 )
2228 )
2229 user_url = f"http://localhost:3000/user/{user1.username}"
2230 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2232 event_id = res.event_id
2234 moderator.approve_event_occurrence(event_id)
2236 with events_session(token1) as api:
2237 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2239 email = email_collector.pop_for_mods(last=True)
2241 assert user_url in email.plain
2242 assert event_url in email.plain
2244 # can't send another req
2245 with pytest.raises(grpc.RpcError) as err:
2246 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2247 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2248 assert err.value.details() == "You have already requested a community invite for this event."
2250 # another user can send one though
2251 with events_session(token3) as api:
2252 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2254 # but not a non-admin
2255 with events_session(token2) as api:
2256 with pytest.raises(grpc.RpcError) as err:
2257 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2258 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2259 assert err.value.details() == "You're not allowed to edit that event."
2261 with real_editor_session(token5) as editor:
2262 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2263 assert len(res.requests) == 2
2264 assert res.requests[0].user_id == user1.id
2265 assert res.requests[0].approx_users_to_notify == 3
2266 assert res.requests[1].user_id == user3.id
2267 assert res.requests[1].approx_users_to_notify == 3
2269 editor.DecideEventCommunityInviteRequest(
2270 editor_pb2.DecideEventCommunityInviteRequestReq(
2271 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2272 approve=False,
2273 )
2274 )
2276 editor.DecideEventCommunityInviteRequest(
2277 editor_pb2.DecideEventCommunityInviteRequestReq(
2278 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2279 approve=True,
2280 )
2281 )
2283 # not after approve
2284 with events_session(token4) as api:
2285 with pytest.raises(grpc.RpcError) as err:
2286 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2287 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2288 assert err.value.details() == "A community invite has already been sent out for this event."
2291def test_update_event_should_notify_queues_job():
2292 user, token = generate_user()
2293 start = now()
2295 with session_scope() as session:
2296 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2298 # create an event
2299 with events_session(token) as api:
2300 create_res = api.CreateEvent(
2301 events_pb2.CreateEventReq(
2302 title="Dummy Title",
2303 content="Dummy content.",
2304 parent_community_id=c_id,
2305 location=events_pb2.EventLocation(
2306 address="Near Null Island",
2307 lat=1.0,
2308 lng=2.0,
2309 ),
2310 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2311 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2312 timezone="UTC",
2313 )
2314 )
2316 event_id = create_res.event_id
2318 # measure initial background job queue length
2319 with session_scope() as session:
2320 jobs = session.query(BackgroundJob).all()
2321 job_length_before_update = len(jobs)
2323 # update with should_notify=False, expect no change in background job queue
2324 api.UpdateEvent(
2325 events_pb2.UpdateEventReq(
2326 event_id=event_id,
2327 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2328 should_notify=False,
2329 )
2330 )
2332 with session_scope() as session:
2333 jobs = session.query(BackgroundJob).all()
2334 assert len(jobs) == job_length_before_update
2336 # update with should_notify=True, expect one new background job added
2337 api.UpdateEvent(
2338 events_pb2.UpdateEventReq(
2339 event_id=event_id,
2340 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2341 should_notify=True,
2342 )
2343 )
2345 with session_scope() as session:
2346 jobs = session.query(BackgroundJob).all()
2347 assert len(jobs) == job_length_before_update + 1
2350def test_event_photo_key(db):
2351 """Test that events return the photo_key field when a photo is set."""
2352 user, token = generate_user()
2354 start_time = now() + timedelta(hours=2)
2355 end_time = start_time + timedelta(hours=3)
2357 # Create a community and an upload for the event photo
2358 with session_scope() as session:
2359 create_community(session, 0, 2, "Community", [user], [], None)
2360 upload = Upload(
2361 key="test_event_photo_key_123",
2362 filename="test_event_photo_key_123.jpg",
2363 creator_user_id=user.id,
2364 )
2365 session.add(upload)
2367 with events_session(token) as api:
2368 # Create event without photo
2369 res = api.CreateEvent(
2370 events_pb2.CreateEventReq(
2371 title="Event Without Photo",
2372 content="No photo content.",
2373 photo_key=None,
2374 location=events_pb2.EventLocation(
2375 address="Near Null Island",
2376 lat=0.1,
2377 lng=0.2,
2378 ),
2379 start_time=Timestamp_from_datetime(start_time),
2380 end_time=Timestamp_from_datetime(end_time),
2381 timezone="UTC",
2382 )
2383 )
2385 assert res.photo_key == ""
2386 assert res.photo_url == ""
2388 # Create event with photo
2389 res_with_photo = api.CreateEvent(
2390 events_pb2.CreateEventReq(
2391 title="Event With Photo",
2392 content="Has photo content.",
2393 photo_key="test_event_photo_key_123",
2394 location=events_pb2.EventLocation(
2395 address="Near Null Island",
2396 lat=0.1,
2397 lng=0.2,
2398 ),
2399 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)),
2400 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)),
2401 timezone="UTC",
2402 )
2403 )
2405 assert res_with_photo.photo_key == "test_event_photo_key_123"
2406 assert "test_event_photo_key_123" in res_with_photo.photo_url
2408 event_id = res_with_photo.event_id
2410 # Verify photo_key is returned when getting the event
2411 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2412 assert get_res.photo_key == "test_event_photo_key_123"
2413 assert "test_event_photo_key_123" in get_res.photo_url
2416def test_event_created_with_shadowed_visibility(db):
2417 """Events start in SHADOWED state when created."""
2418 user, token = generate_user()
2420 with session_scope() as session:
2421 create_community(session, 0, 2, "Community", [user], [], None)
2423 start_time = now() + timedelta(hours=2)
2424 end_time = start_time + timedelta(hours=3)
2426 with events_session(token) as api:
2427 res = api.CreateEvent(
2428 events_pb2.CreateEventReq(
2429 title="Test UMS Event",
2430 content="UMS content.",
2431 location=events_pb2.EventLocation(
2432 address="Near Null Island",
2433 lat=0.1,
2434 lng=0.2,
2435 ),
2436 start_time=Timestamp_from_datetime(start_time),
2437 end_time=Timestamp_from_datetime(end_time),
2438 timezone="UTC",
2439 )
2440 )
2441 event_id = res.event_id
2443 with session_scope() as session:
2444 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2445 mod_state = session.execute(
2446 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2447 ).scalar_one()
2448 assert mod_state.visibility == ModerationVisibility.shadowed
2451def test_shadowed_event_visible_to_creator_only(db):
2452 """SHADOWED events are visible to the creator but not to other users."""
2453 user1, token1 = generate_user()
2454 user2, token2 = generate_user()
2456 with session_scope() as session:
2457 create_community(session, 0, 2, "Community", [user1], [], None)
2459 start_time = now() + timedelta(hours=2)
2460 end_time = start_time + timedelta(hours=3)
2462 with events_session(token1) as api:
2463 res = api.CreateEvent(
2464 events_pb2.CreateEventReq(
2465 title="Shadowed Event",
2466 content="Content.",
2467 location=events_pb2.EventLocation(
2468 address="Near Null Island",
2469 lat=0.1,
2470 lng=0.2,
2471 ),
2472 start_time=Timestamp_from_datetime(start_time),
2473 end_time=Timestamp_from_datetime(end_time),
2474 timezone="UTC",
2475 )
2476 )
2477 event_id = res.event_id
2479 # Creator can see it
2480 with events_session(token1) as api:
2481 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2482 assert res.title == "Shadowed Event"
2484 # Other user cannot
2485 with events_session(token2) as api:
2486 with pytest.raises(grpc.RpcError) as e:
2487 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2488 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2491def test_event_visible_after_approval(db, moderator: Moderator):
2492 """Events become visible to all users after moderation approval."""
2493 user1, token1 = generate_user()
2494 user2, token2 = generate_user()
2496 with session_scope() as session:
2497 create_community(session, 0, 2, "Community", [user1], [], None)
2499 start_time = now() + timedelta(hours=2)
2500 end_time = start_time + timedelta(hours=3)
2502 with events_session(token1) as api:
2503 res = api.CreateEvent(
2504 events_pb2.CreateEventReq(
2505 title="Approved Event",
2506 content="Content.",
2507 location=events_pb2.EventLocation(
2508 address="Near Null Island",
2509 lat=0.1,
2510 lng=0.2,
2511 ),
2512 start_time=Timestamp_from_datetime(start_time),
2513 end_time=Timestamp_from_datetime(end_time),
2514 timezone="UTC",
2515 )
2516 )
2517 event_id = res.event_id
2519 # Other user cannot see it yet
2520 with events_session(token2) as api:
2521 with pytest.raises(grpc.RpcError) as e:
2522 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2523 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2525 # Approve the event
2526 moderator.approve_event_occurrence(event_id)
2528 # Now other user can see it
2529 with events_session(token2) as api:
2530 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2531 assert res.title == "Approved Event"
2534def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2535 """SHADOWED events appear in lists for the creator but not for other users."""
2536 user1, token1 = generate_user()
2537 user2, token2 = generate_user()
2539 with session_scope() as session:
2540 create_community(session, 0, 2, "Community", [user1], [], None)
2542 start_time = now() + timedelta(hours=2)
2543 end_time = start_time + timedelta(hours=3)
2545 with events_session(token1) as api:
2546 res = api.CreateEvent(
2547 events_pb2.CreateEventReq(
2548 title="List Test Event",
2549 content="Content.",
2550 location=events_pb2.EventLocation(
2551 address="Near Null Island",
2552 lat=0.1,
2553 lng=0.2,
2554 ),
2555 start_time=Timestamp_from_datetime(start_time),
2556 end_time=Timestamp_from_datetime(end_time),
2557 timezone="UTC",
2558 )
2559 )
2560 event_id = res.event_id
2562 # Creator can see their own SHADOWED event in lists
2563 with events_session(token1) as api:
2564 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2565 event_ids = [e.event_id for e in list_res.events]
2566 assert event_id in event_ids
2568 # Other user cannot see the SHADOWED event in lists
2569 with events_session(token2) as api:
2570 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2571 event_ids = [e.event_id for e in list_res.events]
2572 assert event_id not in event_ids
2574 # After approval, other user can see it
2575 moderator.approve_event_occurrence(event_id)
2577 with events_session(token2) as api:
2578 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2579 event_ids = [e.event_id for e in list_res.events]
2580 assert event_id in event_ids
2583def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2584 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2585 user1, token1 = generate_user()
2586 user2, token2 = generate_user()
2588 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2589 with session_scope() as session:
2590 world = create_community(session, 0, 10, "World", [user1], [], None)
2591 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2592 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2593 create_community(session, 0, 2, "Child", [user2], [], region)
2595 start_time = now() + timedelta(hours=2)
2596 end_time = start_time + timedelta(hours=3)
2598 with events_session(token1) as api:
2599 res = api.CreateEvent(
2600 events_pb2.CreateEventReq(
2601 title="Deferred Event",
2602 content="Content.",
2603 location=events_pb2.EventLocation(
2604 address="Near Null Island",
2605 lat=0.1,
2606 lng=0.2,
2607 ),
2608 start_time=Timestamp_from_datetime(start_time),
2609 end_time=Timestamp_from_datetime(end_time),
2610 timezone="UTC",
2611 )
2612 )
2613 event_id = res.event_id
2615 # Process all jobs — notification should be deferred (event is SHADOWED)
2616 process_jobs()
2618 with session_scope() as session:
2619 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2620 # Notification was created with moderation_state_id for deferral
2621 assert notif.moderation_state_id is not None
2622 # No delivery exists (deferred because event is SHADOWED)
2623 delivery_count = session.execute(
2624 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2625 ).scalar_one_or_none()
2626 assert delivery_count is None
2628 # Approve the event — handle_notification is re-queued for deferred notifications
2629 moderator.approve_event_occurrence(event_id)
2631 # Verify handle_notification job was queued
2632 with session_scope() as session:
2633 pending_jobs = (
2634 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2635 .scalars()
2636 .all()
2637 )
2638 assert any("handle_notification" in j.job_type for j in pending_jobs)
2641def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2642 """Event update notifications should carry the event's moderation_state_id for deferral."""
2643 user1, token1 = generate_user()
2644 user2, token2 = generate_user()
2646 with session_scope() as session:
2647 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2649 start_time = now() + timedelta(hours=2)
2650 end_time = start_time + timedelta(hours=3)
2652 with events_session(token1) as api:
2653 res = api.CreateEvent(
2654 events_pb2.CreateEventReq(
2655 title="Update Test",
2656 content="Content.",
2657 location=events_pb2.EventLocation(
2658 address="Near Null Island",
2659 lat=0.1,
2660 lng=0.2,
2661 ),
2662 start_time=Timestamp_from_datetime(start_time),
2663 end_time=Timestamp_from_datetime(end_time),
2664 timezone="UTC",
2665 )
2666 )
2667 event_id = res.event_id
2669 moderator.approve_event_occurrence(event_id)
2670 process_jobs()
2671 # Clear any create notifications
2672 while push_collector.count_for_user(user2.id): 2672 ↛ 2673line 2672 didn't jump to line 2673 because the condition on line 2672 was never true
2673 push_collector.pop_for_user(user2.id)
2675 # User2 subscribes to the event
2676 with events_session(token2) as api:
2677 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2679 # User1 updates the event with should_notify=True
2680 with events_session(token1) as api:
2681 api.UpdateEvent(
2682 events_pb2.UpdateEventReq(
2683 event_id=event_id,
2684 title=wrappers_pb2.StringValue(value="Updated Title"),
2685 should_notify=True,
2686 )
2687 )
2689 process_jobs()
2691 # Verify that the update notification for user2 has moderation_state_id set
2692 with session_scope() as session:
2693 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2695 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2696 # Find the update notification (most recent one)
2697 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2698 assert len(update_notifs) == 1
2699 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2702def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2703 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2704 user1, token1 = generate_user()
2705 user2, token2 = generate_user()
2707 with session_scope() as session:
2708 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2710 start_time = now() + timedelta(hours=2)
2711 end_time = start_time + timedelta(hours=3)
2713 with events_session(token1) as api:
2714 res = api.CreateEvent(
2715 events_pb2.CreateEventReq(
2716 title="Cancel Test",
2717 content="Content.",
2718 location=events_pb2.EventLocation(
2719 address="Near Null Island",
2720 lat=0.1,
2721 lng=0.2,
2722 ),
2723 start_time=Timestamp_from_datetime(start_time),
2724 end_time=Timestamp_from_datetime(end_time),
2725 timezone="UTC",
2726 )
2727 )
2728 event_id = res.event_id
2730 moderator.approve_event_occurrence(event_id)
2731 process_jobs()
2732 while push_collector.count_for_user(user2.id): 2732 ↛ 2733line 2732 didn't jump to line 2733 because the condition on line 2732 was never true
2733 push_collector.pop_for_user(user2.id)
2735 # User2 subscribes
2736 with events_session(token2) as api:
2737 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2739 # User1 cancels the event
2740 with events_session(token1) as api:
2741 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2743 process_jobs()
2745 # Verify that the cancel notification for user2 has moderation_state_id set
2746 with session_scope() as session:
2747 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2749 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2750 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2751 assert len(cancel_notifs) == 1
2752 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2755def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2756 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2757 user1, token1 = generate_user()
2758 user2, token2 = generate_user()
2760 with session_scope() as session:
2761 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2763 # Create event starting 23 hours from now (within 24h reminder window)
2764 start_time = now() + timedelta(hours=23)
2765 end_time = start_time + timedelta(hours=1)
2767 with events_session(token1) as api:
2768 res = api.CreateEvent(
2769 events_pb2.CreateEventReq(
2770 title="Reminder Test",
2771 content="Content.",
2772 location=events_pb2.EventLocation(
2773 address="Near Null Island",
2774 lat=0.1,
2775 lng=0.2,
2776 ),
2777 start_time=Timestamp_from_datetime(start_time),
2778 end_time=Timestamp_from_datetime(end_time),
2779 timezone="UTC",
2780 )
2781 )
2782 event_id = res.event_id
2784 moderator.approve_event_occurrence(event_id)
2785 process_jobs()
2786 while push_collector.count_for_user(user2.id): 2786 ↛ 2787line 2786 didn't jump to line 2787 because the condition on line 2786 was never true
2787 push_collector.pop_for_user(user2.id)
2789 # User2 marks attendance
2790 with events_session(token2) as api:
2791 api.SetEventAttendance(
2792 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2793 )
2795 # Run the event reminder handler
2796 send_event_reminders(empty_pb2.Empty())
2797 process_jobs()
2799 # Verify that the reminder notification for user2 has moderation_state_id set
2800 with session_scope() as session:
2801 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2803 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2804 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2805 assert len(reminder_notifs) == 1
2806 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2809def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2810 """Event reminders should not be sent for cancelled events."""
2811 user1, token1 = generate_user()
2812 user2, token2 = generate_user()
2814 with session_scope() as session:
2815 create_community(session, 0, 2, "Community", [user2], [], None)
2817 # Create event starting 23 hours from now (within 24h reminder window)
2818 start_time = now() + timedelta(hours=23)
2819 end_time = start_time + timedelta(hours=1)
2821 with events_session(token1) as api:
2822 res = api.CreateEvent(
2823 events_pb2.CreateEventReq(
2824 title="Cancelled Reminder Test",
2825 content="Content.",
2826 location=events_pb2.EventLocation(
2827 address="Near Null Island",
2828 lat=0.1,
2829 lng=0.2,
2830 ),
2831 start_time=Timestamp_from_datetime(start_time),
2832 end_time=Timestamp_from_datetime(end_time),
2833 timezone="UTC",
2834 )
2835 )
2836 event_id = res.event_id
2838 moderator.approve_event_occurrence(event_id)
2839 process_jobs()
2841 # User2 marks attendance
2842 with events_session(token2) as api:
2843 api.SetEventAttendance(
2844 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2845 )
2847 # User1 cancels the event
2848 with events_session(token1) as api:
2849 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2851 process_jobs()
2852 # Drain any cancellation-related notifications so we can cleanly assert on reminders
2853 while push_collector.count_for_user(user2.id):
2854 push_collector.pop_for_user(user2.id)
2856 # Run the event reminder handler
2857 send_event_reminders(empty_pb2.Empty())
2858 process_jobs()
2860 # Verify that no reminder notification was sent for user2
2861 with session_scope() as session:
2862 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2863 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2864 assert len(reminder_notifs) == 0
2867@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
2868def test_event_reminder_not_sent_for_invisible_attendee(
2869 db, push_collector: PushCollector, moderator: Moderator, invisible_field
2870):
2871 user1, token1 = generate_user()
2872 user2, token2 = generate_user()
2874 with session_scope() as session:
2875 create_community(session, 0, 2, "Community", [user2], [], None)
2877 start_time = now() + timedelta(hours=23)
2878 end_time = start_time + timedelta(hours=1)
2880 with events_session(token1) as api:
2881 res = api.CreateEvent(
2882 events_pb2.CreateEventReq(
2883 title="Invisible Attendee Reminder Test",
2884 content="Content.",
2885 location=events_pb2.EventLocation(
2886 address="Near Null Island",
2887 lat=0.1,
2888 lng=0.2,
2889 ),
2890 start_time=Timestamp_from_datetime(start_time),
2891 end_time=Timestamp_from_datetime(end_time),
2892 timezone="UTC",
2893 )
2894 )
2895 event_id = res.event_id
2897 moderator.approve_event_occurrence(event_id)
2898 process_jobs()
2900 with events_session(token2) as api:
2901 api.SetEventAttendance(
2902 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2903 )
2905 with session_scope() as session:
2906 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
2908 send_event_reminders(empty_pb2.Empty())
2909 process_jobs()
2911 with session_scope() as session:
2912 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2913 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2914 assert len(reminder_notifs) == 0
2917def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
2918 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
2919 user1, token1 = generate_user()
2920 user2, token2 = generate_user()
2922 with session_scope() as session:
2923 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
2925 start = now()
2927 # User1 creates event A with 3 occurrences
2928 event_a_ids = []
2929 with events_session(token1) as api:
2930 res = api.CreateEvent(
2931 events_pb2.CreateEventReq(
2932 title="Event A",
2933 content="Content A.",
2934 parent_community_id=c_id,
2935 location=events_pb2.EventLocation(
2936 address="Near Null Island",
2937 lat=0.1,
2938 lng=0.2,
2939 ),
2940 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2941 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
2942 timezone="UTC",
2943 )
2944 )
2945 event_a_ids.append(res.event_id)
2946 for i in range(2):
2947 res = api.ScheduleEvent(
2948 events_pb2.ScheduleEventReq(
2949 event_id=event_a_ids[-1],
2950 content=f"A occurrence {i}",
2951 location=events_pb2.EventLocation(
2952 address="Near Null Island",
2953 lat=0.1,
2954 lng=0.2,
2955 ),
2956 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
2957 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
2958 timezone="UTC",
2959 )
2960 )
2961 event_a_ids.append(res.event_id)
2963 # User2 creates event B with 2 occurrences
2964 event_b_ids = []
2965 with events_session(token2) as api:
2966 res = api.CreateEvent(
2967 events_pb2.CreateEventReq(
2968 title="Event B",
2969 content="Content B.",
2970 parent_community_id=c_id,
2971 location=events_pb2.EventLocation(
2972 address="Near Null Island",
2973 lat=0.1,
2974 lng=0.2,
2975 ),
2976 start_time=Timestamp_from_datetime(start + timedelta(hours=10)),
2977 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)),
2978 timezone="UTC",
2979 )
2980 )
2981 event_b_ids.append(res.event_id)
2982 res = api.ScheduleEvent(
2983 events_pb2.ScheduleEventReq(
2984 event_id=event_b_ids[-1],
2985 content="B occurrence 1",
2986 location=events_pb2.EventLocation(
2987 address="Near Null Island",
2988 lat=0.1,
2989 lng=0.2,
2990 ),
2991 start_time=Timestamp_from_datetime(start + timedelta(hours=11)),
2992 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)),
2993 timezone="UTC",
2994 )
2995 )
2996 event_b_ids.append(res.event_id)
2998 moderator.approve_event_occurrence(event_a_ids[0])
2999 moderator.approve_event_occurrence(event_b_ids[0])
3001 # List occurrences for event A — should only get event A's 3 occurrences
3002 with events_session(token1) as api:
3003 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3004 returned_ids = [e.event_id for e in res.events]
3005 assert sorted(returned_ids) == sorted(event_a_ids)
3007 # List occurrences for event B — should only get event B's 2 occurrences
3008 with events_session(token2) as api:
3009 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3010 returned_ids = [e.event_id for e in res.events]
3011 assert sorted(returned_ids) == sorted(event_b_ids)
3014def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3015 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3016 user1, token1 = generate_user()
3017 user2, token2 = generate_user()
3019 with session_scope() as session:
3020 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3022 start_time = now() + timedelta(hours=2)
3023 end_time = start_time + timedelta(hours=3)
3025 with events_session(token1) as api:
3026 res = api.CreateEvent(
3027 events_pb2.CreateEventReq(
3028 title="Comment Test",
3029 content="Content.",
3030 parent_community_id=c_id,
3031 location=events_pb2.EventLocation(
3032 address="Near Null Island",
3033 lat=0.1,
3034 lng=0.2,
3035 ),
3036 start_time=Timestamp_from_datetime(start_time),
3037 end_time=Timestamp_from_datetime(end_time),
3038 timezone="UTC",
3039 )
3040 )
3041 event_id = res.event_id
3042 thread_id = res.thread.thread_id
3044 moderator.approve_event_occurrence(event_id)
3045 process_jobs()
3046 while push_collector.count_for_user(user1.id): 3046 ↛ 3047line 3046 didn't jump to line 3047 because the condition on line 3046 was never true
3047 push_collector.pop_for_user(user1.id)
3049 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3050 with events_session(token1) as api:
3051 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3053 # User2 posts a top-level comment on the event thread
3054 with threads_session(token2) as api:
3055 comment_thread_id = api.PostReply(
3056 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3057 ).thread_id
3059 process_jobs()
3061 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3062 comment_db_id = comment_thread_id // 10
3063 with session_scope() as session:
3064 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3066 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3067 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3068 assert len(comment_notifs) == 1
3069 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3072def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3073 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3074 user1, token1 = generate_user()
3075 user2, token2 = generate_user()
3076 user3, token3 = generate_user()
3078 with session_scope() as session:
3079 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3081 start_time = now() + timedelta(hours=2)
3082 end_time = start_time + timedelta(hours=3)
3084 with events_session(token1) as api:
3085 res = api.CreateEvent(
3086 events_pb2.CreateEventReq(
3087 title="Reply Test",
3088 content="Content.",
3089 location=events_pb2.EventLocation(
3090 address="Near Null Island",
3091 lat=0.1,
3092 lng=0.2,
3093 ),
3094 start_time=Timestamp_from_datetime(start_time),
3095 end_time=Timestamp_from_datetime(end_time),
3096 timezone="UTC",
3097 )
3098 )
3099 event_id = res.event_id
3100 thread_id = res.thread.thread_id
3102 moderator.approve_event_occurrence(event_id)
3103 process_jobs()
3104 while push_collector.count_for_user(user1.id): 3104 ↛ 3105line 3104 didn't jump to line 3105 because the condition on line 3104 was never true
3105 push_collector.pop_for_user(user1.id)
3107 # User2 posts a top-level comment
3108 with threads_session(token2) as api:
3109 comment_thread_id = api.PostReply(
3110 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3111 ).thread_id
3113 process_jobs()
3114 while push_collector.count_for_user(user1.id): 3114 ↛ 3115line 3114 didn't jump to line 3115 because the condition on line 3114 was never true
3115 push_collector.pop_for_user(user1.id)
3117 # User3 replies to user2's comment (depth=2 reply)
3118 with threads_session(token3) as api:
3119 nested_reply_thread_id = api.PostReply(
3120 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3121 ).thread_id
3123 process_jobs()
3125 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3126 nested_reply_db_id = nested_reply_thread_id // 10
3127 with session_scope() as session:
3128 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3130 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3131 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3132 assert len(reply_notifs) == 1
3133 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id