Coverage for app/backend/src/tests/test_events.py: 99%
1641 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 12:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 12:25 +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.fixtures.timewarp import FrozenTimewarp
35from tests.test_communities import create_community, create_group
38@pytest.fixture(autouse=True)
39def _(testconfig):
40 pass
43def to_event_time_granularity(value: datetime) -> datetime:
44 """Events are scheduled at the minute granularity."""
45 return value.replace(second=0, microsecond=0)
48def is_utc_or_gmt(timezone: str) -> bool:
49 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT.
50 # Tests should be agnostic to which one we're using.
51 return timezone in ("Etc/UTC", "Etc/GMT")
54def test_CreateEvent(db, frozen_timewarp, push_collector: PushCollector, moderator: Moderator):
55 # test cases:
56 # can create event
57 # cannot create event with missing details
58 # can't create event that starts in the past
59 # can create in different timezones
61 # event creator
62 user1, token1 = generate_user()
63 # community moderator
64 user2, token2 = generate_user()
65 # third party
66 user3, token3 = generate_user()
68 with session_scope() as session:
69 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
71 time_before = now()
72 start_time = now() + timedelta(hours=2)
73 end_time = start_time + timedelta(hours=3)
75 # Can create an event
76 with events_session(token1) as api:
77 res = api.CreateEvent(
78 events_pb2.CreateEventReq(
79 title="Dummy Title",
80 content="Dummy content.",
81 photo_key=None,
82 location=events_pb2.EventLocation(
83 address="Near Null Island",
84 lat=0.1,
85 lng=0.2,
86 ),
87 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
88 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
89 )
90 )
92 assert res.is_next
93 assert res.title == "Dummy Title"
94 assert res.slug == "dummy-title"
95 assert res.content == "Dummy content."
96 assert not res.photo_url
97 assert res.HasField("location")
98 assert res.location.lat == 0.1
99 assert res.location.lng == 0.2
100 assert res.location.address == "Near Null Island"
101 assert time_before <= to_aware_datetime(res.created) <= now()
102 assert time_before <= to_aware_datetime(res.last_edited) <= now()
103 assert res.creator_user_id == user1.id
104 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
105 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
106 assert is_utc_or_gmt(res.timezone)
107 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
108 assert res.organizer
109 assert res.subscriber
110 assert res.going_count == 1
111 assert res.organizer_count == 1
112 assert res.subscriber_count == 1
113 assert res.owner_user_id == user1.id
114 assert not res.owner_community_id
115 assert not res.owner_group_id
116 assert res.thread.thread_id
117 assert res.can_edit
118 assert not res.can_moderate
120 event_id = res.event_id
122 # Approve the event so other users can see it
123 moderator.approve_event_occurrence(event_id)
125 with events_session(token2) as api:
126 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
128 assert res.is_next
129 assert res.title == "Dummy Title"
130 assert res.slug == "dummy-title"
131 assert res.content == "Dummy content."
132 assert not res.photo_url
133 assert res.HasField("location")
134 assert res.location.lat == 0.1
135 assert res.location.lng == 0.2
136 assert res.location.address == "Near Null Island"
137 assert time_before <= to_aware_datetime(res.created) <= now()
138 assert time_before <= to_aware_datetime(res.last_edited) <= now()
139 assert res.creator_user_id == user1.id
140 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
141 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
142 assert is_utc_or_gmt(res.timezone)
143 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
144 assert not res.organizer
145 assert not res.subscriber
146 assert res.going_count == 1
147 assert res.organizer_count == 1
148 assert res.subscriber_count == 1
149 assert res.owner_user_id == user1.id
150 assert not res.owner_community_id
151 assert not res.owner_group_id
152 assert res.thread.thread_id
153 assert res.can_edit
154 assert res.can_moderate
156 with events_session(token3) as api:
157 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
159 assert res.is_next
160 assert res.title == "Dummy Title"
161 assert res.slug == "dummy-title"
162 assert res.content == "Dummy content."
163 assert not res.photo_url
164 assert res.HasField("location")
165 assert res.location.lat == 0.1
166 assert res.location.lng == 0.2
167 assert res.location.address == "Near Null Island"
168 assert time_before <= to_aware_datetime(res.created) <= now()
169 assert time_before <= to_aware_datetime(res.last_edited) <= now()
170 assert res.creator_user_id == user1.id
171 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
172 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
173 assert is_utc_or_gmt(res.timezone)
174 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
175 assert not res.organizer
176 assert not res.subscriber
177 assert res.going_count == 1
178 assert res.organizer_count == 1
179 assert res.subscriber_count == 1
180 assert res.owner_user_id == user1.id
181 assert not res.owner_community_id
182 assert not res.owner_group_id
183 assert res.thread.thread_id
184 assert not res.can_edit
185 assert not res.can_moderate
187 # Failure cases
188 with events_session(token1) as api:
189 with pytest.raises(grpc.RpcError) as e:
190 api.CreateEvent(
191 events_pb2.CreateEventReq(
192 # title="Dummy Title",
193 content="Dummy content.",
194 photo_key=None,
195 location=events_pb2.EventLocation(
196 address="Near Null Island",
197 lat=0.1,
198 lng=0.2,
199 ),
200 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
201 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
202 )
203 )
204 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
205 assert e.value.details() == "Missing event title."
207 with pytest.raises(grpc.RpcError) as e:
208 api.CreateEvent(
209 events_pb2.CreateEventReq(
210 title="Dummy Title",
211 # content="Dummy content.",
212 photo_key=None,
213 location=events_pb2.EventLocation(
214 address="Near Null Island",
215 lat=0.1,
216 lng=0.2,
217 ),
218 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
219 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
220 )
221 )
222 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
223 assert e.value.details() == "Missing event content."
225 with pytest.raises(grpc.RpcError) as e:
226 api.CreateEvent(
227 events_pb2.CreateEventReq(
228 title="Dummy Title",
229 content="Dummy content.",
230 photo_key="nonexistent",
231 location=events_pb2.EventLocation(
232 address="Near Null Island",
233 lat=0.1,
234 lng=0.2,
235 ),
236 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
237 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
238 )
239 )
240 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
241 assert e.value.details() == "Photo not found."
243 with pytest.raises(grpc.RpcError) as e:
244 api.CreateEvent(
245 events_pb2.CreateEventReq(
246 title="Dummy Title",
247 content="Dummy content.",
248 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
249 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
250 )
251 )
252 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
253 assert e.value.details() == "Missing event address or location."
255 with pytest.raises(grpc.RpcError) as e:
256 api.CreateEvent(
257 events_pb2.CreateEventReq(
258 title="Dummy Title",
259 content="Dummy content.",
260 location=events_pb2.EventLocation(
261 address="Near Null Island",
262 ),
263 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
264 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
265 )
266 )
267 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
268 assert e.value.details() == "Invalid coordinate."
270 with pytest.raises(grpc.RpcError) as e:
271 api.CreateEvent(
272 events_pb2.CreateEventReq(
273 title="Dummy Title",
274 content="Dummy content.",
275 location=events_pb2.EventLocation(
276 lat=0.1,
277 lng=0.1,
278 ),
279 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
280 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
281 )
282 )
283 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
284 assert e.value.details() == "Missing event address or location."
286 with pytest.raises(grpc.RpcError) as e:
287 api.CreateEvent(
288 events_pb2.CreateEventReq(
289 title="Dummy Title",
290 content="Dummy content.",
291 location=events_pb2.EventLocation(
292 address="Near Null Island",
293 lat=0.1,
294 lng=0.2,
295 ),
296 start_datetime_iso8601_local=datetime_to_iso8601_local(now() - timedelta(hours=2)),
297 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
298 )
299 )
300 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
301 assert e.value.details() == "The event must be in the future."
303 with pytest.raises(grpc.RpcError) as e:
304 api.CreateEvent(
305 events_pb2.CreateEventReq(
306 title="Dummy Title",
307 content="Dummy content.",
308 location=events_pb2.EventLocation(
309 address="Near Null Island",
310 lat=0.1,
311 lng=0.2,
312 ),
313 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
314 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
315 )
316 )
317 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
318 assert e.value.details() == "The event must end after it starts."
320 with pytest.raises(grpc.RpcError) as e:
321 api.CreateEvent(
322 events_pb2.CreateEventReq(
323 title="Dummy Title",
324 content="Dummy content.",
325 location=events_pb2.EventLocation(
326 address="Near Null Island",
327 lat=0.1,
328 lng=0.2,
329 ),
330 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=2)),
331 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=5)),
332 )
333 )
334 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
335 assert e.value.details() == "The event needs to start within the next year."
337 with pytest.raises(grpc.RpcError) as e:
338 api.CreateEvent(
339 events_pb2.CreateEventReq(
340 title="Dummy Title",
341 content="Dummy content.",
342 location=events_pb2.EventLocation(
343 address="Near Null Island",
344 lat=0.1,
345 lng=0.2,
346 ),
347 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
348 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=100)),
349 )
350 )
351 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
352 assert e.value.details() == "Events cannot last longer than 7 days."
355def test_CreateEvent_incomplete_profile(db):
356 user1, token1 = generate_user(complete_profile=False)
357 user2, token2 = generate_user()
359 with session_scope() as session:
360 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
362 start_time = now() + timedelta(hours=2)
363 end_time = start_time + timedelta(hours=3)
365 with events_session(token1) as api:
366 with pytest.raises(grpc.RpcError) as e:
367 api.CreateEvent(
368 events_pb2.CreateEventReq(
369 title="Dummy Title",
370 content="Dummy content.",
371 photo_key=None,
372 location=events_pb2.EventLocation(
373 address="Near Null Island",
374 lat=0.1,
375 lng=0.2,
376 ),
377 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
378 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
379 )
380 )
381 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
382 assert e.value.details() == "You have to complete your profile before you can create an event."
385def test_ScheduleEvent(db, frozen_timewarp):
386 # test cases:
387 # can schedule a new event occurrence
389 user, token = generate_user()
391 with session_scope() as session:
392 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
394 time_before = now()
395 start_time = now() + timedelta(hours=2)
396 end_time = start_time + timedelta(hours=3)
398 with events_session(token) as api:
399 create_res = api.CreateEvent(
400 events_pb2.CreateEventReq(
401 title="Dummy Title",
402 content="Dummy content.",
403 parent_community_id=c_id,
404 location=events_pb2.EventLocation(
405 address="Near Null Island",
406 lat=0.1,
407 lng=0.2,
408 ),
409 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
410 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
411 )
412 )
414 new_start_time = now() + timedelta(hours=6)
415 new_end_time = new_start_time + timedelta(hours=2)
417 schedule_res = api.ScheduleEvent(
418 events_pb2.ScheduleEventReq(
419 event_id=create_res.event_id,
420 content="New event occurrence",
421 location=events_pb2.EventLocation(
422 address="A bit further but still near Null Island",
423 lat=0.3,
424 lng=0.2,
425 ),
426 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time),
427 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time),
428 )
429 )
431 # Each occurrence is independent and has an independent thread
432 assert schedule_res.event_id != create_res.event_id
433 assert schedule_res.thread.thread_id != create_res.thread.thread_id
435 res = api.GetEvent(events_pb2.GetEventReq(event_id=schedule_res.event_id))
437 assert not res.is_next
438 assert res.title == "Dummy Title"
439 assert res.slug == "dummy-title"
440 assert res.content == "New event occurrence"
441 assert not res.photo_url
442 assert res.HasField("location")
443 assert res.location.lat == 0.3
444 assert res.location.lng == 0.2
445 assert res.location.address == "A bit further but still near Null Island"
446 assert time_before <= to_aware_datetime(res.created) <= now()
447 assert time_before <= to_aware_datetime(res.last_edited) <= now()
448 assert res.creator_user_id == user.id
449 assert to_aware_datetime(res.start_time) == to_event_time_granularity(new_start_time)
450 assert to_aware_datetime(res.end_time) == to_event_time_granularity(new_end_time)
451 assert is_utc_or_gmt(res.timezone)
452 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
453 assert res.organizer
454 assert res.subscriber
455 assert res.going_count == 1
456 assert res.organizer_count == 1
457 assert res.subscriber_count == 1
458 assert res.owner_user_id == user.id
459 assert not res.owner_community_id
460 assert not res.owner_group_id
461 assert res.thread.thread_id
462 assert res.can_edit
463 assert res.can_moderate
466def test_cannot_overlap_occurrences_schedule(db):
467 user, token = generate_user()
469 with session_scope() as session:
470 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
472 start = now()
474 with events_session(token) as api:
475 res = api.CreateEvent(
476 events_pb2.CreateEventReq(
477 title="Dummy Title",
478 content="Dummy content.",
479 parent_community_id=c_id,
480 location=events_pb2.EventLocation(
481 address="Near Null Island",
482 lat=0.1,
483 lng=0.2,
484 ),
485 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
486 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
487 )
488 )
490 with pytest.raises(grpc.RpcError) as e:
491 api.ScheduleEvent(
492 events_pb2.ScheduleEventReq(
493 event_id=res.event_id,
494 content="New event occurrence",
495 location=events_pb2.EventLocation(
496 address="A bit further but still near Null Island",
497 lat=0.3,
498 lng=0.2,
499 ),
500 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
501 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
502 )
503 )
504 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
505 assert e.value.details() == "An event cannot have overlapping occurrences."
508def test_cannot_overlap_occurrences_update(db):
509 user, token = generate_user()
511 with session_scope() as session:
512 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
514 start = now()
516 with events_session(token) as api:
517 res = api.CreateEvent(
518 events_pb2.CreateEventReq(
519 title="Dummy Title",
520 content="Dummy content.",
521 parent_community_id=c_id,
522 location=events_pb2.EventLocation(
523 address="Near Null Island",
524 lat=0.1,
525 lng=0.2,
526 ),
527 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
528 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
529 )
530 )
532 event_id = api.ScheduleEvent(
533 events_pb2.ScheduleEventReq(
534 event_id=res.event_id,
535 content="New event occurrence",
536 location=events_pb2.EventLocation(
537 address="A bit further but still near Null Island",
538 lat=0.3,
539 lng=0.2,
540 ),
541 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
542 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
543 )
544 ).event_id
546 # can overlap with this current existing occurrence
547 api.UpdateEvent(
548 events_pb2.UpdateEventReq(
549 event_id=event_id,
550 start_datetime_iso8601_local=wrappers_pb2.StringValue(
551 value=datetime_to_iso8601_local(start + timedelta(hours=5))
552 ),
553 end_datetime_iso8601_local=wrappers_pb2.StringValue(
554 value=datetime_to_iso8601_local(start + timedelta(hours=6))
555 ),
556 )
557 )
559 with pytest.raises(grpc.RpcError) as e:
560 api.UpdateEvent(
561 events_pb2.UpdateEventReq(
562 event_id=event_id,
563 start_datetime_iso8601_local=wrappers_pb2.StringValue(
564 value=datetime_to_iso8601_local(start + timedelta(hours=2))
565 ),
566 end_datetime_iso8601_local=wrappers_pb2.StringValue(
567 value=datetime_to_iso8601_local(start + timedelta(hours=4))
568 ),
569 )
570 )
571 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
572 assert e.value.details() == "An event cannot have overlapping occurrences."
575def test_UpdateEvent_single(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator):
576 # test cases:
577 # owner can update
578 # community owner can update
579 # notifies attendees
581 # event creator
582 user1, token1 = generate_user()
583 # community moderator
584 user2, token2 = generate_user()
585 # third parties
586 user3, token3 = generate_user()
587 user4, token4 = generate_user()
588 user5, token5 = generate_user()
589 user6, token6 = generate_user()
591 with session_scope() as session:
592 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
594 time_before = now()
595 start_time = now() + timedelta(hours=2)
596 end_time = start_time + timedelta(hours=3)
598 with events_session(token1) as api:
599 res = api.CreateEvent(
600 events_pb2.CreateEventReq(
601 title="Dummy Title",
602 content="Dummy content.",
603 parent_community_id=c_id,
604 location=events_pb2.EventLocation(
605 address="Near Null Island",
606 lat=0.1,
607 lng=0.2,
608 ),
609 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
610 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
611 )
612 )
614 event_id = res.event_id
616 moderator.approve_event_occurrence(event_id)
618 with events_session(token4) as api:
619 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
621 with events_session(token5) as api:
622 api.SetEventAttendance(
623 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
624 )
626 with events_session(token6) as api:
627 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
629 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change
630 frozen_timewarp.advance(timedelta(minutes=1))
631 time_before_update = now()
633 with events_session(token1) as api:
634 res = api.UpdateEvent(
635 events_pb2.UpdateEventReq(
636 event_id=event_id,
637 )
638 )
640 with events_session(token1) as api:
641 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
643 assert res.is_next
644 assert res.title == "Dummy Title"
645 assert res.slug == "dummy-title"
646 assert res.content == "Dummy content."
647 assert not res.photo_url
648 assert res.HasField("location")
649 assert res.location.lat == 0.1
650 assert res.location.lng == 0.2
651 assert res.location.address == "Near Null Island"
652 assert time_before <= to_aware_datetime(res.created) <= time_before_update
653 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
654 assert res.creator_user_id == user1.id
655 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
656 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
657 assert is_utc_or_gmt(res.timezone)
658 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
659 assert res.organizer
660 assert res.subscriber
661 assert res.going_count == 2
662 assert res.organizer_count == 1
663 assert res.subscriber_count == 3
664 assert res.owner_user_id == user1.id
665 assert not res.owner_community_id
666 assert not res.owner_group_id
667 assert res.thread.thread_id
668 assert res.can_edit
669 assert not res.can_moderate
671 with events_session(token2) as api:
672 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
674 assert res.is_next
675 assert res.title == "Dummy Title"
676 assert res.slug == "dummy-title"
677 assert res.content == "Dummy content."
678 assert not res.photo_url
679 assert res.HasField("location")
680 assert res.location.lat == 0.1
681 assert res.location.lng == 0.2
682 assert res.location.address == "Near Null Island"
683 assert time_before <= to_aware_datetime(res.created) <= time_before_update
684 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
685 assert res.creator_user_id == user1.id
686 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
687 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
688 assert is_utc_or_gmt(res.timezone)
689 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
690 assert not res.organizer
691 assert not res.subscriber
692 assert res.going_count == 2
693 assert res.organizer_count == 1
694 assert res.subscriber_count == 3
695 assert res.owner_user_id == user1.id
696 assert not res.owner_community_id
697 assert not res.owner_group_id
698 assert res.thread.thread_id
699 assert res.can_edit
700 assert res.can_moderate
702 with events_session(token3) as api:
703 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
705 assert res.is_next
706 assert res.title == "Dummy Title"
707 assert res.slug == "dummy-title"
708 assert res.content == "Dummy content."
709 assert not res.photo_url
710 assert res.HasField("location")
711 assert res.location.lat == 0.1
712 assert res.location.lng == 0.2
713 assert res.location.address == "Near Null Island"
714 assert time_before <= to_aware_datetime(res.created) <= time_before_update
715 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
716 assert res.creator_user_id == user1.id
717 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
718 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
719 assert is_utc_or_gmt(res.timezone)
720 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
721 assert not res.organizer
722 assert not res.subscriber
723 assert res.going_count == 2
724 assert res.organizer_count == 1
725 assert res.subscriber_count == 3
726 assert res.owner_user_id == user1.id
727 assert not res.owner_community_id
728 assert not res.owner_group_id
729 assert res.thread.thread_id
730 assert not res.can_edit
731 assert not res.can_moderate
733 with events_session(token1) as api:
734 res = api.UpdateEvent(
735 events_pb2.UpdateEventReq(
736 event_id=event_id,
737 location=events_pb2.EventLocation(
738 address="Nearer Null Island",
739 lat=0.01,
740 lng=0.02,
741 ),
742 )
743 )
745 with events_session(token3) as api:
746 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
748 assert res.HasField("location")
749 assert res.location.address == "Nearer Null Island"
750 assert res.location.lat == 0.01
751 assert res.location.lng == 0.02
754def test_UpdateEvent_all(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator):
755 # event creator
756 user1, token1 = generate_user()
757 # community moderator
758 user2, token2 = generate_user()
759 # third parties
760 user3, token3 = generate_user()
761 user4, token4 = generate_user()
762 user5, token5 = generate_user()
763 user6, token6 = generate_user()
765 with session_scope() as session:
766 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
768 time_before = now()
769 start_time = now() + timedelta(hours=1)
770 end_time = start_time + timedelta(hours=1.5)
772 event_ids = []
774 with events_session(token1) as api:
775 res = api.CreateEvent(
776 events_pb2.CreateEventReq(
777 title="Dummy Title",
778 content="0th occurrence",
779 location=events_pb2.EventLocation(
780 address="Near Null Island",
781 lat=0.1,
782 lng=0.2,
783 ),
784 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
785 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
786 )
787 )
789 event_id = res.event_id
790 event_ids.append(event_id)
792 moderator.approve_event_occurrence(event_id)
794 with events_session(token4) as api:
795 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
797 with events_session(token5) as api:
798 api.SetEventAttendance(
799 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
800 )
802 with events_session(token6) as api:
803 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
805 with events_session(token1) as api:
806 for i in range(5):
807 res = api.ScheduleEvent(
808 events_pb2.ScheduleEventReq(
809 event_id=event_ids[-1],
810 content=f"{i + 1}th occurrence",
811 location=events_pb2.EventLocation(
812 address="Near Null Island",
813 lat=0.1,
814 lng=0.2,
815 ),
816 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2 + i)),
817 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2.5 + i)),
818 )
819 )
821 event_ids.append(res.event_id)
823 # Approve all scheduled occurrences
824 for eid in event_ids[1:]:
825 moderator.approve_event_occurrence(eid)
827 updated_event_id = event_ids[3]
829 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change
830 frozen_timewarp.advance(timedelta(minutes=1))
831 time_before_update = now()
833 with events_session(token1) as api:
834 res = api.UpdateEvent(
835 events_pb2.UpdateEventReq(
836 event_id=updated_event_id,
837 title=wrappers_pb2.StringValue(value="New Title"),
838 content=wrappers_pb2.StringValue(value="New content."),
839 location=events_pb2.EventLocation(
840 address="Not so near Null Island",
841 lat=0.2,
842 lng=0.2,
843 ),
844 update_all_future=True,
845 )
846 )
848 time_after_update = now()
850 with events_session(token2) as api:
851 for i in range(3):
852 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
853 assert res.content == f"{i}th occurrence"
854 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
856 for i in range(3, 6):
857 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
858 assert res.content == "New content."
859 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
862def test_UpdateEvent_all_leaves_other_events_alone(db, moderator: Moderator):
863 """update_all_future must only touch the occurrences of the event being edited."""
864 # creator of the event that gets edited
865 user1, token1 = generate_user()
866 # creator of an unrelated event in the same time window
867 user2, token2 = generate_user()
868 # community moderator, so that neither creator has edit rights on the other's event
869 user3, token3 = generate_user()
871 with session_scope() as session:
872 create_community(session, 0, 2, "Community", [user3], [], None)
874 start_time = now() + timedelta(hours=1)
876 with events_session(token1) as api:
877 edited_id = api.CreateEvent(
878 events_pb2.CreateEventReq(
879 title="Edited Event",
880 content="0th occurrence",
881 location=events_pb2.EventLocation(
882 address="Near Null Island",
883 lat=0.1,
884 lng=0.2,
885 ),
886 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
887 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=1)),
888 )
889 ).event_id
891 second_id = api.ScheduleEvent(
892 events_pb2.ScheduleEventReq(
893 event_id=edited_id,
894 content="1th occurrence",
895 location=events_pb2.EventLocation(
896 address="Near Null Island",
897 lat=0.1,
898 lng=0.2,
899 ),
900 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2)),
901 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=3)),
902 )
903 ).event_id
905 # an occurrence of a different event, starting after the edited one and ending well after the cutoff
906 with events_session(token2) as api:
907 other_id = api.CreateEvent(
908 events_pb2.CreateEventReq(
909 title="Other Event",
910 content="Other content.",
911 location=events_pb2.EventLocation(
912 address="Somewhere else entirely",
913 lat=0.5,
914 lng=0.5,
915 ),
916 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=4)),
917 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=5)),
918 )
919 ).event_id
921 for occurrence_id in (edited_id, second_id, other_id):
922 moderator.approve_event_occurrence(occurrence_id)
924 with events_session(token2) as api:
925 other_before = api.GetEvent(events_pb2.GetEventReq(event_id=other_id))
927 with events_session(token1) as api:
928 api.UpdateEvent(
929 events_pb2.UpdateEventReq(
930 event_id=edited_id,
931 content=wrappers_pb2.StringValue(value="New content."),
932 location=events_pb2.EventLocation(
933 address="Not so near Null Island",
934 lat=0.2,
935 lng=0.2,
936 ),
937 update_all_future=True,
938 )
939 )
941 with events_session(token3) as api:
942 for occurrence_id in (edited_id, second_id):
943 res = api.GetEvent(events_pb2.GetEventReq(event_id=occurrence_id))
944 assert res.content == "New content."
945 assert res.location.address == "Not so near Null Island"
947 res = api.GetEvent(events_pb2.GetEventReq(event_id=other_id))
948 assert res.content == "Other content."
949 assert res.location.address == "Somewhere else entirely"
950 assert res.location.lat == 0.5
951 assert res.location.lng == 0.5
952 assert res.timezone == other_before.timezone
953 assert res.last_edited == other_before.last_edited
956def test_UpdateEvent_all_cant_change_times(db, moderator: Moderator):
957 """Every future occurrence would get the same times, which the exclusion constraint forbids."""
958 user1, token1 = generate_user()
959 user2, token2 = generate_user()
961 with session_scope() as session:
962 create_community(session, 0, 2, "Community", [user2], [], None)
964 start_time = now() + timedelta(hours=1)
966 with events_session(token1) as api:
967 event_id = api.CreateEvent(
968 events_pb2.CreateEventReq(
969 title="Dummy Title",
970 content="0th occurrence",
971 location=events_pb2.EventLocation(
972 address="Near Null Island",
973 lat=0.1,
974 lng=0.2,
975 ),
976 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
977 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=1)),
978 )
979 ).event_id
981 api.ScheduleEvent(
982 events_pb2.ScheduleEventReq(
983 event_id=event_id,
984 content="1th occurrence",
985 location=events_pb2.EventLocation(
986 address="Near Null Island",
987 lat=0.1,
988 lng=0.2,
989 ),
990 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2)),
991 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=3)),
992 )
993 )
995 with pytest.raises(grpc.RpcError) as e:
996 api.UpdateEvent(
997 events_pb2.UpdateEventReq(
998 event_id=event_id,
999 start_datetime_iso8601_local=wrappers_pb2.StringValue(
1000 value=datetime_to_iso8601_local(start_time + timedelta(minutes=30))
1001 ),
1002 end_datetime_iso8601_local=wrappers_pb2.StringValue(
1003 value=datetime_to_iso8601_local(start_time + timedelta(hours=1, minutes=30))
1004 ),
1005 update_all_future=True,
1006 )
1007 )
1008 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1009 assert e.value.details() == "You cannot update all events if you're modifying start or end times."
1012def test_GetEvent(db, frozen_timewarp, moderator: Moderator):
1013 # event creator
1014 user1, token1 = generate_user()
1015 # community moderator
1016 user2, token2 = generate_user()
1017 # third parties
1018 user3, token3 = generate_user()
1019 user4, token4 = generate_user()
1020 user5, token5 = generate_user()
1021 user6, token6 = generate_user()
1023 with session_scope() as session:
1024 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1026 time_before = now()
1027 start_time = now() + timedelta(hours=2)
1028 end_time = start_time + timedelta(hours=3)
1030 with events_session(token1) as api:
1031 # in person event
1032 res = api.CreateEvent(
1033 events_pb2.CreateEventReq(
1034 title="Dummy Title",
1035 content="Dummy content.",
1036 location=events_pb2.EventLocation(
1037 address="Near Null Island",
1038 lat=0.1,
1039 lng=0.2,
1040 ),
1041 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1042 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1043 )
1044 )
1046 event_id = res.event_id
1048 moderator.approve_event_occurrence(event_id)
1050 with events_session(token4) as api:
1051 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1053 with events_session(token5) as api:
1054 api.SetEventAttendance(
1055 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1056 )
1058 with events_session(token6) as api:
1059 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1061 with events_session(token1) as api:
1062 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1064 assert res.is_next
1065 assert res.title == "Dummy Title"
1066 assert res.slug == "dummy-title"
1067 assert res.content == "Dummy content."
1068 assert not res.photo_url
1069 assert res.HasField("location")
1070 assert res.location.lat == 0.1
1071 assert res.location.lng == 0.2
1072 assert res.location.address == "Near Null Island"
1073 assert time_before <= to_aware_datetime(res.created) <= now()
1074 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1075 assert res.creator_user_id == user1.id
1076 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
1077 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
1078 assert is_utc_or_gmt(res.timezone)
1079 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
1080 assert res.organizer
1081 assert res.subscriber
1082 assert res.going_count == 2
1083 assert res.organizer_count == 1
1084 assert res.subscriber_count == 3
1085 assert res.owner_user_id == user1.id
1086 assert not res.owner_community_id
1087 assert not res.owner_group_id
1088 assert res.thread.thread_id
1089 assert res.can_edit
1090 assert not res.can_moderate
1092 with events_session(token2) as api:
1093 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1095 assert res.is_next
1096 assert res.title == "Dummy Title"
1097 assert res.slug == "dummy-title"
1098 assert res.content == "Dummy content."
1099 assert not res.photo_url
1100 assert res.HasField("location")
1101 assert res.location.lat == 0.1
1102 assert res.location.lng == 0.2
1103 assert res.location.address == "Near Null Island"
1104 assert time_before <= to_aware_datetime(res.created) <= now()
1105 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1106 assert res.creator_user_id == user1.id
1107 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
1108 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
1109 assert is_utc_or_gmt(res.timezone)
1110 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1111 assert not res.organizer
1112 assert not res.subscriber
1113 assert res.going_count == 2
1114 assert res.organizer_count == 1
1115 assert res.subscriber_count == 3
1116 assert res.owner_user_id == user1.id
1117 assert not res.owner_community_id
1118 assert not res.owner_group_id
1119 assert res.thread.thread_id
1120 assert res.can_edit
1121 assert res.can_moderate
1123 with events_session(token3) as api:
1124 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1126 assert res.is_next
1127 assert res.title == "Dummy Title"
1128 assert res.slug == "dummy-title"
1129 assert res.content == "Dummy content."
1130 assert not res.photo_url
1131 assert res.HasField("location")
1132 assert res.location.lat == 0.1
1133 assert res.location.lng == 0.2
1134 assert res.location.address == "Near Null Island"
1135 assert time_before <= to_aware_datetime(res.created) <= now()
1136 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1137 assert res.creator_user_id == user1.id
1138 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
1139 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
1140 assert is_utc_or_gmt(res.timezone)
1141 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1142 assert not res.organizer
1143 assert not res.subscriber
1144 assert res.going_count == 2
1145 assert res.organizer_count == 1
1146 assert res.subscriber_count == 3
1147 assert res.owner_user_id == user1.id
1148 assert not res.owner_community_id
1149 assert not res.owner_group_id
1150 assert res.thread.thread_id
1151 assert not res.can_edit
1152 assert not res.can_moderate
1155def test_CancelEvent(db, moderator: Moderator):
1156 # event creator
1157 user1, token1 = generate_user()
1158 # community moderator
1159 user2, token2 = generate_user()
1160 # third parties
1161 user3, token3 = generate_user()
1162 user4, token4 = generate_user()
1163 user5, token5 = generate_user()
1164 user6, token6 = generate_user()
1166 with session_scope() as session:
1167 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1169 start_time = now() + timedelta(hours=2)
1170 end_time = start_time + timedelta(hours=3)
1172 with events_session(token1) as api:
1173 res = api.CreateEvent(
1174 events_pb2.CreateEventReq(
1175 title="Dummy Title",
1176 content="Dummy content.",
1177 location=events_pb2.EventLocation(
1178 address="Near Null Island",
1179 lat=0.1,
1180 lng=0.2,
1181 ),
1182 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1183 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1184 )
1185 )
1187 event_id = res.event_id
1189 moderator.approve_event_occurrence(event_id)
1191 with events_session(token4) as api:
1192 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1194 with events_session(token5) as api:
1195 api.SetEventAttendance(
1196 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1197 )
1199 with events_session(token6) as api:
1200 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1202 with events_session(token1) as api:
1203 res = api.CancelEvent(
1204 events_pb2.CancelEventReq(
1205 event_id=event_id,
1206 )
1207 )
1209 with events_session(token1) as api:
1210 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1211 assert res.is_cancelled
1213 with events_session(token1) as api:
1214 with pytest.raises(grpc.RpcError) as e:
1215 api.UpdateEvent(
1216 events_pb2.UpdateEventReq(
1217 event_id=event_id,
1218 title=wrappers_pb2.StringValue(value="New Title"),
1219 )
1220 )
1221 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1222 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1224 with pytest.raises(grpc.RpcError) as e:
1225 api.InviteEventOrganizer(
1226 events_pb2.InviteEventOrganizerReq(
1227 event_id=event_id,
1228 user_id=user3.id,
1229 )
1230 )
1231 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1232 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1234 with pytest.raises(grpc.RpcError) as e:
1235 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1236 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1237 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1239 with events_session(token3) as api:
1240 with pytest.raises(grpc.RpcError) as e:
1241 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1242 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1243 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1245 with pytest.raises(grpc.RpcError) as e:
1246 api.SetEventAttendance(
1247 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1248 )
1249 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1250 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1252 with events_session(token1) as api:
1253 for include_cancelled in [True, False]:
1254 res = api.ListEventOccurrences(
1255 events_pb2.ListEventOccurrencesReq(
1256 event_id=event_id,
1257 include_cancelled=include_cancelled,
1258 )
1259 )
1260 if include_cancelled:
1261 assert len(res.events) > 0
1262 else:
1263 assert len(res.events) == 0
1265 res = api.ListMyEvents(
1266 events_pb2.ListMyEventsReq(
1267 include_cancelled=include_cancelled,
1268 )
1269 )
1270 if include_cancelled:
1271 assert len(res.events) > 0
1272 else:
1273 assert len(res.events) == 0
1276def test_ListEventAttendees(db, moderator: Moderator):
1277 # event creator
1278 user1, token1 = generate_user()
1279 # others
1280 user2, token2 = generate_user()
1281 user3, token3 = generate_user()
1282 user4, token4 = generate_user()
1283 user5, token5 = generate_user()
1284 user6, token6 = generate_user()
1286 with session_scope() as session:
1287 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1289 with events_session(token1) as api:
1290 event_id = api.CreateEvent(
1291 events_pb2.CreateEventReq(
1292 title="Dummy Title",
1293 content="Dummy content.",
1294 location=events_pb2.EventLocation(
1295 address="Near Null Island",
1296 lat=0.1,
1297 lng=0.2,
1298 ),
1299 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1300 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1301 )
1302 ).event_id
1304 moderator.approve_event_occurrence(event_id)
1306 for token in [token2, token3, token4, token5]:
1307 with events_session(token) as api:
1308 api.SetEventAttendance(
1309 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1310 )
1312 with events_session(token6) as api:
1313 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1315 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1316 assert res.attendee_user_ids == [user1.id, user2.id]
1318 res = api.ListEventAttendees(
1319 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1320 )
1321 assert res.attendee_user_ids == [user3.id, user4.id]
1323 res = api.ListEventAttendees(
1324 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1325 )
1326 assert res.attendee_user_ids == [user5.id]
1327 assert not res.next_page_token
1330def test_ListEventSubscribers(db, moderator: Moderator):
1331 # event creator
1332 user1, token1 = generate_user()
1333 # others
1334 user2, token2 = generate_user()
1335 user3, token3 = generate_user()
1336 user4, token4 = generate_user()
1337 user5, token5 = generate_user()
1338 user6, token6 = generate_user()
1340 with session_scope() as session:
1341 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1343 with events_session(token1) as api:
1344 event_id = api.CreateEvent(
1345 events_pb2.CreateEventReq(
1346 title="Dummy Title",
1347 content="Dummy content.",
1348 location=events_pb2.EventLocation(
1349 address="Near Null Island",
1350 lat=0.1,
1351 lng=0.2,
1352 ),
1353 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1354 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1355 )
1356 ).event_id
1358 moderator.approve_event_occurrence(event_id)
1360 for token in [token2, token3, token4, token5]:
1361 with events_session(token) as api:
1362 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1364 with events_session(token6) as api:
1365 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1367 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1368 assert res.subscriber_user_ids == [user1.id, user2.id]
1370 res = api.ListEventSubscribers(
1371 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1372 )
1373 assert res.subscriber_user_ids == [user3.id, user4.id]
1375 res = api.ListEventSubscribers(
1376 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1377 )
1378 assert res.subscriber_user_ids == [user5.id]
1379 assert not res.next_page_token
1382def test_ListEventOrganizers(db, moderator: Moderator):
1383 # event creator
1384 user1, token1 = generate_user()
1385 # others
1386 user2, token2 = generate_user()
1387 user3, token3 = generate_user()
1388 user4, token4 = generate_user()
1389 user5, token5 = generate_user()
1390 user6, token6 = generate_user()
1392 with session_scope() as session:
1393 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1395 with events_session(token1) as api:
1396 event_id = api.CreateEvent(
1397 events_pb2.CreateEventReq(
1398 title="Dummy Title",
1399 content="Dummy content.",
1400 location=events_pb2.EventLocation(
1401 address="Near Null Island",
1402 lat=0.1,
1403 lng=0.2,
1404 ),
1405 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1406 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1407 )
1408 ).event_id
1410 moderator.approve_event_occurrence(event_id)
1412 with events_session(token1) as api:
1413 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1414 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1416 with events_session(token6) as api:
1417 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1419 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1420 assert res.organizer_user_ids == [user1.id, user2.id]
1422 res = api.ListEventOrganizers(
1423 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1424 )
1425 assert res.organizer_user_ids == [user3.id, user4.id]
1427 res = api.ListEventOrganizers(
1428 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1429 )
1430 assert res.organizer_user_ids == [user5.id]
1431 assert not res.next_page_token
1434def test_TransferEvent(db):
1435 user1, token1 = generate_user()
1436 user2, token2 = generate_user()
1437 user3, token3 = generate_user()
1438 user4, token4 = generate_user()
1440 with session_scope() as session:
1441 c = create_community(session, 0, 2, "Community", [user3], [], None)
1442 h = create_group(session, "Group", [user4], [], c)
1443 c_id = c.id
1444 h_id = h.id
1446 with events_session(token1) as api:
1447 event_id = api.CreateEvent(
1448 events_pb2.CreateEventReq(
1449 title="Dummy Title",
1450 content="Dummy content.",
1451 location=events_pb2.EventLocation(
1452 address="Near Null Island",
1453 lat=0.1,
1454 lng=0.2,
1455 ),
1456 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1457 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1458 )
1459 ).event_id
1461 api.TransferEvent(
1462 events_pb2.TransferEventReq(
1463 event_id=event_id,
1464 new_owner_community_id=c_id,
1465 )
1466 )
1468 # remove ourselves as organizer, otherwise we can still edit it
1469 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1471 with pytest.raises(grpc.RpcError) as e:
1472 api.TransferEvent(
1473 events_pb2.TransferEventReq(
1474 event_id=event_id,
1475 new_owner_group_id=h_id,
1476 )
1477 )
1478 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1479 assert e.value.details() == "You're not allowed to transfer that event."
1481 event_id = api.CreateEvent(
1482 events_pb2.CreateEventReq(
1483 title="Dummy Title",
1484 content="Dummy content.",
1485 location=events_pb2.EventLocation(
1486 address="Near Null Island",
1487 lat=0.1,
1488 lng=0.2,
1489 ),
1490 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1491 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1492 )
1493 ).event_id
1495 api.TransferEvent(
1496 events_pb2.TransferEventReq(
1497 event_id=event_id,
1498 new_owner_group_id=h_id,
1499 )
1500 )
1502 # remove ourselves as organizer, otherwise we can still edit it
1503 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1505 with pytest.raises(grpc.RpcError) as e:
1506 api.TransferEvent(
1507 events_pb2.TransferEventReq(
1508 event_id=event_id,
1509 new_owner_community_id=c_id,
1510 )
1511 )
1512 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1513 assert e.value.details() == "You're not allowed to transfer that event."
1516def test_SetEventSubscription(db, moderator: Moderator):
1517 user1, token1 = generate_user()
1518 user2, token2 = generate_user()
1520 with session_scope() as session:
1521 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1523 with events_session(token1) as api:
1524 event_id = api.CreateEvent(
1525 events_pb2.CreateEventReq(
1526 title="Dummy Title",
1527 content="Dummy content.",
1528 location=events_pb2.EventLocation(
1529 address="Near Null Island",
1530 lat=0.1,
1531 lng=0.2,
1532 ),
1533 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1534 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1535 )
1536 ).event_id
1538 moderator.approve_event_occurrence(event_id)
1540 with events_session(token2) as api:
1541 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1542 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1543 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1544 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1545 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1548def test_SetEventAttendance(db, moderator: Moderator):
1549 user1, token1 = generate_user()
1550 user2, token2 = generate_user()
1552 with session_scope() as session:
1553 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1555 with events_session(token1) as api:
1556 event_id = api.CreateEvent(
1557 events_pb2.CreateEventReq(
1558 title="Dummy Title",
1559 content="Dummy content.",
1560 location=events_pb2.EventLocation(
1561 address="Near Null Island",
1562 lat=0.1,
1563 lng=0.2,
1564 ),
1565 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1566 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1567 )
1568 ).event_id
1570 moderator.approve_event_occurrence(event_id)
1572 with events_session(token2) as api:
1573 assert (
1574 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1575 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1576 )
1577 api.SetEventAttendance(
1578 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1579 )
1580 assert (
1581 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1582 == events_pb2.ATTENDANCE_STATE_GOING
1583 )
1584 api.SetEventAttendance(
1585 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1586 )
1587 assert (
1588 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1589 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1590 )
1593def test_InviteEventOrganizer(db, moderator: Moderator):
1594 user1, token1 = generate_user()
1595 user2, token2 = generate_user()
1597 with session_scope() as session:
1598 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1600 with events_session(token1) as api:
1601 event_id = api.CreateEvent(
1602 events_pb2.CreateEventReq(
1603 title="Dummy Title",
1604 content="Dummy content.",
1605 location=events_pb2.EventLocation(
1606 address="Near Null Island",
1607 lat=0.1,
1608 lng=0.2,
1609 ),
1610 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1611 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1612 )
1613 ).event_id
1615 moderator.approve_event_occurrence(event_id)
1617 with events_session(token2) as api:
1618 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1620 with pytest.raises(grpc.RpcError) as e:
1621 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1622 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1623 assert e.value.details() == "You're not allowed to edit that event."
1625 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1627 with events_session(token1) as api:
1628 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1630 with events_session(token2) as api:
1631 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1634def test_ListEventOccurrences(db):
1635 user1, token1 = generate_user()
1636 user2, token2 = generate_user()
1637 user3, token3 = generate_user()
1639 with session_scope() as session:
1640 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1642 start = now()
1644 event_ids = []
1646 with events_session(token1) as api:
1647 res = api.CreateEvent(
1648 events_pb2.CreateEventReq(
1649 title="First occurrence",
1650 content="Dummy content.",
1651 parent_community_id=c_id,
1652 location=events_pb2.EventLocation(
1653 address="Near Null Island",
1654 lat=0.1,
1655 lng=0.2,
1656 ),
1657 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1658 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
1659 )
1660 )
1662 event_ids.append(res.event_id)
1664 for i in range(5):
1665 res = api.ScheduleEvent(
1666 events_pb2.ScheduleEventReq(
1667 event_id=event_ids[-1],
1668 content=f"{i}th occurrence",
1669 location=events_pb2.EventLocation(
1670 address="Near Null Island",
1671 lat=0.1,
1672 lng=0.2,
1673 ),
1674 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
1675 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
1676 )
1677 )
1679 event_ids.append(res.event_id)
1681 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1682 assert [event.event_id for event in res.events] == event_ids[:2]
1684 res = api.ListEventOccurrences(
1685 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1686 )
1687 assert [event.event_id for event in res.events] == event_ids[2:4]
1689 res = api.ListEventOccurrences(
1690 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1691 )
1692 assert [event.event_id for event in res.events] == event_ids[4:6]
1693 assert not res.next_page_token
1696def test_ListMyEvents(db, moderator: Moderator):
1697 user1, token1 = generate_user()
1698 user2, token2 = generate_user()
1699 user3, token3 = generate_user()
1700 user4, token4 = generate_user()
1701 user5, token5 = generate_user()
1703 with session_scope() as session:
1704 # Create global (world) -> macroregion -> region -> subregion hierarchy
1705 # my_communities_exclude_global filters out world, macroregion, and region level communities
1706 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1707 c_id = global_community.id
1708 macroregion_community = create_community(
1709 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1710 )
1711 region_community = create_community(
1712 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1713 )
1714 subregion_community = create_community(
1715 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1716 )
1717 c2_id = subregion_community.id
1719 start = now()
1721 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1722 return events_pb2.CreateEventReq(
1723 title="Dummy Title",
1724 content="Dummy content.",
1725 location=events_pb2.EventLocation(
1726 address="Near Null Island",
1727 lat=0.1,
1728 lng=0.2,
1729 ),
1730 parent_community_id=community_id,
1731 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now)),
1732 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now + 0.5)),
1733 )
1735 with events_session(token1) as api:
1736 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1738 moderator.approve_event_occurrence(e2)
1740 with events_session(token2) as api:
1741 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1743 moderator.approve_event_occurrence(e1)
1745 with events_session(token1) as api:
1746 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1748 moderator.approve_event_occurrence(e3)
1750 with events_session(token2) as api:
1751 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1753 moderator.approve_event_occurrence(e5)
1755 with events_session(token3) as api:
1756 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1758 moderator.approve_event_occurrence(e4)
1760 with events_session(token4) as api:
1761 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1763 moderator.approve_event_occurrence(e6)
1765 with events_session(token1) as api:
1766 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1768 with events_session(token1) as api:
1769 api.SetEventAttendance(
1770 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1771 )
1772 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1774 with events_session(token2) as api:
1775 api.SetEventAttendance(
1776 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1777 )
1779 with events_session(token3) as api:
1780 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1782 with events_session(token1) as api:
1783 # test pagination with token first
1784 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1785 assert [event.event_id for event in res.events] == [e1, e2]
1786 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1787 assert [event.event_id for event in res.events] == [e3, e4]
1788 assert not res.next_page_token
1790 res = api.ListMyEvents(
1791 events_pb2.ListMyEventsReq(
1792 subscribed=True,
1793 attending=True,
1794 organizing=True,
1795 )
1796 )
1797 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1799 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1800 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1802 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1803 assert [event.event_id for event in res.events] == [e2, e3, e4]
1805 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1806 assert [event.event_id for event in res.events] == [e1, e2, e3]
1808 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1809 assert [event.event_id for event in res.events] == [e2, e3]
1811 with events_session(token1) as api:
1812 # Test pagination with page_number and verify total_items
1813 res = api.ListMyEvents(
1814 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1815 )
1816 assert [event.event_id for event in res.events] == [e1, e2]
1817 assert res.total_items == 4
1819 res = api.ListMyEvents(
1820 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1821 )
1822 assert [event.event_id for event in res.events] == [e3, e4]
1823 assert res.total_items == 4
1825 # Verify no more pages
1826 res = api.ListMyEvents(
1827 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1828 )
1829 assert not res.events
1830 assert res.total_items == 4
1832 with events_session(token2) as api:
1833 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1834 assert [event.event_id for event in res.events] == [e1, e3, e5]
1836 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1837 assert [event.event_id for event in res.events] == [e1, e5]
1839 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1840 assert [event.event_id for event in res.events] == [e1, e3, e5]
1842 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1843 assert [event.event_id for event in res.events] == [e1, e5]
1845 with events_session(token3) as api:
1846 # user3 is member of both global (c_id) and child (c2_id) communities
1847 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1848 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1850 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1851 assert [event.event_id for event in res.events] == [e2, e4]
1853 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1854 assert [event.event_id for event in res.events] == [e4]
1856 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1857 assert [event.event_id for event in res.events] == [e3, e4]
1859 # my_communities returns events from both communities user3 is a member of
1860 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1861 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1863 # my_communities_exclude_global filters out events from global community (node_id=1)
1864 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1865 assert [event.event_id for event in res.events] == [e6]
1867 # my_communities_exclude_global works independently of my_communities flag
1868 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1869 assert [event.event_id for event in res.events] == [e6]
1871 # my_communities_exclude_global filters organizing results too
1872 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1873 assert [event.event_id for event in res.events] == []
1875 # my_communities_exclude_global filters subscribed results too
1876 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1877 assert [event.event_id for event in res.events] == []
1879 with events_session(token5) as api:
1880 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1881 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1884def _paginate_my_events(api, page_size: int) -> list[int]:
1885 event_ids = []
1886 page_token = ""
1887 for _ in range(10): 1887 ↛ 1893line 1887 didn't jump to line 1893 because the loop on line 1887 didn't complete
1888 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=page_size, page_token=page_token))
1889 event_ids += [event.event_id for event in res.events]
1890 page_token = res.next_page_token
1891 if not page_token:
1892 return event_ids
1893 raise AssertionError("pagination did not terminate")
1896def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator):
1897 user, token = generate_user()
1899 with session_scope() as session:
1900 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1902 start = now()
1904 def new_event(start_offset: timedelta, duration: timedelta) -> events_pb2.CreateEventReq:
1905 return events_pb2.CreateEventReq(
1906 title="Dummy Title",
1907 content="Dummy content.",
1908 location=events_pb2.EventLocation(
1909 address="Near Null Island",
1910 lat=0.1,
1911 lng=0.2,
1912 ),
1913 parent_community_id=c_id,
1914 start_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset),
1915 end_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset + duration),
1916 )
1918 with events_session(token) as api:
1919 # a multi-day event overlapping all the short events below: it ends last but starts first,
1920 # so an end time based cursor would repeat it on every page and skip the short events
1921 long_event = api.CreateEvent(new_event(timedelta(hours=1), timedelta(days=3))).event_id
1922 short_events = [
1923 api.CreateEvent(new_event(timedelta(hours=2 + i), timedelta(hours=1))).event_id for i in range(4)
1924 ]
1926 for event_id in [long_event, *short_events]:
1927 moderator.approve_event_occurrence(event_id)
1929 with events_session(token) as api:
1930 assert _paginate_my_events(api, page_size=2) == [long_event, *short_events]
1933def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator):
1934 user, token = generate_user()
1936 with session_scope() as session:
1937 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1939 start = now()
1941 with events_session(token) as api:
1942 event_ids = [
1943 api.CreateEvent(
1944 events_pb2.CreateEventReq(
1945 title="Dummy Title",
1946 content="Dummy content.",
1947 location=events_pb2.EventLocation(
1948 address="Near Null Island",
1949 lat=0.1,
1950 lng=0.2,
1951 ),
1952 parent_community_id=c_id,
1953 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1954 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
1955 )
1956 ).event_id
1957 for _ in range(5)
1958 ]
1960 for event_id in event_ids:
1961 moderator.approve_event_occurrence(event_id)
1963 with events_session(token) as api:
1964 assert _paginate_my_events(api, page_size=2) == event_ids
1967def test_list_my_events_exclude_attending(db, moderator: Moderator):
1968 user1, token1 = generate_user()
1969 user2, token2 = generate_user()
1971 with session_scope() as session:
1972 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1973 c_id = c.id
1975 start = now()
1977 def make_event(hours):
1978 return events_pb2.CreateEventReq(
1979 title="Test Event",
1980 content="Test content.",
1981 location=events_pb2.EventLocation(
1982 address="Near Null Island",
1983 lat=0.1,
1984 lng=0.2,
1985 ),
1986 parent_community_id=c_id,
1987 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours)),
1988 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours + 1)),
1989 )
1991 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1992 with events_session(token1) as api:
1993 e_own = api.CreateEvent(make_event(1)).event_id
1995 with events_session(token2) as api:
1996 e_attending = api.CreateEvent(make_event(2)).event_id
1997 e_community_only = api.CreateEvent(make_event(3)).event_id
1998 # e_both: user1 will be both organizer and attendee
1999 e_both = api.CreateEvent(make_event(4)).event_id
2001 moderator.approve_event_occurrence(e_own)
2002 moderator.approve_event_occurrence(e_attending)
2003 moderator.approve_event_occurrence(e_community_only)
2004 moderator.approve_event_occurrence(e_both)
2006 # invite user1 as organizer of e_both
2007 with events_session(token2) as api:
2008 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
2010 # user1 RSVPs to e_attending and e_both
2011 with events_session(token1) as api:
2012 api.SetEventAttendance(
2013 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2014 )
2015 api.SetEventAttendance(
2016 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2017 )
2019 with events_session(token1) as api:
2020 # baseline: all four community events visible
2021 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
2022 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
2024 # exclude_attending removes events user1 is attending (e_attending, e_both)
2025 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
2026 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
2027 assert [e.event_id for e in res.events] == [e_community_only]
2029 # exclude_attending with attending=True: invalid combination
2030 with pytest.raises(grpc.RpcError) as e:
2031 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
2032 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2034 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
2035 with events_session(token2) as api:
2036 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
2037 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
2038 assert [e.event_id for e in res.events] == [e_own]
2041def test_RemoveEventOrganizer(db, moderator: Moderator):
2042 user1, token1 = generate_user()
2043 user2, token2 = generate_user()
2045 with session_scope() as session:
2046 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2048 with events_session(token1) as api:
2049 event_id = api.CreateEvent(
2050 events_pb2.CreateEventReq(
2051 title="Dummy Title",
2052 content="Dummy content.",
2053 location=events_pb2.EventLocation(
2054 address="Near Null Island",
2055 lat=0.1,
2056 lng=0.2,
2057 ),
2058 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2059 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2060 )
2061 ).event_id
2063 moderator.approve_event_occurrence(event_id)
2065 with events_session(token2) as api:
2066 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2068 with pytest.raises(grpc.RpcError) as e:
2069 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2070 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2071 assert e.value.details() == "You're not allowed to edit that event."
2073 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2075 with events_session(token1) as api:
2076 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2078 with pytest.raises(grpc.RpcError) as e:
2079 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2080 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2081 assert e.value.details() == "You cannot remove the event owner as an organizer."
2083 with events_session(token2) as api:
2084 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2085 assert res.organizer
2086 assert res.organizer_count == 2
2087 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2088 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2090 with pytest.raises(grpc.RpcError) as e:
2091 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2092 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2093 assert e.value.details() == "You're not allowed to edit that event."
2095 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2096 assert not res.organizer
2097 assert res.organizer_count == 1
2099 # Test that event owner can remove co-organizers
2100 with events_session(token1) as api:
2101 # Add user2 back as organizer
2102 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2104 # Verify user2 is now an organizer
2105 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2106 assert res.organizer_count == 2
2108 # Event owner can remove co-organizer
2109 api.RemoveEventOrganizer(
2110 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
2111 )
2113 # Verify user2 is no longer an organizer
2114 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2115 assert res.organizer_count == 1
2117 # Test that non-organizers cannot remove other organizers
2118 with events_session(token2) as api:
2119 # User2 cannot invite themselves as organizer (not the owner)
2120 with pytest.raises(grpc.RpcError) as e:
2121 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2122 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2123 assert e.value.details() == "You're not allowed to edit that event."
2125 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
2126 with events_session(token1) as api:
2127 # Add user2 back as organizer
2128 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2131def test_ListEventAttendees_regression(db):
2132 # see issue #1617:
2133 #
2134 # 1. Create an event
2135 # 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`
2136 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
2137 # 4. Change the current user's attendance state to "going" again
2138 #
2139 # **Expected behaviour**
2140 # `ListEventAttendees` should return the current user's ID
2141 #
2142 # **Actual/current behaviour**
2143 # `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
2145 user1, token1 = generate_user()
2146 user2, token2 = generate_user()
2147 user3, token3 = generate_user()
2148 user4, token4 = generate_user()
2149 user5, token5 = generate_user()
2151 with session_scope() as session:
2152 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2154 start_time = now() + timedelta(hours=2)
2155 end_time = start_time + timedelta(hours=3)
2157 with events_session(token1) as api:
2158 res = api.CreateEvent(
2159 events_pb2.CreateEventReq(
2160 title="Dummy Title",
2161 content="Dummy content.",
2162 location=events_pb2.EventLocation(
2163 address="Near Null Island",
2164 lat=0.1,
2165 lng=0.2,
2166 ),
2167 parent_community_id=c_id,
2168 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2169 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2170 )
2171 )
2173 res = api.TransferEvent(
2174 events_pb2.TransferEventReq(
2175 event_id=res.event_id,
2176 new_owner_community_id=c_id,
2177 )
2178 )
2180 event_id = res.event_id
2182 api.SetEventAttendance(
2183 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2184 )
2185 api.SetEventAttendance(
2186 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2187 )
2189 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2190 assert len(res.attendee_user_ids) == 1
2191 assert res.attendee_user_ids[0] == user1.id
2194def test_GetEventCalendarFile(db, moderator: Moderator):
2195 user1, token1 = generate_user()
2196 user2, token2 = generate_user()
2198 with session_scope() as session:
2199 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2201 start_time = now() + timedelta(hours=2)
2202 end_time = start_time + timedelta(hours=3)
2204 with events_session(token1) as api:
2205 created_event: events_pb2.Event = api.CreateEvent(
2206 events_pb2.CreateEventReq(
2207 title="Dummy Title",
2208 content="Dummy content.",
2209 parent_community_id=c_id,
2210 location=events_pb2.EventLocation(
2211 address="Near Null Island",
2212 lat=0.1,
2213 lng=0.2,
2214 ),
2215 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2216 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2217 )
2218 )
2219 event_id = created_event.event_id
2221 moderator.approve_event_occurrence(event_id)
2223 with events_session(token1) as api:
2224 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
2225 assert file_res.content_type == "text/calendar"
2226 ics_string = file_res.data.decode("utf-8")
2227 assert "SUMMARY:Dummy Title" in ics_string
2228 assert "DESCRIPTION:Dummy content." in ics_string
2229 assert "LOCATION:Near Null Island" in ics_string
2230 assert "STATUS:CANCELLED" not in ics_string
2231 pre_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
2232 assert pre_cancel_match is not None
2233 pre_cancel_sequence = int(pre_cancel_match.group(1))
2235 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2237 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
2238 ics_string = file_res.data.decode("utf-8")
2239 assert "SUMMARY:Cancelled: Dummy Title" in ics_string
2240 assert "STATUS:CANCELLED" in ics_string
2241 post_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
2242 assert post_cancel_match is not None
2243 post_cancel_sequence = int(post_cancel_match.group(1))
2244 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal.
2245 assert post_cancel_sequence >= pre_cancel_sequence
2248def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2249 user1, token1 = generate_user()
2250 user2, token2 = generate_user()
2251 user3, token3 = generate_user()
2252 user4, token4 = generate_user()
2254 with session_scope() as session:
2255 c = create_community(session, 0, 2, "Community", [user3], [], None)
2256 h = create_group(session, "Group", [user4], [], c)
2257 c_id = c.id
2258 h_id = h.id
2259 user4_id = user4.id
2261 with events_session(token1) as api:
2262 event = api.CreateEvent(
2263 events_pb2.CreateEventReq(
2264 title="Dummy Title",
2265 content="Dummy content.",
2266 location=events_pb2.EventLocation(
2267 address="Near Null Island",
2268 lat=0.1,
2269 lng=0.2,
2270 ),
2271 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2272 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2273 )
2274 )
2276 moderator.approve_event_occurrence(event.event_id)
2278 with threads_session(token2) as api:
2279 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2281 moderator.approve_thread_post(reply_id)
2283 with events_session(token3) as api:
2284 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2285 assert res.thread.num_responses == 1
2287 with threads_session(token3) as api:
2288 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2289 assert len(ret.replies) == 1
2290 assert not ret.next_page_token
2291 assert ret.replies[0].thread_id == reply_id
2292 assert ret.replies[0].content == "hi"
2293 assert ret.replies[0].author_user_id == user2.id
2294 assert ret.replies[0].num_replies == 0
2296 nested_reply_id = api.PostReply(
2297 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2298 ).thread_id
2300 moderator.approve_thread_post(nested_reply_id)
2302 process_jobs()
2304 push = push_collector.pop_for_user(user1.id, last=True)
2305 assert push.topic_action == NotificationTopicAction.event__comment.display
2306 assert push.content.title == f"{user2.name} • Dummy Title"
2307 assert push.content.ios_title == user2.name
2308 assert push.content.ios_subtitle == "Commented on Dummy Title"
2309 assert push.content.body == "hi"
2311 push = push_collector.pop_for_user(user2.id, last=True)
2312 assert push.content.title == f"{user3.name} • Dummy Title"
2314 assert push_collector.count_for_user(user4_id) == 0
2317def test_can_overlap_other_events_schedule_regression(db):
2318 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2319 user, token = generate_user()
2321 with session_scope() as session:
2322 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2324 start = now()
2326 with events_session(token) as api:
2327 # create another event, should be able to overlap with this one
2328 api.CreateEvent(
2329 events_pb2.CreateEventReq(
2330 title="Dummy Title",
2331 content="Dummy content.",
2332 parent_community_id=c_id,
2333 location=events_pb2.EventLocation(
2334 address="Near Null Island",
2335 lat=0.1,
2336 lng=0.2,
2337 ),
2338 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2339 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)),
2340 )
2341 )
2343 # this event
2344 res = api.CreateEvent(
2345 events_pb2.CreateEventReq(
2346 title="Dummy Title",
2347 content="Dummy content.",
2348 parent_community_id=c_id,
2349 location=events_pb2.EventLocation(
2350 address="Near Null Island",
2351 lat=0.1,
2352 lng=0.2,
2353 ),
2354 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2355 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
2356 )
2357 )
2359 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2360 api.ScheduleEvent(
2361 events_pb2.ScheduleEventReq(
2362 event_id=res.event_id,
2363 content="New event occurrence",
2364 location=events_pb2.EventLocation(
2365 address="A bit further but still near Null Island",
2366 lat=0.3,
2367 lng=0.2,
2368 ),
2369 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2370 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2371 )
2372 )
2375def test_can_overlap_other_events_update_regression(db):
2376 user, token = generate_user()
2378 with session_scope() as session:
2379 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2381 start = now()
2383 with events_session(token) as api:
2384 # create another event, should be able to overlap with this one
2385 api.CreateEvent(
2386 events_pb2.CreateEventReq(
2387 title="Dummy Title",
2388 content="Dummy content.",
2389 parent_community_id=c_id,
2390 location=events_pb2.EventLocation(
2391 address="Near Null Island",
2392 lat=0.1,
2393 lng=0.2,
2394 ),
2395 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2396 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2397 )
2398 )
2400 res = api.CreateEvent(
2401 events_pb2.CreateEventReq(
2402 title="Dummy Title",
2403 content="Dummy content.",
2404 parent_community_id=c_id,
2405 location=events_pb2.EventLocation(
2406 address="Near Null Island",
2407 lat=0.1,
2408 lng=0.2,
2409 ),
2410 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)),
2411 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)),
2412 )
2413 )
2415 event_id = api.ScheduleEvent(
2416 events_pb2.ScheduleEventReq(
2417 event_id=res.event_id,
2418 content="New event occurrence",
2419 location=events_pb2.EventLocation(
2420 address="A bit further but still near Null Island",
2421 lat=0.3,
2422 lng=0.2,
2423 ),
2424 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2425 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2426 )
2427 ).event_id
2429 # can overlap with this current existing occurrence
2430 api.UpdateEvent(
2431 events_pb2.UpdateEventReq(
2432 event_id=event_id,
2433 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2434 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2435 ),
2436 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2437 value=datetime_to_iso8601_local(start + timedelta(hours=6))
2438 ),
2439 )
2440 )
2442 api.UpdateEvent(
2443 events_pb2.UpdateEventReq(
2444 event_id=event_id,
2445 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2446 value=datetime_to_iso8601_local(start + timedelta(hours=2))
2447 ),
2448 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2449 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2450 ),
2451 )
2452 )
2455def test_list_past_events_regression(db):
2456 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2457 user, token = generate_user()
2459 with session_scope() as session:
2460 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2462 start = now()
2464 with events_session(token) as api:
2465 api.CreateEvent(
2466 events_pb2.CreateEventReq(
2467 title="Dummy Title",
2468 content="Dummy content.",
2469 parent_community_id=c_id,
2470 location=events_pb2.EventLocation(
2471 address="Near Null Island",
2472 lat=0.1,
2473 lng=0.2,
2474 ),
2475 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2476 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2477 )
2478 )
2480 with session_scope() as session:
2481 session.execute(
2482 update(EventOccurrence).values(
2483 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2484 )
2485 )
2487 with events_session(token) as api:
2488 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2489 assert len(res.events) == 1
2492def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2493 user1, token1 = generate_user(complete_profile=True)
2494 user2, token2 = generate_user()
2495 user3, token3 = generate_user()
2496 user4, token4 = generate_user()
2497 user5, token5 = generate_user(is_superuser=True)
2499 with session_scope() as session:
2500 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2501 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2502 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2503 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2505 enforce_community_memberships()
2507 with events_session(token1) as api:
2508 res = api.CreateEvent(
2509 events_pb2.CreateEventReq(
2510 title="Dummy Title",
2511 content="Dummy content.",
2512 parent_community_id=c_id,
2513 location=events_pb2.EventLocation(
2514 address="Near Null Island",
2515 lat=0.1,
2516 lng=0.2,
2517 ),
2518 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2519 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2520 )
2521 )
2522 user_url = f"http://localhost:3000/user/{user1.username}"
2523 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2525 event_id = res.event_id
2527 moderator.approve_event_occurrence(event_id)
2529 with events_session(token1) as api:
2530 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2532 email = email_collector.pop_for_mods(last=True)
2534 assert user_url in email.plain
2535 assert event_url in email.plain
2537 # can't send another req
2538 with pytest.raises(grpc.RpcError) as err:
2539 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2540 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2541 assert err.value.details() == "You have already requested a community invite for this event."
2543 # another user can send one though
2544 with events_session(token3) as api:
2545 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2547 # but not a non-admin
2548 with events_session(token2) as api:
2549 with pytest.raises(grpc.RpcError) as err:
2550 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2551 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2552 assert err.value.details() == "You're not allowed to edit that event."
2554 with real_editor_session(token5) as editor:
2555 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2556 assert len(res.requests) == 2
2557 assert res.requests[0].user_id == user1.id
2558 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain)
2559 assert res.requests[0].approx_users_to_notify == 2
2560 assert res.requests[1].user_id == user3.id
2561 assert res.requests[1].approx_users_to_notify == 2
2563 editor.DecideEventCommunityInviteRequest(
2564 editor_pb2.DecideEventCommunityInviteRequestReq(
2565 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2566 approve=False,
2567 )
2568 )
2570 editor.DecideEventCommunityInviteRequest(
2571 editor_pb2.DecideEventCommunityInviteRequestReq(
2572 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2573 approve=True,
2574 )
2575 )
2577 # not after approve
2578 with events_session(token4) as api:
2579 with pytest.raises(grpc.RpcError) as err:
2580 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2581 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2582 assert err.value.details() == "A community invite has already been sent out for this event."
2585def test_list_decided_community_invite_requests(db, moderator: Moderator):
2586 user1, token1 = generate_user()
2587 user2, token2 = generate_user()
2588 user3, token3 = generate_user()
2589 superuser, superuser_token = generate_user(is_superuser=True)
2591 with session_scope() as session:
2592 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2593 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2594 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2595 c_id = create_community(session, 0, 2, "Community", [user1, user2, user3], [], r).id
2597 enforce_community_memberships()
2599 def create_event_and_request_invite(token: str, title: str) -> str:
2600 with events_session(token) as api:
2601 res = api.CreateEvent(
2602 events_pb2.CreateEventReq(
2603 title=title,
2604 content="Dummy content.",
2605 parent_community_id=c_id,
2606 location=events_pb2.EventLocation(
2607 address="Near Null Island",
2608 lat=0.1,
2609 lng=0.2,
2610 ),
2611 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2612 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2613 )
2614 )
2615 moderator.approve_event_occurrence(res.event_id)
2616 with events_session(token) as api:
2617 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=res.event_id))
2618 return f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2620 event1_url = create_event_and_request_invite(token1, "Approved Event")
2621 event2_url = create_event_and_request_invite(token2, "Declined Event")
2622 # this one stays pending
2623 create_event_and_request_invite(token3, "Pending Event")
2625 with real_editor_session(superuser_token) as editor:
2626 pending = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2627 assert len(pending.requests) == 3
2628 by_user = {req.user_id: req.event_community_invite_request_id for req in pending.requests}
2630 # nothing decided yet
2631 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
2632 assert len(res.requests) == 0
2633 assert not res.next_page_token
2635 editor.DecideEventCommunityInviteRequest(
2636 editor_pb2.DecideEventCommunityInviteRequestReq(
2637 event_community_invite_request_id=by_user[user1.id],
2638 approve=True,
2639 )
2640 )
2641 editor.DecideEventCommunityInviteRequest(
2642 editor_pb2.DecideEventCommunityInviteRequestReq(
2643 event_community_invite_request_id=by_user[user2.id],
2644 approve=False,
2645 )
2646 )
2648 # the pending one is not returned, and the most recently decided comes first
2649 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
2650 assert len(res.requests) == 2
2651 assert not res.next_page_token
2653 declined, approved = res.requests
2655 assert declined.event_community_invite_request_id == by_user[user2.id]
2656 assert declined.user_id == user2.id
2657 assert declined.event_url == event2_url
2658 assert declined.community_id == c_id
2659 assert declined.decided_by_user_id == superuser.id
2660 assert not declined.approved
2661 assert declined.created.ToDatetime() <= declined.decided.ToDatetime()
2663 assert approved.event_community_invite_request_id == by_user[user1.id]
2664 assert approved.user_id == user1.id
2665 assert approved.event_url == event1_url
2666 assert approved.community_id == c_id
2667 assert approved.decided_by_user_id == superuser.id
2668 assert approved.approved
2669 assert approved.decided.ToDatetime() <= declined.decided.ToDatetime()
2671 # filtering
2672 res = editor.ListDecidedEventCommunityInviteRequests(
2673 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=True))
2674 )
2675 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
2677 res = editor.ListDecidedEventCommunityInviteRequests(
2678 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=False))
2679 )
2680 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
2682 # pagination
2683 res = editor.ListDecidedEventCommunityInviteRequests(
2684 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1)
2685 )
2686 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
2687 assert res.next_page_token
2689 res = editor.ListDecidedEventCommunityInviteRequests(
2690 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1, page_token=res.next_page_token)
2691 )
2692 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
2693 assert not res.next_page_token
2696def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator):
2697 # Regression: users who already RSVP'd (or organize the event) must not get the
2698 # community invite notification when it is approved.
2699 organizer, organizer_token = generate_user()
2700 attendee, attendee_token = generate_user()
2701 member, _ = generate_user()
2702 superuser, superuser_token = generate_user(is_superuser=True)
2704 with session_scope() as session:
2705 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2706 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2707 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2708 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id
2710 enforce_community_memberships()
2712 with events_session(organizer_token) as api:
2713 event_id = api.CreateEvent(
2714 events_pb2.CreateEventReq(
2715 title="Dummy Title",
2716 content="Dummy content.",
2717 parent_community_id=c_id,
2718 location=events_pb2.EventLocation(
2719 address="Near Null Island",
2720 lat=0.1,
2721 lng=0.2,
2722 ),
2723 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2724 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2725 )
2726 ).event_id
2728 moderator.approve_event_occurrence(event_id)
2730 # the attendee RSVPs before the community invite is approved
2731 with events_session(attendee_token) as api:
2732 api.SetEventAttendance(
2733 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2734 )
2736 with events_session(organizer_token) as api:
2737 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2739 with real_editor_session(superuser_token) as editor:
2740 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2741 editor.DecideEventCommunityInviteRequest(
2742 editor_pb2.DecideEventCommunityInviteRequestReq(
2743 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2744 approve=True,
2745 )
2746 )
2748 process_jobs()
2750 with session_scope() as session:
2752 def invite_notification_count(user_id: int) -> int:
2753 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all()
2754 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved])
2756 # a plain community member gets the invite...
2757 assert invite_notification_count(member.id) == 1
2758 # ...but the attendee and the organizer don't
2759 assert invite_notification_count(attendee.id) == 0
2760 assert invite_notification_count(organizer.id) == 0
2763def test_update_event_should_notify_queues_job():
2764 user, token = generate_user()
2765 start = now()
2767 with session_scope() as session:
2768 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2770 # create an event
2771 with events_session(token) as api:
2772 create_res = api.CreateEvent(
2773 events_pb2.CreateEventReq(
2774 title="Dummy Title",
2775 content="Dummy content.",
2776 parent_community_id=c_id,
2777 location=events_pb2.EventLocation(
2778 address="Near Null Island",
2779 lat=1.0,
2780 lng=2.0,
2781 ),
2782 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2783 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2784 )
2785 )
2787 event_id = create_res.event_id
2789 # measure initial background job queue length
2790 with session_scope() as session:
2791 jobs = session.query(BackgroundJob).all()
2792 job_length_before_update = len(jobs)
2794 # update with should_notify=False, expect no change in background job queue
2795 api.UpdateEvent(
2796 events_pb2.UpdateEventReq(
2797 event_id=event_id,
2798 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2799 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2800 ),
2801 should_notify=False,
2802 )
2803 )
2805 with session_scope() as session:
2806 jobs = session.query(BackgroundJob).all()
2807 assert len(jobs) == job_length_before_update
2809 # update with should_notify=True, expect one new background job added
2810 api.UpdateEvent(
2811 events_pb2.UpdateEventReq(
2812 event_id=event_id,
2813 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2814 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2815 ),
2816 should_notify=True,
2817 )
2818 )
2820 with session_scope() as session:
2821 jobs = session.query(BackgroundJob).all()
2822 assert len(jobs) == job_length_before_update + 1
2825def test_event_photo_key(db):
2826 """Test that events return the photo_key field when a photo is set."""
2827 user, token = generate_user()
2829 start_time = now() + timedelta(hours=2)
2830 end_time = start_time + timedelta(hours=3)
2832 # Create a community and an upload for the event photo
2833 with session_scope() as session:
2834 create_community(session, 0, 2, "Community", [user], [], None)
2835 upload = Upload(
2836 key="test_event_photo_key_123",
2837 filename="test_event_photo_key_123.jpg",
2838 creator_user_id=user.id,
2839 )
2840 session.add(upload)
2842 with events_session(token) as api:
2843 # Create event without photo
2844 res = api.CreateEvent(
2845 events_pb2.CreateEventReq(
2846 title="Event Without Photo",
2847 content="No photo content.",
2848 photo_key=None,
2849 location=events_pb2.EventLocation(
2850 address="Near Null Island",
2851 lat=0.1,
2852 lng=0.2,
2853 ),
2854 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2855 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2856 )
2857 )
2859 assert res.photo_key == ""
2860 assert res.photo_url == ""
2862 # Create event with photo
2863 res_with_photo = api.CreateEvent(
2864 events_pb2.CreateEventReq(
2865 title="Event With Photo",
2866 content="Has photo content.",
2867 photo_key="test_event_photo_key_123",
2868 location=events_pb2.EventLocation(
2869 address="Near Null Island",
2870 lat=0.1,
2871 lng=0.2,
2872 ),
2873 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)),
2874 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)),
2875 )
2876 )
2878 assert res_with_photo.photo_key == "test_event_photo_key_123"
2879 assert "test_event_photo_key_123" in res_with_photo.photo_url
2881 event_id = res_with_photo.event_id
2883 # Verify photo_key is returned when getting the event
2884 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2885 assert get_res.photo_key == "test_event_photo_key_123"
2886 assert "test_event_photo_key_123" in get_res.photo_url
2889def test_event_timezone(db):
2890 user, token = generate_user()
2892 with session_scope() as session:
2893 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2895 # Midnight future day, UTC timezone
2896 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2897 end_time = start_time + timedelta(days=1)
2899 with events_session(token) as api:
2900 create_res: events_pb2.Event = api.CreateEvent(
2901 events_pb2.CreateEventReq(
2902 title="Dummy Title",
2903 content="Dummy content.",
2904 photo_key=None,
2905 parent_community_id=c_id,
2906 # timezone_areas.sql-fake has a region for Europe/Helsinki
2907 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831),
2908 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2909 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2910 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2911 )
2912 )
2914 # Backend should have deduced the helsinki timezone when creating the event,
2915 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2916 assert create_res.timezone == "Europe/Helsinki"
2917 assert to_aware_datetime(create_res.start_time).hour != 0
2918 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2920 # Now update its location such that it gets a new timezone
2921 update_res: events_pb2.Event = api.UpdateEvent(
2922 events_pb2.UpdateEventReq(
2923 event_id=create_res.event_id,
2924 # timezone_areas.sql-fake has a region for America/New_York
2925 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974),
2926 )
2927 )
2929 # The user didn't touch the datetime components on the frontend,
2930 # so they expect the event to be at the same local time (midnight),
2931 # but now in the New York timezone.
2932 assert update_res.timezone == "America/New_York"
2933 assert update_res.start_time != create_res.start_time
2934 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2935 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2937 # Also validate GetEvent
2938 get_res: events_pb2.Event = api.GetEvent(
2939 events_pb2.GetEventReq(
2940 event_id=create_res.event_id,
2941 )
2942 )
2944 assert get_res.timezone == update_res.timezone
2945 assert get_res.start_time == update_res.start_time
2948def test_event_created_with_shadowed_visibility(db):
2949 """Events start in SHADOWED state when created."""
2950 user, token = generate_user()
2952 with session_scope() as session:
2953 create_community(session, 0, 2, "Community", [user], [], None)
2955 start_time = now() + timedelta(hours=2)
2956 end_time = start_time + timedelta(hours=3)
2958 with events_session(token) as api:
2959 res = api.CreateEvent(
2960 events_pb2.CreateEventReq(
2961 title="Test UMS Event",
2962 content="UMS content.",
2963 location=events_pb2.EventLocation(
2964 address="Near Null Island",
2965 lat=0.1,
2966 lng=0.2,
2967 ),
2968 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2969 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2970 )
2971 )
2972 event_id = res.event_id
2974 with session_scope() as session:
2975 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2976 mod_state = session.execute(
2977 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2978 ).scalar_one()
2979 assert mod_state.visibility == ModerationVisibility.shadowed
2982def test_shadowed_event_visible_to_creator_only(db):
2983 """SHADOWED events are visible to the creator but not to other users."""
2984 user1, token1 = generate_user()
2985 user2, token2 = generate_user()
2987 with session_scope() as session:
2988 create_community(session, 0, 2, "Community", [user1], [], None)
2990 start_time = now() + timedelta(hours=2)
2991 end_time = start_time + timedelta(hours=3)
2993 with events_session(token1) as api:
2994 res = api.CreateEvent(
2995 events_pb2.CreateEventReq(
2996 title="Shadowed Event",
2997 content="Content.",
2998 location=events_pb2.EventLocation(
2999 address="Near Null Island",
3000 lat=0.1,
3001 lng=0.2,
3002 ),
3003 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3004 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3005 )
3006 )
3007 event_id = res.event_id
3009 # Creator can see it
3010 with events_session(token1) as api:
3011 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3012 assert res.title == "Shadowed Event"
3014 # Other user cannot
3015 with events_session(token2) as api:
3016 with pytest.raises(grpc.RpcError) as e:
3017 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3018 assert e.value.code() == grpc.StatusCode.NOT_FOUND
3021def test_event_visible_after_approval(db, moderator: Moderator):
3022 """Events become visible to all users after moderation approval."""
3023 user1, token1 = generate_user()
3024 user2, token2 = generate_user()
3026 with session_scope() as session:
3027 create_community(session, 0, 2, "Community", [user1], [], None)
3029 start_time = now() + timedelta(hours=2)
3030 end_time = start_time + timedelta(hours=3)
3032 with events_session(token1) as api:
3033 res = api.CreateEvent(
3034 events_pb2.CreateEventReq(
3035 title="Approved Event",
3036 content="Content.",
3037 location=events_pb2.EventLocation(
3038 address="Near Null Island",
3039 lat=0.1,
3040 lng=0.2,
3041 ),
3042 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3043 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3044 )
3045 )
3046 event_id = res.event_id
3048 # Other user cannot see it yet
3049 with events_session(token2) as api:
3050 with pytest.raises(grpc.RpcError) as e:
3051 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3052 assert e.value.code() == grpc.StatusCode.NOT_FOUND
3054 # Approve the event
3055 moderator.approve_event_occurrence(event_id)
3057 # Now other user can see it
3058 with events_session(token2) as api:
3059 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3060 assert res.title == "Approved Event"
3063def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
3064 """SHADOWED events appear in lists for the creator but not for other users."""
3065 user1, token1 = generate_user()
3066 user2, token2 = generate_user()
3068 with session_scope() as session:
3069 create_community(session, 0, 2, "Community", [user1], [], None)
3071 start_time = now() + timedelta(hours=2)
3072 end_time = start_time + timedelta(hours=3)
3074 with events_session(token1) as api:
3075 res = api.CreateEvent(
3076 events_pb2.CreateEventReq(
3077 title="List Test Event",
3078 content="Content.",
3079 location=events_pb2.EventLocation(
3080 address="Near Null Island",
3081 lat=0.1,
3082 lng=0.2,
3083 ),
3084 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3085 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3086 )
3087 )
3088 event_id = res.event_id
3090 # Creator can see their own SHADOWED event in lists
3091 with events_session(token1) as api:
3092 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
3093 event_ids = [e.event_id for e in list_res.events]
3094 assert event_id in event_ids
3096 # Other user cannot see the SHADOWED event in lists
3097 with events_session(token2) as api:
3098 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
3099 event_ids = [e.event_id for e in list_res.events]
3100 assert event_id not in event_ids
3102 # After approval, other user can see it
3103 moderator.approve_event_occurrence(event_id)
3105 with events_session(token2) as api:
3106 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
3107 event_ids = [e.event_id for e in list_res.events]
3108 assert event_id in event_ids
3111def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
3112 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
3113 user1, token1 = generate_user()
3114 user2, token2 = generate_user()
3116 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
3117 with session_scope() as session:
3118 world = create_community(session, 0, 10, "World", [user1], [], None)
3119 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
3120 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
3121 create_community(session, 0, 2, "Child", [user2], [], region)
3123 start_time = now() + timedelta(hours=2)
3124 end_time = start_time + timedelta(hours=3)
3126 with events_session(token1) as api:
3127 res = api.CreateEvent(
3128 events_pb2.CreateEventReq(
3129 title="Deferred Event",
3130 content="Content.",
3131 location=events_pb2.EventLocation(
3132 address="Near Null Island",
3133 lat=0.1,
3134 lng=0.2,
3135 ),
3136 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3137 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3138 )
3139 )
3140 event_id = res.event_id
3142 # Process all jobs — notification should be deferred (event is SHADOWED)
3143 process_jobs()
3145 with session_scope() as session:
3146 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
3147 # Notification was created with moderation_state_id for deferral
3148 assert notif.moderation_state_id is not None
3149 # No delivery exists (deferred because event is SHADOWED)
3150 delivery_count = session.execute(
3151 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
3152 ).scalar_one_or_none()
3153 assert delivery_count is None
3155 # Approve the event — handle_notification is re-queued for deferred notifications
3156 moderator.approve_event_occurrence(event_id)
3158 # Verify handle_notification job was queued
3159 with session_scope() as session:
3160 pending_jobs = (
3161 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
3162 .scalars()
3163 .all()
3164 )
3165 assert any("handle_notification" in j.job_type for j in pending_jobs)
3168def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3169 """Event update notifications should carry the event's moderation_state_id for deferral."""
3170 user1, token1 = generate_user()
3171 user2, token2 = generate_user()
3173 with session_scope() as session:
3174 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3176 start_time = now() + timedelta(hours=2)
3177 end_time = start_time + timedelta(hours=3)
3179 with events_session(token1) as api:
3180 res = api.CreateEvent(
3181 events_pb2.CreateEventReq(
3182 title="Update Test",
3183 content="Content.",
3184 location=events_pb2.EventLocation(
3185 address="Near Null Island",
3186 lat=0.1,
3187 lng=0.2,
3188 ),
3189 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3190 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3191 )
3192 )
3193 event_id = res.event_id
3195 moderator.approve_event_occurrence(event_id)
3196 process_jobs()
3197 # Clear any create notifications
3198 while push_collector.count_for_user(user2.id): 3198 ↛ 3199line 3198 didn't jump to line 3199 because the condition on line 3198 was never true
3199 push_collector.pop_for_user(user2.id)
3201 # User2 subscribes to the event
3202 with events_session(token2) as api:
3203 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3205 # User1 updates the event with should_notify=True
3206 with events_session(token1) as api:
3207 api.UpdateEvent(
3208 events_pb2.UpdateEventReq(
3209 event_id=event_id,
3210 title=wrappers_pb2.StringValue(value="Updated Title"),
3211 should_notify=True,
3212 )
3213 )
3215 process_jobs()
3217 # Verify that the update notification for user2 has moderation_state_id set
3218 with session_scope() as session:
3219 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3221 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3222 # Find the update notification (most recent one)
3223 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
3224 assert len(update_notifs) == 1
3225 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
3228def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3229 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
3230 user1, token1 = generate_user()
3231 user2, token2 = generate_user()
3233 with session_scope() as session:
3234 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3236 start_time = now() + timedelta(hours=2)
3237 end_time = start_time + timedelta(hours=3)
3239 with events_session(token1) as api:
3240 res = api.CreateEvent(
3241 events_pb2.CreateEventReq(
3242 title="Cancel Test",
3243 content="Content.",
3244 location=events_pb2.EventLocation(
3245 address="Near Null Island",
3246 lat=0.1,
3247 lng=0.2,
3248 ),
3249 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3250 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3251 )
3252 )
3253 event_id = res.event_id
3255 moderator.approve_event_occurrence(event_id)
3256 process_jobs()
3257 while push_collector.count_for_user(user2.id): 3257 ↛ 3258line 3257 didn't jump to line 3258 because the condition on line 3257 was never true
3258 push_collector.pop_for_user(user2.id)
3260 # User2 subscribes
3261 with events_session(token2) as api:
3262 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3264 # User1 cancels the event
3265 with events_session(token1) as api:
3266 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3268 process_jobs()
3270 # Verify that the cancel notification for user2 has moderation_state_id set
3271 with session_scope() as session:
3272 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3274 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3275 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
3276 assert len(cancel_notifs) == 1
3277 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
3280def test_event_update_and_cancel_notifications_not_sent_to_actor(
3281 db, push_collector: PushCollector, moderator: Moderator
3282):
3283 """The user who updates or cancels an event shouldn't be notified about their own action."""
3284 organizer_user, organizer_token = generate_user()
3285 attendee_user, attendee_token = generate_user()
3287 with session_scope() as session:
3288 create_community(session, 0, 2, "Community", [attendee_user], [], None)
3290 start_time = now() + timedelta(hours=2)
3291 end_time = start_time + timedelta(hours=3)
3293 with events_session(organizer_token) as api:
3294 res = api.CreateEvent(
3295 events_pb2.CreateEventReq(
3296 title="Actor Test",
3297 content="Content.",
3298 location=events_pb2.EventLocation(
3299 address="Near Null Island",
3300 lat=0.1,
3301 lng=0.2,
3302 ),
3303 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3304 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3305 )
3306 )
3307 event_id = res.event_id
3309 moderator.approve_event_occurrence(event_id)
3310 process_jobs()
3312 # The attendee subscribes to notifications
3313 with events_session(attendee_token) as api:
3314 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3316 # The organizer updates and then cancels their own event
3317 with events_session(organizer_token) as api:
3318 api.UpdateEvent(
3319 events_pb2.UpdateEventReq(
3320 event_id=event_id,
3321 title=wrappers_pb2.StringValue(value="Updated Title"),
3322 should_notify=True,
3323 )
3324 )
3325 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3327 process_jobs()
3329 # The organizer should not receive any notifications
3330 assert push_collector.count_for_user(organizer_user.id) == 0
3332 # But the attendee should receive both the update and cancel notifications
3333 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__update.display
3334 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__cancel.display
3337def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3338 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
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", [user2], [], None).id
3345 # Create event starting 23 hours from now (within 24h reminder window)
3346 start_time = now() + timedelta(hours=23)
3347 end_time = start_time + timedelta(hours=1)
3349 with events_session(token1) as api:
3350 res = api.CreateEvent(
3351 events_pb2.CreateEventReq(
3352 title="Reminder Test",
3353 content="Content.",
3354 location=events_pb2.EventLocation(
3355 address="Near Null Island",
3356 lat=0.1,
3357 lng=0.2,
3358 ),
3359 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3360 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3361 )
3362 )
3363 event_id = res.event_id
3365 moderator.approve_event_occurrence(event_id)
3366 process_jobs()
3367 while push_collector.count_for_user(user2.id): 3367 ↛ 3368line 3367 didn't jump to line 3368 because the condition on line 3367 was never true
3368 push_collector.pop_for_user(user2.id)
3370 # User2 marks attendance
3371 with events_session(token2) as api:
3372 api.SetEventAttendance(
3373 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3374 )
3376 # Run the event reminder handler
3377 send_event_reminders(empty_pb2.Empty())
3378 process_jobs()
3380 # Verify that the reminder notification for user2 has moderation_state_id set
3381 with session_scope() as session:
3382 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3384 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3385 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
3386 assert len(reminder_notifs) == 1
3387 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
3390def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
3391 """Event reminders should not be sent for cancelled events."""
3392 user1, token1 = generate_user()
3393 user2, token2 = generate_user()
3395 with session_scope() as session:
3396 create_community(session, 0, 2, "Community", [user2], [], None)
3398 # Create event starting 23 hours from now (within 24h reminder window)
3399 start_time = now() + timedelta(hours=23)
3400 end_time = start_time + timedelta(hours=1)
3402 with events_session(token1) as api:
3403 res = api.CreateEvent(
3404 events_pb2.CreateEventReq(
3405 title="Cancelled Reminder Test",
3406 content="Content.",
3407 location=events_pb2.EventLocation(
3408 address="Near Null Island",
3409 lat=0.1,
3410 lng=0.2,
3411 ),
3412 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3413 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3414 )
3415 )
3416 event_id = res.event_id
3418 moderator.approve_event_occurrence(event_id)
3419 process_jobs()
3421 # User2 marks attendance
3422 with events_session(token2) as api:
3423 api.SetEventAttendance(
3424 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3425 )
3427 # User1 cancels the event
3428 with events_session(token1) as api:
3429 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3431 process_jobs()
3432 # Drain any cancellation-related notifications so we can cleanly assert on reminders
3433 while push_collector.count_for_user(user2.id):
3434 push_collector.pop_for_user(user2.id)
3436 # Run the event reminder handler
3437 send_event_reminders(empty_pb2.Empty())
3438 process_jobs()
3440 # Verify that no reminder notification was sent for user2
3441 with session_scope() as session:
3442 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3443 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3444 assert len(reminder_notifs) == 0
3447@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
3448def test_event_reminder_not_sent_for_invisible_attendee(
3449 db, push_collector: PushCollector, moderator: Moderator, invisible_field
3450):
3451 user1, token1 = generate_user()
3452 user2, token2 = generate_user()
3454 with session_scope() as session:
3455 create_community(session, 0, 2, "Community", [user2], [], None)
3457 start_time = now() + timedelta(hours=23)
3458 end_time = start_time + timedelta(hours=1)
3460 with events_session(token1) as api:
3461 res = api.CreateEvent(
3462 events_pb2.CreateEventReq(
3463 title="Invisible Attendee Reminder Test",
3464 content="Content.",
3465 location=events_pb2.EventLocation(
3466 address="Near Null Island",
3467 lat=0.1,
3468 lng=0.2,
3469 ),
3470 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3471 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3472 )
3473 )
3474 event_id = res.event_id
3476 moderator.approve_event_occurrence(event_id)
3477 process_jobs()
3479 with events_session(token2) as api:
3480 api.SetEventAttendance(
3481 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3482 )
3484 with session_scope() as session:
3485 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3487 send_event_reminders(empty_pb2.Empty())
3488 process_jobs()
3490 with session_scope() as session:
3491 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3492 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3493 assert len(reminder_notifs) == 0
3496def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3497 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3498 user1, token1 = generate_user()
3499 user2, token2 = generate_user()
3501 with session_scope() as session:
3502 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3504 start = now()
3506 # User1 creates event A with 3 occurrences
3507 event_a_ids = []
3508 with events_session(token1) as api:
3509 res = api.CreateEvent(
3510 events_pb2.CreateEventReq(
3511 title="Event A",
3512 content="Content A.",
3513 parent_community_id=c_id,
3514 location=events_pb2.EventLocation(
3515 address="Near Null Island",
3516 lat=0.1,
3517 lng=0.2,
3518 ),
3519 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
3520 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
3521 )
3522 )
3523 event_a_ids.append(res.event_id)
3524 for i in range(2):
3525 res = api.ScheduleEvent(
3526 events_pb2.ScheduleEventReq(
3527 event_id=event_a_ids[-1],
3528 content=f"A occurrence {i}",
3529 location=events_pb2.EventLocation(
3530 address="Near Null Island",
3531 lat=0.1,
3532 lng=0.2,
3533 ),
3534 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
3535 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
3536 )
3537 )
3538 event_a_ids.append(res.event_id)
3540 # User2 creates event B with 2 occurrences
3541 event_b_ids = []
3542 with events_session(token2) as api:
3543 res = api.CreateEvent(
3544 events_pb2.CreateEventReq(
3545 title="Event B",
3546 content="Content B.",
3547 parent_community_id=c_id,
3548 location=events_pb2.EventLocation(
3549 address="Near Null Island",
3550 lat=0.1,
3551 lng=0.2,
3552 ),
3553 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)),
3554 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)),
3555 )
3556 )
3557 event_b_ids.append(res.event_id)
3558 res = api.ScheduleEvent(
3559 events_pb2.ScheduleEventReq(
3560 event_id=event_b_ids[-1],
3561 content="B occurrence 1",
3562 location=events_pb2.EventLocation(
3563 address="Near Null Island",
3564 lat=0.1,
3565 lng=0.2,
3566 ),
3567 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)),
3568 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)),
3569 )
3570 )
3571 event_b_ids.append(res.event_id)
3573 moderator.approve_event_occurrence(event_a_ids[0])
3574 moderator.approve_event_occurrence(event_b_ids[0])
3576 # List occurrences for event A — should only get event A's 3 occurrences
3577 with events_session(token1) as api:
3578 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3579 returned_ids = [e.event_id for e in res.events]
3580 assert sorted(returned_ids) == sorted(event_a_ids)
3582 # List occurrences for event B — should only get event B's 2 occurrences
3583 with events_session(token2) as api:
3584 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3585 returned_ids = [e.event_id for e in res.events]
3586 assert sorted(returned_ids) == sorted(event_b_ids)
3589def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3590 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3591 user1, token1 = generate_user()
3592 user2, token2 = generate_user()
3594 with session_scope() as session:
3595 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3597 start_time = now() + timedelta(hours=2)
3598 end_time = start_time + timedelta(hours=3)
3600 with events_session(token1) as api:
3601 res = api.CreateEvent(
3602 events_pb2.CreateEventReq(
3603 title="Comment Test",
3604 content="Content.",
3605 parent_community_id=c_id,
3606 location=events_pb2.EventLocation(
3607 address="Near Null Island",
3608 lat=0.1,
3609 lng=0.2,
3610 ),
3611 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3612 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3613 )
3614 )
3615 event_id = res.event_id
3616 thread_id = res.thread.thread_id
3618 moderator.approve_event_occurrence(event_id)
3619 process_jobs()
3620 while push_collector.count_for_user(user1.id): 3620 ↛ 3621line 3620 didn't jump to line 3621 because the condition on line 3620 was never true
3621 push_collector.pop_for_user(user1.id)
3623 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3624 with events_session(token1) as api:
3625 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3627 # User2 posts a top-level comment on the event thread
3628 with threads_session(token2) as api:
3629 comment_thread_id = api.PostReply(
3630 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3631 ).thread_id
3633 process_jobs()
3635 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3636 comment_db_id = comment_thread_id // 10
3637 with session_scope() as session:
3638 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3640 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3641 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3642 assert len(comment_notifs) == 1
3643 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3646def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3647 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3648 user1, token1 = generate_user()
3649 user2, token2 = generate_user()
3650 user3, token3 = generate_user()
3652 with session_scope() as session:
3653 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3655 start_time = now() + timedelta(hours=2)
3656 end_time = start_time + timedelta(hours=3)
3658 with events_session(token1) as api:
3659 res = api.CreateEvent(
3660 events_pb2.CreateEventReq(
3661 title="Reply Test",
3662 content="Content.",
3663 location=events_pb2.EventLocation(
3664 address="Near Null Island",
3665 lat=0.1,
3666 lng=0.2,
3667 ),
3668 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3669 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3670 )
3671 )
3672 event_id = res.event_id
3673 thread_id = res.thread.thread_id
3675 moderator.approve_event_occurrence(event_id)
3676 process_jobs()
3677 while push_collector.count_for_user(user1.id): 3677 ↛ 3678line 3677 didn't jump to line 3678 because the condition on line 3677 was never true
3678 push_collector.pop_for_user(user1.id)
3680 # User2 posts a top-level comment
3681 with threads_session(token2) as api:
3682 comment_thread_id = api.PostReply(
3683 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3684 ).thread_id
3686 process_jobs()
3687 while push_collector.count_for_user(user1.id): 3687 ↛ 3688line 3687 didn't jump to line 3688 because the condition on line 3687 was never true
3688 push_collector.pop_for_user(user1.id)
3690 # User3 replies to user2's comment (depth=2 reply)
3691 with threads_session(token3) as api:
3692 nested_reply_thread_id = api.PostReply(
3693 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3694 ).thread_id
3696 process_jobs()
3698 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3699 nested_reply_db_id = nested_reply_thread_id // 10
3700 with session_scope() as session:
3701 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3703 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3704 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3705 assert len(reply_notifs) == 1
3706 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id