Coverage for app/backend/src/tests/test_events.py: 99%
1638 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +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
38def to_event_time_granularity(value: datetime) -> datetime:
39 """Events are scheduled at the minute granularity."""
40 return value.replace(second=0, microsecond=0)
43def is_utc_or_gmt(timezone: str) -> bool:
44 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT.
45 # Tests should be agnostic to which one we're using.
46 return timezone in ("Etc/UTC", "Etc/GMT")
49def test_CreateEvent(db, frozen_timewarp, push_collector: PushCollector, moderator: Moderator):
50 # test cases:
51 # can create event
52 # cannot create event with missing details
53 # can't create event that starts in the past
54 # can create in different timezones
56 # event creator
57 user1, token1 = generate_user()
58 # community moderator
59 user2, token2 = generate_user()
60 # third party
61 user3, token3 = generate_user()
63 with session_scope() as session:
64 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
66 time_before = now()
67 start_time = now() + timedelta(hours=2)
68 end_time = start_time + timedelta(hours=3)
70 # Can create an event
71 with events_session(token1) as api:
72 res = api.CreateEvent(
73 events_pb2.CreateEventReq(
74 title="Dummy Title",
75 content="Dummy content.",
76 photo_key=None,
77 location=events_pb2.EventLocation(
78 address="Near Null Island",
79 lat=0.1,
80 lng=0.2,
81 ),
82 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
83 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
84 )
85 )
87 assert res.is_next
88 assert res.title == "Dummy Title"
89 assert res.slug == "dummy-title"
90 assert res.content == "Dummy content."
91 assert not res.photo_url
92 assert res.HasField("location")
93 assert res.location.lat == 0.1
94 assert res.location.lng == 0.2
95 assert res.location.address == "Near Null Island"
96 assert time_before <= to_aware_datetime(res.created) <= now()
97 assert time_before <= to_aware_datetime(res.last_edited) <= now()
98 assert res.creator_user_id == user1.id
99 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
100 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
101 assert is_utc_or_gmt(res.timezone)
102 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
103 assert res.organizer
104 assert res.subscriber
105 assert res.going_count == 1
106 assert res.organizer_count == 1
107 assert res.subscriber_count == 1
108 assert res.owner_user_id == user1.id
109 assert not res.owner_community_id
110 assert not res.owner_group_id
111 assert res.thread.thread_id
112 assert res.can_edit
113 assert not res.can_moderate
115 event_id = res.event_id
117 # Approve the event so other users can see it
118 moderator.approve_event_occurrence(event_id)
120 with events_session(token2) as api:
121 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
123 assert res.is_next
124 assert res.title == "Dummy Title"
125 assert res.slug == "dummy-title"
126 assert res.content == "Dummy content."
127 assert not res.photo_url
128 assert res.HasField("location")
129 assert res.location.lat == 0.1
130 assert res.location.lng == 0.2
131 assert res.location.address == "Near Null Island"
132 assert time_before <= to_aware_datetime(res.created) <= now()
133 assert time_before <= to_aware_datetime(res.last_edited) <= now()
134 assert res.creator_user_id == user1.id
135 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
136 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
137 assert is_utc_or_gmt(res.timezone)
138 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
139 assert not res.organizer
140 assert not res.subscriber
141 assert res.going_count == 1
142 assert res.organizer_count == 1
143 assert res.subscriber_count == 1
144 assert res.owner_user_id == user1.id
145 assert not res.owner_community_id
146 assert not res.owner_group_id
147 assert res.thread.thread_id
148 assert res.can_edit
149 assert res.can_moderate
151 with events_session(token3) as api:
152 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
154 assert res.is_next
155 assert res.title == "Dummy Title"
156 assert res.slug == "dummy-title"
157 assert res.content == "Dummy content."
158 assert not res.photo_url
159 assert res.HasField("location")
160 assert res.location.lat == 0.1
161 assert res.location.lng == 0.2
162 assert res.location.address == "Near Null Island"
163 assert time_before <= to_aware_datetime(res.created) <= now()
164 assert time_before <= to_aware_datetime(res.last_edited) <= now()
165 assert res.creator_user_id == user1.id
166 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
167 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
168 assert is_utc_or_gmt(res.timezone)
169 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
170 assert not res.organizer
171 assert not res.subscriber
172 assert res.going_count == 1
173 assert res.organizer_count == 1
174 assert res.subscriber_count == 1
175 assert res.owner_user_id == user1.id
176 assert not res.owner_community_id
177 assert not res.owner_group_id
178 assert res.thread.thread_id
179 assert not res.can_edit
180 assert not res.can_moderate
182 # Failure cases
183 with events_session(token1) as api:
184 with pytest.raises(grpc.RpcError) as e:
185 api.CreateEvent(
186 events_pb2.CreateEventReq(
187 # title="Dummy Title",
188 content="Dummy content.",
189 photo_key=None,
190 location=events_pb2.EventLocation(
191 address="Near Null Island",
192 lat=0.1,
193 lng=0.2,
194 ),
195 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
196 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
197 )
198 )
199 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
200 assert e.value.details() == "Missing event title."
202 with pytest.raises(grpc.RpcError) as e:
203 api.CreateEvent(
204 events_pb2.CreateEventReq(
205 title="Dummy Title",
206 # content="Dummy content.",
207 photo_key=None,
208 location=events_pb2.EventLocation(
209 address="Near Null Island",
210 lat=0.1,
211 lng=0.2,
212 ),
213 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
214 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
215 )
216 )
217 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
218 assert e.value.details() == "Missing event content."
220 with pytest.raises(grpc.RpcError) as e:
221 api.CreateEvent(
222 events_pb2.CreateEventReq(
223 title="Dummy Title",
224 content="Dummy content.",
225 photo_key="nonexistent",
226 location=events_pb2.EventLocation(
227 address="Near Null Island",
228 lat=0.1,
229 lng=0.2,
230 ),
231 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
232 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
233 )
234 )
235 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
236 assert e.value.details() == "Photo not found."
238 with pytest.raises(grpc.RpcError) as e:
239 api.CreateEvent(
240 events_pb2.CreateEventReq(
241 title="Dummy Title",
242 content="Dummy content.",
243 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
244 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
245 )
246 )
247 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
248 assert e.value.details() == "Missing event address or location."
250 with pytest.raises(grpc.RpcError) as e:
251 api.CreateEvent(
252 events_pb2.CreateEventReq(
253 title="Dummy Title",
254 content="Dummy content.",
255 location=events_pb2.EventLocation(
256 address="Near Null Island",
257 ),
258 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
259 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
260 )
261 )
262 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
263 assert e.value.details() == "Invalid coordinate."
265 with pytest.raises(grpc.RpcError) as e:
266 api.CreateEvent(
267 events_pb2.CreateEventReq(
268 title="Dummy Title",
269 content="Dummy content.",
270 location=events_pb2.EventLocation(
271 lat=0.1,
272 lng=0.1,
273 ),
274 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
275 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
276 )
277 )
278 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
279 assert e.value.details() == "Missing event address or location."
281 with pytest.raises(grpc.RpcError) as e:
282 api.CreateEvent(
283 events_pb2.CreateEventReq(
284 title="Dummy Title",
285 content="Dummy content.",
286 location=events_pb2.EventLocation(
287 address="Near Null Island",
288 lat=0.1,
289 lng=0.2,
290 ),
291 start_datetime_iso8601_local=datetime_to_iso8601_local(now() - timedelta(hours=2)),
292 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
293 )
294 )
295 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
296 assert e.value.details() == "The event must be in the future."
298 with pytest.raises(grpc.RpcError) as e:
299 api.CreateEvent(
300 events_pb2.CreateEventReq(
301 title="Dummy Title",
302 content="Dummy content.",
303 location=events_pb2.EventLocation(
304 address="Near Null Island",
305 lat=0.1,
306 lng=0.2,
307 ),
308 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
309 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
310 )
311 )
312 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
313 assert e.value.details() == "The event must end after it starts."
315 with pytest.raises(grpc.RpcError) as e:
316 api.CreateEvent(
317 events_pb2.CreateEventReq(
318 title="Dummy Title",
319 content="Dummy content.",
320 location=events_pb2.EventLocation(
321 address="Near Null Island",
322 lat=0.1,
323 lng=0.2,
324 ),
325 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=2)),
326 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=5)),
327 )
328 )
329 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
330 assert e.value.details() == "The event needs to start within the next year."
332 with pytest.raises(grpc.RpcError) as e:
333 api.CreateEvent(
334 events_pb2.CreateEventReq(
335 title="Dummy Title",
336 content="Dummy content.",
337 location=events_pb2.EventLocation(
338 address="Near Null Island",
339 lat=0.1,
340 lng=0.2,
341 ),
342 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
343 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=100)),
344 )
345 )
346 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
347 assert e.value.details() == "Events cannot last longer than 7 days."
350def test_CreateEvent_incomplete_profile(db):
351 user1, token1 = generate_user(complete_profile=False)
352 user2, token2 = generate_user()
354 with session_scope() as session:
355 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
357 start_time = now() + timedelta(hours=2)
358 end_time = start_time + timedelta(hours=3)
360 with events_session(token1) as api:
361 with pytest.raises(grpc.RpcError) as e:
362 api.CreateEvent(
363 events_pb2.CreateEventReq(
364 title="Dummy Title",
365 content="Dummy content.",
366 photo_key=None,
367 location=events_pb2.EventLocation(
368 address="Near Null Island",
369 lat=0.1,
370 lng=0.2,
371 ),
372 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
373 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
374 )
375 )
376 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
377 assert e.value.details() == "You have to complete your profile before you can create an event."
380def test_ScheduleEvent(db, frozen_timewarp):
381 # test cases:
382 # can schedule a new event occurrence
384 user, token = generate_user()
386 with session_scope() as session:
387 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
389 time_before = now()
390 start_time = now() + timedelta(hours=2)
391 end_time = start_time + timedelta(hours=3)
393 with events_session(token) as api:
394 create_res = api.CreateEvent(
395 events_pb2.CreateEventReq(
396 title="Dummy Title",
397 content="Dummy content.",
398 parent_community_id=c_id,
399 location=events_pb2.EventLocation(
400 address="Near Null Island",
401 lat=0.1,
402 lng=0.2,
403 ),
404 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
405 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
406 )
407 )
409 new_start_time = now() + timedelta(hours=6)
410 new_end_time = new_start_time + timedelta(hours=2)
412 schedule_res = api.ScheduleEvent(
413 events_pb2.ScheduleEventReq(
414 event_id=create_res.event_id,
415 content="New event occurrence",
416 location=events_pb2.EventLocation(
417 address="A bit further but still near Null Island",
418 lat=0.3,
419 lng=0.2,
420 ),
421 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time),
422 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time),
423 )
424 )
426 # Each occurrence is independent and has an independent thread
427 assert schedule_res.event_id != create_res.event_id
428 assert schedule_res.thread.thread_id != create_res.thread.thread_id
430 res = api.GetEvent(events_pb2.GetEventReq(event_id=schedule_res.event_id))
432 assert not res.is_next
433 assert res.title == "Dummy Title"
434 assert res.slug == "dummy-title"
435 assert res.content == "New event occurrence"
436 assert not res.photo_url
437 assert res.HasField("location")
438 assert res.location.lat == 0.3
439 assert res.location.lng == 0.2
440 assert res.location.address == "A bit further but still near Null Island"
441 assert time_before <= to_aware_datetime(res.created) <= now()
442 assert time_before <= to_aware_datetime(res.last_edited) <= now()
443 assert res.creator_user_id == user.id
444 assert to_aware_datetime(res.start_time) == to_event_time_granularity(new_start_time)
445 assert to_aware_datetime(res.end_time) == to_event_time_granularity(new_end_time)
446 assert is_utc_or_gmt(res.timezone)
447 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
448 assert res.organizer
449 assert res.subscriber
450 assert res.going_count == 1
451 assert res.organizer_count == 1
452 assert res.subscriber_count == 1
453 assert res.owner_user_id == user.id
454 assert not res.owner_community_id
455 assert not res.owner_group_id
456 assert res.thread.thread_id
457 assert res.can_edit
458 assert res.can_moderate
461def test_cannot_overlap_occurrences_schedule(db):
462 user, token = generate_user()
464 with session_scope() as session:
465 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
467 start = now()
469 with events_session(token) as api:
470 res = api.CreateEvent(
471 events_pb2.CreateEventReq(
472 title="Dummy Title",
473 content="Dummy content.",
474 parent_community_id=c_id,
475 location=events_pb2.EventLocation(
476 address="Near Null Island",
477 lat=0.1,
478 lng=0.2,
479 ),
480 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
481 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
482 )
483 )
485 with pytest.raises(grpc.RpcError) as e:
486 api.ScheduleEvent(
487 events_pb2.ScheduleEventReq(
488 event_id=res.event_id,
489 content="New event occurrence",
490 location=events_pb2.EventLocation(
491 address="A bit further but still near Null Island",
492 lat=0.3,
493 lng=0.2,
494 ),
495 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
496 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
497 )
498 )
499 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
500 assert e.value.details() == "An event cannot have overlapping occurrences."
503def test_cannot_overlap_occurrences_update(db):
504 user, token = generate_user()
506 with session_scope() as session:
507 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
509 start = now()
511 with events_session(token) as api:
512 res = api.CreateEvent(
513 events_pb2.CreateEventReq(
514 title="Dummy Title",
515 content="Dummy content.",
516 parent_community_id=c_id,
517 location=events_pb2.EventLocation(
518 address="Near Null Island",
519 lat=0.1,
520 lng=0.2,
521 ),
522 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
523 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
524 )
525 )
527 event_id = api.ScheduleEvent(
528 events_pb2.ScheduleEventReq(
529 event_id=res.event_id,
530 content="New event occurrence",
531 location=events_pb2.EventLocation(
532 address="A bit further but still near Null Island",
533 lat=0.3,
534 lng=0.2,
535 ),
536 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
537 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
538 )
539 ).event_id
541 # can overlap with this current existing occurrence
542 api.UpdateEvent(
543 events_pb2.UpdateEventReq(
544 event_id=event_id,
545 start_datetime_iso8601_local=wrappers_pb2.StringValue(
546 value=datetime_to_iso8601_local(start + timedelta(hours=5))
547 ),
548 end_datetime_iso8601_local=wrappers_pb2.StringValue(
549 value=datetime_to_iso8601_local(start + timedelta(hours=6))
550 ),
551 )
552 )
554 with pytest.raises(grpc.RpcError) as e:
555 api.UpdateEvent(
556 events_pb2.UpdateEventReq(
557 event_id=event_id,
558 start_datetime_iso8601_local=wrappers_pb2.StringValue(
559 value=datetime_to_iso8601_local(start + timedelta(hours=2))
560 ),
561 end_datetime_iso8601_local=wrappers_pb2.StringValue(
562 value=datetime_to_iso8601_local(start + timedelta(hours=4))
563 ),
564 )
565 )
566 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
567 assert e.value.details() == "An event cannot have overlapping occurrences."
570def test_UpdateEvent_single(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator):
571 # test cases:
572 # owner can update
573 # community owner can update
574 # notifies attendees
576 # event creator
577 user1, token1 = generate_user()
578 # community moderator
579 user2, token2 = generate_user()
580 # third parties
581 user3, token3 = generate_user()
582 user4, token4 = generate_user()
583 user5, token5 = generate_user()
584 user6, token6 = generate_user()
586 with session_scope() as session:
587 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
589 time_before = now()
590 start_time = now() + timedelta(hours=2)
591 end_time = start_time + timedelta(hours=3)
593 with events_session(token1) as api:
594 res = api.CreateEvent(
595 events_pb2.CreateEventReq(
596 title="Dummy Title",
597 content="Dummy content.",
598 parent_community_id=c_id,
599 location=events_pb2.EventLocation(
600 address="Near Null Island",
601 lat=0.1,
602 lng=0.2,
603 ),
604 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
605 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
606 )
607 )
609 event_id = res.event_id
611 moderator.approve_event_occurrence(event_id)
613 with events_session(token4) as api:
614 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
616 with events_session(token5) as api:
617 api.SetEventAttendance(
618 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
619 )
621 with events_session(token6) as api:
622 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
624 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change
625 frozen_timewarp.advance(timedelta(minutes=1))
626 time_before_update = now()
628 with events_session(token1) as api:
629 res = api.UpdateEvent(
630 events_pb2.UpdateEventReq(
631 event_id=event_id,
632 )
633 )
635 with events_session(token1) as api:
636 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
638 assert res.is_next
639 assert res.title == "Dummy Title"
640 assert res.slug == "dummy-title"
641 assert res.content == "Dummy content."
642 assert not res.photo_url
643 assert res.HasField("location")
644 assert res.location.lat == 0.1
645 assert res.location.lng == 0.2
646 assert res.location.address == "Near Null Island"
647 assert time_before <= to_aware_datetime(res.created) <= time_before_update
648 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
649 assert res.creator_user_id == user1.id
650 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
651 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
652 assert is_utc_or_gmt(res.timezone)
653 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
654 assert res.organizer
655 assert res.subscriber
656 assert res.going_count == 2
657 assert res.organizer_count == 1
658 assert res.subscriber_count == 3
659 assert res.owner_user_id == user1.id
660 assert not res.owner_community_id
661 assert not res.owner_group_id
662 assert res.thread.thread_id
663 assert res.can_edit
664 assert not res.can_moderate
666 with events_session(token2) as api:
667 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
669 assert res.is_next
670 assert res.title == "Dummy Title"
671 assert res.slug == "dummy-title"
672 assert res.content == "Dummy content."
673 assert not res.photo_url
674 assert res.HasField("location")
675 assert res.location.lat == 0.1
676 assert res.location.lng == 0.2
677 assert res.location.address == "Near Null Island"
678 assert time_before <= to_aware_datetime(res.created) <= time_before_update
679 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
680 assert res.creator_user_id == user1.id
681 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
682 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
683 assert is_utc_or_gmt(res.timezone)
684 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
685 assert not res.organizer
686 assert not res.subscriber
687 assert res.going_count == 2
688 assert res.organizer_count == 1
689 assert res.subscriber_count == 3
690 assert res.owner_user_id == user1.id
691 assert not res.owner_community_id
692 assert not res.owner_group_id
693 assert res.thread.thread_id
694 assert res.can_edit
695 assert res.can_moderate
697 with events_session(token3) as api:
698 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
700 assert res.is_next
701 assert res.title == "Dummy Title"
702 assert res.slug == "dummy-title"
703 assert res.content == "Dummy content."
704 assert not res.photo_url
705 assert res.HasField("location")
706 assert res.location.lat == 0.1
707 assert res.location.lng == 0.2
708 assert res.location.address == "Near Null Island"
709 assert time_before <= to_aware_datetime(res.created) <= time_before_update
710 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
711 assert res.creator_user_id == user1.id
712 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
713 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
714 assert is_utc_or_gmt(res.timezone)
715 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
716 assert not res.organizer
717 assert not res.subscriber
718 assert res.going_count == 2
719 assert res.organizer_count == 1
720 assert res.subscriber_count == 3
721 assert res.owner_user_id == user1.id
722 assert not res.owner_community_id
723 assert not res.owner_group_id
724 assert res.thread.thread_id
725 assert not res.can_edit
726 assert not res.can_moderate
728 with events_session(token1) as api:
729 res = api.UpdateEvent(
730 events_pb2.UpdateEventReq(
731 event_id=event_id,
732 location=events_pb2.EventLocation(
733 address="Nearer Null Island",
734 lat=0.01,
735 lng=0.02,
736 ),
737 )
738 )
740 with events_session(token3) as api:
741 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
743 assert res.HasField("location")
744 assert res.location.address == "Nearer Null Island"
745 assert res.location.lat == 0.01
746 assert res.location.lng == 0.02
749def test_UpdateEvent_all(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator):
750 # event creator
751 user1, token1 = generate_user()
752 # community moderator
753 user2, token2 = generate_user()
754 # third parties
755 user3, token3 = generate_user()
756 user4, token4 = generate_user()
757 user5, token5 = generate_user()
758 user6, token6 = generate_user()
760 with session_scope() as session:
761 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
763 time_before = now()
764 start_time = now() + timedelta(hours=1)
765 end_time = start_time + timedelta(hours=1.5)
767 event_ids = []
769 with events_session(token1) as api:
770 res = api.CreateEvent(
771 events_pb2.CreateEventReq(
772 title="Dummy Title",
773 content="0th occurrence",
774 location=events_pb2.EventLocation(
775 address="Near Null Island",
776 lat=0.1,
777 lng=0.2,
778 ),
779 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
780 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
781 )
782 )
784 event_id = res.event_id
785 event_ids.append(event_id)
787 moderator.approve_event_occurrence(event_id)
789 with events_session(token4) as api:
790 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
792 with events_session(token5) as api:
793 api.SetEventAttendance(
794 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
795 )
797 with events_session(token6) as api:
798 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
800 with events_session(token1) as api:
801 for i in range(5):
802 res = api.ScheduleEvent(
803 events_pb2.ScheduleEventReq(
804 event_id=event_ids[-1],
805 content=f"{i + 1}th occurrence",
806 location=events_pb2.EventLocation(
807 address="Near Null Island",
808 lat=0.1,
809 lng=0.2,
810 ),
811 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2 + i)),
812 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2.5 + i)),
813 )
814 )
816 event_ids.append(res.event_id)
818 # Approve all scheduled occurrences
819 for eid in event_ids[1:]:
820 moderator.approve_event_occurrence(eid)
822 updated_event_id = event_ids[3]
824 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change
825 frozen_timewarp.advance(timedelta(minutes=1))
826 time_before_update = now()
828 with events_session(token1) as api:
829 res = api.UpdateEvent(
830 events_pb2.UpdateEventReq(
831 event_id=updated_event_id,
832 title=wrappers_pb2.StringValue(value="New Title"),
833 content=wrappers_pb2.StringValue(value="New content."),
834 location=events_pb2.EventLocation(
835 address="Not so near Null Island",
836 lat=0.2,
837 lng=0.2,
838 ),
839 update_all_future=True,
840 )
841 )
843 time_after_update = now()
845 with events_session(token2) as api:
846 for i in range(3):
847 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
848 assert res.content == f"{i}th occurrence"
849 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
851 for i in range(3, 6):
852 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
853 assert res.content == "New content."
854 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
857def test_UpdateEvent_all_leaves_other_events_alone(db, moderator: Moderator):
858 """update_all_future must only touch the occurrences of the event being edited."""
859 # creator of the event that gets edited
860 user1, token1 = generate_user()
861 # creator of an unrelated event in the same time window
862 user2, token2 = generate_user()
863 # community moderator, so that neither creator has edit rights on the other's event
864 user3, token3 = generate_user()
866 with session_scope() as session:
867 create_community(session, 0, 2, "Community", [user3], [], None)
869 start_time = now() + timedelta(hours=1)
871 with events_session(token1) as api:
872 edited_id = api.CreateEvent(
873 events_pb2.CreateEventReq(
874 title="Edited Event",
875 content="0th occurrence",
876 location=events_pb2.EventLocation(
877 address="Near Null Island",
878 lat=0.1,
879 lng=0.2,
880 ),
881 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
882 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=1)),
883 )
884 ).event_id
886 second_id = api.ScheduleEvent(
887 events_pb2.ScheduleEventReq(
888 event_id=edited_id,
889 content="1th occurrence",
890 location=events_pb2.EventLocation(
891 address="Near Null Island",
892 lat=0.1,
893 lng=0.2,
894 ),
895 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2)),
896 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=3)),
897 )
898 ).event_id
900 # an occurrence of a different event, starting after the edited one and ending well after the cutoff
901 with events_session(token2) as api:
902 other_id = api.CreateEvent(
903 events_pb2.CreateEventReq(
904 title="Other Event",
905 content="Other content.",
906 location=events_pb2.EventLocation(
907 address="Somewhere else entirely",
908 lat=0.5,
909 lng=0.5,
910 ),
911 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=4)),
912 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=5)),
913 )
914 ).event_id
916 for occurrence_id in (edited_id, second_id, other_id):
917 moderator.approve_event_occurrence(occurrence_id)
919 with events_session(token2) as api:
920 other_before = api.GetEvent(events_pb2.GetEventReq(event_id=other_id))
922 with events_session(token1) as api:
923 api.UpdateEvent(
924 events_pb2.UpdateEventReq(
925 event_id=edited_id,
926 content=wrappers_pb2.StringValue(value="New content."),
927 location=events_pb2.EventLocation(
928 address="Not so near Null Island",
929 lat=0.2,
930 lng=0.2,
931 ),
932 update_all_future=True,
933 )
934 )
936 with events_session(token3) as api:
937 for occurrence_id in (edited_id, second_id):
938 res = api.GetEvent(events_pb2.GetEventReq(event_id=occurrence_id))
939 assert res.content == "New content."
940 assert res.location.address == "Not so near Null Island"
942 res = api.GetEvent(events_pb2.GetEventReq(event_id=other_id))
943 assert res.content == "Other content."
944 assert res.location.address == "Somewhere else entirely"
945 assert res.location.lat == 0.5
946 assert res.location.lng == 0.5
947 assert res.timezone == other_before.timezone
948 assert res.last_edited == other_before.last_edited
951def test_UpdateEvent_all_cant_change_times(db, moderator: Moderator):
952 """Every future occurrence would get the same times, which the exclusion constraint forbids."""
953 user1, token1 = generate_user()
954 user2, token2 = generate_user()
956 with session_scope() as session:
957 create_community(session, 0, 2, "Community", [user2], [], None)
959 start_time = now() + timedelta(hours=1)
961 with events_session(token1) as api:
962 event_id = api.CreateEvent(
963 events_pb2.CreateEventReq(
964 title="Dummy Title",
965 content="0th occurrence",
966 location=events_pb2.EventLocation(
967 address="Near Null Island",
968 lat=0.1,
969 lng=0.2,
970 ),
971 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
972 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=1)),
973 )
974 ).event_id
976 api.ScheduleEvent(
977 events_pb2.ScheduleEventReq(
978 event_id=event_id,
979 content="1th occurrence",
980 location=events_pb2.EventLocation(
981 address="Near Null Island",
982 lat=0.1,
983 lng=0.2,
984 ),
985 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2)),
986 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=3)),
987 )
988 )
990 with pytest.raises(grpc.RpcError) as e:
991 api.UpdateEvent(
992 events_pb2.UpdateEventReq(
993 event_id=event_id,
994 start_datetime_iso8601_local=wrappers_pb2.StringValue(
995 value=datetime_to_iso8601_local(start_time + timedelta(minutes=30))
996 ),
997 end_datetime_iso8601_local=wrappers_pb2.StringValue(
998 value=datetime_to_iso8601_local(start_time + timedelta(hours=1, minutes=30))
999 ),
1000 update_all_future=True,
1001 )
1002 )
1003 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1004 assert e.value.details() == "You cannot update all events if you're modifying start or end times."
1007def test_GetEvent(db, frozen_timewarp, moderator: Moderator):
1008 # event creator
1009 user1, token1 = generate_user()
1010 # community moderator
1011 user2, token2 = generate_user()
1012 # third parties
1013 user3, token3 = generate_user()
1014 user4, token4 = generate_user()
1015 user5, token5 = generate_user()
1016 user6, token6 = generate_user()
1018 with session_scope() as session:
1019 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1021 time_before = now()
1022 start_time = now() + timedelta(hours=2)
1023 end_time = start_time + timedelta(hours=3)
1025 with events_session(token1) as api:
1026 # in person event
1027 res = api.CreateEvent(
1028 events_pb2.CreateEventReq(
1029 title="Dummy Title",
1030 content="Dummy content.",
1031 location=events_pb2.EventLocation(
1032 address="Near Null Island",
1033 lat=0.1,
1034 lng=0.2,
1035 ),
1036 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1037 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1038 )
1039 )
1041 event_id = res.event_id
1043 moderator.approve_event_occurrence(event_id)
1045 with events_session(token4) as api:
1046 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1048 with events_session(token5) as api:
1049 api.SetEventAttendance(
1050 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1051 )
1053 with events_session(token6) as api:
1054 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1056 with events_session(token1) as api:
1057 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1059 assert res.is_next
1060 assert res.title == "Dummy Title"
1061 assert res.slug == "dummy-title"
1062 assert res.content == "Dummy content."
1063 assert not res.photo_url
1064 assert res.HasField("location")
1065 assert res.location.lat == 0.1
1066 assert res.location.lng == 0.2
1067 assert res.location.address == "Near Null Island"
1068 assert time_before <= to_aware_datetime(res.created) <= now()
1069 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1070 assert res.creator_user_id == user1.id
1071 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
1072 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
1073 assert is_utc_or_gmt(res.timezone)
1074 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
1075 assert res.organizer
1076 assert res.subscriber
1077 assert res.going_count == 2
1078 assert res.organizer_count == 1
1079 assert res.subscriber_count == 3
1080 assert res.owner_user_id == user1.id
1081 assert not res.owner_community_id
1082 assert not res.owner_group_id
1083 assert res.thread.thread_id
1084 assert res.can_edit
1085 assert not res.can_moderate
1087 with events_session(token2) as api:
1088 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1090 assert res.is_next
1091 assert res.title == "Dummy Title"
1092 assert res.slug == "dummy-title"
1093 assert res.content == "Dummy content."
1094 assert not res.photo_url
1095 assert res.HasField("location")
1096 assert res.location.lat == 0.1
1097 assert res.location.lng == 0.2
1098 assert res.location.address == "Near Null Island"
1099 assert time_before <= to_aware_datetime(res.created) <= now()
1100 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1101 assert res.creator_user_id == user1.id
1102 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
1103 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
1104 assert is_utc_or_gmt(res.timezone)
1105 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1106 assert not res.organizer
1107 assert not res.subscriber
1108 assert res.going_count == 2
1109 assert res.organizer_count == 1
1110 assert res.subscriber_count == 3
1111 assert res.owner_user_id == user1.id
1112 assert not res.owner_community_id
1113 assert not res.owner_group_id
1114 assert res.thread.thread_id
1115 assert res.can_edit
1116 assert res.can_moderate
1118 with events_session(token3) as api:
1119 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1121 assert res.is_next
1122 assert res.title == "Dummy Title"
1123 assert res.slug == "dummy-title"
1124 assert res.content == "Dummy content."
1125 assert not res.photo_url
1126 assert res.HasField("location")
1127 assert res.location.lat == 0.1
1128 assert res.location.lng == 0.2
1129 assert res.location.address == "Near Null Island"
1130 assert time_before <= to_aware_datetime(res.created) <= now()
1131 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1132 assert res.creator_user_id == user1.id
1133 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
1134 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
1135 assert is_utc_or_gmt(res.timezone)
1136 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1137 assert not res.organizer
1138 assert not res.subscriber
1139 assert res.going_count == 2
1140 assert res.organizer_count == 1
1141 assert res.subscriber_count == 3
1142 assert res.owner_user_id == user1.id
1143 assert not res.owner_community_id
1144 assert not res.owner_group_id
1145 assert res.thread.thread_id
1146 assert not res.can_edit
1147 assert not res.can_moderate
1150def test_CancelEvent(db, moderator: Moderator):
1151 # event creator
1152 user1, token1 = generate_user()
1153 # community moderator
1154 user2, token2 = generate_user()
1155 # third parties
1156 user3, token3 = generate_user()
1157 user4, token4 = generate_user()
1158 user5, token5 = generate_user()
1159 user6, token6 = generate_user()
1161 with session_scope() as session:
1162 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1164 start_time = now() + timedelta(hours=2)
1165 end_time = start_time + timedelta(hours=3)
1167 with events_session(token1) as api:
1168 res = api.CreateEvent(
1169 events_pb2.CreateEventReq(
1170 title="Dummy Title",
1171 content="Dummy content.",
1172 location=events_pb2.EventLocation(
1173 address="Near Null Island",
1174 lat=0.1,
1175 lng=0.2,
1176 ),
1177 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1178 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1179 )
1180 )
1182 event_id = res.event_id
1184 moderator.approve_event_occurrence(event_id)
1186 with events_session(token4) as api:
1187 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1189 with events_session(token5) as api:
1190 api.SetEventAttendance(
1191 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1192 )
1194 with events_session(token6) as api:
1195 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1197 with events_session(token1) as api:
1198 res = api.CancelEvent(
1199 events_pb2.CancelEventReq(
1200 event_id=event_id,
1201 )
1202 )
1204 with events_session(token1) as api:
1205 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1206 assert res.is_cancelled
1208 with events_session(token1) as api:
1209 with pytest.raises(grpc.RpcError) as e:
1210 api.UpdateEvent(
1211 events_pb2.UpdateEventReq(
1212 event_id=event_id,
1213 title=wrappers_pb2.StringValue(value="New Title"),
1214 )
1215 )
1216 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1217 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1219 with pytest.raises(grpc.RpcError) as e:
1220 api.InviteEventOrganizer(
1221 events_pb2.InviteEventOrganizerReq(
1222 event_id=event_id,
1223 user_id=user3.id,
1224 )
1225 )
1226 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1227 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1229 with pytest.raises(grpc.RpcError) as e:
1230 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
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 events_session(token3) as api:
1235 with pytest.raises(grpc.RpcError) as e:
1236 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1237 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1238 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1240 with pytest.raises(grpc.RpcError) as e:
1241 api.SetEventAttendance(
1242 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1243 )
1244 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1245 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1247 with events_session(token1) as api:
1248 for include_cancelled in [True, False]:
1249 res = api.ListEventOccurrences(
1250 events_pb2.ListEventOccurrencesReq(
1251 event_id=event_id,
1252 include_cancelled=include_cancelled,
1253 )
1254 )
1255 if include_cancelled:
1256 assert len(res.events) > 0
1257 else:
1258 assert len(res.events) == 0
1260 res = api.ListMyEvents(
1261 events_pb2.ListMyEventsReq(
1262 include_cancelled=include_cancelled,
1263 )
1264 )
1265 if include_cancelled:
1266 assert len(res.events) > 0
1267 else:
1268 assert len(res.events) == 0
1271def test_ListEventAttendees(db, moderator: Moderator):
1272 # event creator
1273 user1, token1 = generate_user()
1274 # others
1275 user2, token2 = generate_user()
1276 user3, token3 = generate_user()
1277 user4, token4 = generate_user()
1278 user5, token5 = generate_user()
1279 user6, token6 = generate_user()
1281 with session_scope() as session:
1282 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1284 with events_session(token1) as api:
1285 event_id = api.CreateEvent(
1286 events_pb2.CreateEventReq(
1287 title="Dummy Title",
1288 content="Dummy content.",
1289 location=events_pb2.EventLocation(
1290 address="Near Null Island",
1291 lat=0.1,
1292 lng=0.2,
1293 ),
1294 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1295 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1296 )
1297 ).event_id
1299 moderator.approve_event_occurrence(event_id)
1301 for token in [token2, token3, token4, token5]:
1302 with events_session(token) as api:
1303 api.SetEventAttendance(
1304 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1305 )
1307 with events_session(token6) as api:
1308 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1310 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1311 assert res.attendee_user_ids == [user1.id, user2.id]
1313 res = api.ListEventAttendees(
1314 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1315 )
1316 assert res.attendee_user_ids == [user3.id, user4.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 == [user5.id]
1322 assert not res.next_page_token
1325def test_ListEventSubscribers(db, moderator: Moderator):
1326 # event creator
1327 user1, token1 = generate_user()
1328 # others
1329 user2, token2 = generate_user()
1330 user3, token3 = generate_user()
1331 user4, token4 = generate_user()
1332 user5, token5 = generate_user()
1333 user6, token6 = generate_user()
1335 with session_scope() as session:
1336 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1338 with events_session(token1) as api:
1339 event_id = api.CreateEvent(
1340 events_pb2.CreateEventReq(
1341 title="Dummy Title",
1342 content="Dummy content.",
1343 location=events_pb2.EventLocation(
1344 address="Near Null Island",
1345 lat=0.1,
1346 lng=0.2,
1347 ),
1348 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1349 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1350 )
1351 ).event_id
1353 moderator.approve_event_occurrence(event_id)
1355 for token in [token2, token3, token4, token5]:
1356 with events_session(token) as api:
1357 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1359 with events_session(token6) as api:
1360 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1362 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1363 assert res.subscriber_user_ids == [user1.id, user2.id]
1365 res = api.ListEventSubscribers(
1366 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1367 )
1368 assert res.subscriber_user_ids == [user3.id, user4.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 == [user5.id]
1374 assert not res.next_page_token
1377def test_ListEventOrganizers(db, moderator: Moderator):
1378 # event creator
1379 user1, token1 = generate_user()
1380 # others
1381 user2, token2 = generate_user()
1382 user3, token3 = generate_user()
1383 user4, token4 = generate_user()
1384 user5, token5 = generate_user()
1385 user6, token6 = generate_user()
1387 with session_scope() as session:
1388 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1390 with events_session(token1) as api:
1391 event_id = api.CreateEvent(
1392 events_pb2.CreateEventReq(
1393 title="Dummy Title",
1394 content="Dummy content.",
1395 location=events_pb2.EventLocation(
1396 address="Near Null Island",
1397 lat=0.1,
1398 lng=0.2,
1399 ),
1400 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1401 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1402 )
1403 ).event_id
1405 moderator.approve_event_occurrence(event_id)
1407 with events_session(token1) as api:
1408 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1409 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1411 with events_session(token6) as api:
1412 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1414 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1415 assert res.organizer_user_ids == [user1.id, user2.id]
1417 res = api.ListEventOrganizers(
1418 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1419 )
1420 assert res.organizer_user_ids == [user3.id, user4.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 == [user5.id]
1426 assert not res.next_page_token
1429def test_TransferEvent(db):
1430 user1, token1 = generate_user()
1431 user2, token2 = generate_user()
1432 user3, token3 = generate_user()
1433 user4, token4 = generate_user()
1435 with session_scope() as session:
1436 c = create_community(session, 0, 2, "Community", [user3], [], None)
1437 h = create_group(session, "Group", [user4], [], c)
1438 c_id = c.id
1439 h_id = h.id
1441 with events_session(token1) as api:
1442 event_id = api.CreateEvent(
1443 events_pb2.CreateEventReq(
1444 title="Dummy Title",
1445 content="Dummy content.",
1446 location=events_pb2.EventLocation(
1447 address="Near Null Island",
1448 lat=0.1,
1449 lng=0.2,
1450 ),
1451 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1452 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1453 )
1454 ).event_id
1456 api.TransferEvent(
1457 events_pb2.TransferEventReq(
1458 event_id=event_id,
1459 new_owner_community_id=c_id,
1460 )
1461 )
1463 # remove ourselves as organizer, otherwise we can still edit it
1464 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1466 with pytest.raises(grpc.RpcError) as e:
1467 api.TransferEvent(
1468 events_pb2.TransferEventReq(
1469 event_id=event_id,
1470 new_owner_group_id=h_id,
1471 )
1472 )
1473 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1474 assert e.value.details() == "You're not allowed to transfer that event."
1476 event_id = api.CreateEvent(
1477 events_pb2.CreateEventReq(
1478 title="Dummy Title",
1479 content="Dummy content.",
1480 location=events_pb2.EventLocation(
1481 address="Near Null Island",
1482 lat=0.1,
1483 lng=0.2,
1484 ),
1485 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1486 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1487 )
1488 ).event_id
1490 api.TransferEvent(
1491 events_pb2.TransferEventReq(
1492 event_id=event_id,
1493 new_owner_group_id=h_id,
1494 )
1495 )
1497 # remove ourselves as organizer, otherwise we can still edit it
1498 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1500 with pytest.raises(grpc.RpcError) as e:
1501 api.TransferEvent(
1502 events_pb2.TransferEventReq(
1503 event_id=event_id,
1504 new_owner_community_id=c_id,
1505 )
1506 )
1507 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1508 assert e.value.details() == "You're not allowed to transfer that event."
1511def test_SetEventSubscription(db, moderator: Moderator):
1512 user1, token1 = generate_user()
1513 user2, token2 = generate_user()
1515 with session_scope() as session:
1516 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1518 with events_session(token1) as api:
1519 event_id = api.CreateEvent(
1520 events_pb2.CreateEventReq(
1521 title="Dummy Title",
1522 content="Dummy content.",
1523 location=events_pb2.EventLocation(
1524 address="Near Null Island",
1525 lat=0.1,
1526 lng=0.2,
1527 ),
1528 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1529 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1530 )
1531 ).event_id
1533 moderator.approve_event_occurrence(event_id)
1535 with events_session(token2) as api:
1536 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1537 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1538 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1539 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1540 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1543def test_SetEventAttendance(db, moderator: Moderator):
1544 user1, token1 = generate_user()
1545 user2, token2 = generate_user()
1547 with session_scope() as session:
1548 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1550 with events_session(token1) as api:
1551 event_id = api.CreateEvent(
1552 events_pb2.CreateEventReq(
1553 title="Dummy Title",
1554 content="Dummy content.",
1555 location=events_pb2.EventLocation(
1556 address="Near Null Island",
1557 lat=0.1,
1558 lng=0.2,
1559 ),
1560 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1561 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1562 )
1563 ).event_id
1565 moderator.approve_event_occurrence(event_id)
1567 with events_session(token2) as api:
1568 assert (
1569 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1570 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1571 )
1572 api.SetEventAttendance(
1573 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1574 )
1575 assert (
1576 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1577 == events_pb2.ATTENDANCE_STATE_GOING
1578 )
1579 api.SetEventAttendance(
1580 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1581 )
1582 assert (
1583 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1584 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1585 )
1588def test_InviteEventOrganizer(db, moderator: Moderator):
1589 user1, token1 = generate_user()
1590 user2, token2 = generate_user()
1592 with session_scope() as session:
1593 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1595 with events_session(token1) as api:
1596 event_id = api.CreateEvent(
1597 events_pb2.CreateEventReq(
1598 title="Dummy Title",
1599 content="Dummy content.",
1600 location=events_pb2.EventLocation(
1601 address="Near Null Island",
1602 lat=0.1,
1603 lng=0.2,
1604 ),
1605 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1606 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1607 )
1608 ).event_id
1610 moderator.approve_event_occurrence(event_id)
1612 with events_session(token2) as api:
1613 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1615 with pytest.raises(grpc.RpcError) as e:
1616 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1617 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1618 assert e.value.details() == "You're not allowed to edit that event."
1620 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1622 with events_session(token1) as api:
1623 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1625 with events_session(token2) as api:
1626 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1629def test_ListEventOccurrences(db):
1630 user1, token1 = generate_user()
1631 user2, token2 = generate_user()
1632 user3, token3 = generate_user()
1634 with session_scope() as session:
1635 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1637 start = now()
1639 event_ids = []
1641 with events_session(token1) as api:
1642 res = api.CreateEvent(
1643 events_pb2.CreateEventReq(
1644 title="First occurrence",
1645 content="Dummy content.",
1646 parent_community_id=c_id,
1647 location=events_pb2.EventLocation(
1648 address="Near Null Island",
1649 lat=0.1,
1650 lng=0.2,
1651 ),
1652 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1653 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
1654 )
1655 )
1657 event_ids.append(res.event_id)
1659 for i in range(5):
1660 res = api.ScheduleEvent(
1661 events_pb2.ScheduleEventReq(
1662 event_id=event_ids[-1],
1663 content=f"{i}th occurrence",
1664 location=events_pb2.EventLocation(
1665 address="Near Null Island",
1666 lat=0.1,
1667 lng=0.2,
1668 ),
1669 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
1670 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
1671 )
1672 )
1674 event_ids.append(res.event_id)
1676 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1677 assert [event.event_id for event in res.events] == event_ids[:2]
1679 res = api.ListEventOccurrences(
1680 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1681 )
1682 assert [event.event_id for event in res.events] == event_ids[2:4]
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[4:6]
1688 assert not res.next_page_token
1691def test_ListMyEvents(db, moderator: Moderator):
1692 user1, token1 = generate_user()
1693 user2, token2 = generate_user()
1694 user3, token3 = generate_user()
1695 user4, token4 = generate_user()
1696 user5, token5 = generate_user()
1698 with session_scope() as session:
1699 # Create global (world) -> macroregion -> region -> subregion hierarchy
1700 # my_communities_exclude_global filters out world, macroregion, and region level communities
1701 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1702 c_id = global_community.id
1703 macroregion_community = create_community(
1704 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1705 )
1706 region_community = create_community(
1707 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1708 )
1709 subregion_community = create_community(
1710 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1711 )
1712 c2_id = subregion_community.id
1714 start = now()
1716 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1717 return events_pb2.CreateEventReq(
1718 title="Dummy Title",
1719 content="Dummy content.",
1720 location=events_pb2.EventLocation(
1721 address="Near Null Island",
1722 lat=0.1,
1723 lng=0.2,
1724 ),
1725 parent_community_id=community_id,
1726 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now)),
1727 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now + 0.5)),
1728 )
1730 with events_session(token1) as api:
1731 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1733 moderator.approve_event_occurrence(e2)
1735 with events_session(token2) as api:
1736 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1738 moderator.approve_event_occurrence(e1)
1740 with events_session(token1) as api:
1741 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1743 moderator.approve_event_occurrence(e3)
1745 with events_session(token2) as api:
1746 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1748 moderator.approve_event_occurrence(e5)
1750 with events_session(token3) as api:
1751 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1753 moderator.approve_event_occurrence(e4)
1755 with events_session(token4) as api:
1756 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1758 moderator.approve_event_occurrence(e6)
1760 with events_session(token1) as api:
1761 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1763 with events_session(token1) as api:
1764 api.SetEventAttendance(
1765 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1766 )
1767 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1769 with events_session(token2) as api:
1770 api.SetEventAttendance(
1771 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1772 )
1774 with events_session(token3) as api:
1775 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1777 with events_session(token1) as api:
1778 # test pagination with token first
1779 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1780 assert [event.event_id for event in res.events] == [e1, e2]
1781 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1782 assert [event.event_id for event in res.events] == [e3, e4]
1783 assert not res.next_page_token
1785 res = api.ListMyEvents(
1786 events_pb2.ListMyEventsReq(
1787 subscribed=True,
1788 attending=True,
1789 organizing=True,
1790 )
1791 )
1792 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1794 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1795 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1797 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1798 assert [event.event_id for event in res.events] == [e2, e3, e4]
1800 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1801 assert [event.event_id for event in res.events] == [e1, e2, e3]
1803 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1804 assert [event.event_id for event in res.events] == [e2, e3]
1806 with events_session(token1) as api:
1807 # Test pagination with page_number and verify total_items
1808 res = api.ListMyEvents(
1809 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1810 )
1811 assert [event.event_id for event in res.events] == [e1, e2]
1812 assert res.total_items == 4
1814 res = api.ListMyEvents(
1815 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1816 )
1817 assert [event.event_id for event in res.events] == [e3, e4]
1818 assert res.total_items == 4
1820 # Verify no more pages
1821 res = api.ListMyEvents(
1822 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1823 )
1824 assert not res.events
1825 assert res.total_items == 4
1827 with events_session(token2) as api:
1828 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1829 assert [event.event_id for event in res.events] == [e1, e3, e5]
1831 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1832 assert [event.event_id for event in res.events] == [e1, e5]
1834 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1835 assert [event.event_id for event in res.events] == [e1, e3, e5]
1837 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1838 assert [event.event_id for event in res.events] == [e1, e5]
1840 with events_session(token3) as api:
1841 # user3 is member of both global (c_id) and child (c2_id) communities
1842 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1843 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1845 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1846 assert [event.event_id for event in res.events] == [e2, e4]
1848 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1849 assert [event.event_id for event in res.events] == [e4]
1851 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1852 assert [event.event_id for event in res.events] == [e3, e4]
1854 # my_communities returns events from both communities user3 is a member of
1855 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1856 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1858 # my_communities_exclude_global filters out events from global community (node_id=1)
1859 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1860 assert [event.event_id for event in res.events] == [e6]
1862 # my_communities_exclude_global works independently of my_communities flag
1863 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1864 assert [event.event_id for event in res.events] == [e6]
1866 # my_communities_exclude_global filters organizing results too
1867 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1868 assert [event.event_id for event in res.events] == []
1870 # my_communities_exclude_global filters subscribed results too
1871 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1872 assert [event.event_id for event in res.events] == []
1874 with events_session(token5) as api:
1875 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1876 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1879def _paginate_my_events(api, page_size: int) -> list[int]:
1880 event_ids = []
1881 page_token = ""
1882 for _ in range(10): 1882 ↛ 1888line 1882 didn't jump to line 1888 because the loop on line 1882 didn't complete
1883 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=page_size, page_token=page_token))
1884 event_ids += [event.event_id for event in res.events]
1885 page_token = res.next_page_token
1886 if not page_token:
1887 return event_ids
1888 raise AssertionError("pagination did not terminate")
1891def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator):
1892 user, token = generate_user()
1894 with session_scope() as session:
1895 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1897 start = now()
1899 def new_event(start_offset: timedelta, duration: timedelta) -> events_pb2.CreateEventReq:
1900 return events_pb2.CreateEventReq(
1901 title="Dummy Title",
1902 content="Dummy content.",
1903 location=events_pb2.EventLocation(
1904 address="Near Null Island",
1905 lat=0.1,
1906 lng=0.2,
1907 ),
1908 parent_community_id=c_id,
1909 start_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset),
1910 end_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset + duration),
1911 )
1913 with events_session(token) as api:
1914 # a multi-day event overlapping all the short events below: it ends last but starts first,
1915 # so an end time based cursor would repeat it on every page and skip the short events
1916 long_event = api.CreateEvent(new_event(timedelta(hours=1), timedelta(days=3))).event_id
1917 short_events = [
1918 api.CreateEvent(new_event(timedelta(hours=2 + i), timedelta(hours=1))).event_id for i in range(4)
1919 ]
1921 for event_id in [long_event, *short_events]:
1922 moderator.approve_event_occurrence(event_id)
1924 with events_session(token) as api:
1925 assert _paginate_my_events(api, page_size=2) == [long_event, *short_events]
1928def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator):
1929 user, token = generate_user()
1931 with session_scope() as session:
1932 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1934 start = now()
1936 with events_session(token) as api:
1937 event_ids = [
1938 api.CreateEvent(
1939 events_pb2.CreateEventReq(
1940 title="Dummy Title",
1941 content="Dummy content.",
1942 location=events_pb2.EventLocation(
1943 address="Near Null Island",
1944 lat=0.1,
1945 lng=0.2,
1946 ),
1947 parent_community_id=c_id,
1948 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1949 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
1950 )
1951 ).event_id
1952 for _ in range(5)
1953 ]
1955 for event_id in event_ids:
1956 moderator.approve_event_occurrence(event_id)
1958 with events_session(token) as api:
1959 assert _paginate_my_events(api, page_size=2) == event_ids
1962def test_list_my_events_exclude_attending(db, moderator: Moderator):
1963 user1, token1 = generate_user()
1964 user2, token2 = generate_user()
1966 with session_scope() as session:
1967 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1968 c_id = c.id
1970 start = now()
1972 def make_event(hours):
1973 return events_pb2.CreateEventReq(
1974 title="Test Event",
1975 content="Test content.",
1976 location=events_pb2.EventLocation(
1977 address="Near Null Island",
1978 lat=0.1,
1979 lng=0.2,
1980 ),
1981 parent_community_id=c_id,
1982 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours)),
1983 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours + 1)),
1984 )
1986 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1987 with events_session(token1) as api:
1988 e_own = api.CreateEvent(make_event(1)).event_id
1990 with events_session(token2) as api:
1991 e_attending = api.CreateEvent(make_event(2)).event_id
1992 e_community_only = api.CreateEvent(make_event(3)).event_id
1993 # e_both: user1 will be both organizer and attendee
1994 e_both = api.CreateEvent(make_event(4)).event_id
1996 moderator.approve_event_occurrence(e_own)
1997 moderator.approve_event_occurrence(e_attending)
1998 moderator.approve_event_occurrence(e_community_only)
1999 moderator.approve_event_occurrence(e_both)
2001 # invite user1 as organizer of e_both
2002 with events_session(token2) as api:
2003 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
2005 # user1 RSVPs to e_attending and e_both
2006 with events_session(token1) as api:
2007 api.SetEventAttendance(
2008 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2009 )
2010 api.SetEventAttendance(
2011 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2012 )
2014 with events_session(token1) as api:
2015 # baseline: all four community events visible
2016 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
2017 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
2019 # exclude_attending removes events user1 is attending (e_attending, e_both)
2020 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
2021 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
2022 assert [e.event_id for e in res.events] == [e_community_only]
2024 # exclude_attending with attending=True: invalid combination
2025 with pytest.raises(grpc.RpcError) as e:
2026 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
2027 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
2029 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
2030 with events_session(token2) as api:
2031 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
2032 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
2033 assert [e.event_id for e in res.events] == [e_own]
2036def test_RemoveEventOrganizer(db, moderator: Moderator):
2037 user1, token1 = generate_user()
2038 user2, token2 = generate_user()
2040 with session_scope() as session:
2041 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2043 with events_session(token1) as api:
2044 event_id = api.CreateEvent(
2045 events_pb2.CreateEventReq(
2046 title="Dummy Title",
2047 content="Dummy content.",
2048 location=events_pb2.EventLocation(
2049 address="Near Null Island",
2050 lat=0.1,
2051 lng=0.2,
2052 ),
2053 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2054 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2055 )
2056 ).event_id
2058 moderator.approve_event_occurrence(event_id)
2060 with events_session(token2) as api:
2061 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2063 with pytest.raises(grpc.RpcError) as e:
2064 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2065 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2066 assert e.value.details() == "You're not allowed to edit that event."
2068 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2070 with events_session(token1) as api:
2071 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2073 with pytest.raises(grpc.RpcError) as e:
2074 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2075 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2076 assert e.value.details() == "You cannot remove the event owner as an organizer."
2078 with events_session(token2) as api:
2079 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2080 assert res.organizer
2081 assert res.organizer_count == 2
2082 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2083 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2085 with pytest.raises(grpc.RpcError) as e:
2086 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2087 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2088 assert e.value.details() == "You're not allowed to edit that event."
2090 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2091 assert not res.organizer
2092 assert res.organizer_count == 1
2094 # Test that event owner can remove co-organizers
2095 with events_session(token1) as api:
2096 # Add user2 back as organizer
2097 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2099 # Verify user2 is now an organizer
2100 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2101 assert res.organizer_count == 2
2103 # Event owner can remove co-organizer
2104 api.RemoveEventOrganizer(
2105 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
2106 )
2108 # Verify user2 is no longer an organizer
2109 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2110 assert res.organizer_count == 1
2112 # Test that non-organizers cannot remove other organizers
2113 with events_session(token2) as api:
2114 # User2 cannot invite themselves as organizer (not the owner)
2115 with pytest.raises(grpc.RpcError) as e:
2116 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2117 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2118 assert e.value.details() == "You're not allowed to edit that event."
2120 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
2121 with events_session(token1) as api:
2122 # Add user2 back as organizer
2123 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2126def test_ListEventAttendees_regression(db):
2127 # see issue #1617:
2128 #
2129 # 1. Create an event
2130 # 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`
2131 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
2132 # 4. Change the current user's attendance state to "going" again
2133 #
2134 # **Expected behaviour**
2135 # `ListEventAttendees` should return the current user's ID
2136 #
2137 # **Actual/current behaviour**
2138 # `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
2140 user1, token1 = generate_user()
2141 user2, token2 = generate_user()
2142 user3, token3 = generate_user()
2143 user4, token4 = generate_user()
2144 user5, token5 = generate_user()
2146 with session_scope() as session:
2147 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2149 start_time = now() + timedelta(hours=2)
2150 end_time = start_time + timedelta(hours=3)
2152 with events_session(token1) as api:
2153 res = api.CreateEvent(
2154 events_pb2.CreateEventReq(
2155 title="Dummy Title",
2156 content="Dummy content.",
2157 location=events_pb2.EventLocation(
2158 address="Near Null Island",
2159 lat=0.1,
2160 lng=0.2,
2161 ),
2162 parent_community_id=c_id,
2163 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2164 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2165 )
2166 )
2168 res = api.TransferEvent(
2169 events_pb2.TransferEventReq(
2170 event_id=res.event_id,
2171 new_owner_community_id=c_id,
2172 )
2173 )
2175 event_id = res.event_id
2177 api.SetEventAttendance(
2178 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2179 )
2180 api.SetEventAttendance(
2181 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2182 )
2184 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2185 assert len(res.attendee_user_ids) == 1
2186 assert res.attendee_user_ids[0] == user1.id
2189def test_GetEventCalendarFile(db, moderator: Moderator):
2190 user1, token1 = generate_user()
2191 user2, token2 = generate_user()
2193 with session_scope() as session:
2194 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2196 start_time = now() + timedelta(hours=2)
2197 end_time = start_time + timedelta(hours=3)
2199 with events_session(token1) as api:
2200 created_event: events_pb2.Event = api.CreateEvent(
2201 events_pb2.CreateEventReq(
2202 title="Dummy Title",
2203 content="Dummy content.",
2204 parent_community_id=c_id,
2205 location=events_pb2.EventLocation(
2206 address="Near Null Island",
2207 lat=0.1,
2208 lng=0.2,
2209 ),
2210 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2211 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2212 )
2213 )
2214 event_id = created_event.event_id
2216 moderator.approve_event_occurrence(event_id)
2218 with events_session(token1) as api:
2219 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
2220 assert file_res.content_type == "text/calendar"
2221 ics_string = file_res.data.decode("utf-8")
2222 assert "SUMMARY:Dummy Title" in ics_string
2223 assert "DESCRIPTION:Dummy content." in ics_string
2224 assert "LOCATION:Near Null Island" in ics_string
2225 assert "STATUS:CANCELLED" not in ics_string
2226 pre_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
2227 assert pre_cancel_match is not None
2228 pre_cancel_sequence = int(pre_cancel_match.group(1))
2230 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2232 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
2233 ics_string = file_res.data.decode("utf-8")
2234 assert "SUMMARY:Cancelled: Dummy Title" in ics_string
2235 assert "STATUS:CANCELLED" in ics_string
2236 post_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
2237 assert post_cancel_match is not None
2238 post_cancel_sequence = int(post_cancel_match.group(1))
2239 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal.
2240 assert post_cancel_sequence >= pre_cancel_sequence
2243def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2244 user1, token1 = generate_user()
2245 user2, token2 = generate_user()
2246 user3, token3 = generate_user()
2247 user4, token4 = generate_user()
2249 with session_scope() as session:
2250 c = create_community(session, 0, 2, "Community", [user3], [], None)
2251 h = create_group(session, "Group", [user4], [], c)
2252 c_id = c.id
2253 h_id = h.id
2254 user4_id = user4.id
2256 with events_session(token1) as api:
2257 event = api.CreateEvent(
2258 events_pb2.CreateEventReq(
2259 title="Dummy Title",
2260 content="Dummy content.",
2261 location=events_pb2.EventLocation(
2262 address="Near Null Island",
2263 lat=0.1,
2264 lng=0.2,
2265 ),
2266 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2267 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2268 )
2269 )
2271 moderator.approve_event_occurrence(event.event_id)
2273 with threads_session(token2) as api:
2274 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2276 moderator.approve_thread_post(reply_id)
2278 with events_session(token3) as api:
2279 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2280 assert res.thread.num_responses == 1
2282 with threads_session(token3) as api:
2283 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2284 assert len(ret.replies) == 1
2285 assert not ret.next_page_token
2286 assert ret.replies[0].thread_id == reply_id
2287 assert ret.replies[0].content == "hi"
2288 assert ret.replies[0].author_user_id == user2.id
2289 assert ret.replies[0].num_replies == 0
2291 nested_reply_id = api.PostReply(
2292 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2293 ).thread_id
2295 moderator.approve_thread_post(nested_reply_id)
2297 process_jobs()
2299 push = push_collector.pop_for_user(user1.id, last=True)
2300 assert push.topic_action == NotificationTopicAction.event__comment.display
2301 assert push.content.title == f"{user2.name} • Dummy Title"
2302 assert push.content.ios_title == user2.name
2303 assert push.content.ios_subtitle == "Commented on Dummy Title"
2304 assert push.content.body == "hi"
2306 push = push_collector.pop_for_user(user2.id, last=True)
2307 assert push.content.title == f"{user3.name} • Dummy Title"
2309 assert push_collector.count_for_user(user4_id) == 0
2312def test_can_overlap_other_events_schedule_regression(db):
2313 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2314 user, token = generate_user()
2316 with session_scope() as session:
2317 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2319 start = now()
2321 with events_session(token) as api:
2322 # create another event, should be able to overlap with this one
2323 api.CreateEvent(
2324 events_pb2.CreateEventReq(
2325 title="Dummy Title",
2326 content="Dummy content.",
2327 parent_community_id=c_id,
2328 location=events_pb2.EventLocation(
2329 address="Near Null Island",
2330 lat=0.1,
2331 lng=0.2,
2332 ),
2333 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2334 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)),
2335 )
2336 )
2338 # this event
2339 res = api.CreateEvent(
2340 events_pb2.CreateEventReq(
2341 title="Dummy Title",
2342 content="Dummy content.",
2343 parent_community_id=c_id,
2344 location=events_pb2.EventLocation(
2345 address="Near Null Island",
2346 lat=0.1,
2347 lng=0.2,
2348 ),
2349 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2350 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
2351 )
2352 )
2354 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2355 api.ScheduleEvent(
2356 events_pb2.ScheduleEventReq(
2357 event_id=res.event_id,
2358 content="New event occurrence",
2359 location=events_pb2.EventLocation(
2360 address="A bit further but still near Null Island",
2361 lat=0.3,
2362 lng=0.2,
2363 ),
2364 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2365 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2366 )
2367 )
2370def test_can_overlap_other_events_update_regression(db):
2371 user, token = generate_user()
2373 with session_scope() as session:
2374 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2376 start = now()
2378 with events_session(token) as api:
2379 # create another event, should be able to overlap with this one
2380 api.CreateEvent(
2381 events_pb2.CreateEventReq(
2382 title="Dummy Title",
2383 content="Dummy content.",
2384 parent_community_id=c_id,
2385 location=events_pb2.EventLocation(
2386 address="Near Null Island",
2387 lat=0.1,
2388 lng=0.2,
2389 ),
2390 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2391 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2392 )
2393 )
2395 res = api.CreateEvent(
2396 events_pb2.CreateEventReq(
2397 title="Dummy Title",
2398 content="Dummy content.",
2399 parent_community_id=c_id,
2400 location=events_pb2.EventLocation(
2401 address="Near Null Island",
2402 lat=0.1,
2403 lng=0.2,
2404 ),
2405 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)),
2406 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)),
2407 )
2408 )
2410 event_id = api.ScheduleEvent(
2411 events_pb2.ScheduleEventReq(
2412 event_id=res.event_id,
2413 content="New event occurrence",
2414 location=events_pb2.EventLocation(
2415 address="A bit further but still near Null Island",
2416 lat=0.3,
2417 lng=0.2,
2418 ),
2419 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2420 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2421 )
2422 ).event_id
2424 # can overlap with this current existing occurrence
2425 api.UpdateEvent(
2426 events_pb2.UpdateEventReq(
2427 event_id=event_id,
2428 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2429 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2430 ),
2431 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2432 value=datetime_to_iso8601_local(start + timedelta(hours=6))
2433 ),
2434 )
2435 )
2437 api.UpdateEvent(
2438 events_pb2.UpdateEventReq(
2439 event_id=event_id,
2440 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2441 value=datetime_to_iso8601_local(start + timedelta(hours=2))
2442 ),
2443 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2444 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2445 ),
2446 )
2447 )
2450def test_list_past_events_regression(db):
2451 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2452 user, token = generate_user()
2454 with session_scope() as session:
2455 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2457 start = now()
2459 with events_session(token) as api:
2460 api.CreateEvent(
2461 events_pb2.CreateEventReq(
2462 title="Dummy Title",
2463 content="Dummy content.",
2464 parent_community_id=c_id,
2465 location=events_pb2.EventLocation(
2466 address="Near Null Island",
2467 lat=0.1,
2468 lng=0.2,
2469 ),
2470 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2471 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2472 )
2473 )
2475 with session_scope() as session:
2476 session.execute(
2477 update(EventOccurrence).values(
2478 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2479 )
2480 )
2482 with events_session(token) as api:
2483 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2484 assert len(res.events) == 1
2487def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2488 user1, token1 = generate_user(complete_profile=True)
2489 user2, token2 = generate_user()
2490 user3, token3 = generate_user()
2491 user4, token4 = generate_user()
2492 user5, token5 = generate_user(is_superuser=True)
2494 with session_scope() as session:
2495 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2496 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2497 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2498 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2500 enforce_community_memberships()
2502 with events_session(token1) as api:
2503 res = api.CreateEvent(
2504 events_pb2.CreateEventReq(
2505 title="Dummy Title",
2506 content="Dummy content.",
2507 parent_community_id=c_id,
2508 location=events_pb2.EventLocation(
2509 address="Near Null Island",
2510 lat=0.1,
2511 lng=0.2,
2512 ),
2513 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2514 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2515 )
2516 )
2517 user_url = f"http://localhost:3000/user/{user1.username}"
2518 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2520 event_id = res.event_id
2522 moderator.approve_event_occurrence(event_id)
2524 with events_session(token1) as api:
2525 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2527 email = email_collector.pop_for_mods(last=True)
2529 assert user_url in email.plain
2530 assert event_url in email.plain
2532 # can't send another req
2533 with pytest.raises(grpc.RpcError) as err:
2534 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2535 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2536 assert err.value.details() == "You have already requested a community invite for this event."
2538 # another user can send one though
2539 with events_session(token3) as api:
2540 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2542 # but not a non-admin
2543 with events_session(token2) as api:
2544 with pytest.raises(grpc.RpcError) as err:
2545 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2546 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2547 assert err.value.details() == "You're not allowed to edit that event."
2549 with real_editor_session(token5) as editor:
2550 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2551 assert len(res.requests) == 2
2552 assert res.requests[0].user_id == user1.id
2553 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain)
2554 assert res.requests[0].approx_users_to_notify == 2
2555 assert res.requests[1].user_id == user3.id
2556 assert res.requests[1].approx_users_to_notify == 2
2558 editor.DecideEventCommunityInviteRequest(
2559 editor_pb2.DecideEventCommunityInviteRequestReq(
2560 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2561 approve=False,
2562 )
2563 )
2565 editor.DecideEventCommunityInviteRequest(
2566 editor_pb2.DecideEventCommunityInviteRequestReq(
2567 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2568 approve=True,
2569 )
2570 )
2572 # not after approve
2573 with events_session(token4) as api:
2574 with pytest.raises(grpc.RpcError) as err:
2575 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2576 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2577 assert err.value.details() == "A community invite has already been sent out for this event."
2580def test_list_decided_community_invite_requests(db, moderator: Moderator):
2581 user1, token1 = generate_user()
2582 user2, token2 = generate_user()
2583 user3, token3 = generate_user()
2584 superuser, superuser_token = generate_user(is_superuser=True)
2586 with session_scope() as session:
2587 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2588 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2589 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2590 c_id = create_community(session, 0, 2, "Community", [user1, user2, user3], [], r).id
2592 enforce_community_memberships()
2594 def create_event_and_request_invite(token: str, title: str) -> str:
2595 with events_session(token) as api:
2596 res = api.CreateEvent(
2597 events_pb2.CreateEventReq(
2598 title=title,
2599 content="Dummy content.",
2600 parent_community_id=c_id,
2601 location=events_pb2.EventLocation(
2602 address="Near Null Island",
2603 lat=0.1,
2604 lng=0.2,
2605 ),
2606 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2607 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2608 )
2609 )
2610 moderator.approve_event_occurrence(res.event_id)
2611 with events_session(token) as api:
2612 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=res.event_id))
2613 return f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2615 event1_url = create_event_and_request_invite(token1, "Approved Event")
2616 event2_url = create_event_and_request_invite(token2, "Declined Event")
2617 # this one stays pending
2618 create_event_and_request_invite(token3, "Pending Event")
2620 with real_editor_session(superuser_token) as editor:
2621 pending = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2622 assert len(pending.requests) == 3
2623 by_user = {req.user_id: req.event_community_invite_request_id for req in pending.requests}
2625 # nothing decided yet
2626 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
2627 assert len(res.requests) == 0
2628 assert not res.next_page_token
2630 editor.DecideEventCommunityInviteRequest(
2631 editor_pb2.DecideEventCommunityInviteRequestReq(
2632 event_community_invite_request_id=by_user[user1.id],
2633 approve=True,
2634 )
2635 )
2636 editor.DecideEventCommunityInviteRequest(
2637 editor_pb2.DecideEventCommunityInviteRequestReq(
2638 event_community_invite_request_id=by_user[user2.id],
2639 approve=False,
2640 )
2641 )
2643 # the pending one is not returned, and the most recently decided comes first
2644 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
2645 assert len(res.requests) == 2
2646 assert not res.next_page_token
2648 declined, approved = res.requests
2650 assert declined.event_community_invite_request_id == by_user[user2.id]
2651 assert declined.user_id == user2.id
2652 assert declined.event_url == event2_url
2653 assert declined.community_id == c_id
2654 assert declined.decided_by_user_id == superuser.id
2655 assert not declined.approved
2656 assert declined.created.ToDatetime() <= declined.decided.ToDatetime()
2658 assert approved.event_community_invite_request_id == by_user[user1.id]
2659 assert approved.user_id == user1.id
2660 assert approved.event_url == event1_url
2661 assert approved.community_id == c_id
2662 assert approved.decided_by_user_id == superuser.id
2663 assert approved.approved
2664 assert approved.decided.ToDatetime() <= declined.decided.ToDatetime()
2666 # filtering
2667 res = editor.ListDecidedEventCommunityInviteRequests(
2668 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=True))
2669 )
2670 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
2672 res = editor.ListDecidedEventCommunityInviteRequests(
2673 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=False))
2674 )
2675 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
2677 # pagination
2678 res = editor.ListDecidedEventCommunityInviteRequests(
2679 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1)
2680 )
2681 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
2682 assert res.next_page_token
2684 res = editor.ListDecidedEventCommunityInviteRequests(
2685 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1, page_token=res.next_page_token)
2686 )
2687 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
2688 assert not res.next_page_token
2691def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator):
2692 # Regression: users who already RSVP'd (or organize the event) must not get the
2693 # community invite notification when it is approved.
2694 organizer, organizer_token = generate_user()
2695 attendee, attendee_token = generate_user()
2696 member, _ = generate_user()
2697 superuser, superuser_token = generate_user(is_superuser=True)
2699 with session_scope() as session:
2700 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2701 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2702 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2703 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id
2705 enforce_community_memberships()
2707 with events_session(organizer_token) as api:
2708 event_id = api.CreateEvent(
2709 events_pb2.CreateEventReq(
2710 title="Dummy Title",
2711 content="Dummy content.",
2712 parent_community_id=c_id,
2713 location=events_pb2.EventLocation(
2714 address="Near Null Island",
2715 lat=0.1,
2716 lng=0.2,
2717 ),
2718 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2719 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2720 )
2721 ).event_id
2723 moderator.approve_event_occurrence(event_id)
2725 # the attendee RSVPs before the community invite is approved
2726 with events_session(attendee_token) as api:
2727 api.SetEventAttendance(
2728 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2729 )
2731 with events_session(organizer_token) as api:
2732 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2734 with real_editor_session(superuser_token) as editor:
2735 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2736 editor.DecideEventCommunityInviteRequest(
2737 editor_pb2.DecideEventCommunityInviteRequestReq(
2738 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2739 approve=True,
2740 )
2741 )
2743 process_jobs()
2745 with session_scope() as session:
2747 def invite_notification_count(user_id: int) -> int:
2748 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all()
2749 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved])
2751 # a plain community member gets the invite...
2752 assert invite_notification_count(member.id) == 1
2753 # ...but the attendee and the organizer don't
2754 assert invite_notification_count(attendee.id) == 0
2755 assert invite_notification_count(organizer.id) == 0
2758def test_update_event_should_notify_queues_job():
2759 user, token = generate_user()
2760 start = now()
2762 with session_scope() as session:
2763 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2765 # create an event
2766 with events_session(token) as api:
2767 create_res = api.CreateEvent(
2768 events_pb2.CreateEventReq(
2769 title="Dummy Title",
2770 content="Dummy content.",
2771 parent_community_id=c_id,
2772 location=events_pb2.EventLocation(
2773 address="Near Null Island",
2774 lat=1.0,
2775 lng=2.0,
2776 ),
2777 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2778 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2779 )
2780 )
2782 event_id = create_res.event_id
2784 # measure initial background job queue length
2785 with session_scope() as session:
2786 jobs = session.query(BackgroundJob).all()
2787 job_length_before_update = len(jobs)
2789 # update with should_notify=False, expect no change in background job queue
2790 api.UpdateEvent(
2791 events_pb2.UpdateEventReq(
2792 event_id=event_id,
2793 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2794 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2795 ),
2796 should_notify=False,
2797 )
2798 )
2800 with session_scope() as session:
2801 jobs = session.query(BackgroundJob).all()
2802 assert len(jobs) == job_length_before_update
2804 # update with should_notify=True, expect one new background job added
2805 api.UpdateEvent(
2806 events_pb2.UpdateEventReq(
2807 event_id=event_id,
2808 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2809 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2810 ),
2811 should_notify=True,
2812 )
2813 )
2815 with session_scope() as session:
2816 jobs = session.query(BackgroundJob).all()
2817 assert len(jobs) == job_length_before_update + 1
2820def test_event_photo_key(db):
2821 """Test that events return the photo_key field when a photo is set."""
2822 user, token = generate_user()
2824 start_time = now() + timedelta(hours=2)
2825 end_time = start_time + timedelta(hours=3)
2827 # Create a community and an upload for the event photo
2828 with session_scope() as session:
2829 create_community(session, 0, 2, "Community", [user], [], None)
2830 upload = Upload(
2831 key="test_event_photo_key_123",
2832 filename="test_event_photo_key_123.jpg",
2833 creator_user_id=user.id,
2834 )
2835 session.add(upload)
2837 with events_session(token) as api:
2838 # Create event without photo
2839 res = api.CreateEvent(
2840 events_pb2.CreateEventReq(
2841 title="Event Without Photo",
2842 content="No photo content.",
2843 photo_key=None,
2844 location=events_pb2.EventLocation(
2845 address="Near Null Island",
2846 lat=0.1,
2847 lng=0.2,
2848 ),
2849 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2850 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2851 )
2852 )
2854 assert res.photo_key == ""
2855 assert res.photo_url == ""
2857 # Create event with photo
2858 res_with_photo = api.CreateEvent(
2859 events_pb2.CreateEventReq(
2860 title="Event With Photo",
2861 content="Has photo content.",
2862 photo_key="test_event_photo_key_123",
2863 location=events_pb2.EventLocation(
2864 address="Near Null Island",
2865 lat=0.1,
2866 lng=0.2,
2867 ),
2868 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)),
2869 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)),
2870 )
2871 )
2873 assert res_with_photo.photo_key == "test_event_photo_key_123"
2874 assert "test_event_photo_key_123" in res_with_photo.photo_url
2876 event_id = res_with_photo.event_id
2878 # Verify photo_key is returned when getting the event
2879 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2880 assert get_res.photo_key == "test_event_photo_key_123"
2881 assert "test_event_photo_key_123" in get_res.photo_url
2884def test_event_timezone(db):
2885 user, token = generate_user()
2887 with session_scope() as session:
2888 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2890 # Midnight future day, UTC timezone
2891 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2892 end_time = start_time + timedelta(days=1)
2894 with events_session(token) as api:
2895 create_res: events_pb2.Event = api.CreateEvent(
2896 events_pb2.CreateEventReq(
2897 title="Dummy Title",
2898 content="Dummy content.",
2899 photo_key=None,
2900 parent_community_id=c_id,
2901 # timezone_areas.sql-fake has a region for Europe/Helsinki
2902 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831),
2903 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2904 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2905 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2906 )
2907 )
2909 # Backend should have deduced the helsinki timezone when creating the event,
2910 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2911 assert create_res.timezone == "Europe/Helsinki"
2912 assert to_aware_datetime(create_res.start_time).hour != 0
2913 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2915 # Now update its location such that it gets a new timezone
2916 update_res: events_pb2.Event = api.UpdateEvent(
2917 events_pb2.UpdateEventReq(
2918 event_id=create_res.event_id,
2919 # timezone_areas.sql-fake has a region for America/New_York
2920 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974),
2921 )
2922 )
2924 # The user didn't touch the datetime components on the frontend,
2925 # so they expect the event to be at the same local time (midnight),
2926 # but now in the New York timezone.
2927 assert update_res.timezone == "America/New_York"
2928 assert update_res.start_time != create_res.start_time
2929 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2930 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2932 # Also validate GetEvent
2933 get_res: events_pb2.Event = api.GetEvent(
2934 events_pb2.GetEventReq(
2935 event_id=create_res.event_id,
2936 )
2937 )
2939 assert get_res.timezone == update_res.timezone
2940 assert get_res.start_time == update_res.start_time
2943def test_event_created_with_shadowed_visibility(db):
2944 """Events start in SHADOWED state when created."""
2945 user, token = generate_user()
2947 with session_scope() as session:
2948 create_community(session, 0, 2, "Community", [user], [], None)
2950 start_time = now() + timedelta(hours=2)
2951 end_time = start_time + timedelta(hours=3)
2953 with events_session(token) as api:
2954 res = api.CreateEvent(
2955 events_pb2.CreateEventReq(
2956 title="Test UMS Event",
2957 content="UMS content.",
2958 location=events_pb2.EventLocation(
2959 address="Near Null Island",
2960 lat=0.1,
2961 lng=0.2,
2962 ),
2963 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2964 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2965 )
2966 )
2967 event_id = res.event_id
2969 with session_scope() as session:
2970 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2971 mod_state = session.execute(
2972 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2973 ).scalar_one()
2974 assert mod_state.visibility == ModerationVisibility.shadowed
2977def test_shadowed_event_visible_to_creator_only(db):
2978 """SHADOWED events are visible to the creator but not to other users."""
2979 user1, token1 = generate_user()
2980 user2, token2 = generate_user()
2982 with session_scope() as session:
2983 create_community(session, 0, 2, "Community", [user1], [], None)
2985 start_time = now() + timedelta(hours=2)
2986 end_time = start_time + timedelta(hours=3)
2988 with events_session(token1) as api:
2989 res = api.CreateEvent(
2990 events_pb2.CreateEventReq(
2991 title="Shadowed Event",
2992 content="Content.",
2993 location=events_pb2.EventLocation(
2994 address="Near Null Island",
2995 lat=0.1,
2996 lng=0.2,
2997 ),
2998 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2999 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3000 )
3001 )
3002 event_id = res.event_id
3004 # Creator can see it
3005 with events_session(token1) as api:
3006 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3007 assert res.title == "Shadowed Event"
3009 # Other user cannot
3010 with events_session(token2) as api:
3011 with pytest.raises(grpc.RpcError) as e:
3012 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3013 assert e.value.code() == grpc.StatusCode.NOT_FOUND
3016def test_event_visible_after_approval(db, moderator: Moderator):
3017 """Events become visible to all users after moderation approval."""
3018 user1, token1 = generate_user()
3019 user2, token2 = generate_user()
3021 with session_scope() as session:
3022 create_community(session, 0, 2, "Community", [user1], [], None)
3024 start_time = now() + timedelta(hours=2)
3025 end_time = start_time + timedelta(hours=3)
3027 with events_session(token1) as api:
3028 res = api.CreateEvent(
3029 events_pb2.CreateEventReq(
3030 title="Approved Event",
3031 content="Content.",
3032 location=events_pb2.EventLocation(
3033 address="Near Null Island",
3034 lat=0.1,
3035 lng=0.2,
3036 ),
3037 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3038 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3039 )
3040 )
3041 event_id = res.event_id
3043 # Other user cannot see it yet
3044 with events_session(token2) as api:
3045 with pytest.raises(grpc.RpcError) as e:
3046 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3047 assert e.value.code() == grpc.StatusCode.NOT_FOUND
3049 # Approve the event
3050 moderator.approve_event_occurrence(event_id)
3052 # Now other user can see it
3053 with events_session(token2) as api:
3054 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
3055 assert res.title == "Approved Event"
3058def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
3059 """SHADOWED events appear in lists for the creator but not for other users."""
3060 user1, token1 = generate_user()
3061 user2, token2 = generate_user()
3063 with session_scope() as session:
3064 create_community(session, 0, 2, "Community", [user1], [], None)
3066 start_time = now() + timedelta(hours=2)
3067 end_time = start_time + timedelta(hours=3)
3069 with events_session(token1) as api:
3070 res = api.CreateEvent(
3071 events_pb2.CreateEventReq(
3072 title="List Test Event",
3073 content="Content.",
3074 location=events_pb2.EventLocation(
3075 address="Near Null Island",
3076 lat=0.1,
3077 lng=0.2,
3078 ),
3079 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3080 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3081 )
3082 )
3083 event_id = res.event_id
3085 # Creator can see their own SHADOWED event in lists
3086 with events_session(token1) as api:
3087 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
3088 event_ids = [e.event_id for e in list_res.events]
3089 assert event_id in event_ids
3091 # Other user cannot see the SHADOWED event in lists
3092 with events_session(token2) as api:
3093 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
3094 event_ids = [e.event_id for e in list_res.events]
3095 assert event_id not in event_ids
3097 # After approval, other user can see it
3098 moderator.approve_event_occurrence(event_id)
3100 with events_session(token2) as api:
3101 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
3102 event_ids = [e.event_id for e in list_res.events]
3103 assert event_id in event_ids
3106def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
3107 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
3108 user1, token1 = generate_user()
3109 user2, token2 = generate_user()
3111 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
3112 with session_scope() as session:
3113 world = create_community(session, 0, 10, "World", [user1], [], None)
3114 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
3115 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
3116 create_community(session, 0, 2, "Child", [user2], [], region)
3118 start_time = now() + timedelta(hours=2)
3119 end_time = start_time + timedelta(hours=3)
3121 with events_session(token1) as api:
3122 res = api.CreateEvent(
3123 events_pb2.CreateEventReq(
3124 title="Deferred Event",
3125 content="Content.",
3126 location=events_pb2.EventLocation(
3127 address="Near Null Island",
3128 lat=0.1,
3129 lng=0.2,
3130 ),
3131 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3132 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3133 )
3134 )
3135 event_id = res.event_id
3137 # Process all jobs — notification should be deferred (event is SHADOWED)
3138 process_jobs()
3140 with session_scope() as session:
3141 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
3142 # Notification was created with moderation_state_id for deferral
3143 assert notif.moderation_state_id is not None
3144 # No delivery exists (deferred because event is SHADOWED)
3145 delivery_count = session.execute(
3146 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
3147 ).scalar_one_or_none()
3148 assert delivery_count is None
3150 # Approve the event — handle_notification is re-queued for deferred notifications
3151 moderator.approve_event_occurrence(event_id)
3153 # Verify handle_notification job was queued
3154 with session_scope() as session:
3155 pending_jobs = (
3156 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
3157 .scalars()
3158 .all()
3159 )
3160 assert any("handle_notification" in j.job_type for j in pending_jobs)
3163def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3164 """Event update notifications should carry the event's moderation_state_id for deferral."""
3165 user1, token1 = generate_user()
3166 user2, token2 = generate_user()
3168 with session_scope() as session:
3169 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3171 start_time = now() + timedelta(hours=2)
3172 end_time = start_time + timedelta(hours=3)
3174 with events_session(token1) as api:
3175 res = api.CreateEvent(
3176 events_pb2.CreateEventReq(
3177 title="Update Test",
3178 content="Content.",
3179 location=events_pb2.EventLocation(
3180 address="Near Null Island",
3181 lat=0.1,
3182 lng=0.2,
3183 ),
3184 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3185 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3186 )
3187 )
3188 event_id = res.event_id
3190 moderator.approve_event_occurrence(event_id)
3191 process_jobs()
3192 # Clear any create notifications
3193 while push_collector.count_for_user(user2.id): 3193 ↛ 3194line 3193 didn't jump to line 3194 because the condition on line 3193 was never true
3194 push_collector.pop_for_user(user2.id)
3196 # User2 subscribes to the event
3197 with events_session(token2) as api:
3198 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3200 # User1 updates the event with should_notify=True
3201 with events_session(token1) as api:
3202 api.UpdateEvent(
3203 events_pb2.UpdateEventReq(
3204 event_id=event_id,
3205 title=wrappers_pb2.StringValue(value="Updated Title"),
3206 should_notify=True,
3207 )
3208 )
3210 process_jobs()
3212 # Verify that the update notification for user2 has moderation_state_id set
3213 with session_scope() as session:
3214 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3216 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3217 # Find the update notification (most recent one)
3218 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
3219 assert len(update_notifs) == 1
3220 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
3223def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3224 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
3225 user1, token1 = generate_user()
3226 user2, token2 = generate_user()
3228 with session_scope() as session:
3229 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3231 start_time = now() + timedelta(hours=2)
3232 end_time = start_time + timedelta(hours=3)
3234 with events_session(token1) as api:
3235 res = api.CreateEvent(
3236 events_pb2.CreateEventReq(
3237 title="Cancel Test",
3238 content="Content.",
3239 location=events_pb2.EventLocation(
3240 address="Near Null Island",
3241 lat=0.1,
3242 lng=0.2,
3243 ),
3244 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3245 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3246 )
3247 )
3248 event_id = res.event_id
3250 moderator.approve_event_occurrence(event_id)
3251 process_jobs()
3252 while push_collector.count_for_user(user2.id): 3252 ↛ 3253line 3252 didn't jump to line 3253 because the condition on line 3252 was never true
3253 push_collector.pop_for_user(user2.id)
3255 # User2 subscribes
3256 with events_session(token2) as api:
3257 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3259 # User1 cancels the event
3260 with events_session(token1) as api:
3261 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3263 process_jobs()
3265 # Verify that the cancel notification for user2 has moderation_state_id set
3266 with session_scope() as session:
3267 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3269 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3270 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
3271 assert len(cancel_notifs) == 1
3272 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
3275def test_event_update_and_cancel_notifications_not_sent_to_actor(
3276 db, push_collector: PushCollector, moderator: Moderator
3277):
3278 """The user who updates or cancels an event shouldn't be notified about their own action."""
3279 organizer_user, organizer_token = generate_user()
3280 attendee_user, attendee_token = generate_user()
3282 with session_scope() as session:
3283 create_community(session, 0, 2, "Community", [attendee_user], [], None)
3285 start_time = now() + timedelta(hours=2)
3286 end_time = start_time + timedelta(hours=3)
3288 with events_session(organizer_token) as api:
3289 res = api.CreateEvent(
3290 events_pb2.CreateEventReq(
3291 title="Actor Test",
3292 content="Content.",
3293 location=events_pb2.EventLocation(
3294 address="Near Null Island",
3295 lat=0.1,
3296 lng=0.2,
3297 ),
3298 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3299 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3300 )
3301 )
3302 event_id = res.event_id
3304 moderator.approve_event_occurrence(event_id)
3305 process_jobs()
3307 # The attendee subscribes to notifications
3308 with events_session(attendee_token) as api:
3309 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3311 # The organizer updates and then cancels their own event
3312 with events_session(organizer_token) as api:
3313 api.UpdateEvent(
3314 events_pb2.UpdateEventReq(
3315 event_id=event_id,
3316 title=wrappers_pb2.StringValue(value="Updated Title"),
3317 should_notify=True,
3318 )
3319 )
3320 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3322 process_jobs()
3324 # The organizer should not receive any notifications
3325 assert push_collector.count_for_user(organizer_user.id) == 0
3327 # But the attendee should receive both the update and cancel notifications
3328 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__update.display
3329 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__cancel.display
3332def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3333 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
3334 user1, token1 = generate_user()
3335 user2, token2 = generate_user()
3337 with session_scope() as session:
3338 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3340 # Create event starting 23 hours from now (within 24h reminder window)
3341 start_time = now() + timedelta(hours=23)
3342 end_time = start_time + timedelta(hours=1)
3344 with events_session(token1) as api:
3345 res = api.CreateEvent(
3346 events_pb2.CreateEventReq(
3347 title="Reminder Test",
3348 content="Content.",
3349 location=events_pb2.EventLocation(
3350 address="Near Null Island",
3351 lat=0.1,
3352 lng=0.2,
3353 ),
3354 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3355 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3356 )
3357 )
3358 event_id = res.event_id
3360 moderator.approve_event_occurrence(event_id)
3361 process_jobs()
3362 while push_collector.count_for_user(user2.id): 3362 ↛ 3363line 3362 didn't jump to line 3363 because the condition on line 3362 was never true
3363 push_collector.pop_for_user(user2.id)
3365 # User2 marks attendance
3366 with events_session(token2) as api:
3367 api.SetEventAttendance(
3368 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3369 )
3371 # Run the event reminder handler
3372 send_event_reminders(empty_pb2.Empty())
3373 process_jobs()
3375 # Verify that the reminder notification for user2 has moderation_state_id set
3376 with session_scope() as session:
3377 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3379 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3380 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
3381 assert len(reminder_notifs) == 1
3382 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
3385def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
3386 """Event reminders should not be sent for cancelled events."""
3387 user1, token1 = generate_user()
3388 user2, token2 = generate_user()
3390 with session_scope() as session:
3391 create_community(session, 0, 2, "Community", [user2], [], None)
3393 # Create event starting 23 hours from now (within 24h reminder window)
3394 start_time = now() + timedelta(hours=23)
3395 end_time = start_time + timedelta(hours=1)
3397 with events_session(token1) as api:
3398 res = api.CreateEvent(
3399 events_pb2.CreateEventReq(
3400 title="Cancelled Reminder Test",
3401 content="Content.",
3402 location=events_pb2.EventLocation(
3403 address="Near Null Island",
3404 lat=0.1,
3405 lng=0.2,
3406 ),
3407 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3408 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3409 )
3410 )
3411 event_id = res.event_id
3413 moderator.approve_event_occurrence(event_id)
3414 process_jobs()
3416 # User2 marks attendance
3417 with events_session(token2) as api:
3418 api.SetEventAttendance(
3419 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3420 )
3422 # User1 cancels the event
3423 with events_session(token1) as api:
3424 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3426 process_jobs()
3427 # Drain any cancellation-related notifications so we can cleanly assert on reminders
3428 while push_collector.count_for_user(user2.id):
3429 push_collector.pop_for_user(user2.id)
3431 # Run the event reminder handler
3432 send_event_reminders(empty_pb2.Empty())
3433 process_jobs()
3435 # Verify that no reminder notification was sent for user2
3436 with session_scope() as session:
3437 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3438 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3439 assert len(reminder_notifs) == 0
3442@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
3443def test_event_reminder_not_sent_for_invisible_attendee(
3444 db, push_collector: PushCollector, moderator: Moderator, invisible_field
3445):
3446 user1, token1 = generate_user()
3447 user2, token2 = generate_user()
3449 with session_scope() as session:
3450 create_community(session, 0, 2, "Community", [user2], [], None)
3452 start_time = now() + timedelta(hours=23)
3453 end_time = start_time + timedelta(hours=1)
3455 with events_session(token1) as api:
3456 res = api.CreateEvent(
3457 events_pb2.CreateEventReq(
3458 title="Invisible Attendee Reminder Test",
3459 content="Content.",
3460 location=events_pb2.EventLocation(
3461 address="Near Null Island",
3462 lat=0.1,
3463 lng=0.2,
3464 ),
3465 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3466 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3467 )
3468 )
3469 event_id = res.event_id
3471 moderator.approve_event_occurrence(event_id)
3472 process_jobs()
3474 with events_session(token2) as api:
3475 api.SetEventAttendance(
3476 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3477 )
3479 with session_scope() as session:
3480 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3482 send_event_reminders(empty_pb2.Empty())
3483 process_jobs()
3485 with session_scope() as session:
3486 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3487 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3488 assert len(reminder_notifs) == 0
3491def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3492 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3493 user1, token1 = generate_user()
3494 user2, token2 = generate_user()
3496 with session_scope() as session:
3497 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3499 start = now()
3501 # User1 creates event A with 3 occurrences
3502 event_a_ids = []
3503 with events_session(token1) as api:
3504 res = api.CreateEvent(
3505 events_pb2.CreateEventReq(
3506 title="Event A",
3507 content="Content A.",
3508 parent_community_id=c_id,
3509 location=events_pb2.EventLocation(
3510 address="Near Null Island",
3511 lat=0.1,
3512 lng=0.2,
3513 ),
3514 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
3515 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
3516 )
3517 )
3518 event_a_ids.append(res.event_id)
3519 for i in range(2):
3520 res = api.ScheduleEvent(
3521 events_pb2.ScheduleEventReq(
3522 event_id=event_a_ids[-1],
3523 content=f"A occurrence {i}",
3524 location=events_pb2.EventLocation(
3525 address="Near Null Island",
3526 lat=0.1,
3527 lng=0.2,
3528 ),
3529 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
3530 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
3531 )
3532 )
3533 event_a_ids.append(res.event_id)
3535 # User2 creates event B with 2 occurrences
3536 event_b_ids = []
3537 with events_session(token2) as api:
3538 res = api.CreateEvent(
3539 events_pb2.CreateEventReq(
3540 title="Event B",
3541 content="Content B.",
3542 parent_community_id=c_id,
3543 location=events_pb2.EventLocation(
3544 address="Near Null Island",
3545 lat=0.1,
3546 lng=0.2,
3547 ),
3548 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)),
3549 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)),
3550 )
3551 )
3552 event_b_ids.append(res.event_id)
3553 res = api.ScheduleEvent(
3554 events_pb2.ScheduleEventReq(
3555 event_id=event_b_ids[-1],
3556 content="B occurrence 1",
3557 location=events_pb2.EventLocation(
3558 address="Near Null Island",
3559 lat=0.1,
3560 lng=0.2,
3561 ),
3562 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)),
3563 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)),
3564 )
3565 )
3566 event_b_ids.append(res.event_id)
3568 moderator.approve_event_occurrence(event_a_ids[0])
3569 moderator.approve_event_occurrence(event_b_ids[0])
3571 # List occurrences for event A — should only get event A's 3 occurrences
3572 with events_session(token1) as api:
3573 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3574 returned_ids = [e.event_id for e in res.events]
3575 assert sorted(returned_ids) == sorted(event_a_ids)
3577 # List occurrences for event B — should only get event B's 2 occurrences
3578 with events_session(token2) as api:
3579 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3580 returned_ids = [e.event_id for e in res.events]
3581 assert sorted(returned_ids) == sorted(event_b_ids)
3584def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3585 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3586 user1, token1 = generate_user()
3587 user2, token2 = generate_user()
3589 with session_scope() as session:
3590 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3592 start_time = now() + timedelta(hours=2)
3593 end_time = start_time + timedelta(hours=3)
3595 with events_session(token1) as api:
3596 res = api.CreateEvent(
3597 events_pb2.CreateEventReq(
3598 title="Comment Test",
3599 content="Content.",
3600 parent_community_id=c_id,
3601 location=events_pb2.EventLocation(
3602 address="Near Null Island",
3603 lat=0.1,
3604 lng=0.2,
3605 ),
3606 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3607 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3608 )
3609 )
3610 event_id = res.event_id
3611 thread_id = res.thread.thread_id
3613 moderator.approve_event_occurrence(event_id)
3614 process_jobs()
3615 while push_collector.count_for_user(user1.id): 3615 ↛ 3616line 3615 didn't jump to line 3616 because the condition on line 3615 was never true
3616 push_collector.pop_for_user(user1.id)
3618 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3619 with events_session(token1) as api:
3620 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3622 # User2 posts a top-level comment on the event thread
3623 with threads_session(token2) as api:
3624 comment_thread_id = api.PostReply(
3625 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3626 ).thread_id
3628 process_jobs()
3630 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3631 comment_db_id = comment_thread_id // 10
3632 with session_scope() as session:
3633 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3635 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3636 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3637 assert len(comment_notifs) == 1
3638 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3641def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3642 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3643 user1, token1 = generate_user()
3644 user2, token2 = generate_user()
3645 user3, token3 = generate_user()
3647 with session_scope() as session:
3648 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3650 start_time = now() + timedelta(hours=2)
3651 end_time = start_time + timedelta(hours=3)
3653 with events_session(token1) as api:
3654 res = api.CreateEvent(
3655 events_pb2.CreateEventReq(
3656 title="Reply Test",
3657 content="Content.",
3658 location=events_pb2.EventLocation(
3659 address="Near Null Island",
3660 lat=0.1,
3661 lng=0.2,
3662 ),
3663 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3664 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3665 )
3666 )
3667 event_id = res.event_id
3668 thread_id = res.thread.thread_id
3670 moderator.approve_event_occurrence(event_id)
3671 process_jobs()
3672 while push_collector.count_for_user(user1.id): 3672 ↛ 3673line 3672 didn't jump to line 3673 because the condition on line 3672 was never true
3673 push_collector.pop_for_user(user1.id)
3675 # User2 posts a top-level comment
3676 with threads_session(token2) as api:
3677 comment_thread_id = api.PostReply(
3678 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3679 ).thread_id
3681 process_jobs()
3682 while push_collector.count_for_user(user1.id): 3682 ↛ 3683line 3682 didn't jump to line 3683 because the condition on line 3682 was never true
3683 push_collector.pop_for_user(user1.id)
3685 # User3 replies to user2's comment (depth=2 reply)
3686 with threads_session(token3) as api:
3687 nested_reply_thread_id = api.PostReply(
3688 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3689 ).thread_id
3691 process_jobs()
3693 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3694 nested_reply_db_id = nested_reply_thread_id // 10
3695 with session_scope() as session:
3696 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3698 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3699 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3700 assert len(reply_notifs) == 1
3701 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id