Coverage for app/backend/src/tests/test_events.py: 99%
1593 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-04 22:32 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-04 22:32 +0000
1import re
2from datetime import datetime, timedelta
3from zoneinfo import ZoneInfo
5import grpc
6import pytest
7from google.protobuf import empty_pb2, wrappers_pb2
8from psycopg.types.range import TimestamptzRange
9from sqlalchemy import select
10from sqlalchemy.sql.expression import update
12from couchers.db import session_scope
13from couchers.jobs.handlers import send_event_reminders
14from couchers.models import (
15 BackgroundJob,
16 BackgroundJobState,
17 Comment,
18 EventOccurrence,
19 ModerationState,
20 ModerationVisibility,
21 Notification,
22 NotificationDelivery,
23 NotificationTopicAction,
24 Reply,
25 Upload,
26 User,
27)
28from couchers.proto import editor_pb2, events_pb2, threads_pb2
29from couchers.tasks import enforce_community_memberships
30from couchers.utils import datetime_to_iso8601_local, now, to_aware_datetime
31from tests.fixtures.db import generate_user
32from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs
33from tests.fixtures.sessions import events_session, real_editor_session, threads_session
34from tests.test_communities import create_community, create_group
37@pytest.fixture(autouse=True)
38def _(testconfig):
39 pass
42def to_event_time_granularity(value: datetime) -> datetime:
43 """Events are scheduled at the minute granularity."""
44 return value.replace(second=0, microsecond=0)
47def is_utc_or_gmt(timezone: str) -> bool:
48 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT.
49 # Tests should be agnostic to which one we're using.
50 return timezone in ("Etc/UTC", "Etc/GMT")
53def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator):
54 # test cases:
55 # can create event
56 # cannot create event with missing details
57 # can't create event that starts in the past
58 # can create in different timezones
60 # event creator
61 user1, token1 = generate_user()
62 # community moderator
63 user2, token2 = generate_user()
64 # third party
65 user3, token3 = generate_user()
67 with session_scope() as session:
68 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
70 time_before = now()
71 start_time = now() + timedelta(hours=2)
72 end_time = start_time + timedelta(hours=3)
74 # Can create an event
75 with events_session(token1) as api:
76 res = api.CreateEvent(
77 events_pb2.CreateEventReq(
78 title="Dummy Title",
79 content="Dummy content.",
80 photo_key=None,
81 location=events_pb2.EventLocation(
82 address="Near Null Island",
83 lat=0.1,
84 lng=0.2,
85 ),
86 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
87 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
88 )
89 )
91 assert res.is_next
92 assert res.title == "Dummy Title"
93 assert res.slug == "dummy-title"
94 assert res.content == "Dummy content."
95 assert not res.photo_url
96 assert res.HasField("location")
97 assert res.location.lat == 0.1
98 assert res.location.lng == 0.2
99 assert res.location.address == "Near Null Island"
100 assert time_before <= to_aware_datetime(res.created) <= now()
101 assert time_before <= to_aware_datetime(res.last_edited) <= now()
102 assert res.creator_user_id == user1.id
103 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
104 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
105 assert is_utc_or_gmt(res.timezone)
106 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
107 assert res.organizer
108 assert res.subscriber
109 assert res.going_count == 1
110 assert res.organizer_count == 1
111 assert res.subscriber_count == 1
112 assert res.owner_user_id == user1.id
113 assert not res.owner_community_id
114 assert not res.owner_group_id
115 assert res.thread.thread_id
116 assert res.can_edit
117 assert not res.can_moderate
119 event_id = res.event_id
121 # Approve the event so other users can see it
122 moderator.approve_event_occurrence(event_id)
124 with events_session(token2) as api:
125 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
127 assert res.is_next
128 assert res.title == "Dummy Title"
129 assert res.slug == "dummy-title"
130 assert res.content == "Dummy content."
131 assert not res.photo_url
132 assert res.HasField("location")
133 assert res.location.lat == 0.1
134 assert res.location.lng == 0.2
135 assert res.location.address == "Near Null Island"
136 assert time_before <= to_aware_datetime(res.created) <= now()
137 assert time_before <= to_aware_datetime(res.last_edited) <= now()
138 assert res.creator_user_id == user1.id
139 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
140 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
141 assert is_utc_or_gmt(res.timezone)
142 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
143 assert not res.organizer
144 assert not res.subscriber
145 assert res.going_count == 1
146 assert res.organizer_count == 1
147 assert res.subscriber_count == 1
148 assert res.owner_user_id == user1.id
149 assert not res.owner_community_id
150 assert not res.owner_group_id
151 assert res.thread.thread_id
152 assert res.can_edit
153 assert res.can_moderate
155 with events_session(token3) as api:
156 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
158 assert res.is_next
159 assert res.title == "Dummy Title"
160 assert res.slug == "dummy-title"
161 assert res.content == "Dummy content."
162 assert not res.photo_url
163 assert res.HasField("location")
164 assert res.location.lat == 0.1
165 assert res.location.lng == 0.2
166 assert res.location.address == "Near Null Island"
167 assert time_before <= to_aware_datetime(res.created) <= now()
168 assert time_before <= to_aware_datetime(res.last_edited) <= now()
169 assert res.creator_user_id == user1.id
170 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
171 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
172 assert is_utc_or_gmt(res.timezone)
173 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
174 assert not res.organizer
175 assert not res.subscriber
176 assert res.going_count == 1
177 assert res.organizer_count == 1
178 assert res.subscriber_count == 1
179 assert res.owner_user_id == user1.id
180 assert not res.owner_community_id
181 assert not res.owner_group_id
182 assert res.thread.thread_id
183 assert not res.can_edit
184 assert not res.can_moderate
186 # Failure cases
187 with events_session(token1) as api:
188 with pytest.raises(grpc.RpcError) as e:
189 api.CreateEvent(
190 events_pb2.CreateEventReq(
191 # title="Dummy Title",
192 content="Dummy content.",
193 photo_key=None,
194 location=events_pb2.EventLocation(
195 address="Near Null Island",
196 lat=0.1,
197 lng=0.2,
198 ),
199 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
200 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
201 )
202 )
203 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
204 assert e.value.details() == "Missing event title."
206 with pytest.raises(grpc.RpcError) as e:
207 api.CreateEvent(
208 events_pb2.CreateEventReq(
209 title="Dummy Title",
210 # content="Dummy content.",
211 photo_key=None,
212 location=events_pb2.EventLocation(
213 address="Near Null Island",
214 lat=0.1,
215 lng=0.2,
216 ),
217 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
218 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
219 )
220 )
221 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
222 assert e.value.details() == "Missing event content."
224 with pytest.raises(grpc.RpcError) as e:
225 api.CreateEvent(
226 events_pb2.CreateEventReq(
227 title="Dummy Title",
228 content="Dummy content.",
229 photo_key="nonexistent",
230 location=events_pb2.EventLocation(
231 address="Near Null Island",
232 lat=0.1,
233 lng=0.2,
234 ),
235 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
236 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
237 )
238 )
239 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
240 assert e.value.details() == "Photo not found."
242 with pytest.raises(grpc.RpcError) as e:
243 api.CreateEvent(
244 events_pb2.CreateEventReq(
245 title="Dummy Title",
246 content="Dummy content.",
247 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
248 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
249 )
250 )
251 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
252 assert e.value.details() == "Missing event address or location."
254 with pytest.raises(grpc.RpcError) as e:
255 api.CreateEvent(
256 events_pb2.CreateEventReq(
257 title="Dummy Title",
258 content="Dummy content.",
259 location=events_pb2.EventLocation(
260 address="Near Null Island",
261 ),
262 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
263 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
264 )
265 )
266 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
267 assert e.value.details() == "Invalid coordinate."
269 with pytest.raises(grpc.RpcError) as e:
270 api.CreateEvent(
271 events_pb2.CreateEventReq(
272 title="Dummy Title",
273 content="Dummy content.",
274 location=events_pb2.EventLocation(
275 lat=0.1,
276 lng=0.1,
277 ),
278 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
279 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
280 )
281 )
282 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
283 assert e.value.details() == "Missing event address or location."
285 with pytest.raises(grpc.RpcError) as e:
286 api.CreateEvent(
287 events_pb2.CreateEventReq(
288 title="Dummy Title",
289 content="Dummy content.",
290 location=events_pb2.EventLocation(
291 address="Near Null Island",
292 lat=0.1,
293 lng=0.2,
294 ),
295 start_datetime_iso8601_local=datetime_to_iso8601_local(now() - timedelta(hours=2)),
296 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
297 )
298 )
299 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
300 assert e.value.details() == "The event must be in the future."
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_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
313 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
314 )
315 )
316 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
317 assert e.value.details() == "The event must end after it starts."
319 with pytest.raises(grpc.RpcError) as e:
320 api.CreateEvent(
321 events_pb2.CreateEventReq(
322 title="Dummy Title",
323 content="Dummy content.",
324 location=events_pb2.EventLocation(
325 address="Near Null Island",
326 lat=0.1,
327 lng=0.2,
328 ),
329 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=2)),
330 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=5)),
331 )
332 )
333 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
334 assert e.value.details() == "The event needs to start within the next year."
336 with pytest.raises(grpc.RpcError) as e:
337 api.CreateEvent(
338 events_pb2.CreateEventReq(
339 title="Dummy Title",
340 content="Dummy content.",
341 location=events_pb2.EventLocation(
342 address="Near Null Island",
343 lat=0.1,
344 lng=0.2,
345 ),
346 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
347 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=100)),
348 )
349 )
350 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
351 assert e.value.details() == "Events cannot last longer than 7 days."
354def test_CreateEvent_incomplete_profile(db):
355 user1, token1 = generate_user(complete_profile=False)
356 user2, token2 = generate_user()
358 with session_scope() as session:
359 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
361 start_time = now() + timedelta(hours=2)
362 end_time = start_time + timedelta(hours=3)
364 with events_session(token1) as api:
365 with pytest.raises(grpc.RpcError) as e:
366 api.CreateEvent(
367 events_pb2.CreateEventReq(
368 title="Dummy Title",
369 content="Dummy content.",
370 photo_key=None,
371 location=events_pb2.EventLocation(
372 address="Near Null Island",
373 lat=0.1,
374 lng=0.2,
375 ),
376 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
377 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
378 )
379 )
380 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
381 assert e.value.details() == "You have to complete your profile before you can create an event."
384def test_ScheduleEvent(db):
385 # test cases:
386 # can schedule a new event occurrence
388 user, token = generate_user()
390 with session_scope() as session:
391 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
393 time_before = now()
394 start_time = now() + timedelta(hours=2)
395 end_time = start_time + timedelta(hours=3)
397 with events_session(token) as api:
398 res = api.CreateEvent(
399 events_pb2.CreateEventReq(
400 title="Dummy Title",
401 content="Dummy content.",
402 parent_community_id=c_id,
403 location=events_pb2.EventLocation(
404 address="Near Null Island",
405 lat=0.1,
406 lng=0.2,
407 ),
408 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
409 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
410 )
411 )
413 new_start_time = now() + timedelta(hours=6)
414 new_end_time = new_start_time + timedelta(hours=2)
416 res = api.ScheduleEvent(
417 events_pb2.ScheduleEventReq(
418 event_id=res.event_id,
419 content="New event occurrence",
420 location=events_pb2.EventLocation(
421 address="A bit further but still near Null Island",
422 lat=0.3,
423 lng=0.2,
424 ),
425 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time),
426 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time),
427 )
428 )
430 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
432 assert not res.is_next
433 assert res.title == "Dummy Title"
434 assert res.slug == "dummy-title"
435 assert res.content == "New event occurrence"
436 assert not res.photo_url
437 assert res.HasField("location")
438 assert res.location.lat == 0.3
439 assert res.location.lng == 0.2
440 assert res.location.address == "A bit further but still near Null Island"
441 assert time_before <= to_aware_datetime(res.created) <= now()
442 assert time_before <= to_aware_datetime(res.last_edited) <= now()
443 assert res.creator_user_id == user.id
444 assert to_aware_datetime(res.start_time) == to_event_time_granularity(new_start_time)
445 assert to_aware_datetime(res.end_time) == to_event_time_granularity(new_end_time)
446 assert is_utc_or_gmt(res.timezone)
447 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
448 assert res.organizer
449 assert res.subscriber
450 assert res.going_count == 1
451 assert res.organizer_count == 1
452 assert res.subscriber_count == 1
453 assert res.owner_user_id == user.id
454 assert not res.owner_community_id
455 assert not res.owner_group_id
456 assert res.thread.thread_id
457 assert res.can_edit
458 assert res.can_moderate
461def test_cannot_overlap_occurrences_schedule(db):
462 user, token = generate_user()
464 with session_scope() as session:
465 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
467 start = now()
469 with events_session(token) as api:
470 res = api.CreateEvent(
471 events_pb2.CreateEventReq(
472 title="Dummy Title",
473 content="Dummy content.",
474 parent_community_id=c_id,
475 location=events_pb2.EventLocation(
476 address="Near Null Island",
477 lat=0.1,
478 lng=0.2,
479 ),
480 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
481 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
482 )
483 )
485 with pytest.raises(grpc.RpcError) as e:
486 api.ScheduleEvent(
487 events_pb2.ScheduleEventReq(
488 event_id=res.event_id,
489 content="New event occurrence",
490 location=events_pb2.EventLocation(
491 address="A bit further but still near Null Island",
492 lat=0.3,
493 lng=0.2,
494 ),
495 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
496 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
497 )
498 )
499 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
500 assert e.value.details() == "An event cannot have overlapping occurrences."
503def test_cannot_overlap_occurrences_update(db):
504 user, token = generate_user()
506 with session_scope() as session:
507 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
509 start = now()
511 with events_session(token) as api:
512 res = api.CreateEvent(
513 events_pb2.CreateEventReq(
514 title="Dummy Title",
515 content="Dummy content.",
516 parent_community_id=c_id,
517 location=events_pb2.EventLocation(
518 address="Near Null Island",
519 lat=0.1,
520 lng=0.2,
521 ),
522 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
523 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
524 )
525 )
527 event_id = api.ScheduleEvent(
528 events_pb2.ScheduleEventReq(
529 event_id=res.event_id,
530 content="New event occurrence",
531 location=events_pb2.EventLocation(
532 address="A bit further but still near Null Island",
533 lat=0.3,
534 lng=0.2,
535 ),
536 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
537 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
538 )
539 ).event_id
541 # can overlap with this current existing occurrence
542 api.UpdateEvent(
543 events_pb2.UpdateEventReq(
544 event_id=event_id,
545 start_datetime_iso8601_local=wrappers_pb2.StringValue(
546 value=datetime_to_iso8601_local(start + timedelta(hours=5))
547 ),
548 end_datetime_iso8601_local=wrappers_pb2.StringValue(
549 value=datetime_to_iso8601_local(start + timedelta(hours=6))
550 ),
551 )
552 )
554 with pytest.raises(grpc.RpcError) as e:
555 api.UpdateEvent(
556 events_pb2.UpdateEventReq(
557 event_id=event_id,
558 start_datetime_iso8601_local=wrappers_pb2.StringValue(
559 value=datetime_to_iso8601_local(start + timedelta(hours=2))
560 ),
561 end_datetime_iso8601_local=wrappers_pb2.StringValue(
562 value=datetime_to_iso8601_local(start + timedelta(hours=4))
563 ),
564 )
565 )
566 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
567 assert e.value.details() == "An event cannot have overlapping occurrences."
570def test_UpdateEvent_single(db, moderator: Moderator):
571 # test cases:
572 # owner can update
573 # community owner can update
574 # notifies attendees
576 # event creator
577 user1, token1 = generate_user()
578 # community moderator
579 user2, token2 = generate_user()
580 # third parties
581 user3, token3 = generate_user()
582 user4, token4 = generate_user()
583 user5, token5 = generate_user()
584 user6, token6 = generate_user()
586 with session_scope() as session:
587 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
589 time_before = now()
590 start_time = now() + timedelta(hours=2)
591 end_time = start_time + timedelta(hours=3)
593 with events_session(token1) as api:
594 res = api.CreateEvent(
595 events_pb2.CreateEventReq(
596 title="Dummy Title",
597 content="Dummy content.",
598 parent_community_id=c_id,
599 location=events_pb2.EventLocation(
600 address="Near Null Island",
601 lat=0.1,
602 lng=0.2,
603 ),
604 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
605 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
606 )
607 )
609 event_id = res.event_id
611 moderator.approve_event_occurrence(event_id)
613 with events_session(token4) as api:
614 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
616 with events_session(token5) as api:
617 api.SetEventAttendance(
618 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
619 )
621 with events_session(token6) as api:
622 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
624 time_before_update = now()
626 with events_session(token1) as api:
627 res = api.UpdateEvent(
628 events_pb2.UpdateEventReq(
629 event_id=event_id,
630 )
631 )
633 with events_session(token1) as api:
634 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
636 assert res.is_next
637 assert res.title == "Dummy Title"
638 assert res.slug == "dummy-title"
639 assert res.content == "Dummy content."
640 assert not res.photo_url
641 assert res.HasField("location")
642 assert res.location.lat == 0.1
643 assert res.location.lng == 0.2
644 assert res.location.address == "Near Null Island"
645 assert time_before <= to_aware_datetime(res.created) <= time_before_update
646 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
647 assert res.creator_user_id == user1.id
648 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
649 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
650 assert is_utc_or_gmt(res.timezone)
651 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
652 assert res.organizer
653 assert res.subscriber
654 assert res.going_count == 2
655 assert res.organizer_count == 1
656 assert res.subscriber_count == 3
657 assert res.owner_user_id == user1.id
658 assert not res.owner_community_id
659 assert not res.owner_group_id
660 assert res.thread.thread_id
661 assert res.can_edit
662 assert not res.can_moderate
664 with events_session(token2) as api:
665 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
667 assert res.is_next
668 assert res.title == "Dummy Title"
669 assert res.slug == "dummy-title"
670 assert res.content == "Dummy content."
671 assert not res.photo_url
672 assert res.HasField("location")
673 assert res.location.lat == 0.1
674 assert res.location.lng == 0.2
675 assert res.location.address == "Near Null Island"
676 assert time_before <= to_aware_datetime(res.created) <= time_before_update
677 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
678 assert res.creator_user_id == user1.id
679 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
680 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
681 assert is_utc_or_gmt(res.timezone)
682 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
683 assert not res.organizer
684 assert not res.subscriber
685 assert res.going_count == 2
686 assert res.organizer_count == 1
687 assert res.subscriber_count == 3
688 assert res.owner_user_id == user1.id
689 assert not res.owner_community_id
690 assert not res.owner_group_id
691 assert res.thread.thread_id
692 assert res.can_edit
693 assert res.can_moderate
695 with events_session(token3) as api:
696 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
698 assert res.is_next
699 assert res.title == "Dummy Title"
700 assert res.slug == "dummy-title"
701 assert res.content == "Dummy content."
702 assert not res.photo_url
703 assert res.HasField("location")
704 assert res.location.lat == 0.1
705 assert res.location.lng == 0.2
706 assert res.location.address == "Near Null Island"
707 assert time_before <= to_aware_datetime(res.created) <= time_before_update
708 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
709 assert res.creator_user_id == user1.id
710 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
711 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
712 assert is_utc_or_gmt(res.timezone)
713 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
714 assert not res.organizer
715 assert not res.subscriber
716 assert res.going_count == 2
717 assert res.organizer_count == 1
718 assert res.subscriber_count == 3
719 assert res.owner_user_id == user1.id
720 assert not res.owner_community_id
721 assert not res.owner_group_id
722 assert res.thread.thread_id
723 assert not res.can_edit
724 assert not res.can_moderate
726 with events_session(token1) as api:
727 res = api.UpdateEvent(
728 events_pb2.UpdateEventReq(
729 event_id=event_id,
730 location=events_pb2.EventLocation(
731 address="Nearer Null Island",
732 lat=0.01,
733 lng=0.02,
734 ),
735 )
736 )
738 with events_session(token3) as api:
739 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
741 assert res.HasField("location")
742 assert res.location.address == "Nearer Null Island"
743 assert res.location.lat == 0.01
744 assert res.location.lng == 0.02
747def test_UpdateEvent_all(db, moderator: Moderator):
748 # event creator
749 user1, token1 = generate_user()
750 # community moderator
751 user2, token2 = generate_user()
752 # third parties
753 user3, token3 = generate_user()
754 user4, token4 = generate_user()
755 user5, token5 = generate_user()
756 user6, token6 = generate_user()
758 with session_scope() as session:
759 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
761 time_before = now()
762 start_time = now() + timedelta(hours=1)
763 end_time = start_time + timedelta(hours=1.5)
765 event_ids = []
767 with events_session(token1) as api:
768 res = api.CreateEvent(
769 events_pb2.CreateEventReq(
770 title="Dummy Title",
771 content="0th occurrence",
772 location=events_pb2.EventLocation(
773 address="Near Null Island",
774 lat=0.1,
775 lng=0.2,
776 ),
777 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
778 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
779 )
780 )
782 event_id = res.event_id
783 event_ids.append(event_id)
785 moderator.approve_event_occurrence(event_id)
787 with events_session(token4) as api:
788 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
790 with events_session(token5) as api:
791 api.SetEventAttendance(
792 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
793 )
795 with events_session(token6) as api:
796 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
798 with events_session(token1) as api:
799 for i in range(5):
800 res = api.ScheduleEvent(
801 events_pb2.ScheduleEventReq(
802 event_id=event_ids[-1],
803 content=f"{i + 1}th occurrence",
804 location=events_pb2.EventLocation(
805 address="Near Null Island",
806 lat=0.1,
807 lng=0.2,
808 ),
809 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2 + i)),
810 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2.5 + i)),
811 )
812 )
814 event_ids.append(res.event_id)
816 # Approve all scheduled occurrences
817 for eid in event_ids[1:]:
818 moderator.approve_event_occurrence(eid)
820 updated_event_id = event_ids[3]
822 time_before_update = now()
824 with events_session(token1) as api:
825 res = api.UpdateEvent(
826 events_pb2.UpdateEventReq(
827 event_id=updated_event_id,
828 title=wrappers_pb2.StringValue(value="New Title"),
829 content=wrappers_pb2.StringValue(value="New content."),
830 location=events_pb2.EventLocation(
831 address="Not so near Null Island",
832 lat=0.2,
833 lng=0.2,
834 ),
835 update_all_future=True,
836 )
837 )
839 time_after_update = now()
841 with events_session(token2) as api:
842 for i in range(3):
843 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
844 assert res.content == f"{i}th occurrence"
845 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
847 for i in range(3, 6):
848 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
849 assert res.content == "New content."
850 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
853def test_GetEvent(db, moderator: Moderator):
854 # event creator
855 user1, token1 = generate_user()
856 # community moderator
857 user2, token2 = generate_user()
858 # third parties
859 user3, token3 = generate_user()
860 user4, token4 = generate_user()
861 user5, token5 = generate_user()
862 user6, token6 = generate_user()
864 with session_scope() as session:
865 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
867 time_before = now()
868 start_time = now() + timedelta(hours=2)
869 end_time = start_time + timedelta(hours=3)
871 with events_session(token1) as api:
872 # in person event
873 res = api.CreateEvent(
874 events_pb2.CreateEventReq(
875 title="Dummy Title",
876 content="Dummy content.",
877 location=events_pb2.EventLocation(
878 address="Near Null Island",
879 lat=0.1,
880 lng=0.2,
881 ),
882 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
883 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
884 )
885 )
887 event_id = res.event_id
889 moderator.approve_event_occurrence(event_id)
891 with events_session(token4) as api:
892 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
894 with events_session(token5) as api:
895 api.SetEventAttendance(
896 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
897 )
899 with events_session(token6) as api:
900 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
902 with events_session(token1) as api:
903 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
905 assert res.is_next
906 assert res.title == "Dummy Title"
907 assert res.slug == "dummy-title"
908 assert res.content == "Dummy content."
909 assert not res.photo_url
910 assert res.HasField("location")
911 assert res.location.lat == 0.1
912 assert res.location.lng == 0.2
913 assert res.location.address == "Near Null Island"
914 assert time_before <= to_aware_datetime(res.created) <= now()
915 assert time_before <= to_aware_datetime(res.last_edited) <= now()
916 assert res.creator_user_id == user1.id
917 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
918 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
919 assert is_utc_or_gmt(res.timezone)
920 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
921 assert res.organizer
922 assert res.subscriber
923 assert res.going_count == 2
924 assert res.organizer_count == 1
925 assert res.subscriber_count == 3
926 assert res.owner_user_id == user1.id
927 assert not res.owner_community_id
928 assert not res.owner_group_id
929 assert res.thread.thread_id
930 assert res.can_edit
931 assert not res.can_moderate
933 with events_session(token2) as api:
934 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
936 assert res.is_next
937 assert res.title == "Dummy Title"
938 assert res.slug == "dummy-title"
939 assert res.content == "Dummy content."
940 assert not res.photo_url
941 assert res.HasField("location")
942 assert res.location.lat == 0.1
943 assert res.location.lng == 0.2
944 assert res.location.address == "Near Null Island"
945 assert time_before <= to_aware_datetime(res.created) <= now()
946 assert time_before <= to_aware_datetime(res.last_edited) <= now()
947 assert res.creator_user_id == user1.id
948 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
949 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
950 assert is_utc_or_gmt(res.timezone)
951 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
952 assert not res.organizer
953 assert not res.subscriber
954 assert res.going_count == 2
955 assert res.organizer_count == 1
956 assert res.subscriber_count == 3
957 assert res.owner_user_id == user1.id
958 assert not res.owner_community_id
959 assert not res.owner_group_id
960 assert res.thread.thread_id
961 assert res.can_edit
962 assert res.can_moderate
964 with events_session(token3) as api:
965 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
967 assert res.is_next
968 assert res.title == "Dummy Title"
969 assert res.slug == "dummy-title"
970 assert res.content == "Dummy content."
971 assert not res.photo_url
972 assert res.HasField("location")
973 assert res.location.lat == 0.1
974 assert res.location.lng == 0.2
975 assert res.location.address == "Near Null Island"
976 assert time_before <= to_aware_datetime(res.created) <= now()
977 assert time_before <= to_aware_datetime(res.last_edited) <= now()
978 assert res.creator_user_id == user1.id
979 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
980 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
981 assert is_utc_or_gmt(res.timezone)
982 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
983 assert not res.organizer
984 assert not res.subscriber
985 assert res.going_count == 2
986 assert res.organizer_count == 1
987 assert res.subscriber_count == 3
988 assert res.owner_user_id == user1.id
989 assert not res.owner_community_id
990 assert not res.owner_group_id
991 assert res.thread.thread_id
992 assert not res.can_edit
993 assert not res.can_moderate
996def test_CancelEvent(db, moderator: Moderator):
997 # event creator
998 user1, token1 = generate_user()
999 # community moderator
1000 user2, token2 = generate_user()
1001 # third parties
1002 user3, token3 = generate_user()
1003 user4, token4 = generate_user()
1004 user5, token5 = generate_user()
1005 user6, token6 = generate_user()
1007 with session_scope() as session:
1008 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1010 start_time = now() + timedelta(hours=2)
1011 end_time = start_time + timedelta(hours=3)
1013 with events_session(token1) as api:
1014 res = api.CreateEvent(
1015 events_pb2.CreateEventReq(
1016 title="Dummy Title",
1017 content="Dummy content.",
1018 location=events_pb2.EventLocation(
1019 address="Near Null Island",
1020 lat=0.1,
1021 lng=0.2,
1022 ),
1023 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1024 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1025 )
1026 )
1028 event_id = res.event_id
1030 moderator.approve_event_occurrence(event_id)
1032 with events_session(token4) as api:
1033 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1035 with events_session(token5) as api:
1036 api.SetEventAttendance(
1037 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1038 )
1040 with events_session(token6) as api:
1041 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1043 with events_session(token1) as api:
1044 res = api.CancelEvent(
1045 events_pb2.CancelEventReq(
1046 event_id=event_id,
1047 )
1048 )
1050 with events_session(token1) as api:
1051 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1052 assert res.is_cancelled
1054 with events_session(token1) as api:
1055 with pytest.raises(grpc.RpcError) as e:
1056 api.UpdateEvent(
1057 events_pb2.UpdateEventReq(
1058 event_id=event_id,
1059 title=wrappers_pb2.StringValue(value="New Title"),
1060 )
1061 )
1062 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1063 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1065 with pytest.raises(grpc.RpcError) as e:
1066 api.InviteEventOrganizer(
1067 events_pb2.InviteEventOrganizerReq(
1068 event_id=event_id,
1069 user_id=user3.id,
1070 )
1071 )
1072 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1073 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1075 with pytest.raises(grpc.RpcError) as e:
1076 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1077 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1078 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1080 with events_session(token3) as api:
1081 with pytest.raises(grpc.RpcError) as e:
1082 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1083 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1084 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1086 with pytest.raises(grpc.RpcError) as e:
1087 api.SetEventAttendance(
1088 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1089 )
1090 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1091 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1093 with events_session(token1) as api:
1094 for include_cancelled in [True, False]:
1095 res = api.ListEventOccurrences(
1096 events_pb2.ListEventOccurrencesReq(
1097 event_id=event_id,
1098 include_cancelled=include_cancelled,
1099 )
1100 )
1101 if include_cancelled:
1102 assert len(res.events) > 0
1103 else:
1104 assert len(res.events) == 0
1106 res = api.ListMyEvents(
1107 events_pb2.ListMyEventsReq(
1108 include_cancelled=include_cancelled,
1109 )
1110 )
1111 if include_cancelled:
1112 assert len(res.events) > 0
1113 else:
1114 assert len(res.events) == 0
1117def test_ListEventAttendees(db, moderator: Moderator):
1118 # event creator
1119 user1, token1 = generate_user()
1120 # others
1121 user2, token2 = generate_user()
1122 user3, token3 = generate_user()
1123 user4, token4 = generate_user()
1124 user5, token5 = generate_user()
1125 user6, token6 = generate_user()
1127 with session_scope() as session:
1128 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1130 with events_session(token1) as api:
1131 event_id = api.CreateEvent(
1132 events_pb2.CreateEventReq(
1133 title="Dummy Title",
1134 content="Dummy content.",
1135 location=events_pb2.EventLocation(
1136 address="Near Null Island",
1137 lat=0.1,
1138 lng=0.2,
1139 ),
1140 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1141 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1142 )
1143 ).event_id
1145 moderator.approve_event_occurrence(event_id)
1147 for token in [token2, token3, token4, token5]:
1148 with events_session(token) as api:
1149 api.SetEventAttendance(
1150 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1151 )
1153 with events_session(token6) as api:
1154 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1156 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1157 assert res.attendee_user_ids == [user1.id, user2.id]
1159 res = api.ListEventAttendees(
1160 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1161 )
1162 assert res.attendee_user_ids == [user3.id, user4.id]
1164 res = api.ListEventAttendees(
1165 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1166 )
1167 assert res.attendee_user_ids == [user5.id]
1168 assert not res.next_page_token
1171def test_ListEventSubscribers(db, moderator: Moderator):
1172 # event creator
1173 user1, token1 = generate_user()
1174 # others
1175 user2, token2 = generate_user()
1176 user3, token3 = generate_user()
1177 user4, token4 = generate_user()
1178 user5, token5 = generate_user()
1179 user6, token6 = generate_user()
1181 with session_scope() as session:
1182 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1184 with events_session(token1) as api:
1185 event_id = api.CreateEvent(
1186 events_pb2.CreateEventReq(
1187 title="Dummy Title",
1188 content="Dummy content.",
1189 location=events_pb2.EventLocation(
1190 address="Near Null Island",
1191 lat=0.1,
1192 lng=0.2,
1193 ),
1194 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1195 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1196 )
1197 ).event_id
1199 moderator.approve_event_occurrence(event_id)
1201 for token in [token2, token3, token4, token5]:
1202 with events_session(token) as api:
1203 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1205 with events_session(token6) as api:
1206 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1208 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1209 assert res.subscriber_user_ids == [user1.id, user2.id]
1211 res = api.ListEventSubscribers(
1212 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1213 )
1214 assert res.subscriber_user_ids == [user3.id, user4.id]
1216 res = api.ListEventSubscribers(
1217 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1218 )
1219 assert res.subscriber_user_ids == [user5.id]
1220 assert not res.next_page_token
1223def test_ListEventOrganizers(db, moderator: Moderator):
1224 # event creator
1225 user1, token1 = generate_user()
1226 # others
1227 user2, token2 = generate_user()
1228 user3, token3 = generate_user()
1229 user4, token4 = generate_user()
1230 user5, token5 = generate_user()
1231 user6, token6 = generate_user()
1233 with session_scope() as session:
1234 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1236 with events_session(token1) as api:
1237 event_id = api.CreateEvent(
1238 events_pb2.CreateEventReq(
1239 title="Dummy Title",
1240 content="Dummy content.",
1241 location=events_pb2.EventLocation(
1242 address="Near Null Island",
1243 lat=0.1,
1244 lng=0.2,
1245 ),
1246 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1247 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1248 )
1249 ).event_id
1251 moderator.approve_event_occurrence(event_id)
1253 with events_session(token1) as api:
1254 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1255 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1257 with events_session(token6) as api:
1258 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1260 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1261 assert res.organizer_user_ids == [user1.id, user2.id]
1263 res = api.ListEventOrganizers(
1264 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1265 )
1266 assert res.organizer_user_ids == [user3.id, user4.id]
1268 res = api.ListEventOrganizers(
1269 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1270 )
1271 assert res.organizer_user_ids == [user5.id]
1272 assert not res.next_page_token
1275def test_TransferEvent(db):
1276 user1, token1 = generate_user()
1277 user2, token2 = generate_user()
1278 user3, token3 = generate_user()
1279 user4, token4 = generate_user()
1281 with session_scope() as session:
1282 c = create_community(session, 0, 2, "Community", [user3], [], None)
1283 h = create_group(session, "Group", [user4], [], c)
1284 c_id = c.id
1285 h_id = h.id
1287 with events_session(token1) as api:
1288 event_id = api.CreateEvent(
1289 events_pb2.CreateEventReq(
1290 title="Dummy Title",
1291 content="Dummy content.",
1292 location=events_pb2.EventLocation(
1293 address="Near Null Island",
1294 lat=0.1,
1295 lng=0.2,
1296 ),
1297 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1298 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1299 )
1300 ).event_id
1302 api.TransferEvent(
1303 events_pb2.TransferEventReq(
1304 event_id=event_id,
1305 new_owner_community_id=c_id,
1306 )
1307 )
1309 # remove ourselves as organizer, otherwise we can still edit it
1310 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1312 with pytest.raises(grpc.RpcError) as e:
1313 api.TransferEvent(
1314 events_pb2.TransferEventReq(
1315 event_id=event_id,
1316 new_owner_group_id=h_id,
1317 )
1318 )
1319 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1320 assert e.value.details() == "You're not allowed to transfer that event."
1322 event_id = api.CreateEvent(
1323 events_pb2.CreateEventReq(
1324 title="Dummy Title",
1325 content="Dummy content.",
1326 location=events_pb2.EventLocation(
1327 address="Near Null Island",
1328 lat=0.1,
1329 lng=0.2,
1330 ),
1331 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1332 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1333 )
1334 ).event_id
1336 api.TransferEvent(
1337 events_pb2.TransferEventReq(
1338 event_id=event_id,
1339 new_owner_group_id=h_id,
1340 )
1341 )
1343 # remove ourselves as organizer, otherwise we can still edit it
1344 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1346 with pytest.raises(grpc.RpcError) as e:
1347 api.TransferEvent(
1348 events_pb2.TransferEventReq(
1349 event_id=event_id,
1350 new_owner_community_id=c_id,
1351 )
1352 )
1353 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1354 assert e.value.details() == "You're not allowed to transfer that event."
1357def test_SetEventSubscription(db, moderator: Moderator):
1358 user1, token1 = generate_user()
1359 user2, token2 = generate_user()
1361 with session_scope() as session:
1362 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1364 with events_session(token1) as api:
1365 event_id = api.CreateEvent(
1366 events_pb2.CreateEventReq(
1367 title="Dummy Title",
1368 content="Dummy content.",
1369 location=events_pb2.EventLocation(
1370 address="Near Null Island",
1371 lat=0.1,
1372 lng=0.2,
1373 ),
1374 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1375 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1376 )
1377 ).event_id
1379 moderator.approve_event_occurrence(event_id)
1381 with events_session(token2) as api:
1382 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1383 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1384 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1385 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1386 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1389def test_SetEventAttendance(db, moderator: Moderator):
1390 user1, token1 = generate_user()
1391 user2, token2 = generate_user()
1393 with session_scope() as session:
1394 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1396 with events_session(token1) as api:
1397 event_id = api.CreateEvent(
1398 events_pb2.CreateEventReq(
1399 title="Dummy Title",
1400 content="Dummy content.",
1401 location=events_pb2.EventLocation(
1402 address="Near Null Island",
1403 lat=0.1,
1404 lng=0.2,
1405 ),
1406 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1407 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1408 )
1409 ).event_id
1411 moderator.approve_event_occurrence(event_id)
1413 with events_session(token2) as api:
1414 assert (
1415 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1416 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1417 )
1418 api.SetEventAttendance(
1419 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1420 )
1421 assert (
1422 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1423 == events_pb2.ATTENDANCE_STATE_GOING
1424 )
1425 api.SetEventAttendance(
1426 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1427 )
1428 assert (
1429 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1430 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1431 )
1434def test_InviteEventOrganizer(db, moderator: Moderator):
1435 user1, token1 = generate_user()
1436 user2, token2 = generate_user()
1438 with session_scope() as session:
1439 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1441 with events_session(token1) as api:
1442 event_id = api.CreateEvent(
1443 events_pb2.CreateEventReq(
1444 title="Dummy Title",
1445 content="Dummy content.",
1446 location=events_pb2.EventLocation(
1447 address="Near Null Island",
1448 lat=0.1,
1449 lng=0.2,
1450 ),
1451 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1452 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1453 )
1454 ).event_id
1456 moderator.approve_event_occurrence(event_id)
1458 with events_session(token2) as api:
1459 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1461 with pytest.raises(grpc.RpcError) as e:
1462 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1463 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1464 assert e.value.details() == "You're not allowed to edit that event."
1466 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1468 with events_session(token1) as api:
1469 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1471 with events_session(token2) as api:
1472 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1475def test_ListEventOccurrences(db):
1476 user1, token1 = generate_user()
1477 user2, token2 = generate_user()
1478 user3, token3 = generate_user()
1480 with session_scope() as session:
1481 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1483 start = now()
1485 event_ids = []
1487 with events_session(token1) as api:
1488 res = api.CreateEvent(
1489 events_pb2.CreateEventReq(
1490 title="First occurrence",
1491 content="Dummy content.",
1492 parent_community_id=c_id,
1493 location=events_pb2.EventLocation(
1494 address="Near Null Island",
1495 lat=0.1,
1496 lng=0.2,
1497 ),
1498 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1499 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
1500 )
1501 )
1503 event_ids.append(res.event_id)
1505 for i in range(5):
1506 res = api.ScheduleEvent(
1507 events_pb2.ScheduleEventReq(
1508 event_id=event_ids[-1],
1509 content=f"{i}th occurrence",
1510 location=events_pb2.EventLocation(
1511 address="Near Null Island",
1512 lat=0.1,
1513 lng=0.2,
1514 ),
1515 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
1516 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
1517 )
1518 )
1520 event_ids.append(res.event_id)
1522 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1523 assert [event.event_id for event in res.events] == event_ids[:2]
1525 res = api.ListEventOccurrences(
1526 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1527 )
1528 assert [event.event_id for event in res.events] == event_ids[2:4]
1530 res = api.ListEventOccurrences(
1531 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1532 )
1533 assert [event.event_id for event in res.events] == event_ids[4:6]
1534 assert not res.next_page_token
1537def test_ListMyEvents(db, moderator: Moderator):
1538 user1, token1 = generate_user()
1539 user2, token2 = generate_user()
1540 user3, token3 = generate_user()
1541 user4, token4 = generate_user()
1542 user5, token5 = generate_user()
1544 with session_scope() as session:
1545 # Create global (world) -> macroregion -> region -> subregion hierarchy
1546 # my_communities_exclude_global filters out world, macroregion, and region level communities
1547 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1548 c_id = global_community.id
1549 macroregion_community = create_community(
1550 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1551 )
1552 region_community = create_community(
1553 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1554 )
1555 subregion_community = create_community(
1556 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1557 )
1558 c2_id = subregion_community.id
1560 start = now()
1562 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1563 return events_pb2.CreateEventReq(
1564 title="Dummy Title",
1565 content="Dummy content.",
1566 location=events_pb2.EventLocation(
1567 address="Near Null Island",
1568 lat=0.1,
1569 lng=0.2,
1570 ),
1571 parent_community_id=community_id,
1572 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now)),
1573 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now + 0.5)),
1574 )
1576 with events_session(token1) as api:
1577 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1579 moderator.approve_event_occurrence(e2)
1581 with events_session(token2) as api:
1582 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1584 moderator.approve_event_occurrence(e1)
1586 with events_session(token1) as api:
1587 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1589 moderator.approve_event_occurrence(e3)
1591 with events_session(token2) as api:
1592 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1594 moderator.approve_event_occurrence(e5)
1596 with events_session(token3) as api:
1597 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1599 moderator.approve_event_occurrence(e4)
1601 with events_session(token4) as api:
1602 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1604 moderator.approve_event_occurrence(e6)
1606 with events_session(token1) as api:
1607 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1609 with events_session(token1) as api:
1610 api.SetEventAttendance(
1611 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1612 )
1613 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1615 with events_session(token2) as api:
1616 api.SetEventAttendance(
1617 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1618 )
1620 with events_session(token3) as api:
1621 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1623 with events_session(token1) as api:
1624 # test pagination with token first
1625 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1626 assert [event.event_id for event in res.events] == [e1, e2]
1627 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1628 assert [event.event_id for event in res.events] == [e3, e4]
1629 assert not res.next_page_token
1631 res = api.ListMyEvents(
1632 events_pb2.ListMyEventsReq(
1633 subscribed=True,
1634 attending=True,
1635 organizing=True,
1636 )
1637 )
1638 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1640 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1641 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1643 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1644 assert [event.event_id for event in res.events] == [e2, e3, e4]
1646 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1647 assert [event.event_id for event in res.events] == [e1, e2, e3]
1649 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1650 assert [event.event_id for event in res.events] == [e2, e3]
1652 with events_session(token1) as api:
1653 # Test pagination with page_number and verify total_items
1654 res = api.ListMyEvents(
1655 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1656 )
1657 assert [event.event_id for event in res.events] == [e1, e2]
1658 assert res.total_items == 4
1660 res = api.ListMyEvents(
1661 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1662 )
1663 assert [event.event_id for event in res.events] == [e3, e4]
1664 assert res.total_items == 4
1666 # Verify no more pages
1667 res = api.ListMyEvents(
1668 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1669 )
1670 assert not res.events
1671 assert res.total_items == 4
1673 with events_session(token2) as api:
1674 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1675 assert [event.event_id for event in res.events] == [e1, e3, e5]
1677 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1678 assert [event.event_id for event in res.events] == [e1, e5]
1680 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1681 assert [event.event_id for event in res.events] == [e1, e3, e5]
1683 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1684 assert [event.event_id for event in res.events] == [e1, e5]
1686 with events_session(token3) as api:
1687 # user3 is member of both global (c_id) and child (c2_id) communities
1688 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1689 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1691 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1692 assert [event.event_id for event in res.events] == [e2, e4]
1694 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1695 assert [event.event_id for event in res.events] == [e4]
1697 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1698 assert [event.event_id for event in res.events] == [e3, e4]
1700 # my_communities returns events from both communities user3 is a member of
1701 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1702 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1704 # my_communities_exclude_global filters out events from global community (node_id=1)
1705 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1706 assert [event.event_id for event in res.events] == [e6]
1708 # my_communities_exclude_global works independently of my_communities flag
1709 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1710 assert [event.event_id for event in res.events] == [e6]
1712 # my_communities_exclude_global filters organizing results too
1713 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1714 assert [event.event_id for event in res.events] == []
1716 # my_communities_exclude_global filters subscribed results too
1717 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1718 assert [event.event_id for event in res.events] == []
1720 with events_session(token5) as api:
1721 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1722 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1725def _paginate_my_events(api, page_size: int) -> list[int]:
1726 event_ids = []
1727 page_token = ""
1728 for _ in range(10): 1728 ↛ 1734line 1728 didn't jump to line 1734 because the loop on line 1728 didn't complete
1729 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=page_size, page_token=page_token))
1730 event_ids += [event.event_id for event in res.events]
1731 page_token = res.next_page_token
1732 if not page_token:
1733 return event_ids
1734 raise AssertionError("pagination did not terminate")
1737def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator):
1738 user, token = generate_user()
1740 with session_scope() as session:
1741 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1743 start = now()
1745 def new_event(start_offset: timedelta, duration: timedelta) -> events_pb2.CreateEventReq:
1746 return events_pb2.CreateEventReq(
1747 title="Dummy Title",
1748 content="Dummy content.",
1749 location=events_pb2.EventLocation(
1750 address="Near Null Island",
1751 lat=0.1,
1752 lng=0.2,
1753 ),
1754 parent_community_id=c_id,
1755 start_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset),
1756 end_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset + duration),
1757 )
1759 with events_session(token) as api:
1760 # a multi-day event overlapping all the short events below: it ends last but starts first,
1761 # so an end time based cursor would repeat it on every page and skip the short events
1762 long_event = api.CreateEvent(new_event(timedelta(hours=1), timedelta(days=3))).event_id
1763 short_events = [
1764 api.CreateEvent(new_event(timedelta(hours=2 + i), timedelta(hours=1))).event_id for i in range(4)
1765 ]
1767 for event_id in [long_event, *short_events]:
1768 moderator.approve_event_occurrence(event_id)
1770 with events_session(token) as api:
1771 assert _paginate_my_events(api, page_size=2) == [long_event, *short_events]
1774def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator):
1775 user, token = generate_user()
1777 with session_scope() as session:
1778 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1780 start = now()
1782 with events_session(token) as api:
1783 event_ids = [
1784 api.CreateEvent(
1785 events_pb2.CreateEventReq(
1786 title="Dummy Title",
1787 content="Dummy content.",
1788 location=events_pb2.EventLocation(
1789 address="Near Null Island",
1790 lat=0.1,
1791 lng=0.2,
1792 ),
1793 parent_community_id=c_id,
1794 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1795 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
1796 )
1797 ).event_id
1798 for _ in range(5)
1799 ]
1801 for event_id in event_ids:
1802 moderator.approve_event_occurrence(event_id)
1804 with events_session(token) as api:
1805 assert _paginate_my_events(api, page_size=2) == event_ids
1808def test_list_my_events_exclude_attending(db, moderator: Moderator):
1809 user1, token1 = generate_user()
1810 user2, token2 = generate_user()
1812 with session_scope() as session:
1813 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1814 c_id = c.id
1816 start = now()
1818 def make_event(hours):
1819 return events_pb2.CreateEventReq(
1820 title="Test Event",
1821 content="Test content.",
1822 location=events_pb2.EventLocation(
1823 address="Near Null Island",
1824 lat=0.1,
1825 lng=0.2,
1826 ),
1827 parent_community_id=c_id,
1828 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours)),
1829 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours + 1)),
1830 )
1832 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1833 with events_session(token1) as api:
1834 e_own = api.CreateEvent(make_event(1)).event_id
1836 with events_session(token2) as api:
1837 e_attending = api.CreateEvent(make_event(2)).event_id
1838 e_community_only = api.CreateEvent(make_event(3)).event_id
1839 # e_both: user1 will be both organizer and attendee
1840 e_both = api.CreateEvent(make_event(4)).event_id
1842 moderator.approve_event_occurrence(e_own)
1843 moderator.approve_event_occurrence(e_attending)
1844 moderator.approve_event_occurrence(e_community_only)
1845 moderator.approve_event_occurrence(e_both)
1847 # invite user1 as organizer of e_both
1848 with events_session(token2) as api:
1849 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
1851 # user1 RSVPs to e_attending and e_both
1852 with events_session(token1) as api:
1853 api.SetEventAttendance(
1854 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1855 )
1856 api.SetEventAttendance(
1857 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1858 )
1860 with events_session(token1) as api:
1861 # baseline: all four community events visible
1862 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1863 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
1865 # exclude_attending removes events user1 is attending (e_attending, e_both)
1866 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
1867 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1868 assert [e.event_id for e in res.events] == [e_community_only]
1870 # exclude_attending with attending=True: invalid combination
1871 with pytest.raises(grpc.RpcError) as e:
1872 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
1873 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1875 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
1876 with events_session(token2) as api:
1877 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1878 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
1879 assert [e.event_id for e in res.events] == [e_own]
1882def test_RemoveEventOrganizer(db, moderator: Moderator):
1883 user1, token1 = generate_user()
1884 user2, token2 = generate_user()
1886 with session_scope() as session:
1887 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1889 with events_session(token1) as api:
1890 event_id = api.CreateEvent(
1891 events_pb2.CreateEventReq(
1892 title="Dummy Title",
1893 content="Dummy content.",
1894 location=events_pb2.EventLocation(
1895 address="Near Null Island",
1896 lat=0.1,
1897 lng=0.2,
1898 ),
1899 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1900 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1901 )
1902 ).event_id
1904 moderator.approve_event_occurrence(event_id)
1906 with events_session(token2) as api:
1907 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1909 with pytest.raises(grpc.RpcError) as e:
1910 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1911 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1912 assert e.value.details() == "You're not allowed to edit that event."
1914 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1916 with events_session(token1) as api:
1917 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1919 with pytest.raises(grpc.RpcError) as e:
1920 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1921 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1922 assert e.value.details() == "You cannot remove the event owner as an organizer."
1924 with events_session(token2) as api:
1925 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1926 assert res.organizer
1927 assert res.organizer_count == 2
1928 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1929 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1931 with pytest.raises(grpc.RpcError) as e:
1932 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1933 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1934 assert e.value.details() == "You're not allowed to edit that event."
1936 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1937 assert not res.organizer
1938 assert res.organizer_count == 1
1940 # Test that event owner can remove co-organizers
1941 with events_session(token1) as api:
1942 # Add user2 back as organizer
1943 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1945 # Verify user2 is now an organizer
1946 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1947 assert res.organizer_count == 2
1949 # Event owner can remove co-organizer
1950 api.RemoveEventOrganizer(
1951 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
1952 )
1954 # Verify user2 is no longer an organizer
1955 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1956 assert res.organizer_count == 1
1958 # Test that non-organizers cannot remove other organizers
1959 with events_session(token2) as api:
1960 # User2 cannot invite themselves as organizer (not the owner)
1961 with pytest.raises(grpc.RpcError) as e:
1962 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1963 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1964 assert e.value.details() == "You're not allowed to edit that event."
1966 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
1967 with events_session(token1) as api:
1968 # Add user2 back as organizer
1969 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1972def test_ListEventAttendees_regression(db):
1973 # see issue #1617:
1974 #
1975 # 1. Create an event
1976 # 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`
1977 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
1978 # 4. Change the current user's attendance state to "going" again
1979 #
1980 # **Expected behaviour**
1981 # `ListEventAttendees` should return the current user's ID
1982 #
1983 # **Actual/current behaviour**
1984 # `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
1986 user1, token1 = generate_user()
1987 user2, token2 = generate_user()
1988 user3, token3 = generate_user()
1989 user4, token4 = generate_user()
1990 user5, token5 = generate_user()
1992 with session_scope() as session:
1993 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1995 start_time = now() + timedelta(hours=2)
1996 end_time = start_time + timedelta(hours=3)
1998 with events_session(token1) as api:
1999 res = api.CreateEvent(
2000 events_pb2.CreateEventReq(
2001 title="Dummy Title",
2002 content="Dummy content.",
2003 location=events_pb2.EventLocation(
2004 address="Near Null Island",
2005 lat=0.1,
2006 lng=0.2,
2007 ),
2008 parent_community_id=c_id,
2009 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2010 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2011 )
2012 )
2014 res = api.TransferEvent(
2015 events_pb2.TransferEventReq(
2016 event_id=res.event_id,
2017 new_owner_community_id=c_id,
2018 )
2019 )
2021 event_id = res.event_id
2023 api.SetEventAttendance(
2024 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2025 )
2026 api.SetEventAttendance(
2027 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2028 )
2030 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2031 assert len(res.attendee_user_ids) == 1
2032 assert res.attendee_user_ids[0] == user1.id
2035def test_GetEventCalendarFile(db, moderator: Moderator):
2036 user1, token1 = generate_user()
2037 user2, token2 = generate_user()
2039 with session_scope() as session:
2040 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2042 start_time = now() + timedelta(hours=2)
2043 end_time = start_time + timedelta(hours=3)
2045 with events_session(token1) as api:
2046 created_event: events_pb2.Event = api.CreateEvent(
2047 events_pb2.CreateEventReq(
2048 title="Dummy Title",
2049 content="Dummy content.",
2050 parent_community_id=c_id,
2051 location=events_pb2.EventLocation(
2052 address="Near Null Island",
2053 lat=0.1,
2054 lng=0.2,
2055 ),
2056 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2057 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2058 )
2059 )
2060 event_id = created_event.event_id
2062 moderator.approve_event_occurrence(event_id)
2064 with events_session(token1) as api:
2065 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
2066 assert file_res.content_type == "text/calendar"
2067 ics_string = file_res.data.decode("utf-8")
2068 assert "SUMMARY:Dummy Title" in ics_string
2069 assert "DESCRIPTION:Dummy content." in ics_string
2070 assert "LOCATION:Near Null Island" in ics_string
2071 assert "STATUS:CANCELLED" not in ics_string
2072 pre_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
2073 assert pre_cancel_match is not None
2074 pre_cancel_sequence = int(pre_cancel_match.group(1))
2076 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2078 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
2079 ics_string = file_res.data.decode("utf-8")
2080 assert "SUMMARY:Cancelled: Dummy Title" in ics_string
2081 assert "STATUS:CANCELLED" in ics_string
2082 post_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
2083 assert post_cancel_match is not None
2084 post_cancel_sequence = int(post_cancel_match.group(1))
2085 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal.
2086 assert post_cancel_sequence >= pre_cancel_sequence
2089def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2090 user1, token1 = generate_user()
2091 user2, token2 = generate_user()
2092 user3, token3 = generate_user()
2093 user4, token4 = generate_user()
2095 with session_scope() as session:
2096 c = create_community(session, 0, 2, "Community", [user3], [], None)
2097 h = create_group(session, "Group", [user4], [], c)
2098 c_id = c.id
2099 h_id = h.id
2100 user4_id = user4.id
2102 with events_session(token1) as api:
2103 event = api.CreateEvent(
2104 events_pb2.CreateEventReq(
2105 title="Dummy Title",
2106 content="Dummy content.",
2107 location=events_pb2.EventLocation(
2108 address="Near Null Island",
2109 lat=0.1,
2110 lng=0.2,
2111 ),
2112 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2113 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2114 )
2115 )
2117 moderator.approve_event_occurrence(event.event_id)
2119 with threads_session(token2) as api:
2120 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2122 moderator.approve_thread_post(reply_id)
2124 with events_session(token3) as api:
2125 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2126 assert res.thread.num_responses == 1
2128 with threads_session(token3) as api:
2129 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2130 assert len(ret.replies) == 1
2131 assert not ret.next_page_token
2132 assert ret.replies[0].thread_id == reply_id
2133 assert ret.replies[0].content == "hi"
2134 assert ret.replies[0].author_user_id == user2.id
2135 assert ret.replies[0].num_replies == 0
2137 nested_reply_id = api.PostReply(
2138 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2139 ).thread_id
2141 moderator.approve_thread_post(nested_reply_id)
2143 process_jobs()
2145 push = push_collector.pop_for_user(user1.id, last=True)
2146 assert push.topic_action == NotificationTopicAction.event__comment.display
2147 assert push.content.title == f"{user2.name} • Dummy Title"
2148 assert push.content.ios_title == user2.name
2149 assert push.content.ios_subtitle == "Commented on Dummy Title"
2150 assert push.content.body == "hi"
2152 push = push_collector.pop_for_user(user2.id, last=True)
2153 assert push.content.title == f"{user3.name} • Dummy Title"
2155 assert push_collector.count_for_user(user4_id) == 0
2158def test_can_overlap_other_events_schedule_regression(db):
2159 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2160 user, token = generate_user()
2162 with session_scope() as session:
2163 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2165 start = now()
2167 with events_session(token) as api:
2168 # create another event, should be able to overlap with this one
2169 api.CreateEvent(
2170 events_pb2.CreateEventReq(
2171 title="Dummy Title",
2172 content="Dummy content.",
2173 parent_community_id=c_id,
2174 location=events_pb2.EventLocation(
2175 address="Near Null Island",
2176 lat=0.1,
2177 lng=0.2,
2178 ),
2179 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2180 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)),
2181 )
2182 )
2184 # this event
2185 res = api.CreateEvent(
2186 events_pb2.CreateEventReq(
2187 title="Dummy Title",
2188 content="Dummy content.",
2189 parent_community_id=c_id,
2190 location=events_pb2.EventLocation(
2191 address="Near Null Island",
2192 lat=0.1,
2193 lng=0.2,
2194 ),
2195 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2196 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
2197 )
2198 )
2200 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2201 api.ScheduleEvent(
2202 events_pb2.ScheduleEventReq(
2203 event_id=res.event_id,
2204 content="New event occurrence",
2205 location=events_pb2.EventLocation(
2206 address="A bit further but still near Null Island",
2207 lat=0.3,
2208 lng=0.2,
2209 ),
2210 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2211 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2212 )
2213 )
2216def test_can_overlap_other_events_update_regression(db):
2217 user, token = generate_user()
2219 with session_scope() as session:
2220 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2222 start = now()
2224 with events_session(token) as api:
2225 # create another event, should be able to overlap with this one
2226 api.CreateEvent(
2227 events_pb2.CreateEventReq(
2228 title="Dummy Title",
2229 content="Dummy content.",
2230 parent_community_id=c_id,
2231 location=events_pb2.EventLocation(
2232 address="Near Null Island",
2233 lat=0.1,
2234 lng=0.2,
2235 ),
2236 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2237 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2238 )
2239 )
2241 res = api.CreateEvent(
2242 events_pb2.CreateEventReq(
2243 title="Dummy Title",
2244 content="Dummy content.",
2245 parent_community_id=c_id,
2246 location=events_pb2.EventLocation(
2247 address="Near Null Island",
2248 lat=0.1,
2249 lng=0.2,
2250 ),
2251 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)),
2252 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)),
2253 )
2254 )
2256 event_id = api.ScheduleEvent(
2257 events_pb2.ScheduleEventReq(
2258 event_id=res.event_id,
2259 content="New event occurrence",
2260 location=events_pb2.EventLocation(
2261 address="A bit further but still near Null Island",
2262 lat=0.3,
2263 lng=0.2,
2264 ),
2265 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2266 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2267 )
2268 ).event_id
2270 # can overlap with this current existing occurrence
2271 api.UpdateEvent(
2272 events_pb2.UpdateEventReq(
2273 event_id=event_id,
2274 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2275 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2276 ),
2277 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2278 value=datetime_to_iso8601_local(start + timedelta(hours=6))
2279 ),
2280 )
2281 )
2283 api.UpdateEvent(
2284 events_pb2.UpdateEventReq(
2285 event_id=event_id,
2286 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2287 value=datetime_to_iso8601_local(start + timedelta(hours=2))
2288 ),
2289 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2290 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2291 ),
2292 )
2293 )
2296def test_list_past_events_regression(db):
2297 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2298 user, token = generate_user()
2300 with session_scope() as session:
2301 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2303 start = now()
2305 with events_session(token) as api:
2306 api.CreateEvent(
2307 events_pb2.CreateEventReq(
2308 title="Dummy Title",
2309 content="Dummy content.",
2310 parent_community_id=c_id,
2311 location=events_pb2.EventLocation(
2312 address="Near Null Island",
2313 lat=0.1,
2314 lng=0.2,
2315 ),
2316 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2317 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2318 )
2319 )
2321 with session_scope() as session:
2322 session.execute(
2323 update(EventOccurrence).values(
2324 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2325 )
2326 )
2328 with events_session(token) as api:
2329 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2330 assert len(res.events) == 1
2333def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2334 user1, token1 = generate_user(complete_profile=True)
2335 user2, token2 = generate_user()
2336 user3, token3 = generate_user()
2337 user4, token4 = generate_user()
2338 user5, token5 = generate_user(is_superuser=True)
2340 with session_scope() as session:
2341 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2342 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2343 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2344 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2346 enforce_community_memberships()
2348 with events_session(token1) as api:
2349 res = api.CreateEvent(
2350 events_pb2.CreateEventReq(
2351 title="Dummy Title",
2352 content="Dummy content.",
2353 parent_community_id=c_id,
2354 location=events_pb2.EventLocation(
2355 address="Near Null Island",
2356 lat=0.1,
2357 lng=0.2,
2358 ),
2359 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2360 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2361 )
2362 )
2363 user_url = f"http://localhost:3000/user/{user1.username}"
2364 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2366 event_id = res.event_id
2368 moderator.approve_event_occurrence(event_id)
2370 with events_session(token1) as api:
2371 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2373 email = email_collector.pop_for_mods(last=True)
2375 assert user_url in email.plain
2376 assert event_url in email.plain
2378 # can't send another req
2379 with pytest.raises(grpc.RpcError) as err:
2380 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2381 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2382 assert err.value.details() == "You have already requested a community invite for this event."
2384 # another user can send one though
2385 with events_session(token3) as api:
2386 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2388 # but not a non-admin
2389 with events_session(token2) as api:
2390 with pytest.raises(grpc.RpcError) as err:
2391 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2392 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2393 assert err.value.details() == "You're not allowed to edit that event."
2395 with real_editor_session(token5) as editor:
2396 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2397 assert len(res.requests) == 2
2398 assert res.requests[0].user_id == user1.id
2399 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain)
2400 assert res.requests[0].approx_users_to_notify == 2
2401 assert res.requests[1].user_id == user3.id
2402 assert res.requests[1].approx_users_to_notify == 2
2404 editor.DecideEventCommunityInviteRequest(
2405 editor_pb2.DecideEventCommunityInviteRequestReq(
2406 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2407 approve=False,
2408 )
2409 )
2411 editor.DecideEventCommunityInviteRequest(
2412 editor_pb2.DecideEventCommunityInviteRequestReq(
2413 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2414 approve=True,
2415 )
2416 )
2418 # not after approve
2419 with events_session(token4) as api:
2420 with pytest.raises(grpc.RpcError) as err:
2421 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2422 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2423 assert err.value.details() == "A community invite has already been sent out for this event."
2426def test_list_decided_community_invite_requests(db, moderator: Moderator):
2427 user1, token1 = generate_user()
2428 user2, token2 = generate_user()
2429 user3, token3 = generate_user()
2430 superuser, superuser_token = generate_user(is_superuser=True)
2432 with session_scope() as session:
2433 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2434 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2435 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2436 c_id = create_community(session, 0, 2, "Community", [user1, user2, user3], [], r).id
2438 enforce_community_memberships()
2440 def create_event_and_request_invite(token: str, title: str) -> str:
2441 with events_session(token) as api:
2442 res = api.CreateEvent(
2443 events_pb2.CreateEventReq(
2444 title=title,
2445 content="Dummy content.",
2446 parent_community_id=c_id,
2447 location=events_pb2.EventLocation(
2448 address="Near Null Island",
2449 lat=0.1,
2450 lng=0.2,
2451 ),
2452 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2453 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2454 )
2455 )
2456 moderator.approve_event_occurrence(res.event_id)
2457 with events_session(token) as api:
2458 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=res.event_id))
2459 return f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2461 event1_url = create_event_and_request_invite(token1, "Approved Event")
2462 event2_url = create_event_and_request_invite(token2, "Declined Event")
2463 # this one stays pending
2464 create_event_and_request_invite(token3, "Pending Event")
2466 with real_editor_session(superuser_token) as editor:
2467 pending = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2468 assert len(pending.requests) == 3
2469 by_user = {req.user_id: req.event_community_invite_request_id for req in pending.requests}
2471 # nothing decided yet
2472 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
2473 assert len(res.requests) == 0
2474 assert not res.next_page_token
2476 editor.DecideEventCommunityInviteRequest(
2477 editor_pb2.DecideEventCommunityInviteRequestReq(
2478 event_community_invite_request_id=by_user[user1.id],
2479 approve=True,
2480 )
2481 )
2482 editor.DecideEventCommunityInviteRequest(
2483 editor_pb2.DecideEventCommunityInviteRequestReq(
2484 event_community_invite_request_id=by_user[user2.id],
2485 approve=False,
2486 )
2487 )
2489 # the pending one is not returned, and the most recently decided comes first
2490 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
2491 assert len(res.requests) == 2
2492 assert not res.next_page_token
2494 declined, approved = res.requests
2496 assert declined.event_community_invite_request_id == by_user[user2.id]
2497 assert declined.user_id == user2.id
2498 assert declined.event_url == event2_url
2499 assert declined.community_id == c_id
2500 assert declined.decided_by_user_id == superuser.id
2501 assert not declined.approved
2502 assert declined.created.ToDatetime() <= declined.decided.ToDatetime()
2504 assert approved.event_community_invite_request_id == by_user[user1.id]
2505 assert approved.user_id == user1.id
2506 assert approved.event_url == event1_url
2507 assert approved.community_id == c_id
2508 assert approved.decided_by_user_id == superuser.id
2509 assert approved.approved
2510 assert approved.decided.ToDatetime() <= declined.decided.ToDatetime()
2512 # filtering
2513 res = editor.ListDecidedEventCommunityInviteRequests(
2514 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=True))
2515 )
2516 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
2518 res = editor.ListDecidedEventCommunityInviteRequests(
2519 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=False))
2520 )
2521 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
2523 # pagination
2524 res = editor.ListDecidedEventCommunityInviteRequests(
2525 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1)
2526 )
2527 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
2528 assert res.next_page_token
2530 res = editor.ListDecidedEventCommunityInviteRequests(
2531 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1, page_token=res.next_page_token)
2532 )
2533 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
2534 assert not res.next_page_token
2537def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator):
2538 # Regression: users who already RSVP'd (or organize the event) must not get the
2539 # community invite notification when it is approved.
2540 organizer, organizer_token = generate_user()
2541 attendee, attendee_token = generate_user()
2542 member, _ = generate_user()
2543 superuser, superuser_token = generate_user(is_superuser=True)
2545 with session_scope() as session:
2546 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2547 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2548 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2549 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id
2551 enforce_community_memberships()
2553 with events_session(organizer_token) as api:
2554 event_id = api.CreateEvent(
2555 events_pb2.CreateEventReq(
2556 title="Dummy Title",
2557 content="Dummy content.",
2558 parent_community_id=c_id,
2559 location=events_pb2.EventLocation(
2560 address="Near Null Island",
2561 lat=0.1,
2562 lng=0.2,
2563 ),
2564 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2565 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2566 )
2567 ).event_id
2569 moderator.approve_event_occurrence(event_id)
2571 # the attendee RSVPs before the community invite is approved
2572 with events_session(attendee_token) as api:
2573 api.SetEventAttendance(
2574 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2575 )
2577 with events_session(organizer_token) as api:
2578 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2580 with real_editor_session(superuser_token) as editor:
2581 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2582 editor.DecideEventCommunityInviteRequest(
2583 editor_pb2.DecideEventCommunityInviteRequestReq(
2584 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2585 approve=True,
2586 )
2587 )
2589 process_jobs()
2591 with session_scope() as session:
2593 def invite_notification_count(user_id: int) -> int:
2594 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all()
2595 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved])
2597 # a plain community member gets the invite...
2598 assert invite_notification_count(member.id) == 1
2599 # ...but the attendee and the organizer don't
2600 assert invite_notification_count(attendee.id) == 0
2601 assert invite_notification_count(organizer.id) == 0
2604def test_update_event_should_notify_queues_job():
2605 user, token = generate_user()
2606 start = now()
2608 with session_scope() as session:
2609 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2611 # create an event
2612 with events_session(token) as api:
2613 create_res = api.CreateEvent(
2614 events_pb2.CreateEventReq(
2615 title="Dummy Title",
2616 content="Dummy content.",
2617 parent_community_id=c_id,
2618 location=events_pb2.EventLocation(
2619 address="Near Null Island",
2620 lat=1.0,
2621 lng=2.0,
2622 ),
2623 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2624 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2625 )
2626 )
2628 event_id = create_res.event_id
2630 # measure initial background job queue length
2631 with session_scope() as session:
2632 jobs = session.query(BackgroundJob).all()
2633 job_length_before_update = len(jobs)
2635 # update with should_notify=False, expect no change in background job queue
2636 api.UpdateEvent(
2637 events_pb2.UpdateEventReq(
2638 event_id=event_id,
2639 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2640 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2641 ),
2642 should_notify=False,
2643 )
2644 )
2646 with session_scope() as session:
2647 jobs = session.query(BackgroundJob).all()
2648 assert len(jobs) == job_length_before_update
2650 # update with should_notify=True, expect one new background job added
2651 api.UpdateEvent(
2652 events_pb2.UpdateEventReq(
2653 event_id=event_id,
2654 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2655 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2656 ),
2657 should_notify=True,
2658 )
2659 )
2661 with session_scope() as session:
2662 jobs = session.query(BackgroundJob).all()
2663 assert len(jobs) == job_length_before_update + 1
2666def test_event_photo_key(db):
2667 """Test that events return the photo_key field when a photo is set."""
2668 user, token = generate_user()
2670 start_time = now() + timedelta(hours=2)
2671 end_time = start_time + timedelta(hours=3)
2673 # Create a community and an upload for the event photo
2674 with session_scope() as session:
2675 create_community(session, 0, 2, "Community", [user], [], None)
2676 upload = Upload(
2677 key="test_event_photo_key_123",
2678 filename="test_event_photo_key_123.jpg",
2679 creator_user_id=user.id,
2680 )
2681 session.add(upload)
2683 with events_session(token) as api:
2684 # Create event without photo
2685 res = api.CreateEvent(
2686 events_pb2.CreateEventReq(
2687 title="Event Without Photo",
2688 content="No photo content.",
2689 photo_key=None,
2690 location=events_pb2.EventLocation(
2691 address="Near Null Island",
2692 lat=0.1,
2693 lng=0.2,
2694 ),
2695 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2696 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2697 )
2698 )
2700 assert res.photo_key == ""
2701 assert res.photo_url == ""
2703 # Create event with photo
2704 res_with_photo = api.CreateEvent(
2705 events_pb2.CreateEventReq(
2706 title="Event With Photo",
2707 content="Has photo content.",
2708 photo_key="test_event_photo_key_123",
2709 location=events_pb2.EventLocation(
2710 address="Near Null Island",
2711 lat=0.1,
2712 lng=0.2,
2713 ),
2714 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)),
2715 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)),
2716 )
2717 )
2719 assert res_with_photo.photo_key == "test_event_photo_key_123"
2720 assert "test_event_photo_key_123" in res_with_photo.photo_url
2722 event_id = res_with_photo.event_id
2724 # Verify photo_key is returned when getting the event
2725 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2726 assert get_res.photo_key == "test_event_photo_key_123"
2727 assert "test_event_photo_key_123" in get_res.photo_url
2730def test_event_timezone(db):
2731 user, token = generate_user()
2733 with session_scope() as session:
2734 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2736 # Midnight future day, UTC timezone
2737 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2738 end_time = start_time + timedelta(days=1)
2740 with events_session(token) as api:
2741 create_res: events_pb2.Event = api.CreateEvent(
2742 events_pb2.CreateEventReq(
2743 title="Dummy Title",
2744 content="Dummy content.",
2745 photo_key=None,
2746 parent_community_id=c_id,
2747 # timezone_areas.sql-fake has a region for Europe/Helsinki
2748 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831),
2749 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2750 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2751 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2752 )
2753 )
2755 # Backend should have deduced the helsinki timezone when creating the event,
2756 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2757 assert create_res.timezone == "Europe/Helsinki"
2758 assert to_aware_datetime(create_res.start_time).hour != 0
2759 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2761 # Now update its location such that it gets a new timezone
2762 update_res: events_pb2.Event = api.UpdateEvent(
2763 events_pb2.UpdateEventReq(
2764 event_id=create_res.event_id,
2765 # timezone_areas.sql-fake has a region for America/New_York
2766 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974),
2767 )
2768 )
2770 # The user didn't touch the datetime components on the frontend,
2771 # so they expect the event to be at the same local time (midnight),
2772 # but now in the New York timezone.
2773 assert update_res.timezone == "America/New_York"
2774 assert update_res.start_time != create_res.start_time
2775 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2776 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2778 # Also validate GetEvent
2779 get_res: events_pb2.Event = api.GetEvent(
2780 events_pb2.GetEventReq(
2781 event_id=create_res.event_id,
2782 )
2783 )
2785 assert get_res.timezone == update_res.timezone
2786 assert get_res.start_time == update_res.start_time
2789def test_event_created_with_shadowed_visibility(db):
2790 """Events start in SHADOWED state when created."""
2791 user, token = generate_user()
2793 with session_scope() as session:
2794 create_community(session, 0, 2, "Community", [user], [], None)
2796 start_time = now() + timedelta(hours=2)
2797 end_time = start_time + timedelta(hours=3)
2799 with events_session(token) as api:
2800 res = api.CreateEvent(
2801 events_pb2.CreateEventReq(
2802 title="Test UMS Event",
2803 content="UMS content.",
2804 location=events_pb2.EventLocation(
2805 address="Near Null Island",
2806 lat=0.1,
2807 lng=0.2,
2808 ),
2809 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2810 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2811 )
2812 )
2813 event_id = res.event_id
2815 with session_scope() as session:
2816 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2817 mod_state = session.execute(
2818 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2819 ).scalar_one()
2820 assert mod_state.visibility == ModerationVisibility.shadowed
2823def test_shadowed_event_visible_to_creator_only(db):
2824 """SHADOWED events are visible to the creator but not to other users."""
2825 user1, token1 = generate_user()
2826 user2, token2 = generate_user()
2828 with session_scope() as session:
2829 create_community(session, 0, 2, "Community", [user1], [], None)
2831 start_time = now() + timedelta(hours=2)
2832 end_time = start_time + timedelta(hours=3)
2834 with events_session(token1) as api:
2835 res = api.CreateEvent(
2836 events_pb2.CreateEventReq(
2837 title="Shadowed Event",
2838 content="Content.",
2839 location=events_pb2.EventLocation(
2840 address="Near Null Island",
2841 lat=0.1,
2842 lng=0.2,
2843 ),
2844 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2845 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2846 )
2847 )
2848 event_id = res.event_id
2850 # Creator can see it
2851 with events_session(token1) as api:
2852 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2853 assert res.title == "Shadowed Event"
2855 # Other user cannot
2856 with events_session(token2) as api:
2857 with pytest.raises(grpc.RpcError) as e:
2858 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2859 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2862def test_event_visible_after_approval(db, moderator: Moderator):
2863 """Events become visible to all users after moderation approval."""
2864 user1, token1 = generate_user()
2865 user2, token2 = generate_user()
2867 with session_scope() as session:
2868 create_community(session, 0, 2, "Community", [user1], [], None)
2870 start_time = now() + timedelta(hours=2)
2871 end_time = start_time + timedelta(hours=3)
2873 with events_session(token1) as api:
2874 res = api.CreateEvent(
2875 events_pb2.CreateEventReq(
2876 title="Approved Event",
2877 content="Content.",
2878 location=events_pb2.EventLocation(
2879 address="Near Null Island",
2880 lat=0.1,
2881 lng=0.2,
2882 ),
2883 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2884 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2885 )
2886 )
2887 event_id = res.event_id
2889 # Other user cannot see it yet
2890 with events_session(token2) as api:
2891 with pytest.raises(grpc.RpcError) as e:
2892 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2893 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2895 # Approve the event
2896 moderator.approve_event_occurrence(event_id)
2898 # Now other user can see it
2899 with events_session(token2) as api:
2900 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2901 assert res.title == "Approved Event"
2904def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2905 """SHADOWED events appear in lists for the creator but not for other users."""
2906 user1, token1 = generate_user()
2907 user2, token2 = generate_user()
2909 with session_scope() as session:
2910 create_community(session, 0, 2, "Community", [user1], [], None)
2912 start_time = now() + timedelta(hours=2)
2913 end_time = start_time + timedelta(hours=3)
2915 with events_session(token1) as api:
2916 res = api.CreateEvent(
2917 events_pb2.CreateEventReq(
2918 title="List Test Event",
2919 content="Content.",
2920 location=events_pb2.EventLocation(
2921 address="Near Null Island",
2922 lat=0.1,
2923 lng=0.2,
2924 ),
2925 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2926 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2927 )
2928 )
2929 event_id = res.event_id
2931 # Creator can see their own SHADOWED event in lists
2932 with events_session(token1) as api:
2933 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2934 event_ids = [e.event_id for e in list_res.events]
2935 assert event_id in event_ids
2937 # Other user cannot see the SHADOWED event in lists
2938 with events_session(token2) as api:
2939 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2940 event_ids = [e.event_id for e in list_res.events]
2941 assert event_id not in event_ids
2943 # After approval, other user can see it
2944 moderator.approve_event_occurrence(event_id)
2946 with events_session(token2) as api:
2947 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2948 event_ids = [e.event_id for e in list_res.events]
2949 assert event_id in event_ids
2952def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2953 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2954 user1, token1 = generate_user()
2955 user2, token2 = generate_user()
2957 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2958 with session_scope() as session:
2959 world = create_community(session, 0, 10, "World", [user1], [], None)
2960 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2961 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2962 create_community(session, 0, 2, "Child", [user2], [], region)
2964 start_time = now() + timedelta(hours=2)
2965 end_time = start_time + timedelta(hours=3)
2967 with events_session(token1) as api:
2968 res = api.CreateEvent(
2969 events_pb2.CreateEventReq(
2970 title="Deferred Event",
2971 content="Content.",
2972 location=events_pb2.EventLocation(
2973 address="Near Null Island",
2974 lat=0.1,
2975 lng=0.2,
2976 ),
2977 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2978 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2979 )
2980 )
2981 event_id = res.event_id
2983 # Process all jobs — notification should be deferred (event is SHADOWED)
2984 process_jobs()
2986 with session_scope() as session:
2987 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2988 # Notification was created with moderation_state_id for deferral
2989 assert notif.moderation_state_id is not None
2990 # No delivery exists (deferred because event is SHADOWED)
2991 delivery_count = session.execute(
2992 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2993 ).scalar_one_or_none()
2994 assert delivery_count is None
2996 # Approve the event — handle_notification is re-queued for deferred notifications
2997 moderator.approve_event_occurrence(event_id)
2999 # Verify handle_notification job was queued
3000 with session_scope() as session:
3001 pending_jobs = (
3002 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
3003 .scalars()
3004 .all()
3005 )
3006 assert any("handle_notification" in j.job_type for j in pending_jobs)
3009def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3010 """Event update notifications should carry the event's moderation_state_id for deferral."""
3011 user1, token1 = generate_user()
3012 user2, token2 = generate_user()
3014 with session_scope() as session:
3015 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3017 start_time = now() + timedelta(hours=2)
3018 end_time = start_time + timedelta(hours=3)
3020 with events_session(token1) as api:
3021 res = api.CreateEvent(
3022 events_pb2.CreateEventReq(
3023 title="Update Test",
3024 content="Content.",
3025 location=events_pb2.EventLocation(
3026 address="Near Null Island",
3027 lat=0.1,
3028 lng=0.2,
3029 ),
3030 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3031 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3032 )
3033 )
3034 event_id = res.event_id
3036 moderator.approve_event_occurrence(event_id)
3037 process_jobs()
3038 # Clear any create notifications
3039 while push_collector.count_for_user(user2.id): 3039 ↛ 3040line 3039 didn't jump to line 3040 because the condition on line 3039 was never true
3040 push_collector.pop_for_user(user2.id)
3042 # User2 subscribes to the event
3043 with events_session(token2) as api:
3044 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3046 # User1 updates the event with should_notify=True
3047 with events_session(token1) as api:
3048 api.UpdateEvent(
3049 events_pb2.UpdateEventReq(
3050 event_id=event_id,
3051 title=wrappers_pb2.StringValue(value="Updated Title"),
3052 should_notify=True,
3053 )
3054 )
3056 process_jobs()
3058 # Verify that the update notification for user2 has moderation_state_id set
3059 with session_scope() as session:
3060 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3062 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3063 # Find the update notification (most recent one)
3064 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
3065 assert len(update_notifs) == 1
3066 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
3069def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3070 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
3071 user1, token1 = generate_user()
3072 user2, token2 = generate_user()
3074 with session_scope() as session:
3075 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3077 start_time = now() + timedelta(hours=2)
3078 end_time = start_time + timedelta(hours=3)
3080 with events_session(token1) as api:
3081 res = api.CreateEvent(
3082 events_pb2.CreateEventReq(
3083 title="Cancel Test",
3084 content="Content.",
3085 location=events_pb2.EventLocation(
3086 address="Near Null Island",
3087 lat=0.1,
3088 lng=0.2,
3089 ),
3090 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3091 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3092 )
3093 )
3094 event_id = res.event_id
3096 moderator.approve_event_occurrence(event_id)
3097 process_jobs()
3098 while push_collector.count_for_user(user2.id): 3098 ↛ 3099line 3098 didn't jump to line 3099 because the condition on line 3098 was never true
3099 push_collector.pop_for_user(user2.id)
3101 # User2 subscribes
3102 with events_session(token2) as api:
3103 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3105 # User1 cancels the event
3106 with events_session(token1) as api:
3107 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3109 process_jobs()
3111 # Verify that the cancel notification for user2 has moderation_state_id set
3112 with session_scope() as session:
3113 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3115 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3116 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
3117 assert len(cancel_notifs) == 1
3118 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
3121def test_event_update_and_cancel_notifications_not_sent_to_actor(
3122 db, push_collector: PushCollector, moderator: Moderator
3123):
3124 """The user who updates or cancels an event shouldn't be notified about their own action."""
3125 organizer_user, organizer_token = generate_user()
3126 attendee_user, attendee_token = generate_user()
3128 with session_scope() as session:
3129 create_community(session, 0, 2, "Community", [attendee_user], [], None)
3131 start_time = now() + timedelta(hours=2)
3132 end_time = start_time + timedelta(hours=3)
3134 with events_session(organizer_token) as api:
3135 res = api.CreateEvent(
3136 events_pb2.CreateEventReq(
3137 title="Actor Test",
3138 content="Content.",
3139 location=events_pb2.EventLocation(
3140 address="Near Null Island",
3141 lat=0.1,
3142 lng=0.2,
3143 ),
3144 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3145 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3146 )
3147 )
3148 event_id = res.event_id
3150 moderator.approve_event_occurrence(event_id)
3151 process_jobs()
3153 # The attendee subscribes to notifications
3154 with events_session(attendee_token) as api:
3155 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3157 # The organizer updates and then cancels their own event
3158 with events_session(organizer_token) as api:
3159 api.UpdateEvent(
3160 events_pb2.UpdateEventReq(
3161 event_id=event_id,
3162 title=wrappers_pb2.StringValue(value="Updated Title"),
3163 should_notify=True,
3164 )
3165 )
3166 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3168 process_jobs()
3170 # The organizer should not receive any notifications
3171 assert push_collector.count_for_user(organizer_user.id) == 0
3173 # But the attendee should receive both the update and cancel notifications
3174 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__update.display
3175 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__cancel.display
3178def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3179 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
3180 user1, token1 = generate_user()
3181 user2, token2 = generate_user()
3183 with session_scope() as session:
3184 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3186 # Create event starting 23 hours from now (within 24h reminder window)
3187 start_time = now() + timedelta(hours=23)
3188 end_time = start_time + timedelta(hours=1)
3190 with events_session(token1) as api:
3191 res = api.CreateEvent(
3192 events_pb2.CreateEventReq(
3193 title="Reminder Test",
3194 content="Content.",
3195 location=events_pb2.EventLocation(
3196 address="Near Null Island",
3197 lat=0.1,
3198 lng=0.2,
3199 ),
3200 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3201 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3202 )
3203 )
3204 event_id = res.event_id
3206 moderator.approve_event_occurrence(event_id)
3207 process_jobs()
3208 while push_collector.count_for_user(user2.id): 3208 ↛ 3209line 3208 didn't jump to line 3209 because the condition on line 3208 was never true
3209 push_collector.pop_for_user(user2.id)
3211 # User2 marks attendance
3212 with events_session(token2) as api:
3213 api.SetEventAttendance(
3214 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3215 )
3217 # Run the event reminder handler
3218 send_event_reminders(empty_pb2.Empty())
3219 process_jobs()
3221 # Verify that the reminder notification for user2 has moderation_state_id set
3222 with session_scope() as session:
3223 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3225 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3226 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
3227 assert len(reminder_notifs) == 1
3228 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
3231def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
3232 """Event reminders should not be sent for cancelled events."""
3233 user1, token1 = generate_user()
3234 user2, token2 = generate_user()
3236 with session_scope() as session:
3237 create_community(session, 0, 2, "Community", [user2], [], None)
3239 # Create event starting 23 hours from now (within 24h reminder window)
3240 start_time = now() + timedelta(hours=23)
3241 end_time = start_time + timedelta(hours=1)
3243 with events_session(token1) as api:
3244 res = api.CreateEvent(
3245 events_pb2.CreateEventReq(
3246 title="Cancelled Reminder Test",
3247 content="Content.",
3248 location=events_pb2.EventLocation(
3249 address="Near Null Island",
3250 lat=0.1,
3251 lng=0.2,
3252 ),
3253 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3254 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3255 )
3256 )
3257 event_id = res.event_id
3259 moderator.approve_event_occurrence(event_id)
3260 process_jobs()
3262 # User2 marks attendance
3263 with events_session(token2) as api:
3264 api.SetEventAttendance(
3265 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3266 )
3268 # User1 cancels the event
3269 with events_session(token1) as api:
3270 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3272 process_jobs()
3273 # Drain any cancellation-related notifications so we can cleanly assert on reminders
3274 while push_collector.count_for_user(user2.id):
3275 push_collector.pop_for_user(user2.id)
3277 # Run the event reminder handler
3278 send_event_reminders(empty_pb2.Empty())
3279 process_jobs()
3281 # Verify that no reminder notification was sent for user2
3282 with session_scope() as session:
3283 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3284 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3285 assert len(reminder_notifs) == 0
3288@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
3289def test_event_reminder_not_sent_for_invisible_attendee(
3290 db, push_collector: PushCollector, moderator: Moderator, invisible_field
3291):
3292 user1, token1 = generate_user()
3293 user2, token2 = generate_user()
3295 with session_scope() as session:
3296 create_community(session, 0, 2, "Community", [user2], [], None)
3298 start_time = now() + timedelta(hours=23)
3299 end_time = start_time + timedelta(hours=1)
3301 with events_session(token1) as api:
3302 res = api.CreateEvent(
3303 events_pb2.CreateEventReq(
3304 title="Invisible Attendee Reminder Test",
3305 content="Content.",
3306 location=events_pb2.EventLocation(
3307 address="Near Null Island",
3308 lat=0.1,
3309 lng=0.2,
3310 ),
3311 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3312 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3313 )
3314 )
3315 event_id = res.event_id
3317 moderator.approve_event_occurrence(event_id)
3318 process_jobs()
3320 with events_session(token2) as api:
3321 api.SetEventAttendance(
3322 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3323 )
3325 with session_scope() as session:
3326 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3328 send_event_reminders(empty_pb2.Empty())
3329 process_jobs()
3331 with session_scope() as session:
3332 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3333 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3334 assert len(reminder_notifs) == 0
3337def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3338 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3339 user1, token1 = generate_user()
3340 user2, token2 = generate_user()
3342 with session_scope() as session:
3343 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3345 start = now()
3347 # User1 creates event A with 3 occurrences
3348 event_a_ids = []
3349 with events_session(token1) as api:
3350 res = api.CreateEvent(
3351 events_pb2.CreateEventReq(
3352 title="Event A",
3353 content="Content A.",
3354 parent_community_id=c_id,
3355 location=events_pb2.EventLocation(
3356 address="Near Null Island",
3357 lat=0.1,
3358 lng=0.2,
3359 ),
3360 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
3361 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
3362 )
3363 )
3364 event_a_ids.append(res.event_id)
3365 for i in range(2):
3366 res = api.ScheduleEvent(
3367 events_pb2.ScheduleEventReq(
3368 event_id=event_a_ids[-1],
3369 content=f"A occurrence {i}",
3370 location=events_pb2.EventLocation(
3371 address="Near Null Island",
3372 lat=0.1,
3373 lng=0.2,
3374 ),
3375 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
3376 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
3377 )
3378 )
3379 event_a_ids.append(res.event_id)
3381 # User2 creates event B with 2 occurrences
3382 event_b_ids = []
3383 with events_session(token2) as api:
3384 res = api.CreateEvent(
3385 events_pb2.CreateEventReq(
3386 title="Event B",
3387 content="Content B.",
3388 parent_community_id=c_id,
3389 location=events_pb2.EventLocation(
3390 address="Near Null Island",
3391 lat=0.1,
3392 lng=0.2,
3393 ),
3394 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)),
3395 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)),
3396 )
3397 )
3398 event_b_ids.append(res.event_id)
3399 res = api.ScheduleEvent(
3400 events_pb2.ScheduleEventReq(
3401 event_id=event_b_ids[-1],
3402 content="B occurrence 1",
3403 location=events_pb2.EventLocation(
3404 address="Near Null Island",
3405 lat=0.1,
3406 lng=0.2,
3407 ),
3408 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)),
3409 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)),
3410 )
3411 )
3412 event_b_ids.append(res.event_id)
3414 moderator.approve_event_occurrence(event_a_ids[0])
3415 moderator.approve_event_occurrence(event_b_ids[0])
3417 # List occurrences for event A — should only get event A's 3 occurrences
3418 with events_session(token1) as api:
3419 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3420 returned_ids = [e.event_id for e in res.events]
3421 assert sorted(returned_ids) == sorted(event_a_ids)
3423 # List occurrences for event B — should only get event B's 2 occurrences
3424 with events_session(token2) as api:
3425 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3426 returned_ids = [e.event_id for e in res.events]
3427 assert sorted(returned_ids) == sorted(event_b_ids)
3430def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3431 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3432 user1, token1 = generate_user()
3433 user2, token2 = generate_user()
3435 with session_scope() as session:
3436 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3438 start_time = now() + timedelta(hours=2)
3439 end_time = start_time + timedelta(hours=3)
3441 with events_session(token1) as api:
3442 res = api.CreateEvent(
3443 events_pb2.CreateEventReq(
3444 title="Comment Test",
3445 content="Content.",
3446 parent_community_id=c_id,
3447 location=events_pb2.EventLocation(
3448 address="Near Null Island",
3449 lat=0.1,
3450 lng=0.2,
3451 ),
3452 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3453 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3454 )
3455 )
3456 event_id = res.event_id
3457 thread_id = res.thread.thread_id
3459 moderator.approve_event_occurrence(event_id)
3460 process_jobs()
3461 while push_collector.count_for_user(user1.id): 3461 ↛ 3462line 3461 didn't jump to line 3462 because the condition on line 3461 was never true
3462 push_collector.pop_for_user(user1.id)
3464 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3465 with events_session(token1) as api:
3466 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3468 # User2 posts a top-level comment on the event thread
3469 with threads_session(token2) as api:
3470 comment_thread_id = api.PostReply(
3471 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3472 ).thread_id
3474 process_jobs()
3476 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3477 comment_db_id = comment_thread_id // 10
3478 with session_scope() as session:
3479 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3481 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3482 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3483 assert len(comment_notifs) == 1
3484 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3487def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3488 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3489 user1, token1 = generate_user()
3490 user2, token2 = generate_user()
3491 user3, token3 = generate_user()
3493 with session_scope() as session:
3494 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3496 start_time = now() + timedelta(hours=2)
3497 end_time = start_time + timedelta(hours=3)
3499 with events_session(token1) as api:
3500 res = api.CreateEvent(
3501 events_pb2.CreateEventReq(
3502 title="Reply Test",
3503 content="Content.",
3504 location=events_pb2.EventLocation(
3505 address="Near Null Island",
3506 lat=0.1,
3507 lng=0.2,
3508 ),
3509 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3510 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3511 )
3512 )
3513 event_id = res.event_id
3514 thread_id = res.thread.thread_id
3516 moderator.approve_event_occurrence(event_id)
3517 process_jobs()
3518 while push_collector.count_for_user(user1.id): 3518 ↛ 3519line 3518 didn't jump to line 3519 because the condition on line 3518 was never true
3519 push_collector.pop_for_user(user1.id)
3521 # User2 posts a top-level comment
3522 with threads_session(token2) as api:
3523 comment_thread_id = api.PostReply(
3524 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3525 ).thread_id
3527 process_jobs()
3528 while push_collector.count_for_user(user1.id): 3528 ↛ 3529line 3528 didn't jump to line 3529 because the condition on line 3528 was never true
3529 push_collector.pop_for_user(user1.id)
3531 # User3 replies to user2's comment (depth=2 reply)
3532 with threads_session(token3) as api:
3533 nested_reply_thread_id = api.PostReply(
3534 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3535 ).thread_id
3537 process_jobs()
3539 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3540 nested_reply_db_id = nested_reply_thread_id // 10
3541 with session_scope() as session:
3542 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3544 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3545 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3546 assert len(reply_notifs) == 1
3547 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id