Coverage for app/backend/src/tests/test_events.py: 99%
1394 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-03 11:56 +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 now, to_aware_datetime
31from tests.fixtures.api import events as events_api
32from tests.fixtures.db import generate_user
33from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs
34from tests.fixtures.sessions import real_editor_session, threads_session
35from tests.fixtures.timewarp import FrozenTimewarp
36from tests.test_communities import create_community, create_group
37from tests.utils import local_date_time
40def _to_event_time_granularity(value: datetime) -> datetime:
41 """Events are scheduled at the minute granularity."""
42 return value.replace(second=0, microsecond=0)
45def _is_utc_or_gmt(timezone: str) -> bool:
46 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT.
47 # Tests should be agnostic to which one we're using.
48 return timezone in ("Etc/UTC", "Etc/GMT")
51def test_CreateEvent(db, frozen_timewarp, push_collector: PushCollector, moderator: Moderator):
52 # test cases:
53 # can create event
54 # cannot create event with missing details
55 # can't create event that starts in the past
56 # can create in different timezones
58 creator_user, creator_token = generate_user()
59 mod_user, mod_token = generate_user()
60 other_user, other_token = generate_user()
62 with session_scope() as session:
63 c_id = create_community(session, 0, 2, "Community", [mod_user], [], None).id
65 time_before = now()
66 start_time = _to_event_time_granularity(time_before + timedelta(hours=2))
67 end_time = start_time + timedelta(hours=3)
69 # Can create an event
70 create_res = events_api.create_event(
71 token=creator_token,
72 title="Dummy Title",
73 content="Dummy content.",
74 location=events_pb2.EventLocation(
75 address="Near Null Island",
76 lat=0.1,
77 lng=0.2,
78 ),
79 start_date_time=local_date_time(start_time),
80 end_date_time=local_date_time(end_time),
81 approve_by=moderator,
82 )
83 assert create_res.is_next
84 assert create_res.title == "Dummy Title"
85 assert create_res.slug == "dummy-title"
86 assert create_res.content == "Dummy content."
87 assert not create_res.photo_url
88 assert create_res.HasField("location")
89 assert create_res.location.lat == 0.1
90 assert create_res.location.lng == 0.2
91 assert create_res.location.address == "Near Null Island"
92 assert time_before <= to_aware_datetime(create_res.created) <= now()
93 assert time_before <= to_aware_datetime(create_res.last_edited) <= now()
94 assert create_res.creator_user_id == creator_user.id
95 assert to_aware_datetime(create_res.start_time) == start_time
96 assert to_aware_datetime(create_res.end_time) == end_time
97 assert _is_utc_or_gmt(create_res.timezone)
98 assert create_res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
99 assert create_res.organizer
100 assert create_res.subscriber
101 assert create_res.going_count == 1
102 assert create_res.organizer_count == 1
103 assert create_res.subscriber_count == 1
104 assert create_res.owner_user_id == creator_user.id
105 assert not create_res.owner_community_id
106 assert not create_res.owner_group_id
107 assert create_res.thread.thread_id
108 assert create_res.can_edit
109 assert not create_res.can_moderate
111 event_id = create_res.event_id
113 get_res = events_api.get_event(token=mod_token, event_id=event_id)
114 assert get_res.is_next
115 assert get_res.title == "Dummy Title"
116 assert get_res.slug == "dummy-title"
117 assert get_res.content == "Dummy content."
118 assert not get_res.photo_url
119 assert get_res.HasField("location")
120 assert get_res.location.lat == 0.1
121 assert get_res.location.lng == 0.2
122 assert get_res.location.address == "Near Null Island"
123 assert time_before <= to_aware_datetime(get_res.created) <= now()
124 assert time_before <= to_aware_datetime(get_res.last_edited) <= now()
125 assert get_res.creator_user_id == creator_user.id
126 assert to_aware_datetime(get_res.start_time) == start_time
127 assert to_aware_datetime(get_res.end_time) == end_time
128 assert _is_utc_or_gmt(get_res.timezone)
129 assert get_res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
130 assert not get_res.organizer
131 assert not get_res.subscriber
132 assert get_res.going_count == 1
133 assert get_res.organizer_count == 1
134 assert get_res.subscriber_count == 1
135 assert get_res.owner_user_id == creator_user.id
136 assert not get_res.owner_community_id
137 assert not get_res.owner_group_id
138 assert get_res.thread.thread_id
139 assert get_res.can_edit
140 assert get_res.can_moderate
142 other_get_res = events_api.get_event(token=other_token, event_id=event_id)
143 assert other_get_res.is_next
144 assert other_get_res.title == "Dummy Title"
145 assert other_get_res.slug == "dummy-title"
146 assert other_get_res.content == "Dummy content."
147 assert not other_get_res.photo_url
148 assert other_get_res.HasField("location")
149 assert other_get_res.location.lat == 0.1
150 assert other_get_res.location.lng == 0.2
151 assert other_get_res.location.address == "Near Null Island"
152 assert time_before <= to_aware_datetime(other_get_res.created) <= now()
153 assert time_before <= to_aware_datetime(other_get_res.last_edited) <= now()
154 assert other_get_res.creator_user_id == creator_user.id
155 assert to_aware_datetime(other_get_res.start_time) == start_time
156 assert to_aware_datetime(other_get_res.end_time) == end_time
157 assert _is_utc_or_gmt(other_get_res.timezone)
158 assert other_get_res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
159 assert not other_get_res.organizer
160 assert not other_get_res.subscriber
161 assert other_get_res.going_count == 1
162 assert other_get_res.organizer_count == 1
163 assert other_get_res.subscriber_count == 1
164 assert other_get_res.owner_user_id == creator_user.id
165 assert not other_get_res.owner_community_id
166 assert not other_get_res.owner_group_id
167 assert other_get_res.thread.thread_id
168 assert not other_get_res.can_edit
169 assert not other_get_res.can_moderate
171 # Failure cases
172 with pytest.raises(grpc.RpcError) as e:
173 events_api.create_event(token=creator_token, title=None)
174 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
175 assert e.value.details() == "Missing event title."
177 with pytest.raises(grpc.RpcError) as e:
178 events_api.create_event(token=creator_token, content=None)
179 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
180 assert e.value.details() == "Missing event content."
182 with pytest.raises(grpc.RpcError) as e:
183 events_api.create_event(token=creator_token, photo_key="nonexistent")
184 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
185 assert e.value.details() == "Photo not found."
187 with pytest.raises(grpc.RpcError) as e:
188 events_api.create_event(token=creator_token, location=None)
189 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
190 assert e.value.details() == "Missing event address or location."
192 with pytest.raises(grpc.RpcError) as e:
193 events_api.create_event(token=creator_token, location=events_pb2.EventLocation(address="Address only"))
194 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
195 assert e.value.details() == "Invalid coordinate."
197 with pytest.raises(grpc.RpcError) as e:
198 events_api.create_event(token=creator_token, location=events_pb2.EventLocation(lat=0.1, lng=0.1))
199 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
200 assert e.value.details() == "Missing event address or location."
202 with pytest.raises(grpc.RpcError) as e:
203 events_api.create_event(token=creator_token, start_date_time=local_date_time(now() - timedelta(hours=2)))
204 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
205 assert e.value.details() == "The event must be in the future."
207 with pytest.raises(grpc.RpcError) as e:
208 events_api.create_event(
209 token=creator_token,
210 start_date_time=local_date_time(end_time),
211 end_date_time=local_date_time(start_time),
212 )
213 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
214 assert e.value.details() == "The event must end after it starts."
216 with pytest.raises(grpc.RpcError) as e:
217 events_api.create_event(
218 token=creator_token,
219 start_date_time=local_date_time(now() + timedelta(days=500, hours=2)),
220 end_date_time=local_date_time(now() + timedelta(days=500, hours=5)),
221 )
222 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
223 assert e.value.details() == "The event needs to start within the next year."
225 with pytest.raises(grpc.RpcError) as e:
226 events_api.create_event(
227 token=creator_token,
228 start_date_time=local_date_time(start_time),
229 end_date_time=local_date_time(now() + timedelta(days=100)),
230 )
231 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
232 assert e.value.details() == "Events cannot last longer than 7 days."
235def test_CreateEvent_incomplete_profile(db):
236 user1, token1 = generate_user(complete_profile=False)
237 user2, token2 = generate_user()
239 with session_scope() as session:
240 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
242 with pytest.raises(grpc.RpcError) as e:
243 events_api.create_event(token=token1)
244 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
245 assert e.value.details() == "You have to complete your profile before you can create an event."
248def test_ScheduleEvent(db, frozen_timewarp):
249 # test cases:
250 # can schedule a new event occurrence
252 user, token = generate_user()
254 with session_scope() as session:
255 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
257 time_before = now()
258 start_time = _to_event_time_granularity(now() + timedelta(hours=2))
259 end_time = start_time + timedelta(hours=3)
261 create_res = events_api.create_event(
262 token=token,
263 parent_community_id=c_id,
264 start_date_time=local_date_time(start_time),
265 end_date_time=local_date_time(end_time),
266 )
268 new_start_time = _to_event_time_granularity(now() + timedelta(hours=6))
269 new_end_time = new_start_time + timedelta(hours=2)
271 schedule_res = events_api.schedule_event(
272 token=token,
273 event_id=create_res.event_id,
274 content="New event occurrence",
275 location=events_pb2.EventLocation(
276 address="A bit further but still near Null Island",
277 lat=0.3,
278 lng=0.2,
279 ),
280 start_date_time=local_date_time(new_start_time),
281 end_date_time=local_date_time(new_end_time),
282 )
284 # Each occurrence is independent and has an independent thread
285 assert schedule_res.event_id != create_res.event_id
286 assert schedule_res.thread.thread_id != create_res.thread.thread_id
288 res = events_api.get_event(token=token, event_id=schedule_res.event_id)
290 assert not res.is_next
291 assert res.title == "Dummy Title"
292 assert res.slug == "dummy-title"
293 assert res.content == "New event occurrence"
294 assert not res.photo_url
295 assert res.HasField("location")
296 assert res.location.lat == 0.3
297 assert res.location.lng == 0.2
298 assert res.location.address == "A bit further but still near Null Island"
299 assert time_before <= to_aware_datetime(res.created) <= now()
300 assert time_before <= to_aware_datetime(res.last_edited) <= now()
301 assert res.creator_user_id == user.id
302 assert to_aware_datetime(res.start_time) == new_start_time
303 assert to_aware_datetime(res.end_time) == new_end_time
304 assert _is_utc_or_gmt(res.timezone)
305 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
306 assert res.organizer
307 assert res.subscriber
308 assert res.going_count == 1
309 assert res.organizer_count == 1
310 assert res.subscriber_count == 1
311 assert res.owner_user_id == user.id
312 assert not res.owner_community_id
313 assert not res.owner_group_id
314 assert res.thread.thread_id
315 assert res.can_edit
316 assert res.can_moderate
319def test_cannot_overlap_occurrences_schedule(db):
320 user, token = generate_user()
322 with session_scope() as session:
323 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
325 start = now()
327 res = events_api.create_event(
328 token=token,
329 parent_community_id=c_id,
330 start_date_time=local_date_time(start + timedelta(hours=1)),
331 end_date_time=local_date_time(start + timedelta(hours=3)),
332 )
334 with pytest.raises(grpc.RpcError) as e:
335 events_api.schedule_event(
336 token=token,
337 event_id=res.event_id,
338 start_date_time=local_date_time(start + timedelta(hours=2)),
339 end_date_time=local_date_time(start + timedelta(hours=6)),
340 )
341 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
342 assert e.value.details() == "An event cannot have overlapping occurrences."
345def test_cannot_overlap_occurrences_update(db):
346 user, token = generate_user()
348 with session_scope() as session:
349 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
351 start = now()
353 res = events_api.create_event(
354 token=token,
355 parent_community_id=c_id,
356 start_date_time=local_date_time(start + timedelta(hours=1)),
357 end_date_time=local_date_time(start + timedelta(hours=3)),
358 )
360 event_id = events_api.schedule_event(
361 token=token,
362 event_id=res.event_id,
363 content="New event occurrence",
364 location=events_pb2.EventLocation(
365 address="A bit further but still near Null Island",
366 lat=0.3,
367 lng=0.2,
368 ),
369 start_date_time=local_date_time(start + timedelta(hours=4)),
370 end_date_time=local_date_time(start + timedelta(hours=6)),
371 ).event_id
373 # can overlap with this current existing occurrence
374 events_api.update_event(
375 token=token,
376 event_id=event_id,
377 start_date_time=local_date_time(start + timedelta(hours=5)),
378 end_date_time=local_date_time(start + timedelta(hours=6)),
379 )
381 with pytest.raises(grpc.RpcError) as e:
382 events_api.update_event(
383 token=token,
384 event_id=event_id,
385 start_date_time=local_date_time(start + timedelta(hours=2)),
386 end_date_time=local_date_time(start + timedelta(hours=4)),
387 )
388 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
389 assert e.value.details() == "An event cannot have overlapping occurrences."
392def test_UpdateEvent_single(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator):
393 # test cases:
394 # owner can update
395 # community owner can update
396 # notifies attendees
398 # event creator
399 user1, token1 = generate_user()
400 # community moderator
401 user2, token2 = generate_user()
402 # third parties
403 user3, token3 = generate_user()
404 user4, token4 = generate_user()
405 user5, token5 = generate_user()
406 user6, token6 = generate_user()
408 with session_scope() as session:
409 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
411 time_before = now()
412 start_time = _to_event_time_granularity(now() + timedelta(hours=2))
413 end_time = start_time + timedelta(hours=3)
415 res = events_api.create_event(
416 token=token1,
417 parent_community_id=c_id,
418 location=events_pb2.EventLocation(
419 address="Near Null Island",
420 lat=0.1,
421 lng=0.2,
422 ),
423 start_date_time=local_date_time(start_time),
424 end_date_time=local_date_time(end_time),
425 approve_by=moderator,
426 )
428 event_id = res.event_id
430 events_api.set_event_subscription(token=token4, event_id=event_id, subscribe=True)
431 events_api.set_event_attendance(token=token5, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
432 events_api.set_event_subscription(token=token6, event_id=event_id, subscribe=True)
434 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change
435 frozen_timewarp.advance(timedelta(minutes=1))
436 time_before_update = now()
438 res = events_api.update_event(token=token1, event_id=event_id)
439 res = events_api.get_event(token=token1, event_id=event_id)
441 assert res.is_next
442 assert res.title == "Dummy Title"
443 assert res.slug == "dummy-title"
444 assert res.content == "Dummy content."
445 assert not res.photo_url
446 assert res.HasField("location")
447 assert res.location.lat == 0.1
448 assert res.location.lng == 0.2
449 assert res.location.address == "Near Null Island"
450 assert time_before <= to_aware_datetime(res.created) <= time_before_update
451 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
452 assert res.creator_user_id == user1.id
453 assert to_aware_datetime(res.start_time) == start_time
454 assert to_aware_datetime(res.end_time) == end_time
455 assert _is_utc_or_gmt(res.timezone)
456 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
457 assert res.organizer
458 assert res.subscriber
459 assert res.going_count == 2
460 assert res.organizer_count == 1
461 assert res.subscriber_count == 3
462 assert res.owner_user_id == user1.id
463 assert not res.owner_community_id
464 assert not res.owner_group_id
465 assert res.thread.thread_id
466 assert res.can_edit
467 assert not res.can_moderate
469 res = events_api.get_event(token=token2, event_id=event_id)
471 assert res.is_next
472 assert res.title == "Dummy Title"
473 assert res.slug == "dummy-title"
474 assert res.content == "Dummy content."
475 assert not res.photo_url
476 assert res.HasField("location")
477 assert res.location.lat == 0.1
478 assert res.location.lng == 0.2
479 assert res.location.address == "Near Null Island"
480 assert time_before <= to_aware_datetime(res.created) <= time_before_update
481 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
482 assert res.creator_user_id == user1.id
483 assert to_aware_datetime(res.start_time) == start_time
484 assert to_aware_datetime(res.end_time) == end_time
485 assert _is_utc_or_gmt(res.timezone)
486 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
487 assert not res.organizer
488 assert not res.subscriber
489 assert res.going_count == 2
490 assert res.organizer_count == 1
491 assert res.subscriber_count == 3
492 assert res.owner_user_id == user1.id
493 assert not res.owner_community_id
494 assert not res.owner_group_id
495 assert res.thread.thread_id
496 assert res.can_edit
497 assert res.can_moderate
499 res = events_api.get_event(token=token3, event_id=event_id)
501 assert res.is_next
502 assert res.title == "Dummy Title"
503 assert res.slug == "dummy-title"
504 assert res.content == "Dummy content."
505 assert not res.photo_url
506 assert res.HasField("location")
507 assert res.location.lat == 0.1
508 assert res.location.lng == 0.2
509 assert res.location.address == "Near Null Island"
510 assert time_before <= to_aware_datetime(res.created) <= time_before_update
511 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
512 assert res.creator_user_id == user1.id
513 assert to_aware_datetime(res.start_time) == start_time
514 assert to_aware_datetime(res.end_time) == end_time
515 assert _is_utc_or_gmt(res.timezone)
516 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
517 assert not res.organizer
518 assert not res.subscriber
519 assert res.going_count == 2
520 assert res.organizer_count == 1
521 assert res.subscriber_count == 3
522 assert res.owner_user_id == user1.id
523 assert not res.owner_community_id
524 assert not res.owner_group_id
525 assert res.thread.thread_id
526 assert not res.can_edit
527 assert not res.can_moderate
529 res = events_api.update_event(
530 token=token1,
531 event_id=event_id,
532 location=events_pb2.EventLocation(
533 address="Nearer Null Island",
534 lat=0.01,
535 lng=0.02,
536 ),
537 )
539 res = events_api.get_event(token=token3, event_id=event_id)
541 assert res.HasField("location")
542 assert res.location.address == "Nearer Null Island"
543 assert res.location.lat == 0.01
544 assert res.location.lng == 0.02
547def test_UpdateEvent_all(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator):
548 # event creator
549 user1, token1 = generate_user()
550 # community moderator
551 user2, token2 = generate_user()
552 # third parties
553 user3, token3 = generate_user()
554 user4, token4 = generate_user()
555 user5, token5 = generate_user()
556 user6, token6 = generate_user()
558 with session_scope() as session:
559 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
561 time_before = now()
562 start_time = now() + timedelta(hours=1)
563 end_time = start_time + timedelta(hours=1.5)
565 event_ids = []
567 event_id = events_api.create_event(
568 token=token1,
569 content="0th occurrence",
570 start_date_time=local_date_time(start_time),
571 end_date_time=local_date_time(end_time),
572 approve_by=moderator,
573 ).event_id
574 event_ids.append(event_id)
576 events_api.set_event_subscription(token=token4, event_id=event_id, subscribe=True)
577 events_api.set_event_attendance(token=token5, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
578 events_api.set_event_subscription(token=token6, event_id=event_id, subscribe=True)
580 for i in range(5):
581 occurrence_id = events_api.schedule_event(
582 token=token1,
583 event_id=event_ids[-1],
584 content=f"{i + 1}th occurrence",
585 start_date_time=local_date_time(start_time + timedelta(hours=2 + i)),
586 end_date_time=local_date_time(start_time + timedelta(hours=2.5 + i)),
587 approve_by=moderator,
588 ).event_id
589 event_ids.append(occurrence_id)
591 updated_event_id = event_ids[3]
593 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change
594 frozen_timewarp.advance(timedelta(minutes=1))
595 time_before_update = now()
597 res = events_api.update_event(
598 token=token1,
599 event_id=updated_event_id,
600 title="New Title",
601 content="New content.",
602 location=events_pb2.EventLocation(
603 address="Not so near Null Island",
604 lat=0.2,
605 lng=0.2,
606 ),
607 update_all_future=True,
608 )
610 time_after_update = now()
612 for i in range(3):
613 res = events_api.get_event(token=token2, event_id=event_ids[i])
614 assert res.content == f"{i}th occurrence"
615 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
617 for i in range(3, 6):
618 res = events_api.get_event(token=token2, event_id=event_ids[i])
619 assert res.content == "New content."
620 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
623def test_UpdateEvent_all_leaves_other_events_alone(db, moderator: Moderator):
624 """update_all_future must only touch the occurrences of the event being edited."""
625 # creator of the event that gets edited
626 user1, token1 = generate_user()
627 # creator of an unrelated event in the same time window
628 user2, token2 = generate_user()
629 # community moderator, so that neither creator has edit rights on the other's event
630 user3, token3 = generate_user()
632 with session_scope() as session:
633 create_community(session, 0, 2, "Community", [user3], [], None)
635 start_time = now() + timedelta(hours=1)
637 edited_id = events_api.create_event(
638 token=token1,
639 title="Edited Event",
640 content="0th occurrence",
641 start_date_time=local_date_time(start_time),
642 end_date_time=local_date_time(start_time + timedelta(hours=1)),
643 approve_by=moderator,
644 ).event_id
646 second_id = events_api.schedule_event(
647 token=token1,
648 event_id=edited_id,
649 content="1th occurrence",
650 start_date_time=local_date_time(start_time + timedelta(hours=2)),
651 end_date_time=local_date_time(start_time + timedelta(hours=3)),
652 approve_by=moderator,
653 ).event_id
655 # an occurrence of a different event, starting after the edited one and ending well after the cutoff
656 other_id = events_api.create_event(
657 token=token2,
658 title="Other Event",
659 content="Other content.",
660 location=events_pb2.EventLocation(
661 address="Somewhere else entirely",
662 lat=0.5,
663 lng=0.5,
664 ),
665 start_date_time=local_date_time(start_time + timedelta(hours=4)),
666 end_date_time=local_date_time(start_time + timedelta(hours=5)),
667 approve_by=moderator,
668 ).event_id
670 other_before = events_api.get_event(token=token2, event_id=other_id)
672 events_api.update_event(
673 token=token1,
674 event_id=edited_id,
675 content="New content.",
676 location=events_pb2.EventLocation(
677 address="Not so near Null Island",
678 lat=0.2,
679 lng=0.2,
680 ),
681 update_all_future=True,
682 )
684 for occurrence_id in (edited_id, second_id):
685 res = events_api.get_event(token=token3, event_id=occurrence_id)
686 assert res.content == "New content."
687 assert res.location.address == "Not so near Null Island"
689 res = events_api.get_event(token=token3, event_id=other_id)
690 assert res.content == "Other content."
691 assert res.location.address == "Somewhere else entirely"
692 assert res.location.lat == 0.5
693 assert res.location.lng == 0.5
694 assert res.timezone == other_before.timezone
695 assert res.last_edited == other_before.last_edited
698def test_UpdateEvent_all_cant_change_times(db, moderator: Moderator):
699 """Every future occurrence would get the same times, which the exclusion constraint forbids."""
700 user1, token1 = generate_user()
701 user2, token2 = generate_user()
703 with session_scope() as session:
704 create_community(session, 0, 2, "Community", [user2], [], None)
706 start_time = now() + timedelta(hours=1)
708 event_id = events_api.create_event(
709 token=token1,
710 content="0th occurrence",
711 start_date_time=local_date_time(start_time),
712 end_date_time=local_date_time(start_time + timedelta(hours=1)),
713 ).event_id
715 events_api.schedule_event(
716 token=token1,
717 event_id=event_id,
718 content="1th occurrence",
719 start_date_time=local_date_time(start_time + timedelta(hours=2)),
720 end_date_time=local_date_time(start_time + timedelta(hours=3)),
721 )
723 with pytest.raises(grpc.RpcError) as e:
724 events_api.update_event(
725 token=token1,
726 event_id=event_id,
727 start_date_time=local_date_time(start_time + timedelta(minutes=30)),
728 end_date_time=local_date_time(start_time + timedelta(hours=1, minutes=30)),
729 update_all_future=True,
730 )
731 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
732 assert e.value.details() == "You cannot update all events if you're modifying start or end times."
735def test_GetEvent(db, frozen_timewarp, moderator: Moderator):
736 # event creator
737 user1, token1 = generate_user()
738 # community moderator
739 user2, token2 = generate_user()
740 # third parties
741 user3, token3 = generate_user()
742 user4, token4 = generate_user()
743 user5, token5 = generate_user()
744 user6, token6 = generate_user()
746 with session_scope() as session:
747 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
749 time_before = now()
750 start_time = _to_event_time_granularity(now() + timedelta(hours=2))
751 end_time = start_time + timedelta(hours=3)
753 # in person event
754 res = events_api.create_event(
755 token=token1,
756 location=events_pb2.EventLocation(
757 address="Near Null Island",
758 lat=0.1,
759 lng=0.2,
760 ),
761 start_date_time=local_date_time(start_time),
762 end_date_time=local_date_time(end_time),
763 approve_by=moderator,
764 )
766 event_id = res.event_id
768 events_api.set_event_subscription(token=token4, event_id=event_id, subscribe=True)
769 events_api.set_event_attendance(token=token5, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
770 events_api.set_event_subscription(token=token6, event_id=event_id, subscribe=True)
772 res = events_api.get_event(token=token1, event_id=event_id)
774 assert res.is_next
775 assert res.title == "Dummy Title"
776 assert res.slug == "dummy-title"
777 assert res.content == "Dummy content."
778 assert not res.photo_url
779 assert res.HasField("location")
780 assert res.location.lat == 0.1
781 assert res.location.lng == 0.2
782 assert res.location.address == "Near Null Island"
783 assert time_before <= to_aware_datetime(res.created) <= now()
784 assert time_before <= to_aware_datetime(res.last_edited) <= now()
785 assert res.creator_user_id == user1.id
786 assert to_aware_datetime(res.start_time) == start_time
787 assert to_aware_datetime(res.end_time) == end_time
788 assert _is_utc_or_gmt(res.timezone)
789 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
790 assert res.organizer
791 assert res.subscriber
792 assert res.going_count == 2
793 assert res.organizer_count == 1
794 assert res.subscriber_count == 3
795 assert res.owner_user_id == user1.id
796 assert not res.owner_community_id
797 assert not res.owner_group_id
798 assert res.thread.thread_id
799 assert res.can_edit
800 assert not res.can_moderate
802 res = events_api.get_event(token=token2, event_id=event_id)
804 assert res.is_next
805 assert res.title == "Dummy Title"
806 assert res.slug == "dummy-title"
807 assert res.content == "Dummy content."
808 assert not res.photo_url
809 assert res.HasField("location")
810 assert res.location.lat == 0.1
811 assert res.location.lng == 0.2
812 assert res.location.address == "Near Null Island"
813 assert time_before <= to_aware_datetime(res.created) <= now()
814 assert time_before <= to_aware_datetime(res.last_edited) <= now()
815 assert res.creator_user_id == user1.id
816 assert to_aware_datetime(res.start_time) == start_time
817 assert to_aware_datetime(res.end_time) == end_time
818 assert _is_utc_or_gmt(res.timezone)
819 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
820 assert not res.organizer
821 assert not res.subscriber
822 assert res.going_count == 2
823 assert res.organizer_count == 1
824 assert res.subscriber_count == 3
825 assert res.owner_user_id == user1.id
826 assert not res.owner_community_id
827 assert not res.owner_group_id
828 assert res.thread.thread_id
829 assert res.can_edit
830 assert res.can_moderate
832 res = events_api.get_event(token=token3, event_id=event_id)
834 assert res.is_next
835 assert res.title == "Dummy Title"
836 assert res.slug == "dummy-title"
837 assert res.content == "Dummy content."
838 assert not res.photo_url
839 assert res.HasField("location")
840 assert res.location.lat == 0.1
841 assert res.location.lng == 0.2
842 assert res.location.address == "Near Null Island"
843 assert time_before <= to_aware_datetime(res.created) <= now()
844 assert time_before <= to_aware_datetime(res.last_edited) <= now()
845 assert res.creator_user_id == user1.id
846 assert to_aware_datetime(res.start_time) == start_time
847 assert to_aware_datetime(res.end_time) == end_time
848 assert _is_utc_or_gmt(res.timezone)
849 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
850 assert not res.organizer
851 assert not res.subscriber
852 assert res.going_count == 2
853 assert res.organizer_count == 1
854 assert res.subscriber_count == 3
855 assert res.owner_user_id == user1.id
856 assert not res.owner_community_id
857 assert not res.owner_group_id
858 assert res.thread.thread_id
859 assert not res.can_edit
860 assert not res.can_moderate
863def test_CancelEvent(db, moderator: Moderator):
864 # event creator
865 user1, token1 = generate_user()
866 # community moderator
867 user2, token2 = generate_user()
868 # third parties
869 user3, token3 = generate_user()
870 user4, token4 = generate_user()
871 user5, token5 = generate_user()
872 user6, token6 = generate_user()
874 with session_scope() as session:
875 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
877 event_id = events_api.create_event(token=token1, approve_by=moderator).event_id
879 events_api.set_event_subscription(token=token4, event_id=event_id, subscribe=True)
880 events_api.set_event_attendance(token=token5, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
881 events_api.set_event_subscription(token=token6, event_id=event_id, subscribe=True)
883 events_api.cancel_event(token=token1, event_id=event_id)
884 assert events_api.get_event(token=token1, event_id=event_id).is_cancelled
886 with pytest.raises(grpc.RpcError) as e:
887 events_api.update_event(token=token1, event_id=event_id, title="New Title")
888 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
889 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
891 with pytest.raises(grpc.RpcError) as e:
892 events_api.invite_event_organizer(token=token1, event_id=event_id, user_id=user3.id)
893 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
894 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
896 with pytest.raises(grpc.RpcError) as e:
897 events_api.transfer_event(token=token1, event_id=event_id, new_owner_community_id=c_id)
898 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
899 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
901 with pytest.raises(grpc.RpcError) as e:
902 events_api.set_event_subscription(token=token3, event_id=event_id, subscribe=True)
903 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
904 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
906 with pytest.raises(grpc.RpcError) as e:
907 events_api.set_event_attendance(
908 token=token3, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
909 )
910 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
911 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
913 for include_cancelled in [True, False]:
914 occurrences = events_api.list_event_occurrences(
915 token=token1, event_id=event_id, include_cancelled=include_cancelled
916 ).events
917 if include_cancelled:
918 assert len(occurrences) > 0
919 else:
920 assert len(occurrences) == 0
922 mine = events_api.list_my_events(token=token1, include_cancelled=include_cancelled).events
923 if include_cancelled:
924 assert len(mine) > 0
925 else:
926 assert len(mine) == 0
929def test_ListEventAttendees(db, moderator: Moderator):
930 # event creator
931 user1, token1 = generate_user()
932 # others
933 user2, token2 = generate_user()
934 user3, token3 = generate_user()
935 user4, token4 = generate_user()
936 user5, token5 = generate_user()
937 user6, token6 = generate_user()
939 with session_scope() as session:
940 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
942 event_id = events_api.create_event(token=token1, approve_by=moderator).event_id
944 for token in [token2, token3, token4, token5]:
945 events_api.set_event_attendance(
946 token=token, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
947 )
949 assert events_api.get_event(token=token6, event_id=event_id).going_count == 5
951 res = events_api.list_event_attendees(token=token6, event_id=event_id, page_size=2)
952 assert res.attendee_user_ids == [user1.id, user2.id]
954 res = events_api.list_event_attendees(token=token6, event_id=event_id, page_size=2, page_token=res.next_page_token)
955 assert res.attendee_user_ids == [user3.id, user4.id]
957 res = events_api.list_event_attendees(token=token6, event_id=event_id, page_size=2, page_token=res.next_page_token)
958 assert res.attendee_user_ids == [user5.id]
959 assert not res.next_page_token
962def test_ListEventSubscribers(db, moderator: Moderator):
963 # event creator
964 user1, token1 = generate_user()
965 # others
966 user2, token2 = generate_user()
967 user3, token3 = generate_user()
968 user4, token4 = generate_user()
969 user5, token5 = generate_user()
970 user6, token6 = generate_user()
972 with session_scope() as session:
973 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
975 event_id = events_api.create_event(token=token1, approve_by=moderator).event_id
977 for token in [token2, token3, token4, token5]:
978 events_api.set_event_subscription(token=token, event_id=event_id, subscribe=True)
980 assert events_api.get_event(token=token6, event_id=event_id).subscriber_count == 5
982 res = events_api.list_event_subscribers(token=token6, event_id=event_id, page_size=2)
983 assert res.subscriber_user_ids == [user1.id, user2.id]
985 res = events_api.list_event_subscribers(
986 token=token6, event_id=event_id, page_size=2, page_token=res.next_page_token
987 )
988 assert res.subscriber_user_ids == [user3.id, user4.id]
990 res = events_api.list_event_subscribers(
991 token=token6, event_id=event_id, page_size=2, page_token=res.next_page_token
992 )
993 assert res.subscriber_user_ids == [user5.id]
994 assert not res.next_page_token
997def test_ListEventOrganizers(db, moderator: Moderator):
998 # event creator
999 user1, token1 = generate_user()
1000 # others
1001 user2, token2 = generate_user()
1002 user3, token3 = generate_user()
1003 user4, token4 = generate_user()
1004 user5, token5 = generate_user()
1005 user6, token6 = generate_user()
1007 with session_scope() as session:
1008 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1010 event_id = events_api.create_event(token=token1, approve_by=moderator).event_id
1012 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1013 events_api.invite_event_organizer(token=token1, event_id=event_id, user_id=user_id)
1015 assert events_api.get_event(token=token6, event_id=event_id).organizer_count == 5
1017 res = events_api.list_event_organizers(token=token6, event_id=event_id, page_size=2)
1018 assert res.organizer_user_ids == [user1.id, user2.id]
1020 res = events_api.list_event_organizers(token=token6, event_id=event_id, page_size=2, page_token=res.next_page_token)
1021 assert res.organizer_user_ids == [user3.id, user4.id]
1023 res = events_api.list_event_organizers(token=token6, event_id=event_id, page_size=2, page_token=res.next_page_token)
1024 assert res.organizer_user_ids == [user5.id]
1025 assert not res.next_page_token
1028def test_TransferEvent(db):
1029 user1, token1 = generate_user()
1030 user2, token2 = generate_user()
1031 user3, token3 = generate_user()
1032 user4, token4 = generate_user()
1034 with session_scope() as session:
1035 c = create_community(session, 0, 2, "Community", [user3], [], None)
1036 h = create_group(session, "Group", [user4], [], c)
1037 c_id = c.id
1038 h_id = h.id
1040 event_id = events_api.create_event(token=token1).event_id
1041 events_api.transfer_event(token=token1, event_id=event_id, new_owner_community_id=c_id)
1043 # remove ourselves as organizer, otherwise we can still edit it
1044 events_api.remove_event_organizer(token=token1, event_id=event_id)
1046 with pytest.raises(grpc.RpcError) as e:
1047 events_api.transfer_event(token=token1, event_id=event_id, new_owner_group_id=h_id)
1048 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1049 assert e.value.details() == "You're not allowed to transfer that event."
1051 event_id = events_api.create_event(token=token1).event_id
1052 events_api.transfer_event(token=token1, event_id=event_id, new_owner_group_id=h_id)
1054 # remove ourselves as organizer, otherwise we can still edit it
1055 events_api.remove_event_organizer(token=token1, event_id=event_id)
1057 with pytest.raises(grpc.RpcError) as e:
1058 events_api.transfer_event(token=token1, event_id=event_id, new_owner_community_id=c_id)
1059 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1060 assert e.value.details() == "You're not allowed to transfer that event."
1063def test_SetEventSubscription(db, moderator: Moderator):
1064 user1, token1 = generate_user()
1065 user2, token2 = generate_user()
1067 with session_scope() as session:
1068 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1070 event_id = events_api.create_event(
1071 token=token1,
1072 approve_by=moderator,
1073 ).event_id
1075 assert not events_api.get_event(token=token2, event_id=event_id).subscriber
1076 events_api.set_event_subscription(token=token2, event_id=event_id, subscribe=True)
1077 assert events_api.get_event(token=token2, event_id=event_id).subscriber
1078 events_api.set_event_subscription(token=token2, event_id=event_id, subscribe=False)
1079 assert not events_api.get_event(token=token2, event_id=event_id).subscriber
1082def test_SetEventAttendance(db, moderator: Moderator):
1083 user1, token1 = generate_user()
1084 user2, token2 = generate_user()
1086 with session_scope() as session:
1087 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1089 event_id = events_api.create_event(
1090 token=token1,
1091 approve_by=moderator,
1092 ).event_id
1094 assert (
1095 events_api.get_event(token=token2, event_id=event_id).attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1096 )
1097 events_api.set_event_attendance(token=token2, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1098 assert events_api.get_event(token=token2, event_id=event_id).attendance_state == events_pb2.ATTENDANCE_STATE_GOING
1099 events_api.set_event_attendance(
1100 token=token2, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING
1101 )
1102 assert (
1103 events_api.get_event(token=token2, event_id=event_id).attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1104 )
1107def test_InviteEventOrganizer(db, moderator: Moderator):
1108 user1, token1 = generate_user()
1109 user2, token2 = generate_user()
1111 with session_scope() as session:
1112 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1114 event_id = events_api.create_event(
1115 token=token1,
1116 approve_by=moderator,
1117 ).event_id
1119 assert not events_api.get_event(token=token2, event_id=event_id).organizer
1121 with pytest.raises(grpc.RpcError) as e:
1122 events_api.invite_event_organizer(token=token2, event_id=event_id, user_id=user1.id)
1123 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1124 assert e.value.details() == "You're not allowed to edit that event."
1126 assert not events_api.get_event(token=token2, event_id=event_id).organizer
1128 events_api.invite_event_organizer(token=token1, event_id=event_id, user_id=user2.id)
1129 assert events_api.get_event(token=token2, event_id=event_id).organizer
1132def test_ListEventOccurrences(db):
1133 user1, token1 = generate_user()
1134 user2, token2 = generate_user()
1136 with session_scope() as session:
1137 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1139 start = now()
1141 event_ids = [
1142 events_api.create_event(
1143 token=token1,
1144 title="First occurrence",
1145 parent_community_id=c_id,
1146 start_date_time=local_date_time(start + timedelta(hours=1)),
1147 end_date_time=local_date_time(start + timedelta(hours=1.5)),
1148 ).event_id
1149 ]
1151 for i in range(5):
1152 event_ids.append(
1153 events_api.schedule_event(
1154 token=token1,
1155 event_id=event_ids[-1],
1156 content=f"{i}th occurrence",
1157 start_date_time=local_date_time(start + timedelta(hours=2 + i)),
1158 end_date_time=local_date_time(start + timedelta(hours=2.5 + i)),
1159 ).event_id
1160 )
1162 res = events_api.list_event_occurrences(token=token1, event_id=event_ids[-1], page_size=2)
1163 assert [event.event_id for event in res.events] == event_ids[:2]
1165 res = events_api.list_event_occurrences(
1166 token=token1, event_id=event_ids[-1], page_size=2, page_token=res.next_page_token
1167 )
1168 assert [event.event_id for event in res.events] == event_ids[2:4]
1170 res = events_api.list_event_occurrences(
1171 token=token1, event_id=event_ids[-1], page_size=2, page_token=res.next_page_token
1172 )
1173 assert [event.event_id for event in res.events] == event_ids[4:6]
1174 assert not res.next_page_token
1177def test_ListMyEvents(db, moderator: Moderator):
1178 user1, token1 = generate_user()
1179 user2, token2 = generate_user()
1180 user3, token3 = generate_user()
1181 user4, token4 = generate_user()
1182 user5, token5 = generate_user()
1184 with session_scope() as session:
1185 # Create global (world) -> macroregion -> region -> subregion hierarchy
1186 # my_communities_exclude_global filters out world, macroregion, and region level communities
1187 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1188 c_id = global_community.id
1189 macroregion_community = create_community(
1190 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1191 )
1192 region_community = create_community(
1193 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1194 )
1195 subregion_community = create_community(
1196 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1197 )
1198 c2_id = subregion_community.id
1200 start = now()
1202 def new_event(token: str, hours_from_now: int, community_id: int) -> int:
1203 return events_api.create_event(
1204 token=token,
1205 parent_community_id=community_id,
1206 start_date_time=local_date_time(start + timedelta(hours=hours_from_now)),
1207 end_date_time=local_date_time(start + timedelta(hours=hours_from_now + 0.5)),
1208 approve_by=moderator,
1209 ).event_id
1211 e2 = new_event(token1, 2, c_id)
1212 e1 = new_event(token2, 1, c_id)
1213 e3 = new_event(token1, 3, c_id)
1214 e5 = new_event(token2, 5, c_id)
1215 e4 = new_event(token3, 4, c_id)
1216 e6 = new_event(token4, 6, c2_id)
1218 events_api.invite_event_organizer(token=token1, event_id=e3, user_id=user3.id)
1220 events_api.set_event_attendance(token=token1, event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1221 events_api.set_event_subscription(token=token1, event_id=e4, subscribe=True)
1222 events_api.set_event_attendance(token=token2, event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1223 events_api.set_event_subscription(token=token3, event_id=e2, subscribe=True)
1225 # test pagination with token first
1226 list_res = events_api.list_my_events(token=token1, page_size=2)
1227 assert [event.event_id for event in list_res.events] == [e1, e2]
1228 list_res = events_api.list_my_events(token=token1, page_size=2, page_token=list_res.next_page_token)
1229 assert [event.event_id for event in list_res.events] == [e3, e4]
1230 assert not list_res.next_page_token
1232 listed_events = events_api.list_my_events(token=token1, subscribed=True, attending=True, organizing=True).events
1233 assert [event.event_id for event in listed_events] == [e1, e2, e3, e4]
1235 listed_events = events_api.list_my_events(token=token1).events
1236 assert [event.event_id for event in listed_events] == [e1, e2, e3, e4]
1238 listed_events = events_api.list_my_events(token=token1, subscribed=True).events
1239 assert [event.event_id for event in listed_events] == [e2, e3, e4]
1241 listed_events = events_api.list_my_events(token=token1, attending=True).events
1242 assert [event.event_id for event in listed_events] == [e1, e2, e3]
1244 listed_events = events_api.list_my_events(token=token1, organizing=True).events
1245 assert [event.event_id for event in listed_events] == [e2, e3]
1247 # Test pagination with page_number and verify total_items as user1
1248 list_res = events_api.list_my_events(
1249 token=token1, subscribed=True, attending=True, organizing=True, page_size=2, page_number=1
1250 )
1251 assert [event.event_id for event in list_res.events] == [e1, e2]
1252 assert list_res.total_items == 4
1254 list_res = events_api.list_my_events(
1255 token=token1, subscribed=True, attending=True, organizing=True, page_size=2, page_number=2
1256 )
1257 assert [event.event_id for event in list_res.events] == [e3, e4]
1258 assert list_res.total_items == 4
1260 # Verify no more pages
1261 list_res = events_api.list_my_events(
1262 token=token1, subscribed=True, attending=True, organizing=True, page_size=2, page_number=3
1263 )
1264 assert not list_res.events
1265 assert list_res.total_items == 4
1267 # Verify as user2
1268 listed_events = events_api.list_my_events(token=token2).events
1269 assert [event.event_id for event in listed_events] == [e1, e3, e5]
1271 listed_events = events_api.list_my_events(token=token2, subscribed=True).events
1272 assert [event.event_id for event in listed_events] == [e1, e5]
1274 listed_events = events_api.list_my_events(token=token2, attending=True).events
1275 assert [event.event_id for event in listed_events] == [e1, e3, e5]
1277 listed_events = events_api.list_my_events(token=token2, organizing=True).events
1278 assert [event.event_id for event in listed_events] == [e1, e5]
1280 # user3 is member of both global (c_id) and child (c2_id) communities
1281 listed_events = events_api.list_my_events(token=token3).events
1282 assert [event.event_id for event in listed_events] == [e1, e2, e3, e4, e5, e6]
1284 listed_events = events_api.list_my_events(token=token3, subscribed=True).events
1285 assert [event.event_id for event in listed_events] == [e2, e4]
1287 listed_events = events_api.list_my_events(token=token3, attending=True).events
1288 assert [event.event_id for event in listed_events] == [e4]
1290 listed_events = events_api.list_my_events(token=token3, organizing=True).events
1291 assert [event.event_id for event in listed_events] == [e3, e4]
1293 # my_communities returns events from both communities user3 is a member of
1294 listed_events = events_api.list_my_events(token=token3, my_communities=True).events
1295 assert [event.event_id for event in listed_events] == [e1, e2, e3, e4, e5, e6]
1297 # my_communities_exclude_global filters out events from global community (node_id=1)
1298 listed_events = events_api.list_my_events(
1299 token=token3, my_communities=True, my_communities_exclude_global=True
1300 ).events
1301 assert [event.event_id for event in listed_events] == [e6]
1303 # my_communities_exclude_global works independently of my_communities flag
1304 listed_events = events_api.list_my_events(token=token3, my_communities_exclude_global=True).events
1305 assert [event.event_id for event in listed_events] == [e6]
1307 # my_communities_exclude_global filters organizing results too
1308 listed_events = events_api.list_my_events(token=token3, organizing=True, my_communities_exclude_global=True).events
1309 assert [event.event_id for event in listed_events] == []
1311 # my_communities_exclude_global filters subscribed results too
1312 listed_events = events_api.list_my_events(token=token3, subscribed=True, my_communities_exclude_global=True).events
1313 assert [event.event_id for event in listed_events] == []
1315 listed_events = events_api.list_all_events(token=token5).events
1316 assert [event.event_id for event in listed_events] == [e1, e2, e3, e4, e5, e6]
1319def _paginate_my_events(token: str, page_size: int) -> list[int]:
1320 event_ids = []
1321 page_token = ""
1322 for _ in range(10): 1322 ↛ 1328line 1322 didn't jump to line 1328 because the loop on line 1322 didn't complete
1323 res = events_api.list_my_events(token=token, page_size=page_size, page_token=page_token)
1324 event_ids += [event.event_id for event in res.events]
1325 page_token = res.next_page_token
1326 if not page_token:
1327 return event_ids
1328 raise AssertionError("pagination did not terminate")
1331def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator):
1332 user, token = generate_user()
1334 with session_scope() as session:
1335 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1337 start = now()
1339 def new_event(start_offset: timedelta, duration: timedelta) -> int:
1340 return events_api.create_event(
1341 token=token,
1342 parent_community_id=c_id,
1343 start_date_time=local_date_time(start + start_offset),
1344 end_date_time=local_date_time(start + start_offset + duration),
1345 approve_by=moderator,
1346 ).event_id
1348 # a multi-day event overlapping all the short events below: it ends last but starts first,
1349 # so an end time based cursor would repeat it on every page and skip the short events
1350 long_event = new_event(timedelta(hours=1), timedelta(days=3))
1351 short_events = [new_event(timedelta(hours=2 + i), timedelta(hours=1)) for i in range(4)]
1353 assert _paginate_my_events(token, page_size=2) == [long_event, *short_events]
1356def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator):
1357 user, token = generate_user()
1359 with session_scope() as session:
1360 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1362 start = now()
1364 event_ids = [
1365 events_api.create_event(
1366 token=token,
1367 parent_community_id=c_id,
1368 start_date_time=local_date_time(start + timedelta(hours=1)),
1369 end_date_time=local_date_time(start + timedelta(hours=2)),
1370 approve_by=moderator,
1371 ).event_id
1372 for _ in range(5)
1373 ]
1375 assert _paginate_my_events(token, page_size=2) == event_ids
1378def test_list_my_events_exclude_attending(db, moderator: Moderator):
1379 user1, token1 = generate_user()
1380 user2, token2 = generate_user()
1382 with session_scope() as session:
1383 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1384 c_id = c.id
1386 def make_event(token: str) -> int:
1387 return events_api.create_event(token=token, parent_community_id=c_id, approve_by=moderator).event_id
1389 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1390 e_own = make_event(token1)
1391 e_attending = make_event(token2)
1392 e_community_only = make_event(token2)
1393 # e_both: user1 will be both organizer and attendee
1394 e_both = make_event(token2)
1396 # invite user1 as organizer of e_both
1397 events_api.invite_event_organizer(token=token2, event_id=e_both, user_id=user1.id)
1399 # user1 RSVPs to e_attending and e_both
1400 events_api.set_event_attendance(
1401 token=token1, event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
1402 )
1403 events_api.set_event_attendance(token=token1, event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1405 # baseline: all four community events visible
1406 res = events_api.list_my_events(token=token1, my_communities=True)
1407 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
1409 # exclude_attending removes events user1 is attending (e_attending, e_both)
1410 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
1411 res = events_api.list_my_events(token=token1, my_communities=True, exclude_attending=True)
1412 assert [e.event_id for e in res.events] == [e_community_only]
1414 # exclude_attending with attending=True: invalid combination
1415 with pytest.raises(grpc.RpcError) as e:
1416 events_api.list_my_events(token=token1, attending=True, exclude_attending=True)
1417 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1419 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
1420 res = events_api.list_my_events(token=token2, my_communities=True, exclude_attending=True)
1421 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
1422 assert [e.event_id for e in res.events] == [e_own]
1425def test_RemoveEventOrganizer(db, moderator: Moderator):
1426 user1, token1 = generate_user()
1427 user2, token2 = generate_user()
1429 with session_scope() as session:
1430 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1432 event_id = events_api.create_event(token=token1, approve_by=moderator).event_id
1434 assert not events_api.get_event(token=token2, event_id=event_id).organizer
1436 with pytest.raises(grpc.RpcError) as e:
1437 events_api.remove_event_organizer(token=token2, event_id=event_id)
1438 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1439 assert e.value.details() == "You're not allowed to edit that event."
1441 assert not events_api.get_event(token=token2, event_id=event_id).organizer
1443 events_api.invite_event_organizer(token=token1, event_id=event_id, user_id=user2.id)
1445 with pytest.raises(grpc.RpcError) as e:
1446 events_api.remove_event_organizer(token=token1, event_id=event_id)
1447 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1448 assert e.value.details() == "You cannot remove the event owner as an organizer."
1450 res = events_api.get_event(token=token2, event_id=event_id)
1451 assert res.organizer
1452 assert res.organizer_count == 2
1454 events_api.remove_event_organizer(token=token2, event_id=event_id)
1455 assert not events_api.get_event(token=token2, event_id=event_id).organizer
1457 with pytest.raises(grpc.RpcError) as e:
1458 events_api.remove_event_organizer(token=token2, event_id=event_id)
1459 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1460 assert e.value.details() == "You're not allowed to edit that event."
1462 res = events_api.get_event(token=token2, event_id=event_id)
1463 assert not res.organizer
1464 assert res.organizer_count == 1
1466 # Test that event owner can remove co-organizers
1468 # Add user2 back as organizer and verify
1469 events_api.invite_event_organizer(token=token1, event_id=event_id, user_id=user2.id)
1470 res = events_api.get_event(token=token1, event_id=event_id)
1471 assert res.organizer_count == 2
1473 # Event owner can remove co-organizer
1474 events_api.remove_event_organizer(token=token1, event_id=event_id, user_id=user2.id)
1475 res = events_api.get_event(token=token1, event_id=event_id)
1476 assert res.organizer_count == 1
1478 # Test that non-organizers cannot remove other organizers
1479 # User2 cannot invite themselves as organizer (not the owner)
1480 with pytest.raises(grpc.RpcError) as e:
1481 events_api.invite_event_organizer(token=token2, event_id=event_id, user_id=user2.id)
1482 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1483 assert e.value.details() == "You're not allowed to edit that event."
1485 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
1486 events_api.invite_event_organizer(token=token1, event_id=event_id, user_id=user2.id)
1489def test_ListEventAttendees_regression(db):
1490 # see issue #1617:
1491 #
1492 # 1. Create an event
1493 # 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`
1494 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
1495 # 4. Change the current user's attendance state to "going" again
1496 #
1497 # **Expected behaviour**
1498 # `ListEventAttendees` should return the current user's ID
1499 #
1500 # **Actual/current behaviour**
1501 # `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
1503 user1, token1 = generate_user()
1505 with session_scope() as session:
1506 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1508 event_id = events_api.create_event(token=token1, parent_community_id=c_id).event_id
1509 event_id = events_api.transfer_event(token=token1, event_id=event_id, new_owner_community_id=c_id).event_id
1511 events_api.set_event_attendance(
1512 token=token1, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING
1513 )
1514 events_api.set_event_attendance(token=token1, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1516 attendee_user_ids = events_api.list_event_attendees(token=token1, event_id=event_id).attendee_user_ids
1517 assert len(attendee_user_ids) == 1
1518 assert attendee_user_ids[0] == user1.id
1521def test_GetEventCalendarFile(db, moderator: Moderator):
1522 user1, token1 = generate_user()
1523 user2, token2 = generate_user()
1525 with session_scope() as session:
1526 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1528 start_time = now() + timedelta(hours=2)
1529 end_time = start_time + timedelta(hours=3)
1531 event_id = events_api.create_event(
1532 token=token1,
1533 parent_community_id=c_id,
1534 title="Dummy Title",
1535 content="Dummy content.",
1536 location=events_pb2.EventLocation(
1537 address="Near Null Island",
1538 lat=0.1,
1539 lng=0.2,
1540 ),
1541 start_date_time=local_date_time(start_time),
1542 end_date_time=local_date_time(end_time),
1543 approve_by=moderator,
1544 ).event_id
1546 file_res = events_api.get_event_calendar_file(token=token1, event_id=event_id)
1547 assert file_res.content_type == "text/calendar"
1548 ics_string = file_res.data.decode("utf-8")
1549 assert "SUMMARY:Dummy Title" in ics_string
1550 assert "DESCRIPTION:Dummy content." in ics_string
1551 assert "LOCATION:Near Null Island" in ics_string
1552 assert "STATUS:CANCELLED" not in ics_string
1553 pre_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
1554 assert pre_cancel_match is not None
1555 pre_cancel_sequence = int(pre_cancel_match.group(1))
1557 events_api.cancel_event(token=token1, event_id=event_id)
1559 file_res = events_api.get_event_calendar_file(token=token1, event_id=event_id)
1560 ics_string = file_res.data.decode("utf-8")
1561 assert "SUMMARY:Cancelled: Dummy Title" in ics_string
1562 assert "STATUS:CANCELLED" in ics_string
1563 post_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string)
1564 assert post_cancel_match is not None
1565 post_cancel_sequence = int(post_cancel_match.group(1))
1566 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal.
1567 assert post_cancel_sequence >= pre_cancel_sequence
1570def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
1571 user1, token1 = generate_user()
1572 user2, token2 = generate_user()
1573 user3, token3 = generate_user()
1574 user4, token4 = generate_user()
1576 with session_scope() as session:
1577 c = create_community(session, 0, 2, "Community", [user3], [], None)
1578 h = create_group(session, "Group", [user4], [], c)
1579 c_id = c.id
1580 h_id = h.id
1581 user4_id = user4.id
1583 event = events_api.create_event(
1584 token=token1,
1585 approve_by=moderator,
1586 )
1588 with threads_session(token2) as api:
1589 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
1591 moderator.approve_thread_post(reply_id)
1593 res = events_api.get_event(token=token3, event_id=event.event_id)
1594 assert res.thread.num_responses == 1
1596 with threads_session(token3) as api:
1597 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
1598 assert len(ret.replies) == 1
1599 assert not ret.next_page_token
1600 assert ret.replies[0].thread_id == reply_id
1601 assert ret.replies[0].content == "hi"
1602 assert ret.replies[0].author_user_id == user2.id
1603 assert ret.replies[0].num_replies == 0
1605 nested_reply_id = api.PostReply(
1606 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
1607 ).thread_id
1609 moderator.approve_thread_post(nested_reply_id)
1611 process_jobs()
1613 push = push_collector.pop_for_user(user1.id, last=True)
1614 assert push.topic_action == NotificationTopicAction.event__comment.display
1615 assert push.content.title == f"{user2.name} • Dummy Title"
1616 assert push.content.ios_title == user2.name
1617 assert push.content.ios_subtitle == "Commented on Dummy Title"
1618 assert push.content.body == "hi"
1620 push = push_collector.pop_for_user(user2.id, last=True)
1621 assert push.content.title == f"{user3.name} • Dummy Title"
1623 assert push_collector.count_for_user(user4_id) == 0
1626def test_can_overlap_other_events_schedule_regression(db):
1627 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
1628 user, token = generate_user()
1630 with session_scope() as session:
1631 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1633 start = now()
1635 # create another event, should be able to overlap with this one
1636 events_api.create_event(
1637 token=token,
1638 parent_community_id=c_id,
1639 start_date_time=local_date_time(start + timedelta(hours=1)),
1640 end_date_time=local_date_time(start + timedelta(hours=5)),
1641 )
1643 # this event
1644 res = events_api.create_event(
1645 token=token,
1646 parent_community_id=c_id,
1647 start_date_time=local_date_time(start + timedelta(hours=1)),
1648 end_date_time=local_date_time(start + timedelta(hours=2)),
1649 )
1651 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
1652 events_api.schedule_event(
1653 token=token,
1654 event_id=res.event_id,
1655 content="New event occurrence",
1656 location=events_pb2.EventLocation(
1657 address="A bit further but still near Null Island",
1658 lat=0.3,
1659 lng=0.2,
1660 ),
1661 start_date_time=local_date_time(start + timedelta(hours=3)),
1662 end_date_time=local_date_time(start + timedelta(hours=6)),
1663 )
1666def test_can_overlap_other_events_update_regression(db):
1667 user, token = generate_user()
1669 with session_scope() as session:
1670 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1672 start = now()
1674 # create another event, should be able to overlap with this one
1675 events_api.create_event(
1676 token=token,
1677 parent_community_id=c_id,
1678 start_date_time=local_date_time(start + timedelta(hours=1)),
1679 end_date_time=local_date_time(start + timedelta(hours=3)),
1680 )
1682 res = events_api.create_event(
1683 token=token,
1684 parent_community_id=c_id,
1685 start_date_time=local_date_time(start + timedelta(hours=7)),
1686 end_date_time=local_date_time(start + timedelta(hours=8)),
1687 )
1689 event_id = events_api.schedule_event(
1690 token=token,
1691 event_id=res.event_id,
1692 content="New event occurrence",
1693 location=events_pb2.EventLocation(
1694 address="A bit further but still near Null Island",
1695 lat=0.3,
1696 lng=0.2,
1697 ),
1698 start_date_time=local_date_time(start + timedelta(hours=4)),
1699 end_date_time=local_date_time(start + timedelta(hours=6)),
1700 ).event_id
1702 # can overlap with this current existing occurrence
1703 events_api.update_event(
1704 token=token,
1705 event_id=event_id,
1706 start_date_time=local_date_time(start + timedelta(hours=5)),
1707 end_date_time=local_date_time(start + timedelta(hours=6)),
1708 )
1710 events_api.update_event(
1711 token=token,
1712 event_id=event_id,
1713 start_date_time=local_date_time(start + timedelta(hours=2)),
1714 end_date_time=local_date_time(start + timedelta(hours=4)),
1715 )
1718def test_list_past_events_regression(db):
1719 # test for a bug where listing past events didn't work if they didn't have a future occurrence
1720 user, token = generate_user()
1722 with session_scope() as session:
1723 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1725 start = now()
1727 events_api.create_event(
1728 token=token,
1729 parent_community_id=c_id,
1730 start_date_time=local_date_time(start + timedelta(hours=3)),
1731 end_date_time=local_date_time(start + timedelta(hours=4)),
1732 )
1734 with session_scope() as session:
1735 session.execute(
1736 update(EventOccurrence).values(
1737 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
1738 )
1739 )
1741 res = events_api.list_all_events(token=token, past=True)
1742 assert len(res.events) == 1
1745def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
1746 user1, token1 = generate_user(complete_profile=True)
1747 user2, token2 = generate_user()
1748 user3, token3 = generate_user()
1749 user4, token4 = generate_user()
1750 user5, token5 = generate_user(is_superuser=True)
1752 with session_scope() as session:
1753 w = create_community(session, 0, 2, "World Community", [user5], [], None)
1754 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
1755 r = create_community(session, 0, 2, "Region", [user5], [], mr)
1756 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
1758 enforce_community_memberships()
1760 res = events_api.create_event(token=token1, parent_community_id=c_id, approve_by=moderator)
1762 user_url = f"http://localhost:3000/user/{user1.username}"
1763 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
1765 event_id = res.event_id
1766 assert res.community_invite_requested is False
1768 res = events_api.get_event(token=token1, event_id=event_id)
1769 assert res.community_invite_requested is False
1771 events_api.request_community_invite(token=token1, event_id=event_id)
1772 res = events_api.get_event(token=token1, event_id=event_id)
1773 assert res.community_invite_requested is True
1775 email = email_collector.pop_for_mods(last=True)
1776 assert user_url in email.plain
1777 assert event_url in email.plain
1779 # can't send another req
1780 with pytest.raises(grpc.RpcError) as err:
1781 events_api.request_community_invite(token=token1, event_id=event_id)
1782 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1783 assert err.value.details() == "You have already requested a community invite for this event."
1785 # another user can send one though
1786 # api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
1787 res = events_api.get_event(token=token3, event_id=event_id)
1788 assert res.community_invite_requested is False
1790 events_api.request_community_invite(token=token3, event_id=event_id)
1792 res = events_api.get_event(token=token3, event_id=event_id)
1793 assert res.community_invite_requested is True
1795 # but not a non-admin
1796 with pytest.raises(grpc.RpcError) as err:
1797 events_api.request_community_invite(token=token2, event_id=event_id)
1798 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
1799 assert err.value.details() == "You're not allowed to edit that event."
1801 with real_editor_session(token5) as editor:
1802 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
1803 assert len(res.requests) == 2
1804 assert res.requests[0].user_id == user1.id
1805 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain)
1806 assert res.requests[0].approx_users_to_notify == 2
1807 assert res.requests[1].user_id == user3.id
1808 assert res.requests[1].approx_users_to_notify == 2
1810 editor.DecideEventCommunityInviteRequest(
1811 editor_pb2.DecideEventCommunityInviteRequestReq(
1812 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
1813 approve=False,
1814 )
1815 )
1817 editor.DecideEventCommunityInviteRequest(
1818 editor_pb2.DecideEventCommunityInviteRequestReq(
1819 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
1820 approve=True,
1821 )
1822 )
1824 # Once a request is approved, the event still reports that a
1825 # community invite has been requested.
1826 res = events_api.get_event(token=token1, event_id=event_id)
1827 assert res.community_invite_requested is True
1829 # not after approve
1830 with pytest.raises(grpc.RpcError) as err:
1831 events_api.request_community_invite(token=token4, event_id=event_id)
1832 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1833 assert err.value.details() == "A community invite has already been sent out for this event."
1836def test_list_decided_community_invite_requests(db, moderator: Moderator):
1837 user1, token1 = generate_user()
1838 user2, token2 = generate_user()
1839 user3, token3 = generate_user()
1840 superuser, superuser_token = generate_user(is_superuser=True)
1842 with session_scope() as session:
1843 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
1844 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
1845 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
1846 c_id = create_community(session, 0, 2, "Community", [user1, user2, user3], [], r).id
1848 enforce_community_memberships()
1850 def create_event_and_request_invite(token: str, title: str) -> str:
1851 res = events_api.create_event(
1852 token=token,
1853 title=title,
1854 parent_community_id=c_id,
1855 approve_by=moderator,
1856 )
1857 events_api.request_community_invite(token=token, event_id=res.event_id)
1858 return f"http://localhost:3000/event/{res.event_id}/{res.slug}"
1860 event1_url = create_event_and_request_invite(token1, "Approved Event")
1861 event2_url = create_event_and_request_invite(token2, "Declined Event")
1862 # this one stays pending
1863 create_event_and_request_invite(token3, "Pending Event")
1865 with real_editor_session(superuser_token) as editor:
1866 pending = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
1867 assert len(pending.requests) == 3
1868 by_user = {req.user_id: req.event_community_invite_request_id for req in pending.requests}
1870 # nothing decided yet
1871 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
1872 assert len(res.requests) == 0
1873 assert not res.next_page_token
1875 editor.DecideEventCommunityInviteRequest(
1876 editor_pb2.DecideEventCommunityInviteRequestReq(
1877 event_community_invite_request_id=by_user[user1.id],
1878 approve=True,
1879 )
1880 )
1881 editor.DecideEventCommunityInviteRequest(
1882 editor_pb2.DecideEventCommunityInviteRequestReq(
1883 event_community_invite_request_id=by_user[user2.id],
1884 approve=False,
1885 )
1886 )
1888 # the pending one is not returned, and the most recently decided comes first
1889 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq())
1890 assert len(res.requests) == 2
1891 assert not res.next_page_token
1893 declined, approved = res.requests
1895 assert declined.event_community_invite_request_id == by_user[user2.id]
1896 assert declined.user_id == user2.id
1897 assert declined.event_url == event2_url
1898 assert declined.community_id == c_id
1899 assert declined.decided_by_user_id == superuser.id
1900 assert not declined.approved
1901 assert declined.created.ToDatetime() <= declined.decided.ToDatetime()
1903 assert approved.event_community_invite_request_id == by_user[user1.id]
1904 assert approved.user_id == user1.id
1905 assert approved.event_url == event1_url
1906 assert approved.community_id == c_id
1907 assert approved.decided_by_user_id == superuser.id
1908 assert approved.approved
1909 assert approved.decided.ToDatetime() <= declined.decided.ToDatetime()
1911 # filtering
1912 res = editor.ListDecidedEventCommunityInviteRequests(
1913 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=True))
1914 )
1915 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
1917 res = editor.ListDecidedEventCommunityInviteRequests(
1918 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=False))
1919 )
1920 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
1922 # pagination
1923 res = editor.ListDecidedEventCommunityInviteRequests(
1924 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1)
1925 )
1926 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]]
1927 assert res.next_page_token
1929 res = editor.ListDecidedEventCommunityInviteRequests(
1930 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1, page_token=res.next_page_token)
1931 )
1932 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]]
1933 assert not res.next_page_token
1936def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator):
1937 # Regression: users who already RSVP'd (or organize the event) must not get the
1938 # community invite notification when it is approved.
1939 organizer, organizer_token = generate_user()
1940 attendee, attendee_token = generate_user()
1941 member, _ = generate_user()
1942 superuser, superuser_token = generate_user(is_superuser=True)
1944 with session_scope() as session:
1945 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
1946 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
1947 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
1948 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id
1950 enforce_community_memberships()
1952 event_id = events_api.create_event(
1953 token=organizer_token,
1954 parent_community_id=c_id,
1955 approve_by=moderator,
1956 ).event_id
1958 # the attendee RSVPs before the community invite is approved
1959 events_api.set_event_attendance(
1960 token=attendee_token, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING
1961 )
1963 events_api.request_community_invite(token=organizer_token, event_id=event_id)
1965 with real_editor_session(superuser_token) as editor:
1966 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
1967 editor.DecideEventCommunityInviteRequest(
1968 editor_pb2.DecideEventCommunityInviteRequestReq(
1969 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
1970 approve=True,
1971 )
1972 )
1974 process_jobs()
1976 with session_scope() as session:
1978 def invite_notification_count(user_id: int) -> int:
1979 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all()
1980 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved])
1982 # a plain community member gets the invite...
1983 assert invite_notification_count(member.id) == 1
1984 # ...but the attendee and the organizer don't
1985 assert invite_notification_count(attendee.id) == 0
1986 assert invite_notification_count(organizer.id) == 0
1989def test_update_event_should_notify_queues_job():
1990 user, token = generate_user()
1991 start = now()
1993 with session_scope() as session:
1994 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
1996 # create an event
1997 create_res = events_api.create_event(
1998 token=token,
1999 parent_community_id=c_id,
2000 start_date_time=local_date_time(start + timedelta(hours=3)),
2001 end_date_time=local_date_time(start + timedelta(hours=6)),
2002 )
2004 event_id = create_res.event_id
2006 # measure initial background job queue length
2007 with session_scope() as session:
2008 jobs = session.query(BackgroundJob).all()
2009 job_length_before_update = len(jobs)
2011 # update with should_notify=False, expect no change in background job queue
2012 events_api.update_event(
2013 token=token, event_id=event_id, start_date_time=local_date_time(start + timedelta(hours=4)), should_notify=False
2014 )
2016 with session_scope() as session:
2017 jobs = session.query(BackgroundJob).all()
2018 assert len(jobs) == job_length_before_update
2020 # update with should_notify=True, expect one new background job added
2021 events_api.update_event(
2022 token=token, event_id=event_id, start_date_time=local_date_time(start + timedelta(hours=5)), should_notify=True
2023 )
2025 with session_scope() as session:
2026 jobs = session.query(BackgroundJob).all()
2027 assert len(jobs) == job_length_before_update + 1
2030def test_event_photo_key(db):
2031 """Test that events return the photo_key field when a photo is set."""
2032 user, token = generate_user()
2034 # Create a community and an upload for the event photo
2035 with session_scope() as session:
2036 create_community(session, 0, 2, "Community", [user], [], None)
2037 upload = Upload(
2038 key="test_event_photo_key_123",
2039 filename="test_event_photo_key_123.jpg",
2040 creator_user_id=user.id,
2041 )
2042 session.add(upload)
2044 # Create event without photo
2045 res = events_api.create_event(token=token)
2046 assert res.photo_key == ""
2047 assert res.photo_url == ""
2049 # Create event with photo
2050 res_with_photo = events_api.create_event(
2051 token=token,
2052 photo_key="test_event_photo_key_123",
2053 )
2055 assert res_with_photo.photo_key == "test_event_photo_key_123"
2056 assert "test_event_photo_key_123" in res_with_photo.photo_url
2058 event_id = res_with_photo.event_id
2060 # Verify photo_key is returned when getting the event
2061 get_res = events_api.get_event(token=token, event_id=event_id)
2062 assert get_res.photo_key == "test_event_photo_key_123"
2063 assert "test_event_photo_key_123" in get_res.photo_url
2066def test_event_timezone(db):
2067 user, token = generate_user()
2069 with session_scope() as session:
2070 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2072 # Midnight future day, UTC timezone
2073 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2074 end_time = start_time + timedelta(days=1)
2076 create_res = events_api.create_event(
2077 token=token,
2078 parent_community_id=c_id,
2079 location=events_api.EVENT_LOCATION_HELSINKI,
2080 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2081 start_date_time=local_date_time(start_time),
2082 end_date_time=local_date_time(end_time),
2083 )
2085 # Backend should have deduced the helsinki timezone when creating the event,
2086 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2087 assert create_res.timezone == "Europe/Helsinki"
2088 assert to_aware_datetime(create_res.start_time).hour != 0
2089 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2091 # Now update its location such that it gets a new timezone
2092 update_res = events_api.update_event(
2093 token=token,
2094 event_id=create_res.event_id,
2095 location=events_api.EVENT_LOCATION_NEW_YORK,
2096 )
2098 # The user didn't touch the datetime components on the frontend,
2099 # so they expect the event to be at the same local time (midnight),
2100 # but now in the New York timezone.
2101 assert update_res.timezone == "America/New_York"
2102 assert update_res.start_time != create_res.start_time
2103 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2104 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2106 # Also validate GetEvent
2107 get_res = events_api.get_event(token=token, event_id=create_res.event_id)
2108 assert get_res.timezone == update_res.timezone
2109 assert get_res.start_time == update_res.start_time
2112def test_event_created_with_shadowed_visibility(db):
2113 """Events start in SHADOWED state when created."""
2114 user, token = generate_user()
2116 with session_scope() as session:
2117 create_community(session, 0, 2, "Community", [user], [], None)
2119 event_id = events_api.create_event(token=token).event_id
2121 with session_scope() as session:
2122 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2123 mod_state = session.execute(
2124 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2125 ).scalar_one()
2126 assert mod_state.visibility == ModerationVisibility.shadowed
2129def test_shadowed_event_visible_to_creator_only(db):
2130 """SHADOWED events are visible to the creator but not to other users."""
2131 user1, token1 = generate_user()
2132 user2, token2 = generate_user()
2134 with session_scope() as session:
2135 create_community(session, 0, 2, "Community", [user1], [], None)
2137 event_id = events_api.create_event(token=token1, title="Shadowed Event").event_id
2139 # Creator can see it
2140 res = events_api.get_event(token=token1, event_id=event_id)
2141 assert res.title == "Shadowed Event"
2143 # Other user cannot
2144 with pytest.raises(grpc.RpcError) as e:
2145 events_api.get_event(token=token2, event_id=event_id)
2146 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2149def test_event_visible_after_approval(db, moderator: Moderator):
2150 """Events become visible to all users after moderation approval."""
2151 user1, token1 = generate_user()
2152 user2, token2 = generate_user()
2154 with session_scope() as session:
2155 create_community(session, 0, 2, "Community", [user1], [], None)
2157 event_id = events_api.create_event(token=token1, title="Approved Event").event_id
2159 # Other user cannot see it yet
2160 with pytest.raises(grpc.RpcError) as e:
2161 events_api.get_event(token=token2, event_id=event_id)
2162 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2164 # Approve the event
2165 moderator.approve_event_occurrence(event_id)
2167 # Now other user can see it
2168 res = events_api.get_event(token=token2, event_id=event_id)
2169 assert res.title == "Approved Event"
2172def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2173 """SHADOWED events appear in lists for the creator but not for other users."""
2174 user1, token1 = generate_user()
2175 user2, token2 = generate_user()
2177 with session_scope() as session:
2178 create_community(session, 0, 2, "Community", [user1], [], None)
2180 res = events_api.create_event(token=token1)
2182 event_id = res.event_id
2184 # Creator can see their own SHADOWED event in lists
2185 list_res = events_api.list_all_events(token=token1)
2186 event_ids = [e.event_id for e in list_res.events]
2187 assert event_id in event_ids
2189 # Other user cannot see the SHADOWED event in lists
2190 list_res = events_api.list_all_events(token=token2)
2191 event_ids = [e.event_id for e in list_res.events]
2192 assert event_id not in event_ids
2194 # After approval, other user can see it
2195 moderator.approve_event_occurrence(event_id)
2197 list_res = events_api.list_all_events(token=token2)
2198 event_ids = [e.event_id for e in list_res.events]
2199 assert event_id in event_ids
2202def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2203 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2204 user1, token1 = generate_user()
2205 user2, token2 = generate_user()
2207 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2208 with session_scope() as session:
2209 world = create_community(session, 0, 10, "World", [user1], [], None)
2210 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2211 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2212 create_community(session, 0, 2, "Child", [user2], [], region)
2214 event_id = events_api.create_event(token=token1).event_id
2216 # Process all jobs — notification should be deferred (event is SHADOWED)
2217 process_jobs()
2219 with session_scope() as session:
2220 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2221 # Notification was created with moderation_state_id for deferral
2222 assert notif.moderation_state_id is not None
2223 # No delivery exists (deferred because event is SHADOWED)
2224 delivery_count = session.execute(
2225 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2226 ).scalar_one_or_none()
2227 assert delivery_count is None
2229 # Approve the event — handle_notification is re-queued for deferred notifications
2230 moderator.approve_event_occurrence(event_id)
2232 # Verify handle_notification job was queued
2233 with session_scope() as session:
2234 pending_jobs = (
2235 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2236 .scalars()
2237 .all()
2238 )
2239 assert any("handle_notification" in j.job_type for j in pending_jobs)
2242def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2243 """Event update notifications should carry the event's moderation_state_id for deferral."""
2244 user1, token1 = generate_user()
2245 user2, token2 = generate_user()
2247 with session_scope() as session:
2248 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2250 event_id = events_api.create_event(token=token1, title="Update Test", approve_by=moderator).event_id
2252 process_jobs()
2253 # Clear any create notifications
2254 while push_collector.count_for_user(user2.id): 2254 ↛ 2255line 2254 didn't jump to line 2255 because the condition on line 2254 was never true
2255 push_collector.pop_for_user(user2.id)
2257 # User2 subscribes to the event
2258 events_api.set_event_subscription(token=token2, event_id=event_id, subscribe=True)
2260 # User1 updates the event with should_notify=True
2261 events_api.update_event(token=token1, event_id=event_id, title="Updated Title", should_notify=True)
2263 process_jobs()
2265 # Verify that the update notification for user2 has moderation_state_id set
2266 with session_scope() as session:
2267 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2269 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2270 # Find the update notification (most recent one)
2271 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2272 assert len(update_notifs) == 1
2273 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2276def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2277 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2278 user1, token1 = generate_user()
2279 user2, token2 = generate_user()
2281 with session_scope() as session:
2282 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2284 event_id = events_api.create_event(token=token1, approve_by=moderator).event_id
2286 process_jobs()
2287 while push_collector.count_for_user(user2.id): 2287 ↛ 2288line 2287 didn't jump to line 2288 because the condition on line 2287 was never true
2288 push_collector.pop_for_user(user2.id)
2290 # User2 subscribes
2291 events_api.set_event_subscription(token=token2, event_id=event_id, subscribe=True)
2293 # User1 cancels the event
2294 events_api.cancel_event(token=token1, event_id=event_id)
2296 process_jobs()
2298 # Verify that the cancel notification for user2 has moderation_state_id set
2299 with session_scope() as session:
2300 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2302 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2303 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2304 assert len(cancel_notifs) == 1
2305 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2308def test_event_update_and_cancel_notifications_not_sent_to_actor(
2309 db, push_collector: PushCollector, moderator: Moderator
2310):
2311 """The user who updates or cancels an event shouldn't be notified about their own action."""
2312 organizer_user, organizer_token = generate_user()
2313 attendee_user, attendee_token = generate_user()
2315 with session_scope() as session:
2316 create_community(session, 0, 2, "Community", [attendee_user], [], None)
2318 event_id = events_api.create_event(token=organizer_token, title="Actor Test", approve_by=moderator).event_id
2320 process_jobs()
2322 # The attendee subscribes to notifications
2323 events_api.set_event_subscription(token=attendee_token, event_id=event_id, subscribe=True)
2325 # The organizer updates and then cancels their own event
2326 events_api.update_event(token=organizer_token, event_id=event_id, title="Updated Title", should_notify=True)
2327 events_api.cancel_event(token=organizer_token, event_id=event_id)
2329 process_jobs()
2331 # The organizer should not receive any notifications
2332 assert push_collector.count_for_user(organizer_user.id) == 0
2334 # But the attendee should receive both the update and cancel notifications
2335 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__update.display
2336 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__cancel.display
2339def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2340 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2341 user1, token1 = generate_user()
2342 user2, token2 = generate_user()
2344 with session_scope() as session:
2345 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2347 # Create event starting 23 hours from now (within 24h reminder window)
2348 start_time = now() + timedelta(hours=23)
2349 end_time = start_time + timedelta(hours=1)
2351 res = events_api.create_event(
2352 token=token1,
2353 start_date_time=local_date_time(start_time),
2354 end_date_time=local_date_time(end_time),
2355 approve_by=moderator,
2356 )
2358 event_id = res.event_id
2359 process_jobs()
2360 while push_collector.count_for_user(user2.id): 2360 ↛ 2361line 2360 didn't jump to line 2361 because the condition on line 2360 was never true
2361 push_collector.pop_for_user(user2.id)
2363 # User2 marks attendance
2364 events_api.set_event_attendance(token=token2, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2366 # Run the event reminder handler
2367 send_event_reminders(empty_pb2.Empty())
2368 process_jobs()
2370 # Verify that the reminder notification for user2 has moderation_state_id set
2371 with session_scope() as session:
2372 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2374 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2375 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2376 assert len(reminder_notifs) == 1
2377 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2380def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2381 """Event reminders should not be sent for cancelled events."""
2382 user1, token1 = generate_user()
2383 user2, token2 = generate_user()
2385 with session_scope() as session:
2386 create_community(session, 0, 2, "Community", [user2], [], None)
2388 # Create event starting 23 hours from now (within 24h reminder window)
2389 start_time = now() + timedelta(hours=23)
2390 end_time = start_time + timedelta(hours=1)
2392 event_id = events_api.create_event(
2393 token=token1,
2394 start_date_time=local_date_time(start_time),
2395 end_date_time=local_date_time(end_time),
2396 approve_by=moderator,
2397 ).event_id
2399 process_jobs()
2401 # User2 marks attendance
2402 events_api.set_event_attendance(token=token2, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2404 # User1 cancels the event
2405 events_api.cancel_event(token=token1, event_id=event_id)
2407 process_jobs()
2408 # Drain any cancellation-related notifications so we can cleanly assert on reminders
2409 while push_collector.count_for_user(user2.id):
2410 push_collector.pop_for_user(user2.id)
2412 # Run the event reminder handler
2413 send_event_reminders(empty_pb2.Empty())
2414 process_jobs()
2416 # Verify that no reminder notification was sent for user2
2417 with session_scope() as session:
2418 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2419 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2420 assert len(reminder_notifs) == 0
2423@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
2424def test_event_reminder_not_sent_for_invisible_attendee(
2425 db, push_collector: PushCollector, moderator: Moderator, invisible_field
2426):
2427 user1, token1 = generate_user()
2428 user2, token2 = generate_user()
2430 with session_scope() as session:
2431 create_community(session, 0, 2, "Community", [user2], [], None)
2433 start_time = now() + timedelta(hours=23)
2434 end_time = start_time + timedelta(hours=1)
2436 event_id = events_api.create_event(
2437 token=token1,
2438 start_date_time=local_date_time(start_time),
2439 end_date_time=local_date_time(end_time),
2440 approve_by=moderator,
2441 ).event_id
2443 process_jobs()
2445 events_api.set_event_attendance(token=token2, event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2447 with session_scope() as session:
2448 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
2450 send_event_reminders(empty_pb2.Empty())
2451 process_jobs()
2453 with session_scope() as session:
2454 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2455 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2456 assert len(reminder_notifs) == 0
2459def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
2460 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
2461 user1, token1 = generate_user()
2462 user2, token2 = generate_user()
2464 with session_scope() as session:
2465 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
2467 start = now()
2469 # User1 creates event A with 3 occurrences
2470 event_a_ids = [
2471 events_api.create_event(
2472 token=token1,
2473 title="Event A",
2474 content="Content A.",
2475 parent_community_id=c_id,
2476 start_date_time=local_date_time(start + timedelta(hours=1)),
2477 end_date_time=local_date_time(start + timedelta(hours=1.5)),
2478 approve_by=moderator,
2479 ).event_id
2480 ]
2482 for i in range(2):
2483 event_a_ids.append(
2484 events_api.schedule_event(
2485 token=token1,
2486 event_id=event_a_ids[-1],
2487 content=f"A occurrence {i}",
2488 start_date_time=local_date_time(start + timedelta(hours=2 + i)),
2489 end_date_time=local_date_time(start + timedelta(hours=2.5 + i)),
2490 ).event_id
2491 )
2493 # User2 creates event B with 2 occurrences
2494 event_b_ids = [
2495 events_api.create_event(
2496 token=token2,
2497 title="Event B",
2498 content="Content B.",
2499 parent_community_id=c_id,
2500 start_date_time=local_date_time(start + timedelta(hours=10)),
2501 end_date_time=local_date_time(start + timedelta(hours=10.5)),
2502 approve_by=moderator,
2503 ).event_id
2504 ]
2506 event_b_ids.append(
2507 events_api.schedule_event(
2508 token=token2,
2509 event_id=event_b_ids[-1],
2510 content="B occurrence 1",
2511 start_date_time=local_date_time(start + timedelta(hours=11)),
2512 end_date_time=local_date_time(start + timedelta(hours=11.5)),
2513 ).event_id
2514 )
2516 # List occurrences for event A — should only get event A's 3 occurrences
2517 listed_occurrences = events_api.list_event_occurrences(token=token1, event_id=event_a_ids[-1]).events
2518 returned_ids = [e.event_id for e in listed_occurrences]
2519 assert sorted(returned_ids) == sorted(event_a_ids)
2521 # List occurrences for event B — should only get event B's 2 occurrences
2522 listed_occurrences = events_api.list_event_occurrences(token=token2, event_id=event_b_ids[-1]).events
2523 returned_ids = [e.event_id for e in listed_occurrences]
2524 assert sorted(returned_ids) == sorted(event_b_ids)
2527def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2528 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
2529 user1, token1 = generate_user()
2530 user2, token2 = generate_user()
2532 with session_scope() as session:
2533 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2535 res = events_api.create_event(token=token1, parent_community_id=c_id, approve_by=moderator)
2536 event_id = res.event_id
2537 thread_id = res.thread.thread_id
2539 process_jobs()
2540 while push_collector.count_for_user(user1.id): 2540 ↛ 2541line 2540 didn't jump to line 2541 because the condition on line 2540 was never true
2541 push_collector.pop_for_user(user1.id)
2543 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
2544 events_api.set_event_subscription(token=token1, event_id=event_id, subscribe=True)
2546 # User2 posts a top-level comment on the event thread
2547 with threads_session(token2) as api:
2548 comment_thread_id = api.PostReply(
2549 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
2550 ).thread_id
2552 process_jobs()
2554 # The comment notification for user1 should be gated on the comment's own moderation_state_id
2555 comment_db_id = comment_thread_id // 10
2556 with session_scope() as session:
2557 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
2559 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
2560 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
2561 assert len(comment_notifs) == 1
2562 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
2565def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2566 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
2567 user1, token1 = generate_user()
2568 user2, token2 = generate_user()
2569 user3, token3 = generate_user()
2571 with session_scope() as session:
2572 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
2574 res = events_api.create_event(token=token1, approve_by=moderator)
2575 event_id = res.event_id
2576 thread_id = res.thread.thread_id
2578 process_jobs()
2579 while push_collector.count_for_user(user1.id): 2579 ↛ 2580line 2579 didn't jump to line 2580 because the condition on line 2579 was never true
2580 push_collector.pop_for_user(user1.id)
2582 # User2 posts a top-level comment
2583 with threads_session(token2) as api:
2584 comment_thread_id = api.PostReply(
2585 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
2586 ).thread_id
2588 process_jobs()
2589 while push_collector.count_for_user(user1.id): 2589 ↛ 2590line 2589 didn't jump to line 2590 because the condition on line 2589 was never true
2590 push_collector.pop_for_user(user1.id)
2592 # User3 replies to user2's comment (depth=2 reply)
2593 with threads_session(token3) as api:
2594 nested_reply_thread_id = api.PostReply(
2595 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
2596 ).thread_id
2598 process_jobs()
2600 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
2601 nested_reply_db_id = nested_reply_thread_id // 10
2602 with session_scope() as session:
2603 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
2605 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2606 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
2607 assert len(reply_notifs) == 1
2608 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id