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