Coverage for app/backend/src/tests/test_events.py: 99%

1418 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-23 22:46 +0000

1from datetime import datetime, timedelta 

2from zoneinfo import ZoneInfo 

3 

4import grpc 

5import pytest 

6from google.protobuf import empty_pb2, wrappers_pb2 

7from psycopg.types.range import TimestamptzRange 

8from sqlalchemy import select 

9from sqlalchemy.sql.expression import update 

10 

11from couchers.db import session_scope 

12from couchers.jobs.handlers import send_event_reminders 

13from couchers.models import ( 

14 BackgroundJob, 

15 BackgroundJobState, 

16 Comment, 

17 EventOccurrence, 

18 ModerationState, 

19 ModerationVisibility, 

20 Notification, 

21 NotificationDelivery, 

22 NotificationTopicAction, 

23 Reply, 

24 Upload, 

25 User, 

26) 

27from couchers.proto import editor_pb2, events_pb2, threads_pb2 

28from couchers.tasks import enforce_community_memberships 

29from couchers.utils import datetime_to_iso8601_local, now, to_aware_datetime 

30from tests.fixtures.db import generate_user 

31from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs 

32from tests.fixtures.sessions import events_session, real_editor_session, threads_session 

33from tests.test_communities import create_community, create_group 

34 

35 

36@pytest.fixture(autouse=True) 

37def _(testconfig): 

38 pass 

39 

40 

41def to_event_time_granularity(value: datetime) -> datetime: 

42 """Events are scheduled at the minute granularity.""" 

43 return value.replace(second=0, microsecond=0) 

44 

45 

46def is_utc_or_gmt(timezone: str) -> bool: 

47 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT. 

48 # Tests should be agnostic to which one we're using. 

49 return timezone in ("Etc/UTC", "Etc/GMT") 

50 

51 

52def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator): 

53 # test cases: 

54 # can create event 

55 # cannot create event with missing details 

56 # can't create event that starts in the past 

57 # can create in different timezones 

58 

59 # event creator 

60 user1, token1 = generate_user() 

61 # community moderator 

62 user2, token2 = generate_user() 

63 # third party 

64 user3, token3 = generate_user() 

65 

66 with session_scope() as session: 

67 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

68 

69 time_before = now() 

70 start_time = now() + timedelta(hours=2) 

71 end_time = start_time + timedelta(hours=3) 

72 

73 # Can create an event 

74 with events_session(token1) as api: 

75 res = api.CreateEvent( 

76 events_pb2.CreateEventReq( 

77 title="Dummy Title", 

78 content="Dummy content.", 

79 photo_key=None, 

80 location=events_pb2.EventLocation( 

81 address="Near Null Island", 

82 lat=0.1, 

83 lng=0.2, 

84 ), 

85 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

86 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

87 ) 

88 ) 

89 

90 assert res.is_next 

91 assert res.title == "Dummy Title" 

92 assert res.slug == "dummy-title" 

93 assert res.content == "Dummy content." 

94 assert not res.photo_url 

95 assert res.HasField("location") 

96 assert res.location.lat == 0.1 

97 assert res.location.lng == 0.2 

98 assert res.location.address == "Near Null Island" 

99 assert time_before <= to_aware_datetime(res.created) <= now() 

100 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

101 assert res.creator_user_id == user1.id 

102 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

103 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

104 assert is_utc_or_gmt(res.timezone) 

105 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

106 assert res.organizer 

107 assert res.subscriber 

108 assert res.going_count == 1 

109 assert res.organizer_count == 1 

110 assert res.subscriber_count == 1 

111 assert res.owner_user_id == user1.id 

112 assert not res.owner_community_id 

113 assert not res.owner_group_id 

114 assert res.thread.thread_id 

115 assert res.can_edit 

116 assert not res.can_moderate 

117 

118 event_id = res.event_id 

119 

120 # Approve the event so other users can see it 

121 moderator.approve_event_occurrence(event_id) 

122 

123 with events_session(token2) as api: 

124 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

125 

126 assert res.is_next 

127 assert res.title == "Dummy Title" 

128 assert res.slug == "dummy-title" 

129 assert res.content == "Dummy content." 

130 assert not res.photo_url 

131 assert res.HasField("location") 

132 assert res.location.lat == 0.1 

133 assert res.location.lng == 0.2 

134 assert res.location.address == "Near Null Island" 

135 assert time_before <= to_aware_datetime(res.created) <= now() 

136 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

137 assert res.creator_user_id == user1.id 

138 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

139 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

140 assert is_utc_or_gmt(res.timezone) 

141 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

142 assert not res.organizer 

143 assert not res.subscriber 

144 assert res.going_count == 1 

145 assert res.organizer_count == 1 

146 assert res.subscriber_count == 1 

147 assert res.owner_user_id == user1.id 

148 assert not res.owner_community_id 

149 assert not res.owner_group_id 

150 assert res.thread.thread_id 

151 assert res.can_edit 

152 assert res.can_moderate 

153 

154 with events_session(token3) as api: 

155 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

156 

157 assert res.is_next 

158 assert res.title == "Dummy Title" 

159 assert res.slug == "dummy-title" 

160 assert res.content == "Dummy content." 

161 assert not res.photo_url 

162 assert res.HasField("location") 

163 assert res.location.lat == 0.1 

164 assert res.location.lng == 0.2 

165 assert res.location.address == "Near Null Island" 

166 assert time_before <= to_aware_datetime(res.created) <= now() 

167 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

168 assert res.creator_user_id == user1.id 

169 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

170 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

171 assert is_utc_or_gmt(res.timezone) 

172 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

173 assert not res.organizer 

174 assert not res.subscriber 

175 assert res.going_count == 1 

176 assert res.organizer_count == 1 

177 assert res.subscriber_count == 1 

178 assert res.owner_user_id == user1.id 

179 assert not res.owner_community_id 

180 assert not res.owner_group_id 

181 assert res.thread.thread_id 

182 assert not res.can_edit 

183 assert not res.can_moderate 

184 

185 # Failure cases 

186 with events_session(token1) as api: 

187 with pytest.raises(grpc.RpcError) as e: 

188 api.CreateEvent( 

189 events_pb2.CreateEventReq( 

190 # title="Dummy Title", 

191 content="Dummy content.", 

192 photo_key=None, 

193 location=events_pb2.EventLocation( 

194 address="Near Null Island", 

195 lat=0.1, 

196 lng=0.2, 

197 ), 

198 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

199 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

200 ) 

201 ) 

202 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

203 assert e.value.details() == "Missing event title." 

204 

205 with pytest.raises(grpc.RpcError) as e: 

206 api.CreateEvent( 

207 events_pb2.CreateEventReq( 

208 title="Dummy Title", 

209 # content="Dummy content.", 

210 photo_key=None, 

211 location=events_pb2.EventLocation( 

212 address="Near Null Island", 

213 lat=0.1, 

214 lng=0.2, 

215 ), 

216 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

217 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

218 ) 

219 ) 

220 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

221 assert e.value.details() == "Missing event content." 

222 

223 with pytest.raises(grpc.RpcError) as e: 

224 api.CreateEvent( 

225 events_pb2.CreateEventReq( 

226 title="Dummy Title", 

227 content="Dummy content.", 

228 photo_key="nonexistent", 

229 location=events_pb2.EventLocation( 

230 address="Near Null Island", 

231 lat=0.1, 

232 lng=0.2, 

233 ), 

234 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

235 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

236 ) 

237 ) 

238 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

239 assert e.value.details() == "Photo not found." 

240 

241 with pytest.raises(grpc.RpcError) as e: 

242 api.CreateEvent( 

243 events_pb2.CreateEventReq( 

244 title="Dummy Title", 

245 content="Dummy content.", 

246 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

247 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

248 ) 

249 ) 

250 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

251 assert e.value.details() == "Missing event address or location." 

252 

253 with pytest.raises(grpc.RpcError) as e: 

254 api.CreateEvent( 

255 events_pb2.CreateEventReq( 

256 title="Dummy Title", 

257 content="Dummy content.", 

258 location=events_pb2.EventLocation( 

259 address="Near Null Island", 

260 ), 

261 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

262 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

263 ) 

264 ) 

265 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

266 assert e.value.details() == "Invalid coordinate." 

267 

268 with pytest.raises(grpc.RpcError) as e: 

269 api.CreateEvent( 

270 events_pb2.CreateEventReq( 

271 title="Dummy Title", 

272 content="Dummy content.", 

273 location=events_pb2.EventLocation( 

274 lat=0.1, 

275 lng=0.1, 

276 ), 

277 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

278 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

279 ) 

280 ) 

281 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

282 assert e.value.details() == "Missing event address or location." 

283 

284 with pytest.raises(grpc.RpcError) as e: 

285 api.CreateEvent( 

286 events_pb2.CreateEventReq( 

287 title="Dummy Title", 

288 content="Dummy content.", 

289 location=events_pb2.EventLocation( 

290 address="Near Null Island", 

291 lat=0.1, 

292 lng=0.2, 

293 ), 

294 start_datetime_iso8601_local=datetime_to_iso8601_local(now() - timedelta(hours=2)), 

295 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

296 ) 

297 ) 

298 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

299 assert e.value.details() == "The event must be in the future." 

300 

301 with pytest.raises(grpc.RpcError) as e: 

302 api.CreateEvent( 

303 events_pb2.CreateEventReq( 

304 title="Dummy Title", 

305 content="Dummy content.", 

306 location=events_pb2.EventLocation( 

307 address="Near Null Island", 

308 lat=0.1, 

309 lng=0.2, 

310 ), 

311 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

312 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

313 ) 

314 ) 

315 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

316 assert e.value.details() == "The event must end after it starts." 

317 

318 with pytest.raises(grpc.RpcError) as e: 

319 api.CreateEvent( 

320 events_pb2.CreateEventReq( 

321 title="Dummy Title", 

322 content="Dummy content.", 

323 location=events_pb2.EventLocation( 

324 address="Near Null Island", 

325 lat=0.1, 

326 lng=0.2, 

327 ), 

328 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=2)), 

329 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=5)), 

330 ) 

331 ) 

332 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

333 assert e.value.details() == "The event needs to start within the next year." 

334 

335 with pytest.raises(grpc.RpcError) as e: 

336 api.CreateEvent( 

337 events_pb2.CreateEventReq( 

338 title="Dummy Title", 

339 content="Dummy content.", 

340 location=events_pb2.EventLocation( 

341 address="Near Null Island", 

342 lat=0.1, 

343 lng=0.2, 

344 ), 

345 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

346 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=100)), 

347 ) 

348 ) 

349 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

350 assert e.value.details() == "Events cannot last longer than 7 days." 

351 

352 

353def test_CreateEvent_incomplete_profile(db): 

354 user1, token1 = generate_user(complete_profile=False) 

355 user2, token2 = generate_user() 

356 

357 with session_scope() as session: 

358 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

359 

360 start_time = now() + timedelta(hours=2) 

361 end_time = start_time + timedelta(hours=3) 

362 

363 with events_session(token1) as api: 

364 with pytest.raises(grpc.RpcError) as e: 

365 api.CreateEvent( 

366 events_pb2.CreateEventReq( 

367 title="Dummy Title", 

368 content="Dummy content.", 

369 photo_key=None, 

370 location=events_pb2.EventLocation( 

371 address="Near Null Island", 

372 lat=0.1, 

373 lng=0.2, 

374 ), 

375 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

376 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

377 ) 

378 ) 

379 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

380 assert e.value.details() == "You have to complete your profile before you can create an event." 

381 

382 

383def test_ScheduleEvent(db): 

384 # test cases: 

385 # can schedule a new event occurrence 

386 

387 user, token = generate_user() 

388 

389 with session_scope() as session: 

390 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

391 

392 time_before = now() 

393 start_time = now() + timedelta(hours=2) 

394 end_time = start_time + timedelta(hours=3) 

395 

396 with events_session(token) as api: 

397 res = api.CreateEvent( 

398 events_pb2.CreateEventReq( 

399 title="Dummy Title", 

400 content="Dummy content.", 

401 parent_community_id=c_id, 

402 location=events_pb2.EventLocation( 

403 address="Near Null Island", 

404 lat=0.1, 

405 lng=0.2, 

406 ), 

407 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

408 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

409 ) 

410 ) 

411 

412 new_start_time = now() + timedelta(hours=6) 

413 new_end_time = new_start_time + timedelta(hours=2) 

414 

415 res = api.ScheduleEvent( 

416 events_pb2.ScheduleEventReq( 

417 event_id=res.event_id, 

418 content="New event occurrence", 

419 location=events_pb2.EventLocation( 

420 address="A bit further but still near Null Island", 

421 lat=0.3, 

422 lng=0.2, 

423 ), 

424 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time), 

425 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time), 

426 ) 

427 ) 

428 

429 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id)) 

430 

431 assert not res.is_next 

432 assert res.title == "Dummy Title" 

433 assert res.slug == "dummy-title" 

434 assert res.content == "New event occurrence" 

435 assert not res.photo_url 

436 assert res.HasField("location") 

437 assert res.location.lat == 0.3 

438 assert res.location.lng == 0.2 

439 assert res.location.address == "A bit further but still near Null Island" 

440 assert time_before <= to_aware_datetime(res.created) <= now() 

441 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

442 assert res.creator_user_id == user.id 

443 assert to_aware_datetime(res.start_time) == to_event_time_granularity(new_start_time) 

444 assert to_aware_datetime(res.end_time) == to_event_time_granularity(new_end_time) 

445 assert is_utc_or_gmt(res.timezone) 

446 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

447 assert res.organizer 

448 assert res.subscriber 

449 assert res.going_count == 1 

450 assert res.organizer_count == 1 

451 assert res.subscriber_count == 1 

452 assert res.owner_user_id == user.id 

453 assert not res.owner_community_id 

454 assert not res.owner_group_id 

455 assert res.thread.thread_id 

456 assert res.can_edit 

457 assert res.can_moderate 

458 

459 

460def test_cannot_overlap_occurrences_schedule(db): 

461 user, token = generate_user() 

462 

463 with session_scope() as session: 

464 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

465 

466 start = now() 

467 

468 with events_session(token) as api: 

469 res = api.CreateEvent( 

470 events_pb2.CreateEventReq( 

471 title="Dummy Title", 

472 content="Dummy content.", 

473 parent_community_id=c_id, 

474 location=events_pb2.EventLocation( 

475 address="Near Null Island", 

476 lat=0.1, 

477 lng=0.2, 

478 ), 

479 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

480 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)), 

481 ) 

482 ) 

483 

484 with pytest.raises(grpc.RpcError) as e: 

485 api.ScheduleEvent( 

486 events_pb2.ScheduleEventReq( 

487 event_id=res.event_id, 

488 content="New event occurrence", 

489 location=events_pb2.EventLocation( 

490 address="A bit further but still near Null Island", 

491 lat=0.3, 

492 lng=0.2, 

493 ), 

494 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)), 

495 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)), 

496 ) 

497 ) 

498 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

499 assert e.value.details() == "An event cannot have overlapping occurrences." 

500 

501 

502def test_cannot_overlap_occurrences_update(db): 

503 user, token = generate_user() 

504 

505 with session_scope() as session: 

506 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

507 

508 start = now() 

509 

510 with events_session(token) as api: 

511 res = api.CreateEvent( 

512 events_pb2.CreateEventReq( 

513 title="Dummy Title", 

514 content="Dummy content.", 

515 parent_community_id=c_id, 

516 location=events_pb2.EventLocation( 

517 address="Near Null Island", 

518 lat=0.1, 

519 lng=0.2, 

520 ), 

521 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

522 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)), 

523 ) 

524 ) 

525 

526 event_id = api.ScheduleEvent( 

527 events_pb2.ScheduleEventReq( 

528 event_id=res.event_id, 

529 content="New event occurrence", 

530 location=events_pb2.EventLocation( 

531 address="A bit further but still near Null Island", 

532 lat=0.3, 

533 lng=0.2, 

534 ), 

535 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)), 

536 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)), 

537 ) 

538 ).event_id 

539 

540 # can overlap with this current existing occurrence 

541 api.UpdateEvent( 

542 events_pb2.UpdateEventReq( 

543 event_id=event_id, 

544 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

545 value=datetime_to_iso8601_local(start + timedelta(hours=5)) 

546 ), 

547 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

548 value=datetime_to_iso8601_local(start + timedelta(hours=6)) 

549 ), 

550 ) 

551 ) 

552 

553 with pytest.raises(grpc.RpcError) as e: 

554 api.UpdateEvent( 

555 events_pb2.UpdateEventReq( 

556 event_id=event_id, 

557 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

558 value=datetime_to_iso8601_local(start + timedelta(hours=2)) 

559 ), 

560 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

561 value=datetime_to_iso8601_local(start + timedelta(hours=4)) 

562 ), 

563 ) 

564 ) 

565 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

566 assert e.value.details() == "An event cannot have overlapping occurrences." 

567 

568 

569def test_UpdateEvent_single(db, moderator: Moderator): 

570 # test cases: 

571 # owner can update 

572 # community owner can update 

573 # notifies attendees 

574 

575 # event creator 

576 user1, token1 = generate_user() 

577 # community moderator 

578 user2, token2 = generate_user() 

579 # third parties 

580 user3, token3 = generate_user() 

581 user4, token4 = generate_user() 

582 user5, token5 = generate_user() 

583 user6, token6 = generate_user() 

584 

585 with session_scope() as session: 

586 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

587 

588 time_before = now() 

589 start_time = now() + timedelta(hours=2) 

590 end_time = start_time + timedelta(hours=3) 

591 

592 with events_session(token1) as api: 

593 res = api.CreateEvent( 

594 events_pb2.CreateEventReq( 

595 title="Dummy Title", 

596 content="Dummy content.", 

597 parent_community_id=c_id, 

598 location=events_pb2.EventLocation( 

599 address="Near Null Island", 

600 lat=0.1, 

601 lng=0.2, 

602 ), 

603 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

604 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

605 ) 

606 ) 

607 

608 event_id = res.event_id 

609 

610 moderator.approve_event_occurrence(event_id) 

611 

612 with events_session(token4) as api: 

613 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

614 

615 with events_session(token5) as api: 

616 api.SetEventAttendance( 

617 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

618 ) 

619 

620 with events_session(token6) as api: 

621 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

622 

623 time_before_update = now() 

624 

625 with events_session(token1) as api: 

626 res = api.UpdateEvent( 

627 events_pb2.UpdateEventReq( 

628 event_id=event_id, 

629 ) 

630 ) 

631 

632 with events_session(token1) as api: 

633 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

634 

635 assert res.is_next 

636 assert res.title == "Dummy Title" 

637 assert res.slug == "dummy-title" 

638 assert res.content == "Dummy content." 

639 assert not res.photo_url 

640 assert res.HasField("location") 

641 assert res.location.lat == 0.1 

642 assert res.location.lng == 0.2 

643 assert res.location.address == "Near Null Island" 

644 assert time_before <= to_aware_datetime(res.created) <= time_before_update 

645 assert time_before_update <= to_aware_datetime(res.last_edited) <= now() 

646 assert res.creator_user_id == user1.id 

647 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

648 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

649 assert is_utc_or_gmt(res.timezone) 

650 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

651 assert res.organizer 

652 assert res.subscriber 

653 assert res.going_count == 2 

654 assert res.organizer_count == 1 

655 assert res.subscriber_count == 3 

656 assert res.owner_user_id == user1.id 

657 assert not res.owner_community_id 

658 assert not res.owner_group_id 

659 assert res.thread.thread_id 

660 assert res.can_edit 

661 assert not res.can_moderate 

662 

663 with events_session(token2) as api: 

664 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

665 

666 assert res.is_next 

667 assert res.title == "Dummy Title" 

668 assert res.slug == "dummy-title" 

669 assert res.content == "Dummy content." 

670 assert not res.photo_url 

671 assert res.HasField("location") 

672 assert res.location.lat == 0.1 

673 assert res.location.lng == 0.2 

674 assert res.location.address == "Near Null Island" 

675 assert time_before <= to_aware_datetime(res.created) <= time_before_update 

676 assert time_before_update <= to_aware_datetime(res.last_edited) <= now() 

677 assert res.creator_user_id == user1.id 

678 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

679 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

680 assert is_utc_or_gmt(res.timezone) 

681 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

682 assert not res.organizer 

683 assert not res.subscriber 

684 assert res.going_count == 2 

685 assert res.organizer_count == 1 

686 assert res.subscriber_count == 3 

687 assert res.owner_user_id == user1.id 

688 assert not res.owner_community_id 

689 assert not res.owner_group_id 

690 assert res.thread.thread_id 

691 assert res.can_edit 

692 assert res.can_moderate 

693 

694 with events_session(token3) as api: 

695 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

696 

697 assert res.is_next 

698 assert res.title == "Dummy Title" 

699 assert res.slug == "dummy-title" 

700 assert res.content == "Dummy content." 

701 assert not res.photo_url 

702 assert res.HasField("location") 

703 assert res.location.lat == 0.1 

704 assert res.location.lng == 0.2 

705 assert res.location.address == "Near Null Island" 

706 assert time_before <= to_aware_datetime(res.created) <= time_before_update 

707 assert time_before_update <= to_aware_datetime(res.last_edited) <= now() 

708 assert res.creator_user_id == user1.id 

709 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

710 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

711 assert is_utc_or_gmt(res.timezone) 

712 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

713 assert not res.organizer 

714 assert not res.subscriber 

715 assert res.going_count == 2 

716 assert res.organizer_count == 1 

717 assert res.subscriber_count == 3 

718 assert res.owner_user_id == user1.id 

719 assert not res.owner_community_id 

720 assert not res.owner_group_id 

721 assert res.thread.thread_id 

722 assert not res.can_edit 

723 assert not res.can_moderate 

724 

725 with events_session(token1) as api: 

726 res = api.UpdateEvent( 

727 events_pb2.UpdateEventReq( 

728 event_id=event_id, 

729 location=events_pb2.EventLocation( 

730 address="Nearer Null Island", 

731 lat=0.01, 

732 lng=0.02, 

733 ), 

734 ) 

735 ) 

736 

737 with events_session(token3) as api: 

738 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

739 

740 assert res.HasField("location") 

741 assert res.location.address == "Nearer Null Island" 

742 assert res.location.lat == 0.01 

743 assert res.location.lng == 0.02 

744 

745 

746def test_UpdateEvent_all(db, moderator: Moderator): 

747 # event creator 

748 user1, token1 = generate_user() 

749 # community moderator 

750 user2, token2 = generate_user() 

751 # third parties 

752 user3, token3 = generate_user() 

753 user4, token4 = generate_user() 

754 user5, token5 = generate_user() 

755 user6, token6 = generate_user() 

756 

757 with session_scope() as session: 

758 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

759 

760 time_before = now() 

761 start_time = now() + timedelta(hours=1) 

762 end_time = start_time + timedelta(hours=1.5) 

763 

764 event_ids = [] 

765 

766 with events_session(token1) as api: 

767 res = api.CreateEvent( 

768 events_pb2.CreateEventReq( 

769 title="Dummy Title", 

770 content="0th occurrence", 

771 location=events_pb2.EventLocation( 

772 address="Near Null Island", 

773 lat=0.1, 

774 lng=0.2, 

775 ), 

776 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

777 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

778 ) 

779 ) 

780 

781 event_id = res.event_id 

782 event_ids.append(event_id) 

783 

784 moderator.approve_event_occurrence(event_id) 

785 

786 with events_session(token4) as api: 

787 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

788 

789 with events_session(token5) as api: 

790 api.SetEventAttendance( 

791 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

792 ) 

793 

794 with events_session(token6) as api: 

795 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

796 

797 with events_session(token1) as api: 

798 for i in range(5): 

799 res = api.ScheduleEvent( 

800 events_pb2.ScheduleEventReq( 

801 event_id=event_ids[-1], 

802 content=f"{i + 1}th occurrence", 

803 location=events_pb2.EventLocation( 

804 address="Near Null Island", 

805 lat=0.1, 

806 lng=0.2, 

807 ), 

808 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2 + i)), 

809 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2.5 + i)), 

810 ) 

811 ) 

812 

813 event_ids.append(res.event_id) 

814 

815 # Approve all scheduled occurrences 

816 for eid in event_ids[1:]: 

817 moderator.approve_event_occurrence(eid) 

818 

819 updated_event_id = event_ids[3] 

820 

821 time_before_update = now() 

822 

823 with events_session(token1) as api: 

824 res = api.UpdateEvent( 

825 events_pb2.UpdateEventReq( 

826 event_id=updated_event_id, 

827 title=wrappers_pb2.StringValue(value="New Title"), 

828 content=wrappers_pb2.StringValue(value="New content."), 

829 location=events_pb2.EventLocation( 

830 address="Not so near Null Island", 

831 lat=0.2, 

832 lng=0.2, 

833 ), 

834 update_all_future=True, 

835 ) 

836 ) 

837 

838 time_after_update = now() 

839 

840 with events_session(token2) as api: 

841 for i in range(3): 

842 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i])) 

843 assert res.content == f"{i}th occurrence" 

844 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update 

845 

846 for i in range(3, 6): 

847 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i])) 

848 assert res.content == "New content." 

849 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update 

850 

851 

852def test_GetEvent(db, moderator: Moderator): 

853 # event creator 

854 user1, token1 = generate_user() 

855 # community moderator 

856 user2, token2 = generate_user() 

857 # third parties 

858 user3, token3 = generate_user() 

859 user4, token4 = generate_user() 

860 user5, token5 = generate_user() 

861 user6, token6 = generate_user() 

862 

863 with session_scope() as session: 

864 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

865 

866 time_before = now() 

867 start_time = now() + timedelta(hours=2) 

868 end_time = start_time + timedelta(hours=3) 

869 

870 with events_session(token1) as api: 

871 # in person event 

872 res = api.CreateEvent( 

873 events_pb2.CreateEventReq( 

874 title="Dummy Title", 

875 content="Dummy content.", 

876 location=events_pb2.EventLocation( 

877 address="Near Null Island", 

878 lat=0.1, 

879 lng=0.2, 

880 ), 

881 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

882 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

883 ) 

884 ) 

885 

886 event_id = res.event_id 

887 

888 moderator.approve_event_occurrence(event_id) 

889 

890 with events_session(token4) as api: 

891 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

892 

893 with events_session(token5) as api: 

894 api.SetEventAttendance( 

895 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

896 ) 

897 

898 with events_session(token6) as api: 

899 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

900 

901 with events_session(token1) as api: 

902 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

903 

904 assert res.is_next 

905 assert res.title == "Dummy Title" 

906 assert res.slug == "dummy-title" 

907 assert res.content == "Dummy content." 

908 assert not res.photo_url 

909 assert res.HasField("location") 

910 assert res.location.lat == 0.1 

911 assert res.location.lng == 0.2 

912 assert res.location.address == "Near Null Island" 

913 assert time_before <= to_aware_datetime(res.created) <= now() 

914 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

915 assert res.creator_user_id == user1.id 

916 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

917 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

918 assert is_utc_or_gmt(res.timezone) 

919 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

920 assert res.organizer 

921 assert res.subscriber 

922 assert res.going_count == 2 

923 assert res.organizer_count == 1 

924 assert res.subscriber_count == 3 

925 assert res.owner_user_id == user1.id 

926 assert not res.owner_community_id 

927 assert not res.owner_group_id 

928 assert res.thread.thread_id 

929 assert res.can_edit 

930 assert not res.can_moderate 

931 

932 with events_session(token2) as api: 

933 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

934 

935 assert res.is_next 

936 assert res.title == "Dummy Title" 

937 assert res.slug == "dummy-title" 

938 assert res.content == "Dummy content." 

939 assert not res.photo_url 

940 assert res.HasField("location") 

941 assert res.location.lat == 0.1 

942 assert res.location.lng == 0.2 

943 assert res.location.address == "Near Null Island" 

944 assert time_before <= to_aware_datetime(res.created) <= now() 

945 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

946 assert res.creator_user_id == user1.id 

947 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

948 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

949 assert is_utc_or_gmt(res.timezone) 

950 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

951 assert not res.organizer 

952 assert not res.subscriber 

953 assert res.going_count == 2 

954 assert res.organizer_count == 1 

955 assert res.subscriber_count == 3 

956 assert res.owner_user_id == user1.id 

957 assert not res.owner_community_id 

958 assert not res.owner_group_id 

959 assert res.thread.thread_id 

960 assert res.can_edit 

961 assert res.can_moderate 

962 

963 with events_session(token3) as api: 

964 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

965 

966 assert res.is_next 

967 assert res.title == "Dummy Title" 

968 assert res.slug == "dummy-title" 

969 assert res.content == "Dummy content." 

970 assert not res.photo_url 

971 assert res.HasField("location") 

972 assert res.location.lat == 0.1 

973 assert res.location.lng == 0.2 

974 assert res.location.address == "Near Null Island" 

975 assert time_before <= to_aware_datetime(res.created) <= now() 

976 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

977 assert res.creator_user_id == user1.id 

978 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time) 

979 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time) 

980 assert is_utc_or_gmt(res.timezone) 

981 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

982 assert not res.organizer 

983 assert not res.subscriber 

984 assert res.going_count == 2 

985 assert res.organizer_count == 1 

986 assert res.subscriber_count == 3 

987 assert res.owner_user_id == user1.id 

988 assert not res.owner_community_id 

989 assert not res.owner_group_id 

990 assert res.thread.thread_id 

991 assert not res.can_edit 

992 assert not res.can_moderate 

993 

994 

995def test_CancelEvent(db, moderator: Moderator): 

996 # event creator 

997 user1, token1 = generate_user() 

998 # community moderator 

999 user2, token2 = generate_user() 

1000 # third parties 

1001 user3, token3 = generate_user() 

1002 user4, token4 = generate_user() 

1003 user5, token5 = generate_user() 

1004 user6, token6 = generate_user() 

1005 

1006 with session_scope() as session: 

1007 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

1008 

1009 start_time = now() + timedelta(hours=2) 

1010 end_time = start_time + timedelta(hours=3) 

1011 

1012 with events_session(token1) as api: 

1013 res = api.CreateEvent( 

1014 events_pb2.CreateEventReq( 

1015 title="Dummy Title", 

1016 content="Dummy content.", 

1017 location=events_pb2.EventLocation( 

1018 address="Near Null Island", 

1019 lat=0.1, 

1020 lng=0.2, 

1021 ), 

1022 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1023 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1024 ) 

1025 ) 

1026 

1027 event_id = res.event_id 

1028 

1029 moderator.approve_event_occurrence(event_id) 

1030 

1031 with events_session(token4) as api: 

1032 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1033 

1034 with events_session(token5) as api: 

1035 api.SetEventAttendance( 

1036 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1037 ) 

1038 

1039 with events_session(token6) as api: 

1040 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1041 

1042 with events_session(token1) as api: 

1043 res = api.CancelEvent( 

1044 events_pb2.CancelEventReq( 

1045 event_id=event_id, 

1046 ) 

1047 ) 

1048 

1049 with events_session(token1) as api: 

1050 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1051 assert res.is_cancelled 

1052 

1053 with events_session(token1) as api: 

1054 with pytest.raises(grpc.RpcError) as e: 

1055 api.UpdateEvent( 

1056 events_pb2.UpdateEventReq( 

1057 event_id=event_id, 

1058 title=wrappers_pb2.StringValue(value="New Title"), 

1059 ) 

1060 ) 

1061 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1062 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1063 

1064 with pytest.raises(grpc.RpcError) as e: 

1065 api.InviteEventOrganizer( 

1066 events_pb2.InviteEventOrganizerReq( 

1067 event_id=event_id, 

1068 user_id=user3.id, 

1069 ) 

1070 ) 

1071 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1072 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1073 

1074 with pytest.raises(grpc.RpcError) as e: 

1075 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id)) 

1076 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1077 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1078 

1079 with events_session(token3) as api: 

1080 with pytest.raises(grpc.RpcError) as e: 

1081 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1082 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1083 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1084 

1085 with pytest.raises(grpc.RpcError) as e: 

1086 api.SetEventAttendance( 

1087 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1088 ) 

1089 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1090 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1091 

1092 with events_session(token1) as api: 

1093 for include_cancelled in [True, False]: 

1094 res = api.ListEventOccurrences( 

1095 events_pb2.ListEventOccurrencesReq( 

1096 event_id=event_id, 

1097 include_cancelled=include_cancelled, 

1098 ) 

1099 ) 

1100 if include_cancelled: 

1101 assert len(res.events) > 0 

1102 else: 

1103 assert len(res.events) == 0 

1104 

1105 res = api.ListMyEvents( 

1106 events_pb2.ListMyEventsReq( 

1107 include_cancelled=include_cancelled, 

1108 ) 

1109 ) 

1110 if include_cancelled: 

1111 assert len(res.events) > 0 

1112 else: 

1113 assert len(res.events) == 0 

1114 

1115 

1116def test_ListEventAttendees(db, moderator: Moderator): 

1117 # event creator 

1118 user1, token1 = generate_user() 

1119 # others 

1120 user2, token2 = generate_user() 

1121 user3, token3 = generate_user() 

1122 user4, token4 = generate_user() 

1123 user5, token5 = generate_user() 

1124 user6, token6 = generate_user() 

1125 

1126 with session_scope() as session: 

1127 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1128 

1129 with events_session(token1) as api: 

1130 event_id = api.CreateEvent( 

1131 events_pb2.CreateEventReq( 

1132 title="Dummy Title", 

1133 content="Dummy content.", 

1134 location=events_pb2.EventLocation( 

1135 address="Near Null Island", 

1136 lat=0.1, 

1137 lng=0.2, 

1138 ), 

1139 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1140 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1141 ) 

1142 ).event_id 

1143 

1144 moderator.approve_event_occurrence(event_id) 

1145 

1146 for token in [token2, token3, token4, token5]: 

1147 with events_session(token) as api: 

1148 api.SetEventAttendance( 

1149 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1150 ) 

1151 

1152 with events_session(token6) as api: 

1153 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5 

1154 

1155 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2)) 

1156 assert res.attendee_user_ids == [user1.id, user2.id] 

1157 

1158 res = api.ListEventAttendees( 

1159 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1160 ) 

1161 assert res.attendee_user_ids == [user3.id, user4.id] 

1162 

1163 res = api.ListEventAttendees( 

1164 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1165 ) 

1166 assert res.attendee_user_ids == [user5.id] 

1167 assert not res.next_page_token 

1168 

1169 

1170def test_ListEventSubscribers(db, moderator: Moderator): 

1171 # event creator 

1172 user1, token1 = generate_user() 

1173 # others 

1174 user2, token2 = generate_user() 

1175 user3, token3 = generate_user() 

1176 user4, token4 = generate_user() 

1177 user5, token5 = generate_user() 

1178 user6, token6 = generate_user() 

1179 

1180 with session_scope() as session: 

1181 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1182 

1183 with events_session(token1) as api: 

1184 event_id = api.CreateEvent( 

1185 events_pb2.CreateEventReq( 

1186 title="Dummy Title", 

1187 content="Dummy content.", 

1188 location=events_pb2.EventLocation( 

1189 address="Near Null Island", 

1190 lat=0.1, 

1191 lng=0.2, 

1192 ), 

1193 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1194 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1195 ) 

1196 ).event_id 

1197 

1198 moderator.approve_event_occurrence(event_id) 

1199 

1200 for token in [token2, token3, token4, token5]: 

1201 with events_session(token) as api: 

1202 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1203 

1204 with events_session(token6) as api: 

1205 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5 

1206 

1207 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2)) 

1208 assert res.subscriber_user_ids == [user1.id, user2.id] 

1209 

1210 res = api.ListEventSubscribers( 

1211 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1212 ) 

1213 assert res.subscriber_user_ids == [user3.id, user4.id] 

1214 

1215 res = api.ListEventSubscribers( 

1216 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1217 ) 

1218 assert res.subscriber_user_ids == [user5.id] 

1219 assert not res.next_page_token 

1220 

1221 

1222def test_ListEventOrganizers(db, moderator: Moderator): 

1223 # event creator 

1224 user1, token1 = generate_user() 

1225 # others 

1226 user2, token2 = generate_user() 

1227 user3, token3 = generate_user() 

1228 user4, token4 = generate_user() 

1229 user5, token5 = generate_user() 

1230 user6, token6 = generate_user() 

1231 

1232 with session_scope() as session: 

1233 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1234 

1235 with events_session(token1) as api: 

1236 event_id = api.CreateEvent( 

1237 events_pb2.CreateEventReq( 

1238 title="Dummy Title", 

1239 content="Dummy content.", 

1240 location=events_pb2.EventLocation( 

1241 address="Near Null Island", 

1242 lat=0.1, 

1243 lng=0.2, 

1244 ), 

1245 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1246 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1247 ) 

1248 ).event_id 

1249 

1250 moderator.approve_event_occurrence(event_id) 

1251 

1252 with events_session(token1) as api: 

1253 for user_id in [user2.id, user3.id, user4.id, user5.id]: 

1254 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id)) 

1255 

1256 with events_session(token6) as api: 

1257 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5 

1258 

1259 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2)) 

1260 assert res.organizer_user_ids == [user1.id, user2.id] 

1261 

1262 res = api.ListEventOrganizers( 

1263 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1264 ) 

1265 assert res.organizer_user_ids == [user3.id, user4.id] 

1266 

1267 res = api.ListEventOrganizers( 

1268 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1269 ) 

1270 assert res.organizer_user_ids == [user5.id] 

1271 assert not res.next_page_token 

1272 

1273 

1274def test_TransferEvent(db): 

1275 user1, token1 = generate_user() 

1276 user2, token2 = generate_user() 

1277 user3, token3 = generate_user() 

1278 user4, token4 = generate_user() 

1279 

1280 with session_scope() as session: 

1281 c = create_community(session, 0, 2, "Community", [user3], [], None) 

1282 h = create_group(session, "Group", [user4], [], c) 

1283 c_id = c.id 

1284 h_id = h.id 

1285 

1286 with events_session(token1) as api: 

1287 event_id = api.CreateEvent( 

1288 events_pb2.CreateEventReq( 

1289 title="Dummy Title", 

1290 content="Dummy content.", 

1291 location=events_pb2.EventLocation( 

1292 address="Near Null Island", 

1293 lat=0.1, 

1294 lng=0.2, 

1295 ), 

1296 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1297 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1298 ) 

1299 ).event_id 

1300 

1301 api.TransferEvent( 

1302 events_pb2.TransferEventReq( 

1303 event_id=event_id, 

1304 new_owner_community_id=c_id, 

1305 ) 

1306 ) 

1307 

1308 # remove ourselves as organizer, otherwise we can still edit it 

1309 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1310 

1311 with pytest.raises(grpc.RpcError) as e: 

1312 api.TransferEvent( 

1313 events_pb2.TransferEventReq( 

1314 event_id=event_id, 

1315 new_owner_group_id=h_id, 

1316 ) 

1317 ) 

1318 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1319 assert e.value.details() == "You're not allowed to transfer that event." 

1320 

1321 event_id = api.CreateEvent( 

1322 events_pb2.CreateEventReq( 

1323 title="Dummy Title", 

1324 content="Dummy content.", 

1325 location=events_pb2.EventLocation( 

1326 address="Near Null Island", 

1327 lat=0.1, 

1328 lng=0.2, 

1329 ), 

1330 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1331 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1332 ) 

1333 ).event_id 

1334 

1335 api.TransferEvent( 

1336 events_pb2.TransferEventReq( 

1337 event_id=event_id, 

1338 new_owner_group_id=h_id, 

1339 ) 

1340 ) 

1341 

1342 # remove ourselves as organizer, otherwise we can still edit it 

1343 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1344 

1345 with pytest.raises(grpc.RpcError) as e: 

1346 api.TransferEvent( 

1347 events_pb2.TransferEventReq( 

1348 event_id=event_id, 

1349 new_owner_community_id=c_id, 

1350 ) 

1351 ) 

1352 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1353 assert e.value.details() == "You're not allowed to transfer that event." 

1354 

1355 

1356def test_SetEventSubscription(db, moderator: Moderator): 

1357 user1, token1 = generate_user() 

1358 user2, token2 = generate_user() 

1359 

1360 with session_scope() as session: 

1361 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1362 

1363 with events_session(token1) as api: 

1364 event_id = api.CreateEvent( 

1365 events_pb2.CreateEventReq( 

1366 title="Dummy Title", 

1367 content="Dummy content.", 

1368 location=events_pb2.EventLocation( 

1369 address="Near Null Island", 

1370 lat=0.1, 

1371 lng=0.2, 

1372 ), 

1373 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1374 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1375 ) 

1376 ).event_id 

1377 

1378 moderator.approve_event_occurrence(event_id) 

1379 

1380 with events_session(token2) as api: 

1381 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber 

1382 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1383 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber 

1384 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False)) 

1385 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber 

1386 

1387 

1388def test_SetEventAttendance(db, moderator: Moderator): 

1389 user1, token1 = generate_user() 

1390 user2, token2 = generate_user() 

1391 

1392 with session_scope() as session: 

1393 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1394 

1395 with events_session(token1) as api: 

1396 event_id = api.CreateEvent( 

1397 events_pb2.CreateEventReq( 

1398 title="Dummy Title", 

1399 content="Dummy content.", 

1400 location=events_pb2.EventLocation( 

1401 address="Near Null Island", 

1402 lat=0.1, 

1403 lng=0.2, 

1404 ), 

1405 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1406 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1407 ) 

1408 ).event_id 

1409 

1410 moderator.approve_event_occurrence(event_id) 

1411 

1412 with events_session(token2) as api: 

1413 assert ( 

1414 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state 

1415 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1416 ) 

1417 api.SetEventAttendance( 

1418 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1419 ) 

1420 assert ( 

1421 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state 

1422 == events_pb2.ATTENDANCE_STATE_GOING 

1423 ) 

1424 api.SetEventAttendance( 

1425 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING) 

1426 ) 

1427 assert ( 

1428 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state 

1429 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1430 ) 

1431 

1432 

1433def test_InviteEventOrganizer(db, moderator: Moderator): 

1434 user1, token1 = generate_user() 

1435 user2, token2 = generate_user() 

1436 

1437 with session_scope() as session: 

1438 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1439 

1440 with events_session(token1) as api: 

1441 event_id = api.CreateEvent( 

1442 events_pb2.CreateEventReq( 

1443 title="Dummy Title", 

1444 content="Dummy content.", 

1445 location=events_pb2.EventLocation( 

1446 address="Near Null Island", 

1447 lat=0.1, 

1448 lng=0.2, 

1449 ), 

1450 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1451 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1452 ) 

1453 ).event_id 

1454 

1455 moderator.approve_event_occurrence(event_id) 

1456 

1457 with events_session(token2) as api: 

1458 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1459 

1460 with pytest.raises(grpc.RpcError) as e: 

1461 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id)) 

1462 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1463 assert e.value.details() == "You're not allowed to edit that event." 

1464 

1465 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1466 

1467 with events_session(token1) as api: 

1468 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1469 

1470 with events_session(token2) as api: 

1471 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1472 

1473 

1474def test_ListEventOccurrences(db): 

1475 user1, token1 = generate_user() 

1476 user2, token2 = generate_user() 

1477 user3, token3 = generate_user() 

1478 

1479 with session_scope() as session: 

1480 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

1481 

1482 start = now() 

1483 

1484 event_ids = [] 

1485 

1486 with events_session(token1) as api: 

1487 res = api.CreateEvent( 

1488 events_pb2.CreateEventReq( 

1489 title="First occurrence", 

1490 content="Dummy content.", 

1491 parent_community_id=c_id, 

1492 location=events_pb2.EventLocation( 

1493 address="Near Null Island", 

1494 lat=0.1, 

1495 lng=0.2, 

1496 ), 

1497 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

1498 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)), 

1499 ) 

1500 ) 

1501 

1502 event_ids.append(res.event_id) 

1503 

1504 for i in range(5): 

1505 res = api.ScheduleEvent( 

1506 events_pb2.ScheduleEventReq( 

1507 event_id=event_ids[-1], 

1508 content=f"{i}th occurrence", 

1509 location=events_pb2.EventLocation( 

1510 address="Near Null Island", 

1511 lat=0.1, 

1512 lng=0.2, 

1513 ), 

1514 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)), 

1515 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)), 

1516 ) 

1517 ) 

1518 

1519 event_ids.append(res.event_id) 

1520 

1521 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2)) 

1522 assert [event.event_id for event in res.events] == event_ids[:2] 

1523 

1524 res = api.ListEventOccurrences( 

1525 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token) 

1526 ) 

1527 assert [event.event_id for event in res.events] == event_ids[2:4] 

1528 

1529 res = api.ListEventOccurrences( 

1530 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token) 

1531 ) 

1532 assert [event.event_id for event in res.events] == event_ids[4:6] 

1533 assert not res.next_page_token 

1534 

1535 

1536def test_ListMyEvents(db, moderator: Moderator): 

1537 user1, token1 = generate_user() 

1538 user2, token2 = generate_user() 

1539 user3, token3 = generate_user() 

1540 user4, token4 = generate_user() 

1541 user5, token5 = generate_user() 

1542 

1543 with session_scope() as session: 

1544 # Create global (world) -> macroregion -> region -> subregion hierarchy 

1545 # my_communities_exclude_global filters out world, macroregion, and region level communities 

1546 global_community = create_community(session, 0, 100, "Global", [user3], [], None) 

1547 c_id = global_community.id 

1548 macroregion_community = create_community( 

1549 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community 

1550 ) 

1551 region_community = create_community( 

1552 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community 

1553 ) 

1554 subregion_community = create_community( 

1555 session, 0, 25, "Subregion Community", [user3, user4], [], region_community 

1556 ) 

1557 c2_id = subregion_community.id 

1558 

1559 start = now() 

1560 

1561 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq: 

1562 return events_pb2.CreateEventReq( 

1563 title="Dummy Title", 

1564 content="Dummy content.", 

1565 location=events_pb2.EventLocation( 

1566 address="Near Null Island", 

1567 lat=0.1, 

1568 lng=0.2, 

1569 ), 

1570 parent_community_id=community_id, 

1571 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now)), 

1572 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now + 0.5)), 

1573 ) 

1574 

1575 with events_session(token1) as api: 

1576 e2 = api.CreateEvent(new_event(2, c_id)).event_id 

1577 

1578 moderator.approve_event_occurrence(e2) 

1579 

1580 with events_session(token2) as api: 

1581 e1 = api.CreateEvent(new_event(1, c_id)).event_id 

1582 

1583 moderator.approve_event_occurrence(e1) 

1584 

1585 with events_session(token1) as api: 

1586 e3 = api.CreateEvent(new_event(3, c_id)).event_id 

1587 

1588 moderator.approve_event_occurrence(e3) 

1589 

1590 with events_session(token2) as api: 

1591 e5 = api.CreateEvent(new_event(5, c_id)).event_id 

1592 

1593 moderator.approve_event_occurrence(e5) 

1594 

1595 with events_session(token3) as api: 

1596 e4 = api.CreateEvent(new_event(4, c_id)).event_id 

1597 

1598 moderator.approve_event_occurrence(e4) 

1599 

1600 with events_session(token4) as api: 

1601 e6 = api.CreateEvent(new_event(6, c2_id)).event_id 

1602 

1603 moderator.approve_event_occurrence(e6) 

1604 

1605 with events_session(token1) as api: 

1606 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id)) 

1607 

1608 with events_session(token1) as api: 

1609 api.SetEventAttendance( 

1610 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1611 ) 

1612 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True)) 

1613 

1614 with events_session(token2) as api: 

1615 api.SetEventAttendance( 

1616 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1617 ) 

1618 

1619 with events_session(token3) as api: 

1620 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True)) 

1621 

1622 with events_session(token1) as api: 

1623 # test pagination with token first 

1624 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2)) 

1625 assert [event.event_id for event in res.events] == [e1, e2] 

1626 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token)) 

1627 assert [event.event_id for event in res.events] == [e3, e4] 

1628 assert not res.next_page_token 

1629 

1630 res = api.ListMyEvents( 

1631 events_pb2.ListMyEventsReq( 

1632 subscribed=True, 

1633 attending=True, 

1634 organizing=True, 

1635 ) 

1636 ) 

1637 assert [event.event_id for event in res.events] == [e1, e2, e3, e4] 

1638 

1639 res = api.ListMyEvents(events_pb2.ListMyEventsReq()) 

1640 assert [event.event_id for event in res.events] == [e1, e2, e3, e4] 

1641 

1642 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True)) 

1643 assert [event.event_id for event in res.events] == [e2, e3, e4] 

1644 

1645 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True)) 

1646 assert [event.event_id for event in res.events] == [e1, e2, e3] 

1647 

1648 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True)) 

1649 assert [event.event_id for event in res.events] == [e2, e3] 

1650 

1651 with events_session(token1) as api: 

1652 # Test pagination with page_number and verify total_items 

1653 res = api.ListMyEvents( 

1654 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True) 

1655 ) 

1656 assert [event.event_id for event in res.events] == [e1, e2] 

1657 assert res.total_items == 4 

1658 

1659 res = api.ListMyEvents( 

1660 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True) 

1661 ) 

1662 assert [event.event_id for event in res.events] == [e3, e4] 

1663 assert res.total_items == 4 

1664 

1665 # Verify no more pages 

1666 res = api.ListMyEvents( 

1667 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True) 

1668 ) 

1669 assert not res.events 

1670 assert res.total_items == 4 

1671 

1672 with events_session(token2) as api: 

1673 res = api.ListMyEvents(events_pb2.ListMyEventsReq()) 

1674 assert [event.event_id for event in res.events] == [e1, e3, e5] 

1675 

1676 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True)) 

1677 assert [event.event_id for event in res.events] == [e1, e5] 

1678 

1679 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True)) 

1680 assert [event.event_id for event in res.events] == [e1, e3, e5] 

1681 

1682 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True)) 

1683 assert [event.event_id for event in res.events] == [e1, e5] 

1684 

1685 with events_session(token3) as api: 

1686 # user3 is member of both global (c_id) and child (c2_id) communities 

1687 res = api.ListMyEvents(events_pb2.ListMyEventsReq()) 

1688 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6] 

1689 

1690 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True)) 

1691 assert [event.event_id for event in res.events] == [e2, e4] 

1692 

1693 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True)) 

1694 assert [event.event_id for event in res.events] == [e4] 

1695 

1696 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True)) 

1697 assert [event.event_id for event in res.events] == [e3, e4] 

1698 

1699 # my_communities returns events from both communities user3 is a member of 

1700 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True)) 

1701 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6] 

1702 

1703 # my_communities_exclude_global filters out events from global community (node_id=1) 

1704 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True)) 

1705 assert [event.event_id for event in res.events] == [e6] 

1706 

1707 # my_communities_exclude_global works independently of my_communities flag 

1708 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True)) 

1709 assert [event.event_id for event in res.events] == [e6] 

1710 

1711 # my_communities_exclude_global filters organizing results too 

1712 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True)) 

1713 assert [event.event_id for event in res.events] == [] 

1714 

1715 # my_communities_exclude_global filters subscribed results too 

1716 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True)) 

1717 assert [event.event_id for event in res.events] == [] 

1718 

1719 with events_session(token5) as api: 

1720 res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

1721 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6] 

1722 

1723 

1724def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1725 user1, token1 = generate_user() 

1726 user2, token2 = generate_user() 

1727 

1728 with session_scope() as session: 

1729 c = create_community(session, 0, 100, "Community", [user1, user2], [], None) 

1730 c_id = c.id 

1731 

1732 start = now() 

1733 

1734 def make_event(hours): 

1735 return events_pb2.CreateEventReq( 

1736 title="Test Event", 

1737 content="Test content.", 

1738 location=events_pb2.EventLocation( 

1739 address="Near Null Island", 

1740 lat=0.1, 

1741 lng=0.2, 

1742 ), 

1743 parent_community_id=c_id, 

1744 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours)), 

1745 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours + 1)), 

1746 ) 

1747 

1748 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1749 with events_session(token1) as api: 

1750 e_own = api.CreateEvent(make_event(1)).event_id 

1751 

1752 with events_session(token2) as api: 

1753 e_attending = api.CreateEvent(make_event(2)).event_id 

1754 e_community_only = api.CreateEvent(make_event(3)).event_id 

1755 # e_both: user1 will be both organizer and attendee 

1756 e_both = api.CreateEvent(make_event(4)).event_id 

1757 

1758 moderator.approve_event_occurrence(e_own) 

1759 moderator.approve_event_occurrence(e_attending) 

1760 moderator.approve_event_occurrence(e_community_only) 

1761 moderator.approve_event_occurrence(e_both) 

1762 

1763 # invite user1 as organizer of e_both 

1764 with events_session(token2) as api: 

1765 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id)) 

1766 

1767 # user1 RSVPs to e_attending and e_both 

1768 with events_session(token1) as api: 

1769 api.SetEventAttendance( 

1770 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1771 ) 

1772 api.SetEventAttendance( 

1773 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1774 ) 

1775 

1776 with events_session(token1) as api: 

1777 # baseline: all four community events visible 

1778 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True)) 

1779 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both} 

1780 

1781 # exclude_attending removes events user1 is attending (e_attending, e_both) 

1782 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only 

1783 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True)) 

1784 assert [e.event_id for e in res.events] == [e_community_only] 

1785 

1786 # exclude_attending with attending=True: invalid combination 

1787 with pytest.raises(grpc.RpcError) as e: 

1788 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True)) 

1789 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1790 

1791 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it 

1792 with events_session(token2) as api: 

1793 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True)) 

1794 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation) 

1795 assert [e.event_id for e in res.events] == [e_own] 

1796 

1797 

1798def test_RemoveEventOrganizer(db, moderator: Moderator): 

1799 user1, token1 = generate_user() 

1800 user2, token2 = generate_user() 

1801 

1802 with session_scope() as session: 

1803 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1804 

1805 with events_session(token1) as api: 

1806 event_id = api.CreateEvent( 

1807 events_pb2.CreateEventReq( 

1808 title="Dummy Title", 

1809 content="Dummy content.", 

1810 location=events_pb2.EventLocation( 

1811 address="Near Null Island", 

1812 lat=0.1, 

1813 lng=0.2, 

1814 ), 

1815 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1816 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1817 ) 

1818 ).event_id 

1819 

1820 moderator.approve_event_occurrence(event_id) 

1821 

1822 with events_session(token2) as api: 

1823 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1824 

1825 with pytest.raises(grpc.RpcError) as e: 

1826 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1827 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1828 assert e.value.details() == "You're not allowed to edit that event." 

1829 

1830 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1831 

1832 with events_session(token1) as api: 

1833 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1834 

1835 with pytest.raises(grpc.RpcError) as e: 

1836 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1837 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1838 assert e.value.details() == "You cannot remove the event owner as an organizer." 

1839 

1840 with events_session(token2) as api: 

1841 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1842 assert res.organizer 

1843 assert res.organizer_count == 2 

1844 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1845 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1846 

1847 with pytest.raises(grpc.RpcError) as e: 

1848 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1849 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1850 assert e.value.details() == "You're not allowed to edit that event." 

1851 

1852 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1853 assert not res.organizer 

1854 assert res.organizer_count == 1 

1855 

1856 # Test that event owner can remove co-organizers 

1857 with events_session(token1) as api: 

1858 # Add user2 back as organizer 

1859 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1860 

1861 # Verify user2 is now an organizer 

1862 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1863 assert res.organizer_count == 2 

1864 

1865 # Event owner can remove co-organizer 

1866 api.RemoveEventOrganizer( 

1867 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id)) 

1868 ) 

1869 

1870 # Verify user2 is no longer an organizer 

1871 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1872 assert res.organizer_count == 1 

1873 

1874 # Test that non-organizers cannot remove other organizers 

1875 with events_session(token2) as api: 

1876 # User2 cannot invite themselves as organizer (not the owner) 

1877 with pytest.raises(grpc.RpcError) as e: 

1878 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1879 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1880 assert e.value.details() == "You're not allowed to edit that event." 

1881 

1882 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first) 

1883 with events_session(token1) as api: 

1884 # Add user2 back as organizer 

1885 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1886 

1887 

1888def test_ListEventAttendees_regression(db): 

1889 # see issue #1617: 

1890 # 

1891 # 1. Create an event 

1892 # 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` 

1893 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`) 

1894 # 4. Change the current user's attendance state to "going" again 

1895 # 

1896 # **Expected behaviour** 

1897 # `ListEventAttendees` should return the current user's ID 

1898 # 

1899 # **Actual/current behaviour** 

1900 # `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 

1901 

1902 user1, token1 = generate_user() 

1903 user2, token2 = generate_user() 

1904 user3, token3 = generate_user() 

1905 user4, token4 = generate_user() 

1906 user5, token5 = generate_user() 

1907 

1908 with session_scope() as session: 

1909 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1910 

1911 start_time = now() + timedelta(hours=2) 

1912 end_time = start_time + timedelta(hours=3) 

1913 

1914 with events_session(token1) as api: 

1915 res = api.CreateEvent( 

1916 events_pb2.CreateEventReq( 

1917 title="Dummy Title", 

1918 content="Dummy content.", 

1919 location=events_pb2.EventLocation( 

1920 address="Near Null Island", 

1921 lat=0.1, 

1922 lng=0.2, 

1923 ), 

1924 parent_community_id=c_id, 

1925 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1926 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1927 ) 

1928 ) 

1929 

1930 res = api.TransferEvent( 

1931 events_pb2.TransferEventReq( 

1932 event_id=res.event_id, 

1933 new_owner_community_id=c_id, 

1934 ) 

1935 ) 

1936 

1937 event_id = res.event_id 

1938 

1939 api.SetEventAttendance( 

1940 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING) 

1941 ) 

1942 api.SetEventAttendance( 

1943 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1944 ) 

1945 

1946 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id)) 

1947 assert len(res.attendee_user_ids) == 1 

1948 assert res.attendee_user_ids[0] == user1.id 

1949 

1950 

1951def test_event_threads(db, push_collector: PushCollector, moderator: Moderator): 

1952 user1, token1 = generate_user() 

1953 user2, token2 = generate_user() 

1954 user3, token3 = generate_user() 

1955 user4, token4 = generate_user() 

1956 

1957 with session_scope() as session: 

1958 c = create_community(session, 0, 2, "Community", [user3], [], None) 

1959 h = create_group(session, "Group", [user4], [], c) 

1960 c_id = c.id 

1961 h_id = h.id 

1962 user4_id = user4.id 

1963 

1964 with events_session(token1) as api: 

1965 event = api.CreateEvent( 

1966 events_pb2.CreateEventReq( 

1967 title="Dummy Title", 

1968 content="Dummy content.", 

1969 location=events_pb2.EventLocation( 

1970 address="Near Null Island", 

1971 lat=0.1, 

1972 lng=0.2, 

1973 ), 

1974 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)), 

1975 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)), 

1976 ) 

1977 ) 

1978 

1979 moderator.approve_event_occurrence(event.event_id) 

1980 

1981 with threads_session(token2) as api: 

1982 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id 

1983 

1984 moderator.approve_thread_post(reply_id) 

1985 

1986 with events_session(token3) as api: 

1987 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id)) 

1988 assert res.thread.num_responses == 1 

1989 

1990 with threads_session(token3) as api: 

1991 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id)) 

1992 assert len(ret.replies) == 1 

1993 assert not ret.next_page_token 

1994 assert ret.replies[0].thread_id == reply_id 

1995 assert ret.replies[0].content == "hi" 

1996 assert ret.replies[0].author_user_id == user2.id 

1997 assert ret.replies[0].num_replies == 0 

1998 

1999 nested_reply_id = api.PostReply( 

2000 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment") 

2001 ).thread_id 

2002 

2003 moderator.approve_thread_post(nested_reply_id) 

2004 

2005 process_jobs() 

2006 

2007 push = push_collector.pop_for_user(user1.id, last=True) 

2008 assert push.topic_action == NotificationTopicAction.event__comment.display 

2009 assert push.content.title == f"{user2.name} • Dummy Title" 

2010 assert push.content.ios_title == user2.name 

2011 assert push.content.ios_subtitle == "Commented on Dummy Title" 

2012 assert push.content.body == "hi" 

2013 

2014 push = push_collector.pop_for_user(user2.id, last=True) 

2015 assert push.content.title == f"{user3.name} • Dummy Title" 

2016 

2017 assert push_collector.count_for_user(user4_id) == 0 

2018 

2019 

2020def test_can_overlap_other_events_schedule_regression(db): 

2021 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event 

2022 user, token = generate_user() 

2023 

2024 with session_scope() as session: 

2025 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2026 

2027 start = now() 

2028 

2029 with events_session(token) as api: 

2030 # create another event, should be able to overlap with this one 

2031 api.CreateEvent( 

2032 events_pb2.CreateEventReq( 

2033 title="Dummy Title", 

2034 content="Dummy content.", 

2035 parent_community_id=c_id, 

2036 location=events_pb2.EventLocation( 

2037 address="Near Null Island", 

2038 lat=0.1, 

2039 lng=0.2, 

2040 ), 

2041 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

2042 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)), 

2043 ) 

2044 ) 

2045 

2046 # this event 

2047 res = api.CreateEvent( 

2048 events_pb2.CreateEventReq( 

2049 title="Dummy Title", 

2050 content="Dummy content.", 

2051 parent_community_id=c_id, 

2052 location=events_pb2.EventLocation( 

2053 address="Near Null Island", 

2054 lat=0.1, 

2055 lng=0.2, 

2056 ), 

2057 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

2058 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)), 

2059 ) 

2060 ) 

2061 

2062 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem 

2063 api.ScheduleEvent( 

2064 events_pb2.ScheduleEventReq( 

2065 event_id=res.event_id, 

2066 content="New event occurrence", 

2067 location=events_pb2.EventLocation( 

2068 address="A bit further but still near Null Island", 

2069 lat=0.3, 

2070 lng=0.2, 

2071 ), 

2072 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)), 

2073 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)), 

2074 ) 

2075 ) 

2076 

2077 

2078def test_can_overlap_other_events_update_regression(db): 

2079 user, token = generate_user() 

2080 

2081 with session_scope() as session: 

2082 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2083 

2084 start = now() 

2085 

2086 with events_session(token) as api: 

2087 # create another event, should be able to overlap with this one 

2088 api.CreateEvent( 

2089 events_pb2.CreateEventReq( 

2090 title="Dummy Title", 

2091 content="Dummy content.", 

2092 parent_community_id=c_id, 

2093 location=events_pb2.EventLocation( 

2094 address="Near Null Island", 

2095 lat=0.1, 

2096 lng=0.2, 

2097 ), 

2098 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

2099 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)), 

2100 ) 

2101 ) 

2102 

2103 res = api.CreateEvent( 

2104 events_pb2.CreateEventReq( 

2105 title="Dummy Title", 

2106 content="Dummy content.", 

2107 parent_community_id=c_id, 

2108 location=events_pb2.EventLocation( 

2109 address="Near Null Island", 

2110 lat=0.1, 

2111 lng=0.2, 

2112 ), 

2113 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)), 

2114 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)), 

2115 ) 

2116 ) 

2117 

2118 event_id = api.ScheduleEvent( 

2119 events_pb2.ScheduleEventReq( 

2120 event_id=res.event_id, 

2121 content="New event occurrence", 

2122 location=events_pb2.EventLocation( 

2123 address="A bit further but still near Null Island", 

2124 lat=0.3, 

2125 lng=0.2, 

2126 ), 

2127 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)), 

2128 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)), 

2129 ) 

2130 ).event_id 

2131 

2132 # can overlap with this current existing occurrence 

2133 api.UpdateEvent( 

2134 events_pb2.UpdateEventReq( 

2135 event_id=event_id, 

2136 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

2137 value=datetime_to_iso8601_local(start + timedelta(hours=5)) 

2138 ), 

2139 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

2140 value=datetime_to_iso8601_local(start + timedelta(hours=6)) 

2141 ), 

2142 ) 

2143 ) 

2144 

2145 api.UpdateEvent( 

2146 events_pb2.UpdateEventReq( 

2147 event_id=event_id, 

2148 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

2149 value=datetime_to_iso8601_local(start + timedelta(hours=2)) 

2150 ), 

2151 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

2152 value=datetime_to_iso8601_local(start + timedelta(hours=4)) 

2153 ), 

2154 ) 

2155 ) 

2156 

2157 

2158def test_list_past_events_regression(db): 

2159 # test for a bug where listing past events didn't work if they didn't have a future occurrence 

2160 user, token = generate_user() 

2161 

2162 with session_scope() as session: 

2163 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2164 

2165 start = now() 

2166 

2167 with events_session(token) as api: 

2168 api.CreateEvent( 

2169 events_pb2.CreateEventReq( 

2170 title="Dummy Title", 

2171 content="Dummy content.", 

2172 parent_community_id=c_id, 

2173 location=events_pb2.EventLocation( 

2174 address="Near Null Island", 

2175 lat=0.1, 

2176 lng=0.2, 

2177 ), 

2178 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)), 

2179 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)), 

2180 ) 

2181 ) 

2182 

2183 with session_scope() as session: 

2184 session.execute( 

2185 update(EventOccurrence).values( 

2186 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4)) 

2187 ) 

2188 ) 

2189 

2190 with events_session(token) as api: 

2191 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True)) 

2192 assert len(res.events) == 1 

2193 

2194 

2195def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator): 

2196 user1, token1 = generate_user(complete_profile=True) 

2197 user2, token2 = generate_user() 

2198 user3, token3 = generate_user() 

2199 user4, token4 = generate_user() 

2200 user5, token5 = generate_user(is_superuser=True) 

2201 

2202 with session_scope() as session: 

2203 w = create_community(session, 0, 2, "World Community", [user5], [], None) 

2204 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w) 

2205 r = create_community(session, 0, 2, "Region", [user5], [], mr) 

2206 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id 

2207 

2208 enforce_community_memberships() 

2209 

2210 with events_session(token1) as api: 

2211 res = api.CreateEvent( 

2212 events_pb2.CreateEventReq( 

2213 title="Dummy Title", 

2214 content="Dummy content.", 

2215 parent_community_id=c_id, 

2216 location=events_pb2.EventLocation( 

2217 address="Near Null Island", 

2218 lat=0.1, 

2219 lng=0.2, 

2220 ), 

2221 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)), 

2222 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)), 

2223 ) 

2224 ) 

2225 user_url = f"http://localhost:3000/user/{user1.username}" 

2226 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}" 

2227 

2228 event_id = res.event_id 

2229 

2230 moderator.approve_event_occurrence(event_id) 

2231 

2232 with events_session(token1) as api: 

2233 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2234 

2235 email = email_collector.pop_for_mods(last=True) 

2236 

2237 assert user_url in email.plain 

2238 assert event_url in email.plain 

2239 

2240 # can't send another req 

2241 with pytest.raises(grpc.RpcError) as err: 

2242 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2243 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2244 assert err.value.details() == "You have already requested a community invite for this event." 

2245 

2246 # another user can send one though 

2247 with events_session(token3) as api: 

2248 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2249 

2250 # but not a non-admin 

2251 with events_session(token2) as api: 

2252 with pytest.raises(grpc.RpcError) as err: 

2253 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2254 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED 

2255 assert err.value.details() == "You're not allowed to edit that event." 

2256 

2257 with real_editor_session(token5) as editor: 

2258 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq()) 

2259 assert len(res.requests) == 2 

2260 assert res.requests[0].user_id == user1.id 

2261 assert res.requests[0].approx_users_to_notify == 3 

2262 assert res.requests[1].user_id == user3.id 

2263 assert res.requests[1].approx_users_to_notify == 3 

2264 

2265 editor.DecideEventCommunityInviteRequest( 

2266 editor_pb2.DecideEventCommunityInviteRequestReq( 

2267 event_community_invite_request_id=res.requests[0].event_community_invite_request_id, 

2268 approve=False, 

2269 ) 

2270 ) 

2271 

2272 editor.DecideEventCommunityInviteRequest( 

2273 editor_pb2.DecideEventCommunityInviteRequestReq( 

2274 event_community_invite_request_id=res.requests[1].event_community_invite_request_id, 

2275 approve=True, 

2276 ) 

2277 ) 

2278 

2279 # not after approve 

2280 with events_session(token4) as api: 

2281 with pytest.raises(grpc.RpcError) as err: 

2282 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2283 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2284 assert err.value.details() == "A community invite has already been sent out for this event." 

2285 

2286 

2287def test_update_event_should_notify_queues_job(): 

2288 user, token = generate_user() 

2289 start = now() 

2290 

2291 with session_scope() as session: 

2292 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2293 

2294 # create an event 

2295 with events_session(token) as api: 

2296 create_res = api.CreateEvent( 

2297 events_pb2.CreateEventReq( 

2298 title="Dummy Title", 

2299 content="Dummy content.", 

2300 parent_community_id=c_id, 

2301 location=events_pb2.EventLocation( 

2302 address="Near Null Island", 

2303 lat=1.0, 

2304 lng=2.0, 

2305 ), 

2306 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)), 

2307 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)), 

2308 ) 

2309 ) 

2310 

2311 event_id = create_res.event_id 

2312 

2313 # measure initial background job queue length 

2314 with session_scope() as session: 

2315 jobs = session.query(BackgroundJob).all() 

2316 job_length_before_update = len(jobs) 

2317 

2318 # update with should_notify=False, expect no change in background job queue 

2319 api.UpdateEvent( 

2320 events_pb2.UpdateEventReq( 

2321 event_id=event_id, 

2322 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

2323 value=datetime_to_iso8601_local(start + timedelta(hours=4)) 

2324 ), 

2325 should_notify=False, 

2326 ) 

2327 ) 

2328 

2329 with session_scope() as session: 

2330 jobs = session.query(BackgroundJob).all() 

2331 assert len(jobs) == job_length_before_update 

2332 

2333 # update with should_notify=True, expect one new background job added 

2334 api.UpdateEvent( 

2335 events_pb2.UpdateEventReq( 

2336 event_id=event_id, 

2337 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

2338 value=datetime_to_iso8601_local(start + timedelta(hours=5)) 

2339 ), 

2340 should_notify=True, 

2341 ) 

2342 ) 

2343 

2344 with session_scope() as session: 

2345 jobs = session.query(BackgroundJob).all() 

2346 assert len(jobs) == job_length_before_update + 1 

2347 

2348 

2349def test_event_photo_key(db): 

2350 """Test that events return the photo_key field when a photo is set.""" 

2351 user, token = generate_user() 

2352 

2353 start_time = now() + timedelta(hours=2) 

2354 end_time = start_time + timedelta(hours=3) 

2355 

2356 # Create a community and an upload for the event photo 

2357 with session_scope() as session: 

2358 create_community(session, 0, 2, "Community", [user], [], None) 

2359 upload = Upload( 

2360 key="test_event_photo_key_123", 

2361 filename="test_event_photo_key_123.jpg", 

2362 creator_user_id=user.id, 

2363 ) 

2364 session.add(upload) 

2365 

2366 with events_session(token) as api: 

2367 # Create event without photo 

2368 res = api.CreateEvent( 

2369 events_pb2.CreateEventReq( 

2370 title="Event Without Photo", 

2371 content="No photo content.", 

2372 photo_key=None, 

2373 location=events_pb2.EventLocation( 

2374 address="Near Null Island", 

2375 lat=0.1, 

2376 lng=0.2, 

2377 ), 

2378 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2379 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2380 ) 

2381 ) 

2382 

2383 assert res.photo_key == "" 

2384 assert res.photo_url == "" 

2385 

2386 # Create event with photo 

2387 res_with_photo = api.CreateEvent( 

2388 events_pb2.CreateEventReq( 

2389 title="Event With Photo", 

2390 content="Has photo content.", 

2391 photo_key="test_event_photo_key_123", 

2392 location=events_pb2.EventLocation( 

2393 address="Near Null Island", 

2394 lat=0.1, 

2395 lng=0.2, 

2396 ), 

2397 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)), 

2398 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)), 

2399 ) 

2400 ) 

2401 

2402 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2403 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2404 

2405 event_id = res_with_photo.event_id 

2406 

2407 # Verify photo_key is returned when getting the event 

2408 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2409 assert get_res.photo_key == "test_event_photo_key_123" 

2410 assert "test_event_photo_key_123" in get_res.photo_url 

2411 

2412 

2413def test_event_timezone(db): 

2414 user, token = generate_user() 

2415 

2416 with session_scope() as session: 

2417 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2418 

2419 # Midnight future day, UTC timezone 

2420 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0) 

2421 end_time = start_time + timedelta(days=1) 

2422 

2423 with events_session(token) as api: 

2424 create_res: events_pb2.Event = api.CreateEvent( 

2425 events_pb2.CreateEventReq( 

2426 title="Dummy Title", 

2427 content="Dummy content.", 

2428 photo_key=None, 

2429 parent_community_id=c_id, 

2430 # timezone_areas.sql-fake has a region for Europe/Helsinki 

2431 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831), 

2432 # Should result in YYYY-MM-DDT00:00 (midnight local time) 

2433 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2434 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2435 ) 

2436 ) 

2437 

2438 # Backend should have deduced the helsinki timezone when creating the event, 

2439 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC. 

2440 assert create_res.timezone == "Europe/Helsinki" 

2441 assert to_aware_datetime(create_res.start_time).hour != 0 

2442 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0 

2443 

2444 # Now update its location such that it gets a new timezone 

2445 update_res: events_pb2.Event = api.UpdateEvent( 

2446 events_pb2.UpdateEventReq( 

2447 event_id=create_res.event_id, 

2448 # timezone_areas.sql-fake has a region for America/New_York 

2449 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974), 

2450 ) 

2451 ) 

2452 

2453 # The user didn't touch the datetime components on the frontend, 

2454 # so they expect the event to be at the same local time (midnight), 

2455 # but now in the New York timezone. 

2456 assert update_res.timezone == "America/New_York" 

2457 assert update_res.start_time != create_res.start_time 

2458 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0 

2459 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0 

2460 

2461 # Also validate GetEvent 

2462 get_res: events_pb2.Event = api.GetEvent( 

2463 events_pb2.GetEventReq( 

2464 event_id=create_res.event_id, 

2465 ) 

2466 ) 

2467 

2468 assert get_res.timezone == update_res.timezone 

2469 assert get_res.start_time == update_res.start_time 

2470 

2471 

2472def test_event_created_with_shadowed_visibility(db): 

2473 """Events start in SHADOWED state when created.""" 

2474 user, token = generate_user() 

2475 

2476 with session_scope() as session: 

2477 create_community(session, 0, 2, "Community", [user], [], None) 

2478 

2479 start_time = now() + timedelta(hours=2) 

2480 end_time = start_time + timedelta(hours=3) 

2481 

2482 with events_session(token) as api: 

2483 res = api.CreateEvent( 

2484 events_pb2.CreateEventReq( 

2485 title="Test UMS Event", 

2486 content="UMS content.", 

2487 location=events_pb2.EventLocation( 

2488 address="Near Null Island", 

2489 lat=0.1, 

2490 lng=0.2, 

2491 ), 

2492 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2493 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2494 ) 

2495 ) 

2496 event_id = res.event_id 

2497 

2498 with session_scope() as session: 

2499 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2500 mod_state = session.execute( 

2501 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id) 

2502 ).scalar_one() 

2503 assert mod_state.visibility == ModerationVisibility.shadowed 

2504 

2505 

2506def test_shadowed_event_visible_to_creator_only(db): 

2507 """SHADOWED events are visible to the creator but not to other users.""" 

2508 user1, token1 = generate_user() 

2509 user2, token2 = generate_user() 

2510 

2511 with session_scope() as session: 

2512 create_community(session, 0, 2, "Community", [user1], [], None) 

2513 

2514 start_time = now() + timedelta(hours=2) 

2515 end_time = start_time + timedelta(hours=3) 

2516 

2517 with events_session(token1) as api: 

2518 res = api.CreateEvent( 

2519 events_pb2.CreateEventReq( 

2520 title="Shadowed Event", 

2521 content="Content.", 

2522 location=events_pb2.EventLocation( 

2523 address="Near Null Island", 

2524 lat=0.1, 

2525 lng=0.2, 

2526 ), 

2527 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2528 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2529 ) 

2530 ) 

2531 event_id = res.event_id 

2532 

2533 # Creator can see it 

2534 with events_session(token1) as api: 

2535 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2536 assert res.title == "Shadowed Event" 

2537 

2538 # Other user cannot 

2539 with events_session(token2) as api: 

2540 with pytest.raises(grpc.RpcError) as e: 

2541 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2542 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2543 

2544 

2545def test_event_visible_after_approval(db, moderator: Moderator): 

2546 """Events become visible to all users after moderation approval.""" 

2547 user1, token1 = generate_user() 

2548 user2, token2 = generate_user() 

2549 

2550 with session_scope() as session: 

2551 create_community(session, 0, 2, "Community", [user1], [], None) 

2552 

2553 start_time = now() + timedelta(hours=2) 

2554 end_time = start_time + timedelta(hours=3) 

2555 

2556 with events_session(token1) as api: 

2557 res = api.CreateEvent( 

2558 events_pb2.CreateEventReq( 

2559 title="Approved Event", 

2560 content="Content.", 

2561 location=events_pb2.EventLocation( 

2562 address="Near Null Island", 

2563 lat=0.1, 

2564 lng=0.2, 

2565 ), 

2566 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2567 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2568 ) 

2569 ) 

2570 event_id = res.event_id 

2571 

2572 # Other user cannot see it yet 

2573 with events_session(token2) as api: 

2574 with pytest.raises(grpc.RpcError) as e: 

2575 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2576 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2577 

2578 # Approve the event 

2579 moderator.approve_event_occurrence(event_id) 

2580 

2581 # Now other user can see it 

2582 with events_session(token2) as api: 

2583 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2584 assert res.title == "Approved Event" 

2585 

2586 

2587def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

2588 """SHADOWED events appear in lists for the creator but not for other users.""" 

2589 user1, token1 = generate_user() 

2590 user2, token2 = generate_user() 

2591 

2592 with session_scope() as session: 

2593 create_community(session, 0, 2, "Community", [user1], [], None) 

2594 

2595 start_time = now() + timedelta(hours=2) 

2596 end_time = start_time + timedelta(hours=3) 

2597 

2598 with events_session(token1) as api: 

2599 res = api.CreateEvent( 

2600 events_pb2.CreateEventReq( 

2601 title="List Test Event", 

2602 content="Content.", 

2603 location=events_pb2.EventLocation( 

2604 address="Near Null Island", 

2605 lat=0.1, 

2606 lng=0.2, 

2607 ), 

2608 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2609 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2610 ) 

2611 ) 

2612 event_id = res.event_id 

2613 

2614 # Creator can see their own SHADOWED event in lists 

2615 with events_session(token1) as api: 

2616 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

2617 event_ids = [e.event_id for e in list_res.events] 

2618 assert event_id in event_ids 

2619 

2620 # Other user cannot see the SHADOWED event in lists 

2621 with events_session(token2) as api: 

2622 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

2623 event_ids = [e.event_id for e in list_res.events] 

2624 assert event_id not in event_ids 

2625 

2626 # After approval, other user can see it 

2627 moderator.approve_event_occurrence(event_id) 

2628 

2629 with events_session(token2) as api: 

2630 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

2631 event_ids = [e.event_id for e in list_res.events] 

2632 assert event_id in event_ids 

2633 

2634 

2635def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator): 

2636 """Event create notifications are deferred while SHADOWED, then unblocked after approval.""" 

2637 user1, token1 = generate_user() 

2638 user2, token2 = generate_user() 

2639 

2640 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications 

2641 with session_scope() as session: 

2642 world = create_community(session, 0, 10, "World", [user1], [], None) 

2643 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world) 

2644 region = create_community(session, 0, 5, "Region", [user1], [], macroregion) 

2645 create_community(session, 0, 2, "Child", [user2], [], region) 

2646 

2647 start_time = now() + timedelta(hours=2) 

2648 end_time = start_time + timedelta(hours=3) 

2649 

2650 with events_session(token1) as api: 

2651 res = api.CreateEvent( 

2652 events_pb2.CreateEventReq( 

2653 title="Deferred Event", 

2654 content="Content.", 

2655 location=events_pb2.EventLocation( 

2656 address="Near Null Island", 

2657 lat=0.1, 

2658 lng=0.2, 

2659 ), 

2660 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2661 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2662 ) 

2663 ) 

2664 event_id = res.event_id 

2665 

2666 # Process all jobs — notification should be deferred (event is SHADOWED) 

2667 process_jobs() 

2668 

2669 with session_scope() as session: 

2670 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one() 

2671 # Notification was created with moderation_state_id for deferral 

2672 assert notif.moderation_state_id is not None 

2673 # No delivery exists (deferred because event is SHADOWED) 

2674 delivery_count = session.execute( 

2675 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id) 

2676 ).scalar_one_or_none() 

2677 assert delivery_count is None 

2678 

2679 # Approve the event — handle_notification is re-queued for deferred notifications 

2680 moderator.approve_event_occurrence(event_id) 

2681 

2682 # Verify handle_notification job was queued 

2683 with session_scope() as session: 

2684 pending_jobs = ( 

2685 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending)) 

2686 .scalars() 

2687 .all() 

2688 ) 

2689 assert any("handle_notification" in j.job_type for j in pending_jobs) 

2690 

2691 

2692def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

2693 """Event update notifications should carry the event's moderation_state_id for deferral.""" 

2694 user1, token1 = generate_user() 

2695 user2, token2 = generate_user() 

2696 

2697 with session_scope() as session: 

2698 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

2699 

2700 start_time = now() + timedelta(hours=2) 

2701 end_time = start_time + timedelta(hours=3) 

2702 

2703 with events_session(token1) as api: 

2704 res = api.CreateEvent( 

2705 events_pb2.CreateEventReq( 

2706 title="Update Test", 

2707 content="Content.", 

2708 location=events_pb2.EventLocation( 

2709 address="Near Null Island", 

2710 lat=0.1, 

2711 lng=0.2, 

2712 ), 

2713 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2714 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2715 ) 

2716 ) 

2717 event_id = res.event_id 

2718 

2719 moderator.approve_event_occurrence(event_id) 

2720 process_jobs() 

2721 # Clear any create notifications 

2722 while push_collector.count_for_user(user2.id): 2722 ↛ 2723line 2722 didn't jump to line 2723 because the condition on line 2722 was never true

2723 push_collector.pop_for_user(user2.id) 

2724 

2725 # User2 subscribes to the event 

2726 with events_session(token2) as api: 

2727 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

2728 

2729 # User1 updates the event with should_notify=True 

2730 with events_session(token1) as api: 

2731 api.UpdateEvent( 

2732 events_pb2.UpdateEventReq( 

2733 event_id=event_id, 

2734 title=wrappers_pb2.StringValue(value="Updated Title"), 

2735 should_notify=True, 

2736 ) 

2737 ) 

2738 

2739 process_jobs() 

2740 

2741 # Verify that the update notification for user2 has moderation_state_id set 

2742 with session_scope() as session: 

2743 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2744 

2745 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2746 # Find the update notification (most recent one) 

2747 update_notifs = [n for n in notifications if n.topic_action.action == "update"] 

2748 assert len(update_notifs) == 1 

2749 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id 

2750 

2751 

2752def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

2753 """Event cancel notifications should carry the event's moderation_state_id for deferral.""" 

2754 user1, token1 = generate_user() 

2755 user2, token2 = generate_user() 

2756 

2757 with session_scope() as session: 

2758 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

2759 

2760 start_time = now() + timedelta(hours=2) 

2761 end_time = start_time + timedelta(hours=3) 

2762 

2763 with events_session(token1) as api: 

2764 res = api.CreateEvent( 

2765 events_pb2.CreateEventReq( 

2766 title="Cancel Test", 

2767 content="Content.", 

2768 location=events_pb2.EventLocation( 

2769 address="Near Null Island", 

2770 lat=0.1, 

2771 lng=0.2, 

2772 ), 

2773 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2774 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2775 ) 

2776 ) 

2777 event_id = res.event_id 

2778 

2779 moderator.approve_event_occurrence(event_id) 

2780 process_jobs() 

2781 while push_collector.count_for_user(user2.id): 2781 ↛ 2782line 2781 didn't jump to line 2782 because the condition on line 2781 was never true

2782 push_collector.pop_for_user(user2.id) 

2783 

2784 # User2 subscribes 

2785 with events_session(token2) as api: 

2786 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

2787 

2788 # User1 cancels the event 

2789 with events_session(token1) as api: 

2790 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id)) 

2791 

2792 process_jobs() 

2793 

2794 # Verify that the cancel notification for user2 has moderation_state_id set 

2795 with session_scope() as session: 

2796 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2797 

2798 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2799 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"] 

2800 assert len(cancel_notifs) == 1 

2801 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id 

2802 

2803 

2804def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

2805 """Event reminder notifications should carry the event's moderation_state_id for deferral.""" 

2806 user1, token1 = generate_user() 

2807 user2, token2 = generate_user() 

2808 

2809 with session_scope() as session: 

2810 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

2811 

2812 # Create event starting 23 hours from now (within 24h reminder window) 

2813 start_time = now() + timedelta(hours=23) 

2814 end_time = start_time + timedelta(hours=1) 

2815 

2816 with events_session(token1) as api: 

2817 res = api.CreateEvent( 

2818 events_pb2.CreateEventReq( 

2819 title="Reminder Test", 

2820 content="Content.", 

2821 location=events_pb2.EventLocation( 

2822 address="Near Null Island", 

2823 lat=0.1, 

2824 lng=0.2, 

2825 ), 

2826 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2827 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2828 ) 

2829 ) 

2830 event_id = res.event_id 

2831 

2832 moderator.approve_event_occurrence(event_id) 

2833 process_jobs() 

2834 while push_collector.count_for_user(user2.id): 2834 ↛ 2835line 2834 didn't jump to line 2835 because the condition on line 2834 was never true

2835 push_collector.pop_for_user(user2.id) 

2836 

2837 # User2 marks attendance 

2838 with events_session(token2) as api: 

2839 api.SetEventAttendance( 

2840 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

2841 ) 

2842 

2843 # Run the event reminder handler 

2844 send_event_reminders(empty_pb2.Empty()) 

2845 process_jobs() 

2846 

2847 # Verify that the reminder notification for user2 has moderation_state_id set 

2848 with session_scope() as session: 

2849 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2850 

2851 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2852 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"] 

2853 assert len(reminder_notifs) == 1 

2854 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id 

2855 

2856 

2857def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator): 

2858 """Event reminders should not be sent for cancelled events.""" 

2859 user1, token1 = generate_user() 

2860 user2, token2 = generate_user() 

2861 

2862 with session_scope() as session: 

2863 create_community(session, 0, 2, "Community", [user2], [], None) 

2864 

2865 # Create event starting 23 hours from now (within 24h reminder window) 

2866 start_time = now() + timedelta(hours=23) 

2867 end_time = start_time + timedelta(hours=1) 

2868 

2869 with events_session(token1) as api: 

2870 res = api.CreateEvent( 

2871 events_pb2.CreateEventReq( 

2872 title="Cancelled Reminder Test", 

2873 content="Content.", 

2874 location=events_pb2.EventLocation( 

2875 address="Near Null Island", 

2876 lat=0.1, 

2877 lng=0.2, 

2878 ), 

2879 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2880 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2881 ) 

2882 ) 

2883 event_id = res.event_id 

2884 

2885 moderator.approve_event_occurrence(event_id) 

2886 process_jobs() 

2887 

2888 # User2 marks attendance 

2889 with events_session(token2) as api: 

2890 api.SetEventAttendance( 

2891 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

2892 ) 

2893 

2894 # User1 cancels the event 

2895 with events_session(token1) as api: 

2896 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id)) 

2897 

2898 process_jobs() 

2899 # Drain any cancellation-related notifications so we can cleanly assert on reminders 

2900 while push_collector.count_for_user(user2.id): 

2901 push_collector.pop_for_user(user2.id) 

2902 

2903 # Run the event reminder handler 

2904 send_event_reminders(empty_pb2.Empty()) 

2905 process_jobs() 

2906 

2907 # Verify that no reminder notification was sent for user2 

2908 with session_scope() as session: 

2909 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2910 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder] 

2911 assert len(reminder_notifs) == 0 

2912 

2913 

2914@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"]) 

2915def test_event_reminder_not_sent_for_invisible_attendee( 

2916 db, push_collector: PushCollector, moderator: Moderator, invisible_field 

2917): 

2918 user1, token1 = generate_user() 

2919 user2, token2 = generate_user() 

2920 

2921 with session_scope() as session: 

2922 create_community(session, 0, 2, "Community", [user2], [], None) 

2923 

2924 start_time = now() + timedelta(hours=23) 

2925 end_time = start_time + timedelta(hours=1) 

2926 

2927 with events_session(token1) as api: 

2928 res = api.CreateEvent( 

2929 events_pb2.CreateEventReq( 

2930 title="Invisible Attendee Reminder Test", 

2931 content="Content.", 

2932 location=events_pb2.EventLocation( 

2933 address="Near Null Island", 

2934 lat=0.1, 

2935 lng=0.2, 

2936 ), 

2937 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2938 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2939 ) 

2940 ) 

2941 event_id = res.event_id 

2942 

2943 moderator.approve_event_occurrence(event_id) 

2944 process_jobs() 

2945 

2946 with events_session(token2) as api: 

2947 api.SetEventAttendance( 

2948 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

2949 ) 

2950 

2951 with session_scope() as session: 

2952 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()})) 

2953 

2954 send_event_reminders(empty_pb2.Empty()) 

2955 process_jobs() 

2956 

2957 with session_scope() as session: 

2958 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2959 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder] 

2960 assert len(reminder_notifs) == 0 

2961 

2962 

2963def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

2964 """ListEventOccurrences should only return occurrences for the requested event, not other events.""" 

2965 user1, token1 = generate_user() 

2966 user2, token2 = generate_user() 

2967 

2968 with session_scope() as session: 

2969 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id 

2970 

2971 start = now() 

2972 

2973 # User1 creates event A with 3 occurrences 

2974 event_a_ids = [] 

2975 with events_session(token1) as api: 

2976 res = api.CreateEvent( 

2977 events_pb2.CreateEventReq( 

2978 title="Event A", 

2979 content="Content A.", 

2980 parent_community_id=c_id, 

2981 location=events_pb2.EventLocation( 

2982 address="Near Null Island", 

2983 lat=0.1, 

2984 lng=0.2, 

2985 ), 

2986 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)), 

2987 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)), 

2988 ) 

2989 ) 

2990 event_a_ids.append(res.event_id) 

2991 for i in range(2): 

2992 res = api.ScheduleEvent( 

2993 events_pb2.ScheduleEventReq( 

2994 event_id=event_a_ids[-1], 

2995 content=f"A occurrence {i}", 

2996 location=events_pb2.EventLocation( 

2997 address="Near Null Island", 

2998 lat=0.1, 

2999 lng=0.2, 

3000 ), 

3001 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)), 

3002 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)), 

3003 ) 

3004 ) 

3005 event_a_ids.append(res.event_id) 

3006 

3007 # User2 creates event B with 2 occurrences 

3008 event_b_ids = [] 

3009 with events_session(token2) as api: 

3010 res = api.CreateEvent( 

3011 events_pb2.CreateEventReq( 

3012 title="Event B", 

3013 content="Content B.", 

3014 parent_community_id=c_id, 

3015 location=events_pb2.EventLocation( 

3016 address="Near Null Island", 

3017 lat=0.1, 

3018 lng=0.2, 

3019 ), 

3020 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)), 

3021 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)), 

3022 ) 

3023 ) 

3024 event_b_ids.append(res.event_id) 

3025 res = api.ScheduleEvent( 

3026 events_pb2.ScheduleEventReq( 

3027 event_id=event_b_ids[-1], 

3028 content="B occurrence 1", 

3029 location=events_pb2.EventLocation( 

3030 address="Near Null Island", 

3031 lat=0.1, 

3032 lng=0.2, 

3033 ), 

3034 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)), 

3035 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)), 

3036 ) 

3037 ) 

3038 event_b_ids.append(res.event_id) 

3039 

3040 moderator.approve_event_occurrence(event_a_ids[0]) 

3041 moderator.approve_event_occurrence(event_b_ids[0]) 

3042 

3043 # List occurrences for event A — should only get event A's 3 occurrences 

3044 with events_session(token1) as api: 

3045 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1])) 

3046 returned_ids = [e.event_id for e in res.events] 

3047 assert sorted(returned_ids) == sorted(event_a_ids) 

3048 

3049 # List occurrences for event B — should only get event B's 2 occurrences 

3050 with events_session(token2) as api: 

3051 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1])) 

3052 returned_ids = [e.event_id for e in res.events] 

3053 assert sorted(returned_ids) == sorted(event_b_ids) 

3054 

3055 

3056def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

3057 """Event comment notifications should carry the comment's moderation_state_id for deferral.""" 

3058 user1, token1 = generate_user() 

3059 user2, token2 = generate_user() 

3060 

3061 with session_scope() as session: 

3062 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

3063 

3064 start_time = now() + timedelta(hours=2) 

3065 end_time = start_time + timedelta(hours=3) 

3066 

3067 with events_session(token1) as api: 

3068 res = api.CreateEvent( 

3069 events_pb2.CreateEventReq( 

3070 title="Comment Test", 

3071 content="Content.", 

3072 parent_community_id=c_id, 

3073 location=events_pb2.EventLocation( 

3074 address="Near Null Island", 

3075 lat=0.1, 

3076 lng=0.2, 

3077 ), 

3078 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3079 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3080 ) 

3081 ) 

3082 event_id = res.event_id 

3083 thread_id = res.thread.thread_id 

3084 

3085 moderator.approve_event_occurrence(event_id) 

3086 process_jobs() 

3087 while push_collector.count_for_user(user1.id): 3087 ↛ 3088line 3087 didn't jump to line 3088 because the condition on line 3087 was never true

3088 push_collector.pop_for_user(user1.id) 

3089 

3090 # User1 subscribes (creator is auto-subscribed, but let's be explicit) 

3091 with events_session(token1) as api: 

3092 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

3093 

3094 # User2 posts a top-level comment on the event thread 

3095 with threads_session(token2) as api: 

3096 comment_thread_id = api.PostReply( 

3097 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!") 

3098 ).thread_id 

3099 

3100 process_jobs() 

3101 

3102 # The comment notification for user1 should be gated on the comment's own moderation_state_id 

3103 comment_db_id = comment_thread_id // 10 

3104 with session_scope() as session: 

3105 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one() 

3106 

3107 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all() 

3108 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"] 

3109 assert len(comment_notifs) == 1 

3110 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id 

3111 

3112 

3113def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

3114 """Event thread reply notifications should carry the reply's moderation_state_id for deferral.""" 

3115 user1, token1 = generate_user() 

3116 user2, token2 = generate_user() 

3117 user3, token3 = generate_user() 

3118 

3119 with session_scope() as session: 

3120 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id 

3121 

3122 start_time = now() + timedelta(hours=2) 

3123 end_time = start_time + timedelta(hours=3) 

3124 

3125 with events_session(token1) as api: 

3126 res = api.CreateEvent( 

3127 events_pb2.CreateEventReq( 

3128 title="Reply Test", 

3129 content="Content.", 

3130 location=events_pb2.EventLocation( 

3131 address="Near Null Island", 

3132 lat=0.1, 

3133 lng=0.2, 

3134 ), 

3135 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3136 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3137 ) 

3138 ) 

3139 event_id = res.event_id 

3140 thread_id = res.thread.thread_id 

3141 

3142 moderator.approve_event_occurrence(event_id) 

3143 process_jobs() 

3144 while push_collector.count_for_user(user1.id): 3144 ↛ 3145line 3144 didn't jump to line 3145 because the condition on line 3144 was never true

3145 push_collector.pop_for_user(user1.id) 

3146 

3147 # User2 posts a top-level comment 

3148 with threads_session(token2) as api: 

3149 comment_thread_id = api.PostReply( 

3150 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment") 

3151 ).thread_id 

3152 

3153 process_jobs() 

3154 while push_collector.count_for_user(user1.id): 3154 ↛ 3155line 3154 didn't jump to line 3155 because the condition on line 3154 was never true

3155 push_collector.pop_for_user(user1.id) 

3156 

3157 # User3 replies to user2's comment (depth=2 reply) 

3158 with threads_session(token3) as api: 

3159 nested_reply_thread_id = api.PostReply( 

3160 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply") 

3161 ).thread_id 

3162 

3163 process_jobs() 

3164 

3165 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id 

3166 nested_reply_db_id = nested_reply_thread_id // 10 

3167 with session_scope() as session: 

3168 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one() 

3169 

3170 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

3171 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"] 

3172 assert len(reply_notifs) == 1 

3173 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id