Coverage for app/backend/src/tests/test_events.py: 99%
1402 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-19 00:32 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-19 00:32 +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_GetEvent_online(db, moderator: Moderator):
733 """Validate that legacy online events are surfaced as offline events through the API."""
734 user1, token1 = generate_user()
736 with session_scope() as session:
737 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
739 start_time = now() + timedelta(hours=2)
740 end_time = start_time + timedelta(hours=3)
742 # Create as an offline event since the API doesn't support online events anymore.
743 with events_session(token1) as api:
744 create_res: events_pb2.Event = api.CreateEvent(
745 events_pb2.CreateEventReq(
746 title="Dummy Title",
747 content="Dummy content.",
748 parent_community_id=c_id,
749 location=events_pb2.EventLocation(address="Near Null Island", lat=0.1, lng=0.2),
750 start_time=Timestamp_from_datetime(start_time),
751 end_time=Timestamp_from_datetime(end_time),
752 timezone="UTC",
753 )
754 )
756 event_id = create_res.event_id
758 moderator.approve_event_occurrence(event_id)
760 # Tweak the DB object to turn it into a legacy online event
761 with session_scope() as session:
762 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
763 occurrence.geom = None
764 occurrence.address = None
765 occurrence.link = "https://couchers.org/meet/"
767 # Backend should surface it as an offline event
768 with events_session(token1) as api:
769 get_res: events_pb2.Event = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
771 assert get_res.title == "Dummy Title"
772 assert get_res.HasField("location")
773 assert get_res.location.address == "https://couchers.org/meet/"
774 assert get_res.location.lng == 0
775 assert get_res.location.lat == 0
778def test_UpdateEvent_all(db, moderator: Moderator):
779 # event creator
780 user1, token1 = generate_user()
781 # community moderator
782 user2, token2 = generate_user()
783 # third parties
784 user3, token3 = generate_user()
785 user4, token4 = generate_user()
786 user5, token5 = generate_user()
787 user6, token6 = generate_user()
789 with session_scope() as session:
790 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
792 time_before = now()
793 start_time = now() + timedelta(hours=1)
794 end_time = start_time + timedelta(hours=1.5)
796 event_ids = []
798 with events_session(token1) as api:
799 res = api.CreateEvent(
800 events_pb2.CreateEventReq(
801 title="Dummy Title",
802 content="0th occurrence",
803 location=events_pb2.EventLocation(
804 address="Near Null Island",
805 lat=0.1,
806 lng=0.2,
807 ),
808 start_time=Timestamp_from_datetime(start_time),
809 end_time=Timestamp_from_datetime(end_time),
810 timezone="UTC",
811 )
812 )
814 event_id = res.event_id
815 event_ids.append(event_id)
817 moderator.approve_event_occurrence(event_id)
819 with events_session(token4) as api:
820 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
822 with events_session(token5) as api:
823 api.SetEventAttendance(
824 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
825 )
827 with events_session(token6) as api:
828 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
830 with events_session(token1) as api:
831 for i in range(5):
832 res = api.ScheduleEvent(
833 events_pb2.ScheduleEventReq(
834 event_id=event_ids[-1],
835 content=f"{i + 1}th occurrence",
836 location=events_pb2.EventLocation(
837 address="Near Null Island",
838 lat=0.1,
839 lng=0.2,
840 ),
841 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)),
842 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)),
843 timezone="UTC",
844 )
845 )
847 event_ids.append(res.event_id)
849 # Approve all scheduled occurrences
850 for eid in event_ids[1:]:
851 moderator.approve_event_occurrence(eid)
853 updated_event_id = event_ids[3]
855 time_before_update = now()
857 with events_session(token1) as api:
858 res = api.UpdateEvent(
859 events_pb2.UpdateEventReq(
860 event_id=updated_event_id,
861 title=wrappers_pb2.StringValue(value="New Title"),
862 content=wrappers_pb2.StringValue(value="New content."),
863 location=events_pb2.EventLocation(
864 lat=0.2,
865 lng=0.2,
866 ),
867 update_all_future=True,
868 )
869 )
871 time_after_update = now()
873 with events_session(token2) as api:
874 for i in range(3):
875 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
876 assert res.content == f"{i}th occurrence"
877 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
879 for i in range(3, 6):
880 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
881 assert res.content == "New content."
882 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
885def test_GetEvent(db, moderator: Moderator):
886 # event creator
887 user1, token1 = generate_user()
888 # community moderator
889 user2, token2 = generate_user()
890 # third parties
891 user3, token3 = generate_user()
892 user4, token4 = generate_user()
893 user5, token5 = generate_user()
894 user6, token6 = generate_user()
896 with session_scope() as session:
897 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
899 time_before = now()
900 start_time = now() + timedelta(hours=2)
901 end_time = start_time + timedelta(hours=3)
903 with events_session(token1) as api:
904 # in person event
905 res = api.CreateEvent(
906 events_pb2.CreateEventReq(
907 title="Dummy Title",
908 content="Dummy content.",
909 location=events_pb2.EventLocation(
910 address="Near Null Island",
911 lat=0.1,
912 lng=0.2,
913 ),
914 start_time=Timestamp_from_datetime(start_time),
915 end_time=Timestamp_from_datetime(end_time),
916 timezone="UTC",
917 )
918 )
920 event_id = res.event_id
922 moderator.approve_event_occurrence(event_id)
924 with events_session(token4) as api:
925 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
927 with events_session(token5) as api:
928 api.SetEventAttendance(
929 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
930 )
932 with events_session(token6) as api:
933 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
935 with events_session(token1) as api:
936 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
938 assert res.is_next
939 assert res.title == "Dummy Title"
940 assert res.slug == "dummy-title"
941 assert res.content == "Dummy content."
942 assert not res.photo_url
943 assert res.HasField("location")
944 assert res.location.lat == 0.1
945 assert res.location.lng == 0.2
946 assert res.location.address == "Near Null Island"
947 assert time_before <= to_aware_datetime(res.created) <= now()
948 assert time_before <= to_aware_datetime(res.last_edited) <= now()
949 assert res.creator_user_id == user1.id
950 assert to_aware_datetime(res.start_time) == start_time
951 assert to_aware_datetime(res.end_time) == end_time
952 # assert res.timezone == "UTC"
953 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
954 assert res.organizer
955 assert res.subscriber
956 assert res.going_count == 2
957 assert res.organizer_count == 1
958 assert res.subscriber_count == 3
959 assert res.owner_user_id == user1.id
960 assert not res.owner_community_id
961 assert not res.owner_group_id
962 assert res.thread.thread_id
963 assert res.can_edit
964 assert not res.can_moderate
966 with events_session(token2) as api:
967 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
969 assert res.is_next
970 assert res.title == "Dummy Title"
971 assert res.slug == "dummy-title"
972 assert res.content == "Dummy content."
973 assert not res.photo_url
974 assert res.HasField("location")
975 assert res.location.lat == 0.1
976 assert res.location.lng == 0.2
977 assert res.location.address == "Near Null Island"
978 assert time_before <= to_aware_datetime(res.created) <= now()
979 assert time_before <= to_aware_datetime(res.last_edited) <= now()
980 assert res.creator_user_id == user1.id
981 assert to_aware_datetime(res.start_time) == start_time
982 assert to_aware_datetime(res.end_time) == end_time
983 # assert res.timezone == "UTC"
984 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
985 assert not res.organizer
986 assert not res.subscriber
987 assert res.going_count == 2
988 assert res.organizer_count == 1
989 assert res.subscriber_count == 3
990 assert res.owner_user_id == user1.id
991 assert not res.owner_community_id
992 assert not res.owner_group_id
993 assert res.thread.thread_id
994 assert res.can_edit
995 assert res.can_moderate
997 with events_session(token3) as api:
998 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1000 assert res.is_next
1001 assert res.title == "Dummy Title"
1002 assert res.slug == "dummy-title"
1003 assert res.content == "Dummy content."
1004 assert not res.photo_url
1005 assert res.HasField("location")
1006 assert res.location.lat == 0.1
1007 assert res.location.lng == 0.2
1008 assert res.location.address == "Near Null Island"
1009 assert time_before <= to_aware_datetime(res.created) <= now()
1010 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1011 assert res.creator_user_id == user1.id
1012 assert to_aware_datetime(res.start_time) == start_time
1013 assert to_aware_datetime(res.end_time) == end_time
1014 # assert res.timezone == "UTC"
1015 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1016 assert not res.organizer
1017 assert not res.subscriber
1018 assert res.going_count == 2
1019 assert res.organizer_count == 1
1020 assert res.subscriber_count == 3
1021 assert res.owner_user_id == user1.id
1022 assert not res.owner_community_id
1023 assert not res.owner_group_id
1024 assert res.thread.thread_id
1025 assert not res.can_edit
1026 assert not res.can_moderate
1029def test_CancelEvent(db, moderator: Moderator):
1030 # event creator
1031 user1, token1 = generate_user()
1032 # community moderator
1033 user2, token2 = generate_user()
1034 # third parties
1035 user3, token3 = generate_user()
1036 user4, token4 = generate_user()
1037 user5, token5 = generate_user()
1038 user6, token6 = generate_user()
1040 with session_scope() as session:
1041 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1043 start_time = now() + timedelta(hours=2)
1044 end_time = start_time + timedelta(hours=3)
1046 with events_session(token1) as api:
1047 res = api.CreateEvent(
1048 events_pb2.CreateEventReq(
1049 title="Dummy Title",
1050 content="Dummy content.",
1051 location=events_pb2.EventLocation(
1052 address="Near Null Island",
1053 lat=0.1,
1054 lng=0.2,
1055 ),
1056 start_time=Timestamp_from_datetime(start_time),
1057 end_time=Timestamp_from_datetime(end_time),
1058 timezone="UTC",
1059 )
1060 )
1062 event_id = res.event_id
1064 moderator.approve_event_occurrence(event_id)
1066 with events_session(token4) as api:
1067 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1069 with events_session(token5) as api:
1070 api.SetEventAttendance(
1071 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1072 )
1074 with events_session(token6) as api:
1075 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1077 with events_session(token1) as api:
1078 res = api.CancelEvent(
1079 events_pb2.CancelEventReq(
1080 event_id=event_id,
1081 )
1082 )
1084 with events_session(token1) as api:
1085 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1086 assert res.is_cancelled
1088 with events_session(token1) as api:
1089 with pytest.raises(grpc.RpcError) as e:
1090 api.UpdateEvent(
1091 events_pb2.UpdateEventReq(
1092 event_id=event_id,
1093 title=wrappers_pb2.StringValue(value="New Title"),
1094 )
1095 )
1096 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1097 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1099 with pytest.raises(grpc.RpcError) as e:
1100 api.InviteEventOrganizer(
1101 events_pb2.InviteEventOrganizerReq(
1102 event_id=event_id,
1103 user_id=user3.id,
1104 )
1105 )
1106 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1107 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1109 with pytest.raises(grpc.RpcError) as e:
1110 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1111 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1112 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1114 with events_session(token3) as api:
1115 with pytest.raises(grpc.RpcError) as e:
1116 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1117 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1118 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1120 with pytest.raises(grpc.RpcError) as e:
1121 api.SetEventAttendance(
1122 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1123 )
1124 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1125 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1127 with events_session(token1) as api:
1128 for include_cancelled in [True, False]:
1129 res = api.ListEventOccurrences(
1130 events_pb2.ListEventOccurrencesReq(
1131 event_id=event_id,
1132 include_cancelled=include_cancelled,
1133 )
1134 )
1135 if include_cancelled:
1136 assert len(res.events) > 0
1137 else:
1138 assert len(res.events) == 0
1140 res = api.ListMyEvents(
1141 events_pb2.ListMyEventsReq(
1142 include_cancelled=include_cancelled,
1143 )
1144 )
1145 if include_cancelled:
1146 assert len(res.events) > 0
1147 else:
1148 assert len(res.events) == 0
1151def test_ListEventAttendees(db, moderator: Moderator):
1152 # event creator
1153 user1, token1 = generate_user()
1154 # others
1155 user2, token2 = generate_user()
1156 user3, token3 = generate_user()
1157 user4, token4 = generate_user()
1158 user5, token5 = generate_user()
1159 user6, token6 = generate_user()
1161 with session_scope() as session:
1162 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1164 with events_session(token1) as api:
1165 event_id = api.CreateEvent(
1166 events_pb2.CreateEventReq(
1167 title="Dummy Title",
1168 content="Dummy content.",
1169 location=events_pb2.EventLocation(
1170 address="Near Null Island",
1171 lat=0.1,
1172 lng=0.2,
1173 ),
1174 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1175 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1176 timezone="UTC",
1177 )
1178 ).event_id
1180 moderator.approve_event_occurrence(event_id)
1182 for token in [token2, token3, token4, token5]:
1183 with events_session(token) as api:
1184 api.SetEventAttendance(
1185 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1186 )
1188 with events_session(token6) as api:
1189 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1191 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1192 assert res.attendee_user_ids == [user1.id, user2.id]
1194 res = api.ListEventAttendees(
1195 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1196 )
1197 assert res.attendee_user_ids == [user3.id, user4.id]
1199 res = api.ListEventAttendees(
1200 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1201 )
1202 assert res.attendee_user_ids == [user5.id]
1203 assert not res.next_page_token
1206def test_ListEventSubscribers(db, moderator: Moderator):
1207 # event creator
1208 user1, token1 = generate_user()
1209 # others
1210 user2, token2 = generate_user()
1211 user3, token3 = generate_user()
1212 user4, token4 = generate_user()
1213 user5, token5 = generate_user()
1214 user6, token6 = generate_user()
1216 with session_scope() as session:
1217 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1219 with events_session(token1) as api:
1220 event_id = api.CreateEvent(
1221 events_pb2.CreateEventReq(
1222 title="Dummy Title",
1223 content="Dummy content.",
1224 location=events_pb2.EventLocation(
1225 address="Near Null Island",
1226 lat=0.1,
1227 lng=0.2,
1228 ),
1229 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1230 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1231 timezone="UTC",
1232 )
1233 ).event_id
1235 moderator.approve_event_occurrence(event_id)
1237 for token in [token2, token3, token4, token5]:
1238 with events_session(token) as api:
1239 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1241 with events_session(token6) as api:
1242 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1244 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1245 assert res.subscriber_user_ids == [user1.id, user2.id]
1247 res = api.ListEventSubscribers(
1248 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1249 )
1250 assert res.subscriber_user_ids == [user3.id, user4.id]
1252 res = api.ListEventSubscribers(
1253 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1254 )
1255 assert res.subscriber_user_ids == [user5.id]
1256 assert not res.next_page_token
1259def test_ListEventOrganizers(db, moderator: Moderator):
1260 # event creator
1261 user1, token1 = generate_user()
1262 # others
1263 user2, token2 = generate_user()
1264 user3, token3 = generate_user()
1265 user4, token4 = generate_user()
1266 user5, token5 = generate_user()
1267 user6, token6 = generate_user()
1269 with session_scope() as session:
1270 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1272 with events_session(token1) as api:
1273 event_id = api.CreateEvent(
1274 events_pb2.CreateEventReq(
1275 title="Dummy Title",
1276 content="Dummy content.",
1277 location=events_pb2.EventLocation(
1278 address="Near Null Island",
1279 lat=0.1,
1280 lng=0.2,
1281 ),
1282 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1283 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1284 timezone="UTC",
1285 )
1286 ).event_id
1288 moderator.approve_event_occurrence(event_id)
1290 with events_session(token1) as api:
1291 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1292 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1294 with events_session(token6) as api:
1295 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1297 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1298 assert res.organizer_user_ids == [user1.id, user2.id]
1300 res = api.ListEventOrganizers(
1301 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1302 )
1303 assert res.organizer_user_ids == [user3.id, user4.id]
1305 res = api.ListEventOrganizers(
1306 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1307 )
1308 assert res.organizer_user_ids == [user5.id]
1309 assert not res.next_page_token
1312def test_TransferEvent(db):
1313 user1, token1 = generate_user()
1314 user2, token2 = generate_user()
1315 user3, token3 = generate_user()
1316 user4, token4 = generate_user()
1318 with session_scope() as session:
1319 c = create_community(session, 0, 2, "Community", [user3], [], None)
1320 h = create_group(session, "Group", [user4], [], c)
1321 c_id = c.id
1322 h_id = h.id
1324 with events_session(token1) as api:
1325 event_id = api.CreateEvent(
1326 events_pb2.CreateEventReq(
1327 title="Dummy Title",
1328 content="Dummy content.",
1329 location=events_pb2.EventLocation(
1330 address="Near Null Island",
1331 lat=0.1,
1332 lng=0.2,
1333 ),
1334 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1335 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1336 timezone="UTC",
1337 )
1338 ).event_id
1340 api.TransferEvent(
1341 events_pb2.TransferEventReq(
1342 event_id=event_id,
1343 new_owner_community_id=c_id,
1344 )
1345 )
1347 # remove ourselves as organizer, otherwise we can still edit it
1348 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1350 with pytest.raises(grpc.RpcError) as e:
1351 api.TransferEvent(
1352 events_pb2.TransferEventReq(
1353 event_id=event_id,
1354 new_owner_group_id=h_id,
1355 )
1356 )
1357 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1358 assert e.value.details() == "You're not allowed to transfer that event."
1360 event_id = api.CreateEvent(
1361 events_pb2.CreateEventReq(
1362 title="Dummy Title",
1363 content="Dummy content.",
1364 location=events_pb2.EventLocation(
1365 address="Near Null Island",
1366 lat=0.1,
1367 lng=0.2,
1368 ),
1369 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1370 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1371 timezone="UTC",
1372 )
1373 ).event_id
1375 api.TransferEvent(
1376 events_pb2.TransferEventReq(
1377 event_id=event_id,
1378 new_owner_group_id=h_id,
1379 )
1380 )
1382 # remove ourselves as organizer, otherwise we can still edit it
1383 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1385 with pytest.raises(grpc.RpcError) as e:
1386 api.TransferEvent(
1387 events_pb2.TransferEventReq(
1388 event_id=event_id,
1389 new_owner_community_id=c_id,
1390 )
1391 )
1392 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1393 assert e.value.details() == "You're not allowed to transfer that event."
1396def test_SetEventSubscription(db, moderator: Moderator):
1397 user1, token1 = generate_user()
1398 user2, token2 = generate_user()
1400 with session_scope() as session:
1401 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1403 with events_session(token1) as api:
1404 event_id = api.CreateEvent(
1405 events_pb2.CreateEventReq(
1406 title="Dummy Title",
1407 content="Dummy content.",
1408 location=events_pb2.EventLocation(
1409 address="Near Null Island",
1410 lat=0.1,
1411 lng=0.2,
1412 ),
1413 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1414 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1415 timezone="UTC",
1416 )
1417 ).event_id
1419 moderator.approve_event_occurrence(event_id)
1421 with events_session(token2) as api:
1422 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1423 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1424 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1425 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1426 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1429def test_SetEventAttendance(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 (
1456 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1457 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1458 )
1459 api.SetEventAttendance(
1460 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1461 )
1462 assert (
1463 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1464 == events_pb2.ATTENDANCE_STATE_GOING
1465 )
1466 api.SetEventAttendance(
1467 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1468 )
1469 assert (
1470 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1471 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1472 )
1475def test_InviteEventOrganizer(db, moderator: Moderator):
1476 user1, token1 = generate_user()
1477 user2, token2 = generate_user()
1479 with session_scope() as session:
1480 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1482 with events_session(token1) as api:
1483 event_id = api.CreateEvent(
1484 events_pb2.CreateEventReq(
1485 title="Dummy Title",
1486 content="Dummy content.",
1487 location=events_pb2.EventLocation(
1488 address="Near Null Island",
1489 lat=0.1,
1490 lng=0.2,
1491 ),
1492 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1493 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1494 timezone="UTC",
1495 )
1496 ).event_id
1498 moderator.approve_event_occurrence(event_id)
1500 with events_session(token2) as api:
1501 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1503 with pytest.raises(grpc.RpcError) as e:
1504 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1505 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1506 assert e.value.details() == "You're not allowed to edit that event."
1508 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1510 with events_session(token1) as api:
1511 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1513 with events_session(token2) as api:
1514 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1517def test_ListEventOccurrences(db):
1518 user1, token1 = generate_user()
1519 user2, token2 = generate_user()
1520 user3, token3 = generate_user()
1522 with session_scope() as session:
1523 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1525 start = now()
1527 event_ids = []
1529 with events_session(token1) as api:
1530 res = api.CreateEvent(
1531 events_pb2.CreateEventReq(
1532 title="First occurrence",
1533 content="Dummy content.",
1534 parent_community_id=c_id,
1535 location=events_pb2.EventLocation(
1536 address="Near Null Island",
1537 lat=0.1,
1538 lng=0.2,
1539 ),
1540 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
1541 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
1542 timezone="UTC",
1543 )
1544 )
1546 event_ids.append(res.event_id)
1548 for i in range(5):
1549 res = api.ScheduleEvent(
1550 events_pb2.ScheduleEventReq(
1551 event_id=event_ids[-1],
1552 content=f"{i}th occurrence",
1553 location=events_pb2.EventLocation(
1554 address="Near Null Island",
1555 lat=0.1,
1556 lng=0.2,
1557 ),
1558 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
1559 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
1560 timezone="UTC",
1561 )
1562 )
1564 event_ids.append(res.event_id)
1566 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1567 assert [event.event_id for event in res.events] == event_ids[:2]
1569 res = api.ListEventOccurrences(
1570 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1571 )
1572 assert [event.event_id for event in res.events] == event_ids[2:4]
1574 res = api.ListEventOccurrences(
1575 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1576 )
1577 assert [event.event_id for event in res.events] == event_ids[4:6]
1578 assert not res.next_page_token
1581def test_ListMyEvents(db, moderator: Moderator):
1582 user1, token1 = generate_user()
1583 user2, token2 = generate_user()
1584 user3, token3 = generate_user()
1585 user4, token4 = generate_user()
1586 user5, token5 = generate_user()
1588 with session_scope() as session:
1589 # Create global (world) -> macroregion -> region -> subregion hierarchy
1590 # my_communities_exclude_global filters out world, macroregion, and region level communities
1591 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1592 c_id = global_community.id
1593 macroregion_community = create_community(
1594 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1595 )
1596 region_community = create_community(
1597 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1598 )
1599 subregion_community = create_community(
1600 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1601 )
1602 c2_id = subregion_community.id
1604 start = now()
1606 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1607 return events_pb2.CreateEventReq(
1608 title="Dummy Title",
1609 content="Dummy content.",
1610 location=events_pb2.EventLocation(
1611 address="Near Null Island",
1612 lat=0.1,
1613 lng=0.2,
1614 ),
1615 parent_community_id=community_id,
1616 timezone="UTC",
1617 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1618 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1619 )
1621 with events_session(token1) as api:
1622 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1624 moderator.approve_event_occurrence(e2)
1626 with events_session(token2) as api:
1627 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1629 moderator.approve_event_occurrence(e1)
1631 with events_session(token1) as api:
1632 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1634 moderator.approve_event_occurrence(e3)
1636 with events_session(token2) as api:
1637 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1639 moderator.approve_event_occurrence(e5)
1641 with events_session(token3) as api:
1642 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1644 moderator.approve_event_occurrence(e4)
1646 with events_session(token4) as api:
1647 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1649 moderator.approve_event_occurrence(e6)
1651 with events_session(token1) as api:
1652 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1654 with events_session(token1) as api:
1655 api.SetEventAttendance(
1656 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1657 )
1658 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1660 with events_session(token2) as api:
1661 api.SetEventAttendance(
1662 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1663 )
1665 with events_session(token3) as api:
1666 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1668 with events_session(token1) as api:
1669 # test pagination with token first
1670 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1671 assert [event.event_id for event in res.events] == [e1, e2]
1672 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1673 assert [event.event_id for event in res.events] == [e3, e4]
1674 assert not res.next_page_token
1676 res = api.ListMyEvents(
1677 events_pb2.ListMyEventsReq(
1678 subscribed=True,
1679 attending=True,
1680 organizing=True,
1681 )
1682 )
1683 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1685 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1686 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1688 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1689 assert [event.event_id for event in res.events] == [e2, e3, e4]
1691 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1692 assert [event.event_id for event in res.events] == [e1, e2, e3]
1694 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1695 assert [event.event_id for event in res.events] == [e2, e3]
1697 with events_session(token1) as api:
1698 # Test pagination with page_number and verify total_items
1699 res = api.ListMyEvents(
1700 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1701 )
1702 assert [event.event_id for event in res.events] == [e1, e2]
1703 assert res.total_items == 4
1705 res = api.ListMyEvents(
1706 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1707 )
1708 assert [event.event_id for event in res.events] == [e3, e4]
1709 assert res.total_items == 4
1711 # Verify no more pages
1712 res = api.ListMyEvents(
1713 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1714 )
1715 assert not res.events
1716 assert res.total_items == 4
1718 with events_session(token2) as api:
1719 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1720 assert [event.event_id for event in res.events] == [e1, e3, e5]
1722 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1723 assert [event.event_id for event in res.events] == [e1, e5]
1725 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1726 assert [event.event_id for event in res.events] == [e1, e3, e5]
1728 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1729 assert [event.event_id for event in res.events] == [e1, e5]
1731 with events_session(token3) as api:
1732 # user3 is member of both global (c_id) and child (c2_id) communities
1733 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1734 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1736 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1737 assert [event.event_id for event in res.events] == [e2, e4]
1739 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1740 assert [event.event_id for event in res.events] == [e4]
1742 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1743 assert [event.event_id for event in res.events] == [e3, e4]
1745 # my_communities returns events from both communities user3 is a member of
1746 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1747 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1749 # my_communities_exclude_global filters out events from global community (node_id=1)
1750 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1751 assert [event.event_id for event in res.events] == [e6]
1753 # my_communities_exclude_global works independently of my_communities flag
1754 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1755 assert [event.event_id for event in res.events] == [e6]
1757 # my_communities_exclude_global filters organizing results too
1758 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1759 assert [event.event_id for event in res.events] == []
1761 # my_communities_exclude_global filters subscribed results too
1762 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1763 assert [event.event_id for event in res.events] == []
1765 with events_session(token5) as api:
1766 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1767 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1770def test_list_my_events_exclude_attending(db, moderator: Moderator):
1771 user1, token1 = generate_user()
1772 user2, token2 = generate_user()
1774 with session_scope() as session:
1775 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1776 c_id = c.id
1778 start = now()
1780 def make_event(hours):
1781 return events_pb2.CreateEventReq(
1782 title="Test Event",
1783 content="Test content.",
1784 location=events_pb2.EventLocation(
1785 address="Near Null Island",
1786 lat=0.1,
1787 lng=0.2,
1788 ),
1789 parent_community_id=c_id,
1790 timezone="UTC",
1791 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)),
1792 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)),
1793 )
1795 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1796 with events_session(token1) as api:
1797 e_own = api.CreateEvent(make_event(1)).event_id
1799 with events_session(token2) as api:
1800 e_attending = api.CreateEvent(make_event(2)).event_id
1801 e_community_only = api.CreateEvent(make_event(3)).event_id
1802 # e_both: user1 will be both organizer and attendee
1803 e_both = api.CreateEvent(make_event(4)).event_id
1805 moderator.approve_event_occurrence(e_own)
1806 moderator.approve_event_occurrence(e_attending)
1807 moderator.approve_event_occurrence(e_community_only)
1808 moderator.approve_event_occurrence(e_both)
1810 # invite user1 as organizer of e_both
1811 with events_session(token2) as api:
1812 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
1814 # user1 RSVPs to e_attending and e_both
1815 with events_session(token1) as api:
1816 api.SetEventAttendance(
1817 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1818 )
1819 api.SetEventAttendance(
1820 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1821 )
1823 with events_session(token1) as api:
1824 # baseline: all four community events visible
1825 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1826 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
1828 # exclude_attending removes events user1 is attending (e_attending, e_both)
1829 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
1830 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1831 assert [e.event_id for e in res.events] == [e_community_only]
1833 # exclude_attending with attending=True: invalid combination
1834 with pytest.raises(grpc.RpcError) as e:
1835 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
1836 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1838 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
1839 with events_session(token2) as api:
1840 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1841 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
1842 assert [e.event_id for e in res.events] == [e_own]
1845def test_RemoveEventOrganizer(db, moderator: Moderator):
1846 user1, token1 = generate_user()
1847 user2, token2 = generate_user()
1849 with session_scope() as session:
1850 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1852 with events_session(token1) as api:
1853 event_id = api.CreateEvent(
1854 events_pb2.CreateEventReq(
1855 title="Dummy Title",
1856 content="Dummy content.",
1857 location=events_pb2.EventLocation(
1858 address="Near Null Island",
1859 lat=0.1,
1860 lng=0.2,
1861 ),
1862 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1863 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1864 timezone="UTC",
1865 )
1866 ).event_id
1868 moderator.approve_event_occurrence(event_id)
1870 with events_session(token2) as api:
1871 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1873 with pytest.raises(grpc.RpcError) as e:
1874 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1875 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1876 assert e.value.details() == "You're not allowed to edit that event."
1878 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1880 with events_session(token1) as api:
1881 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1883 with pytest.raises(grpc.RpcError) as e:
1884 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1885 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1886 assert e.value.details() == "You cannot remove the event owner as an organizer."
1888 with events_session(token2) as api:
1889 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1890 assert res.organizer
1891 assert res.organizer_count == 2
1892 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1893 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1895 with pytest.raises(grpc.RpcError) as e:
1896 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1897 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1898 assert e.value.details() == "You're not allowed to edit that event."
1900 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1901 assert not res.organizer
1902 assert res.organizer_count == 1
1904 # Test that event owner can remove co-organizers
1905 with events_session(token1) as api:
1906 # Add user2 back as organizer
1907 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1909 # Verify user2 is now an organizer
1910 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1911 assert res.organizer_count == 2
1913 # Event owner can remove co-organizer
1914 api.RemoveEventOrganizer(
1915 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
1916 )
1918 # Verify user2 is no longer an organizer
1919 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1920 assert res.organizer_count == 1
1922 # Test that non-organizers cannot remove other organizers
1923 with events_session(token2) as api:
1924 # User2 cannot invite themselves as organizer (not the owner)
1925 with pytest.raises(grpc.RpcError) as e:
1926 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1927 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1928 assert e.value.details() == "You're not allowed to edit that event."
1930 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
1931 with events_session(token1) as api:
1932 # Add user2 back as organizer
1933 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1936def test_ListEventAttendees_regression(db):
1937 # see issue #1617:
1938 #
1939 # 1. Create an event
1940 # 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`
1941 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
1942 # 4. Change the current user's attendance state to "going" again
1943 #
1944 # **Expected behaviour**
1945 # `ListEventAttendees` should return the current user's ID
1946 #
1947 # **Actual/current behaviour**
1948 # `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
1950 user1, token1 = generate_user()
1951 user2, token2 = generate_user()
1952 user3, token3 = generate_user()
1953 user4, token4 = generate_user()
1954 user5, token5 = generate_user()
1956 with session_scope() as session:
1957 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1959 start_time = now() + timedelta(hours=2)
1960 end_time = start_time + timedelta(hours=3)
1962 with events_session(token1) as api:
1963 res = api.CreateEvent(
1964 events_pb2.CreateEventReq(
1965 title="Dummy Title",
1966 content="Dummy content.",
1967 location=events_pb2.EventLocation(
1968 address="Near Null Island",
1969 lat=0.1,
1970 lng=0.2,
1971 ),
1972 parent_community_id=c_id,
1973 start_time=Timestamp_from_datetime(start_time),
1974 end_time=Timestamp_from_datetime(end_time),
1975 timezone="UTC",
1976 )
1977 )
1979 res = api.TransferEvent(
1980 events_pb2.TransferEventReq(
1981 event_id=res.event_id,
1982 new_owner_community_id=c_id,
1983 )
1984 )
1986 event_id = res.event_id
1988 api.SetEventAttendance(
1989 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1990 )
1991 api.SetEventAttendance(
1992 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1993 )
1995 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
1996 assert len(res.attendee_user_ids) == 1
1997 assert res.attendee_user_ids[0] == user1.id
2000def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2001 user1, token1 = generate_user()
2002 user2, token2 = generate_user()
2003 user3, token3 = generate_user()
2004 user4, token4 = generate_user()
2006 with session_scope() as session:
2007 c = create_community(session, 0, 2, "Community", [user3], [], None)
2008 h = create_group(session, "Group", [user4], [], c)
2009 c_id = c.id
2010 h_id = h.id
2011 user4_id = user4.id
2013 with events_session(token1) as api:
2014 event = api.CreateEvent(
2015 events_pb2.CreateEventReq(
2016 title="Dummy Title",
2017 content="Dummy content.",
2018 location=events_pb2.EventLocation(
2019 address="Near Null Island",
2020 lat=0.1,
2021 lng=0.2,
2022 ),
2023 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2024 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2025 timezone="UTC",
2026 )
2027 )
2029 moderator.approve_event_occurrence(event.event_id)
2031 with threads_session(token2) as api:
2032 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2034 moderator.approve_thread_post(reply_id)
2036 with events_session(token3) as api:
2037 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2038 assert res.thread.num_responses == 1
2040 with threads_session(token3) as api:
2041 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2042 assert len(ret.replies) == 1
2043 assert not ret.next_page_token
2044 assert ret.replies[0].thread_id == reply_id
2045 assert ret.replies[0].content == "hi"
2046 assert ret.replies[0].author_user_id == user2.id
2047 assert ret.replies[0].num_replies == 0
2049 nested_reply_id = api.PostReply(
2050 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2051 ).thread_id
2053 moderator.approve_thread_post(nested_reply_id)
2055 process_jobs()
2057 push = push_collector.pop_for_user(user1.id, last=True)
2058 assert push.topic_action == NotificationTopicAction.event__comment.display
2059 assert push.content.title == f"{user2.name} • Dummy Title"
2060 assert push.content.ios_title == user2.name
2061 assert push.content.ios_subtitle == "Commented on Dummy Title"
2062 assert push.content.body == "hi"
2064 push = push_collector.pop_for_user(user2.id, last=True)
2065 assert push.content.title == f"{user3.name} • Dummy Title"
2067 assert push_collector.count_for_user(user4_id) == 0
2070def test_can_overlap_other_events_schedule_regression(db):
2071 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2072 user, token = generate_user()
2074 with session_scope() as session:
2075 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2077 start = now()
2079 with events_session(token) as api:
2080 # create another event, should be able to overlap with this one
2081 api.CreateEvent(
2082 events_pb2.CreateEventReq(
2083 title="Dummy Title",
2084 content="Dummy content.",
2085 parent_community_id=c_id,
2086 location=events_pb2.EventLocation(
2087 address="Near Null Island",
2088 lat=0.1,
2089 lng=0.2,
2090 ),
2091 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2092 end_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2093 timezone="UTC",
2094 )
2095 )
2097 # this event
2098 res = api.CreateEvent(
2099 events_pb2.CreateEventReq(
2100 title="Dummy Title",
2101 content="Dummy content.",
2102 parent_community_id=c_id,
2103 location=events_pb2.EventLocation(
2104 address="Near Null Island",
2105 lat=0.1,
2106 lng=0.2,
2107 ),
2108 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2109 end_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2110 timezone="UTC",
2111 )
2112 )
2114 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2115 api.ScheduleEvent(
2116 events_pb2.ScheduleEventReq(
2117 event_id=res.event_id,
2118 content="New event occurrence",
2119 location=events_pb2.EventLocation(
2120 address="A bit further but still near Null Island",
2121 lat=0.3,
2122 lng=0.2,
2123 ),
2124 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2125 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2126 timezone="UTC",
2127 )
2128 )
2131def test_can_overlap_other_events_update_regression(db):
2132 user, token = generate_user()
2134 with session_scope() as session:
2135 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2137 start = now()
2139 with events_session(token) as api:
2140 # create another event, should be able to overlap with this one
2141 api.CreateEvent(
2142 events_pb2.CreateEventReq(
2143 title="Dummy Title",
2144 content="Dummy content.",
2145 parent_community_id=c_id,
2146 location=events_pb2.EventLocation(
2147 address="Near Null Island",
2148 lat=0.1,
2149 lng=0.2,
2150 ),
2151 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2152 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2153 timezone="UTC",
2154 )
2155 )
2157 res = api.CreateEvent(
2158 events_pb2.CreateEventReq(
2159 title="Dummy Title",
2160 content="Dummy content.",
2161 parent_community_id=c_id,
2162 location=events_pb2.EventLocation(
2163 address="Near Null Island",
2164 lat=0.1,
2165 lng=0.2,
2166 ),
2167 start_time=Timestamp_from_datetime(start + timedelta(hours=7)),
2168 end_time=Timestamp_from_datetime(start + timedelta(hours=8)),
2169 timezone="UTC",
2170 )
2171 )
2173 event_id = api.ScheduleEvent(
2174 events_pb2.ScheduleEventReq(
2175 event_id=res.event_id,
2176 content="New event occurrence",
2177 location=events_pb2.EventLocation(
2178 address="A bit further but still near Null Island",
2179 lat=0.3,
2180 lng=0.2,
2181 ),
2182 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2183 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2184 timezone="UTC",
2185 )
2186 ).event_id
2188 # can overlap with this current existing occurrence
2189 api.UpdateEvent(
2190 events_pb2.UpdateEventReq(
2191 event_id=event_id,
2192 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2193 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2194 )
2195 )
2197 api.UpdateEvent(
2198 events_pb2.UpdateEventReq(
2199 event_id=event_id,
2200 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2201 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2202 )
2203 )
2206def test_list_past_events_regression(db):
2207 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2208 user, token = generate_user()
2210 with session_scope() as session:
2211 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2213 start = now()
2215 with events_session(token) as api:
2216 api.CreateEvent(
2217 events_pb2.CreateEventReq(
2218 title="Dummy Title",
2219 content="Dummy content.",
2220 parent_community_id=c_id,
2221 location=events_pb2.EventLocation(
2222 address="Near Null Island",
2223 lat=0.1,
2224 lng=0.2,
2225 ),
2226 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2227 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2228 timezone="UTC",
2229 )
2230 )
2232 with session_scope() as session:
2233 session.execute(
2234 update(EventOccurrence).values(
2235 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2236 )
2237 )
2239 with events_session(token) as api:
2240 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2241 assert len(res.events) == 1
2244def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2245 user1, token1 = generate_user(complete_profile=True)
2246 user2, token2 = generate_user()
2247 user3, token3 = generate_user()
2248 user4, token4 = generate_user()
2249 user5, token5 = generate_user(is_superuser=True)
2251 with session_scope() as session:
2252 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2253 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2254 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2255 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2257 enforce_community_memberships()
2259 with events_session(token1) as api:
2260 res = api.CreateEvent(
2261 events_pb2.CreateEventReq(
2262 title="Dummy Title",
2263 content="Dummy content.",
2264 parent_community_id=c_id,
2265 location=events_pb2.EventLocation(
2266 address="Near Null Island",
2267 lat=0.1,
2268 lng=0.2,
2269 ),
2270 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)),
2271 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
2272 timezone="UTC",
2273 )
2274 )
2275 user_url = f"http://localhost:3000/user/{user1.username}"
2276 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2278 event_id = res.event_id
2280 moderator.approve_event_occurrence(event_id)
2282 with events_session(token1) as api:
2283 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2285 email = email_collector.pop_for_mods(last=True)
2287 assert user_url in email.plain
2288 assert event_url in email.plain
2290 # can't send another req
2291 with pytest.raises(grpc.RpcError) as err:
2292 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2293 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2294 assert err.value.details() == "You have already requested a community invite for this event."
2296 # another user can send one though
2297 with events_session(token3) as api:
2298 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2300 # but not a non-admin
2301 with events_session(token2) as api:
2302 with pytest.raises(grpc.RpcError) as err:
2303 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2304 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2305 assert err.value.details() == "You're not allowed to edit that event."
2307 with real_editor_session(token5) as editor:
2308 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2309 assert len(res.requests) == 2
2310 assert res.requests[0].user_id == user1.id
2311 assert res.requests[0].approx_users_to_notify == 3
2312 assert res.requests[1].user_id == user3.id
2313 assert res.requests[1].approx_users_to_notify == 3
2315 editor.DecideEventCommunityInviteRequest(
2316 editor_pb2.DecideEventCommunityInviteRequestReq(
2317 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2318 approve=False,
2319 )
2320 )
2322 editor.DecideEventCommunityInviteRequest(
2323 editor_pb2.DecideEventCommunityInviteRequestReq(
2324 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2325 approve=True,
2326 )
2327 )
2329 # not after approve
2330 with events_session(token4) as api:
2331 with pytest.raises(grpc.RpcError) as err:
2332 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2333 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2334 assert err.value.details() == "A community invite has already been sent out for this event."
2337def test_update_event_should_notify_queues_job():
2338 user, token = generate_user()
2339 start = now()
2341 with session_scope() as session:
2342 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2344 # create an event
2345 with events_session(token) as api:
2346 create_res = api.CreateEvent(
2347 events_pb2.CreateEventReq(
2348 title="Dummy Title",
2349 content="Dummy content.",
2350 parent_community_id=c_id,
2351 location=events_pb2.EventLocation(
2352 address="Near Null Island",
2353 lat=1.0,
2354 lng=2.0,
2355 ),
2356 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2357 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2358 timezone="UTC",
2359 )
2360 )
2362 event_id = create_res.event_id
2364 # measure initial background job queue length
2365 with session_scope() as session:
2366 jobs = session.query(BackgroundJob).all()
2367 job_length_before_update = len(jobs)
2369 # update with should_notify=False, expect no change in background job queue
2370 api.UpdateEvent(
2371 events_pb2.UpdateEventReq(
2372 event_id=event_id,
2373 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2374 should_notify=False,
2375 )
2376 )
2378 with session_scope() as session:
2379 jobs = session.query(BackgroundJob).all()
2380 assert len(jobs) == job_length_before_update
2382 # update with should_notify=True, expect one new background job added
2383 api.UpdateEvent(
2384 events_pb2.UpdateEventReq(
2385 event_id=event_id,
2386 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2387 should_notify=True,
2388 )
2389 )
2391 with session_scope() as session:
2392 jobs = session.query(BackgroundJob).all()
2393 assert len(jobs) == job_length_before_update + 1
2396def test_event_photo_key(db):
2397 """Test that events return the photo_key field when a photo is set."""
2398 user, token = generate_user()
2400 start_time = now() + timedelta(hours=2)
2401 end_time = start_time + timedelta(hours=3)
2403 # Create a community and an upload for the event photo
2404 with session_scope() as session:
2405 create_community(session, 0, 2, "Community", [user], [], None)
2406 upload = Upload(
2407 key="test_event_photo_key_123",
2408 filename="test_event_photo_key_123.jpg",
2409 creator_user_id=user.id,
2410 )
2411 session.add(upload)
2413 with events_session(token) as api:
2414 # Create event without photo
2415 res = api.CreateEvent(
2416 events_pb2.CreateEventReq(
2417 title="Event Without Photo",
2418 content="No photo content.",
2419 photo_key=None,
2420 location=events_pb2.EventLocation(
2421 address="Near Null Island",
2422 lat=0.1,
2423 lng=0.2,
2424 ),
2425 start_time=Timestamp_from_datetime(start_time),
2426 end_time=Timestamp_from_datetime(end_time),
2427 timezone="UTC",
2428 )
2429 )
2431 assert res.photo_key == ""
2432 assert res.photo_url == ""
2434 # Create event with photo
2435 res_with_photo = api.CreateEvent(
2436 events_pb2.CreateEventReq(
2437 title="Event With Photo",
2438 content="Has photo content.",
2439 photo_key="test_event_photo_key_123",
2440 location=events_pb2.EventLocation(
2441 address="Near Null Island",
2442 lat=0.1,
2443 lng=0.2,
2444 ),
2445 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)),
2446 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)),
2447 timezone="UTC",
2448 )
2449 )
2451 assert res_with_photo.photo_key == "test_event_photo_key_123"
2452 assert "test_event_photo_key_123" in res_with_photo.photo_url
2454 event_id = res_with_photo.event_id
2456 # Verify photo_key is returned when getting the event
2457 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2458 assert get_res.photo_key == "test_event_photo_key_123"
2459 assert "test_event_photo_key_123" in get_res.photo_url
2462def test_event_created_with_shadowed_visibility(db):
2463 """Events start in SHADOWED state when created."""
2464 user, token = generate_user()
2466 with session_scope() as session:
2467 create_community(session, 0, 2, "Community", [user], [], None)
2469 start_time = now() + timedelta(hours=2)
2470 end_time = start_time + timedelta(hours=3)
2472 with events_session(token) as api:
2473 res = api.CreateEvent(
2474 events_pb2.CreateEventReq(
2475 title="Test UMS Event",
2476 content="UMS content.",
2477 location=events_pb2.EventLocation(
2478 address="Near Null Island",
2479 lat=0.1,
2480 lng=0.2,
2481 ),
2482 start_time=Timestamp_from_datetime(start_time),
2483 end_time=Timestamp_from_datetime(end_time),
2484 timezone="UTC",
2485 )
2486 )
2487 event_id = res.event_id
2489 with session_scope() as session:
2490 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2491 mod_state = session.execute(
2492 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2493 ).scalar_one()
2494 assert mod_state.visibility == ModerationVisibility.shadowed
2497def test_shadowed_event_visible_to_creator_only(db):
2498 """SHADOWED events are visible to the creator but not to other users."""
2499 user1, token1 = generate_user()
2500 user2, token2 = generate_user()
2502 with session_scope() as session:
2503 create_community(session, 0, 2, "Community", [user1], [], None)
2505 start_time = now() + timedelta(hours=2)
2506 end_time = start_time + timedelta(hours=3)
2508 with events_session(token1) as api:
2509 res = api.CreateEvent(
2510 events_pb2.CreateEventReq(
2511 title="Shadowed Event",
2512 content="Content.",
2513 location=events_pb2.EventLocation(
2514 address="Near Null Island",
2515 lat=0.1,
2516 lng=0.2,
2517 ),
2518 start_time=Timestamp_from_datetime(start_time),
2519 end_time=Timestamp_from_datetime(end_time),
2520 timezone="UTC",
2521 )
2522 )
2523 event_id = res.event_id
2525 # Creator can see it
2526 with events_session(token1) as api:
2527 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2528 assert res.title == "Shadowed Event"
2530 # Other user cannot
2531 with events_session(token2) as api:
2532 with pytest.raises(grpc.RpcError) as e:
2533 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2534 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2537def test_event_visible_after_approval(db, moderator: Moderator):
2538 """Events become visible to all users after moderation approval."""
2539 user1, token1 = generate_user()
2540 user2, token2 = generate_user()
2542 with session_scope() as session:
2543 create_community(session, 0, 2, "Community", [user1], [], None)
2545 start_time = now() + timedelta(hours=2)
2546 end_time = start_time + timedelta(hours=3)
2548 with events_session(token1) as api:
2549 res = api.CreateEvent(
2550 events_pb2.CreateEventReq(
2551 title="Approved Event",
2552 content="Content.",
2553 location=events_pb2.EventLocation(
2554 address="Near Null Island",
2555 lat=0.1,
2556 lng=0.2,
2557 ),
2558 start_time=Timestamp_from_datetime(start_time),
2559 end_time=Timestamp_from_datetime(end_time),
2560 timezone="UTC",
2561 )
2562 )
2563 event_id = res.event_id
2565 # Other user cannot see it yet
2566 with events_session(token2) as api:
2567 with pytest.raises(grpc.RpcError) as e:
2568 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2569 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2571 # Approve the event
2572 moderator.approve_event_occurrence(event_id)
2574 # Now other user can see it
2575 with events_session(token2) as api:
2576 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2577 assert res.title == "Approved Event"
2580def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2581 """SHADOWED events appear in lists for the creator but not for other users."""
2582 user1, token1 = generate_user()
2583 user2, token2 = generate_user()
2585 with session_scope() as session:
2586 create_community(session, 0, 2, "Community", [user1], [], None)
2588 start_time = now() + timedelta(hours=2)
2589 end_time = start_time + timedelta(hours=3)
2591 with events_session(token1) as api:
2592 res = api.CreateEvent(
2593 events_pb2.CreateEventReq(
2594 title="List Test Event",
2595 content="Content.",
2596 location=events_pb2.EventLocation(
2597 address="Near Null Island",
2598 lat=0.1,
2599 lng=0.2,
2600 ),
2601 start_time=Timestamp_from_datetime(start_time),
2602 end_time=Timestamp_from_datetime(end_time),
2603 timezone="UTC",
2604 )
2605 )
2606 event_id = res.event_id
2608 # Creator can see their own SHADOWED event in lists
2609 with events_session(token1) as api:
2610 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2611 event_ids = [e.event_id for e in list_res.events]
2612 assert event_id in event_ids
2614 # Other user cannot see the SHADOWED event in lists
2615 with events_session(token2) as api:
2616 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2617 event_ids = [e.event_id for e in list_res.events]
2618 assert event_id not in event_ids
2620 # After approval, other user can see it
2621 moderator.approve_event_occurrence(event_id)
2623 with events_session(token2) as api:
2624 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2625 event_ids = [e.event_id for e in list_res.events]
2626 assert event_id in event_ids
2629def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2630 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2631 user1, token1 = generate_user()
2632 user2, token2 = generate_user()
2634 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2635 with session_scope() as session:
2636 world = create_community(session, 0, 10, "World", [user1], [], None)
2637 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2638 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2639 create_community(session, 0, 2, "Child", [user2], [], region)
2641 start_time = now() + timedelta(hours=2)
2642 end_time = start_time + timedelta(hours=3)
2644 with events_session(token1) as api:
2645 res = api.CreateEvent(
2646 events_pb2.CreateEventReq(
2647 title="Deferred Event",
2648 content="Content.",
2649 location=events_pb2.EventLocation(
2650 address="Near Null Island",
2651 lat=0.1,
2652 lng=0.2,
2653 ),
2654 start_time=Timestamp_from_datetime(start_time),
2655 end_time=Timestamp_from_datetime(end_time),
2656 timezone="UTC",
2657 )
2658 )
2659 event_id = res.event_id
2661 # Process all jobs — notification should be deferred (event is SHADOWED)
2662 process_jobs()
2664 with session_scope() as session:
2665 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2666 # Notification was created with moderation_state_id for deferral
2667 assert notif.moderation_state_id is not None
2668 # No delivery exists (deferred because event is SHADOWED)
2669 delivery_count = session.execute(
2670 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2671 ).scalar_one_or_none()
2672 assert delivery_count is None
2674 # Approve the event — handle_notification is re-queued for deferred notifications
2675 moderator.approve_event_occurrence(event_id)
2677 # Verify handle_notification job was queued
2678 with session_scope() as session:
2679 pending_jobs = (
2680 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2681 .scalars()
2682 .all()
2683 )
2684 assert any("handle_notification" in j.job_type for j in pending_jobs)
2687def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2688 """Event update notifications should carry the event's moderation_state_id for deferral."""
2689 user1, token1 = generate_user()
2690 user2, token2 = generate_user()
2692 with session_scope() as session:
2693 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2695 start_time = now() + timedelta(hours=2)
2696 end_time = start_time + timedelta(hours=3)
2698 with events_session(token1) as api:
2699 res = api.CreateEvent(
2700 events_pb2.CreateEventReq(
2701 title="Update Test",
2702 content="Content.",
2703 location=events_pb2.EventLocation(
2704 address="Near Null Island",
2705 lat=0.1,
2706 lng=0.2,
2707 ),
2708 start_time=Timestamp_from_datetime(start_time),
2709 end_time=Timestamp_from_datetime(end_time),
2710 timezone="UTC",
2711 )
2712 )
2713 event_id = res.event_id
2715 moderator.approve_event_occurrence(event_id)
2716 process_jobs()
2717 # Clear any create notifications
2718 while push_collector.count_for_user(user2.id): 2718 ↛ 2719line 2718 didn't jump to line 2719 because the condition on line 2718 was never true
2719 push_collector.pop_for_user(user2.id)
2721 # User2 subscribes to the event
2722 with events_session(token2) as api:
2723 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2725 # User1 updates the event with should_notify=True
2726 with events_session(token1) as api:
2727 api.UpdateEvent(
2728 events_pb2.UpdateEventReq(
2729 event_id=event_id,
2730 title=wrappers_pb2.StringValue(value="Updated Title"),
2731 should_notify=True,
2732 )
2733 )
2735 process_jobs()
2737 # Verify that the update notification for user2 has moderation_state_id set
2738 with session_scope() as session:
2739 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2741 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2742 # Find the update notification (most recent one)
2743 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2744 assert len(update_notifs) == 1
2745 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2748def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2749 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2750 user1, token1 = generate_user()
2751 user2, token2 = generate_user()
2753 with session_scope() as session:
2754 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2756 start_time = now() + timedelta(hours=2)
2757 end_time = start_time + timedelta(hours=3)
2759 with events_session(token1) as api:
2760 res = api.CreateEvent(
2761 events_pb2.CreateEventReq(
2762 title="Cancel Test",
2763 content="Content.",
2764 location=events_pb2.EventLocation(
2765 address="Near Null Island",
2766 lat=0.1,
2767 lng=0.2,
2768 ),
2769 start_time=Timestamp_from_datetime(start_time),
2770 end_time=Timestamp_from_datetime(end_time),
2771 timezone="UTC",
2772 )
2773 )
2774 event_id = res.event_id
2776 moderator.approve_event_occurrence(event_id)
2777 process_jobs()
2778 while push_collector.count_for_user(user2.id): 2778 ↛ 2779line 2778 didn't jump to line 2779 because the condition on line 2778 was never true
2779 push_collector.pop_for_user(user2.id)
2781 # User2 subscribes
2782 with events_session(token2) as api:
2783 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2785 # User1 cancels the event
2786 with events_session(token1) as api:
2787 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2789 process_jobs()
2791 # Verify that the cancel notification for user2 has moderation_state_id set
2792 with session_scope() as session:
2793 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2795 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2796 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2797 assert len(cancel_notifs) == 1
2798 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2801def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2802 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2803 user1, token1 = generate_user()
2804 user2, token2 = generate_user()
2806 with session_scope() as session:
2807 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2809 # Create event starting 23 hours from now (within 24h reminder window)
2810 start_time = now() + timedelta(hours=23)
2811 end_time = start_time + timedelta(hours=1)
2813 with events_session(token1) as api:
2814 res = api.CreateEvent(
2815 events_pb2.CreateEventReq(
2816 title="Reminder Test",
2817 content="Content.",
2818 location=events_pb2.EventLocation(
2819 address="Near Null Island",
2820 lat=0.1,
2821 lng=0.2,
2822 ),
2823 start_time=Timestamp_from_datetime(start_time),
2824 end_time=Timestamp_from_datetime(end_time),
2825 timezone="UTC",
2826 )
2827 )
2828 event_id = res.event_id
2830 moderator.approve_event_occurrence(event_id)
2831 process_jobs()
2832 while push_collector.count_for_user(user2.id): 2832 ↛ 2833line 2832 didn't jump to line 2833 because the condition on line 2832 was never true
2833 push_collector.pop_for_user(user2.id)
2835 # User2 marks attendance
2836 with events_session(token2) as api:
2837 api.SetEventAttendance(
2838 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2839 )
2841 # Run the event reminder handler
2842 send_event_reminders(empty_pb2.Empty())
2843 process_jobs()
2845 # Verify that the reminder notification for user2 has moderation_state_id set
2846 with session_scope() as session:
2847 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2849 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2850 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2851 assert len(reminder_notifs) == 1
2852 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2855def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2856 """Event reminders should not be sent for cancelled events."""
2857 user1, token1 = generate_user()
2858 user2, token2 = generate_user()
2860 with session_scope() as session:
2861 create_community(session, 0, 2, "Community", [user2], [], None)
2863 # Create event starting 23 hours from now (within 24h reminder window)
2864 start_time = now() + timedelta(hours=23)
2865 end_time = start_time + timedelta(hours=1)
2867 with events_session(token1) as api:
2868 res = api.CreateEvent(
2869 events_pb2.CreateEventReq(
2870 title="Cancelled Reminder Test",
2871 content="Content.",
2872 location=events_pb2.EventLocation(
2873 address="Near Null Island",
2874 lat=0.1,
2875 lng=0.2,
2876 ),
2877 start_time=Timestamp_from_datetime(start_time),
2878 end_time=Timestamp_from_datetime(end_time),
2879 timezone="UTC",
2880 )
2881 )
2882 event_id = res.event_id
2884 moderator.approve_event_occurrence(event_id)
2885 process_jobs()
2887 # User2 marks attendance
2888 with events_session(token2) as api:
2889 api.SetEventAttendance(
2890 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2891 )
2893 # User1 cancels the event
2894 with events_session(token1) as api:
2895 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2897 process_jobs()
2898 # Drain any cancellation-related notifications so we can cleanly assert on reminders
2899 while push_collector.count_for_user(user2.id):
2900 push_collector.pop_for_user(user2.id)
2902 # Run the event reminder handler
2903 send_event_reminders(empty_pb2.Empty())
2904 process_jobs()
2906 # Verify that no reminder notification was sent for user2
2907 with session_scope() as session:
2908 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2909 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2910 assert len(reminder_notifs) == 0
2913@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
2914def test_event_reminder_not_sent_for_invisible_attendee(
2915 db, push_collector: PushCollector, moderator: Moderator, invisible_field
2916):
2917 user1, token1 = generate_user()
2918 user2, token2 = generate_user()
2920 with session_scope() as session:
2921 create_community(session, 0, 2, "Community", [user2], [], None)
2923 start_time = now() + timedelta(hours=23)
2924 end_time = start_time + timedelta(hours=1)
2926 with events_session(token1) as api:
2927 res = api.CreateEvent(
2928 events_pb2.CreateEventReq(
2929 title="Invisible Attendee Reminder Test",
2930 content="Content.",
2931 location=events_pb2.EventLocation(
2932 address="Near Null Island",
2933 lat=0.1,
2934 lng=0.2,
2935 ),
2936 start_time=Timestamp_from_datetime(start_time),
2937 end_time=Timestamp_from_datetime(end_time),
2938 timezone="UTC",
2939 )
2940 )
2941 event_id = res.event_id
2943 moderator.approve_event_occurrence(event_id)
2944 process_jobs()
2946 with events_session(token2) as api:
2947 api.SetEventAttendance(
2948 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2949 )
2951 with session_scope() as session:
2952 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
2954 send_event_reminders(empty_pb2.Empty())
2955 process_jobs()
2957 with session_scope() as session:
2958 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2959 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2960 assert len(reminder_notifs) == 0
2963def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
2964 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
2965 user1, token1 = generate_user()
2966 user2, token2 = generate_user()
2968 with session_scope() as session:
2969 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
2971 start = now()
2973 # User1 creates event A with 3 occurrences
2974 event_a_ids = []
2975 with events_session(token1) as api:
2976 res = api.CreateEvent(
2977 events_pb2.CreateEventReq(
2978 title="Event A",
2979 content="Content A.",
2980 parent_community_id=c_id,
2981 location=events_pb2.EventLocation(
2982 address="Near Null Island",
2983 lat=0.1,
2984 lng=0.2,
2985 ),
2986 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2987 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
2988 timezone="UTC",
2989 )
2990 )
2991 event_a_ids.append(res.event_id)
2992 for i in range(2):
2993 res = api.ScheduleEvent(
2994 events_pb2.ScheduleEventReq(
2995 event_id=event_a_ids[-1],
2996 content=f"A occurrence {i}",
2997 location=events_pb2.EventLocation(
2998 address="Near Null Island",
2999 lat=0.1,
3000 lng=0.2,
3001 ),
3002 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
3003 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
3004 timezone="UTC",
3005 )
3006 )
3007 event_a_ids.append(res.event_id)
3009 # User2 creates event B with 2 occurrences
3010 event_b_ids = []
3011 with events_session(token2) as api:
3012 res = api.CreateEvent(
3013 events_pb2.CreateEventReq(
3014 title="Event B",
3015 content="Content B.",
3016 parent_community_id=c_id,
3017 location=events_pb2.EventLocation(
3018 address="Near Null Island",
3019 lat=0.1,
3020 lng=0.2,
3021 ),
3022 start_time=Timestamp_from_datetime(start + timedelta(hours=10)),
3023 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)),
3024 timezone="UTC",
3025 )
3026 )
3027 event_b_ids.append(res.event_id)
3028 res = api.ScheduleEvent(
3029 events_pb2.ScheduleEventReq(
3030 event_id=event_b_ids[-1],
3031 content="B occurrence 1",
3032 location=events_pb2.EventLocation(
3033 address="Near Null Island",
3034 lat=0.1,
3035 lng=0.2,
3036 ),
3037 start_time=Timestamp_from_datetime(start + timedelta(hours=11)),
3038 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)),
3039 timezone="UTC",
3040 )
3041 )
3042 event_b_ids.append(res.event_id)
3044 moderator.approve_event_occurrence(event_a_ids[0])
3045 moderator.approve_event_occurrence(event_b_ids[0])
3047 # List occurrences for event A — should only get event A's 3 occurrences
3048 with events_session(token1) as api:
3049 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3050 returned_ids = [e.event_id for e in res.events]
3051 assert sorted(returned_ids) == sorted(event_a_ids)
3053 # List occurrences for event B — should only get event B's 2 occurrences
3054 with events_session(token2) as api:
3055 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3056 returned_ids = [e.event_id for e in res.events]
3057 assert sorted(returned_ids) == sorted(event_b_ids)
3060def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3061 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3062 user1, token1 = generate_user()
3063 user2, token2 = generate_user()
3065 with session_scope() as session:
3066 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3068 start_time = now() + timedelta(hours=2)
3069 end_time = start_time + timedelta(hours=3)
3071 with events_session(token1) as api:
3072 res = api.CreateEvent(
3073 events_pb2.CreateEventReq(
3074 title="Comment Test",
3075 content="Content.",
3076 parent_community_id=c_id,
3077 location=events_pb2.EventLocation(
3078 address="Near Null Island",
3079 lat=0.1,
3080 lng=0.2,
3081 ),
3082 start_time=Timestamp_from_datetime(start_time),
3083 end_time=Timestamp_from_datetime(end_time),
3084 timezone="UTC",
3085 )
3086 )
3087 event_id = res.event_id
3088 thread_id = res.thread.thread_id
3090 moderator.approve_event_occurrence(event_id)
3091 process_jobs()
3092 while push_collector.count_for_user(user1.id): 3092 ↛ 3093line 3092 didn't jump to line 3093 because the condition on line 3092 was never true
3093 push_collector.pop_for_user(user1.id)
3095 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3096 with events_session(token1) as api:
3097 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3099 # User2 posts a top-level comment on the event thread
3100 with threads_session(token2) as api:
3101 comment_thread_id = api.PostReply(
3102 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3103 ).thread_id
3105 process_jobs()
3107 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3108 comment_db_id = comment_thread_id // 10
3109 with session_scope() as session:
3110 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3112 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3113 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3114 assert len(comment_notifs) == 1
3115 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3118def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3119 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3120 user1, token1 = generate_user()
3121 user2, token2 = generate_user()
3122 user3, token3 = generate_user()
3124 with session_scope() as session:
3125 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3127 start_time = now() + timedelta(hours=2)
3128 end_time = start_time + timedelta(hours=3)
3130 with events_session(token1) as api:
3131 res = api.CreateEvent(
3132 events_pb2.CreateEventReq(
3133 title="Reply Test",
3134 content="Content.",
3135 location=events_pb2.EventLocation(
3136 address="Near Null Island",
3137 lat=0.1,
3138 lng=0.2,
3139 ),
3140 start_time=Timestamp_from_datetime(start_time),
3141 end_time=Timestamp_from_datetime(end_time),
3142 timezone="UTC",
3143 )
3144 )
3145 event_id = res.event_id
3146 thread_id = res.thread.thread_id
3148 moderator.approve_event_occurrence(event_id)
3149 process_jobs()
3150 while push_collector.count_for_user(user1.id): 3150 ↛ 3151line 3150 didn't jump to line 3151 because the condition on line 3150 was never true
3151 push_collector.pop_for_user(user1.id)
3153 # User2 posts a top-level comment
3154 with threads_session(token2) as api:
3155 comment_thread_id = api.PostReply(
3156 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3157 ).thread_id
3159 process_jobs()
3160 while push_collector.count_for_user(user1.id): 3160 ↛ 3161line 3160 didn't jump to line 3161 because the condition on line 3160 was never true
3161 push_collector.pop_for_user(user1.id)
3163 # User3 replies to user2's comment (depth=2 reply)
3164 with threads_session(token3) as api:
3165 nested_reply_thread_id = api.PostReply(
3166 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3167 ).thread_id
3169 process_jobs()
3171 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3172 nested_reply_db_id = nested_reply_thread_id // 10
3173 with session_scope() as session:
3174 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3176 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3177 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3178 assert len(reply_notifs) == 1
3179 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id