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

1409 statements  

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

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from psycopg.types.range import TimestamptzRange 

7from sqlalchemy import select 

8from sqlalchemy.sql.expression import update 

9 

10from couchers.db import session_scope 

11from couchers.jobs.handlers import send_event_reminders 

12from couchers.models import ( 

13 BackgroundJob, 

14 BackgroundJobState, 

15 Comment, 

16 EventOccurrence, 

17 ModerationState, 

18 ModerationVisibility, 

19 Notification, 

20 NotificationDelivery, 

21 NotificationTopicAction, 

22 Reply, 

23 Upload, 

24 User, 

25) 

26from couchers.proto import editor_pb2, events_pb2, threads_pb2 

27from couchers.tasks import enforce_community_memberships 

28from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime 

29from tests.fixtures.db import generate_user 

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

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

32from tests.test_communities import create_community, create_group 

33 

34 

35@pytest.fixture(autouse=True) 

36def _(testconfig): 

37 pass 

38 

39 

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

41 # test cases: 

42 # can create event 

43 # cannot create event with missing details 

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

45 # can create in different timezones 

46 

47 # event creator 

48 user1, token1 = generate_user() 

49 # community moderator 

50 user2, token2 = generate_user() 

51 # third party 

52 user3, token3 = generate_user() 

53 

54 with session_scope() as session: 

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

56 

57 time_before = now() 

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

59 end_time = start_time + timedelta(hours=3) 

60 

61 # Can create an event 

62 with events_session(token1) as api: 

63 res = api.CreateEvent( 

64 events_pb2.CreateEventReq( 

65 title="Dummy Title", 

66 content="Dummy content.", 

67 photo_key=None, 

68 location=events_pb2.EventLocation( 

69 address="Near Null Island", 

70 lat=0.1, 

71 lng=0.2, 

72 ), 

73 start_time=Timestamp_from_datetime(start_time), 

74 end_time=Timestamp_from_datetime(end_time), 

75 timezone="UTC", 

76 ) 

77 ) 

78 

79 assert res.is_next 

80 assert res.title == "Dummy Title" 

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

82 assert res.content == "Dummy content." 

83 assert not res.photo_url 

84 assert res.HasField("location") 

85 assert res.location.lat == 0.1 

86 assert res.location.lng == 0.2 

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

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

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

90 assert res.creator_user_id == user1.id 

91 assert to_aware_datetime(res.start_time) == start_time 

92 assert to_aware_datetime(res.end_time) == end_time 

93 # assert res.timezone == "UTC" 

94 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

95 assert res.organizer 

96 assert res.subscriber 

97 assert res.going_count == 1 

98 assert res.organizer_count == 1 

99 assert res.subscriber_count == 1 

100 assert res.owner_user_id == user1.id 

101 assert not res.owner_community_id 

102 assert not res.owner_group_id 

103 assert res.thread.thread_id 

104 assert res.can_edit 

105 assert not res.can_moderate 

106 

107 event_id = res.event_id 

108 

109 # Approve the event so other users can see it 

110 moderator.approve_event_occurrence(event_id) 

111 

112 with events_session(token2) as api: 

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

114 

115 assert res.is_next 

116 assert res.title == "Dummy Title" 

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

118 assert res.content == "Dummy content." 

119 assert not res.photo_url 

120 assert res.HasField("location") 

121 assert res.location.lat == 0.1 

122 assert res.location.lng == 0.2 

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

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

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

126 assert res.creator_user_id == user1.id 

127 assert to_aware_datetime(res.start_time) == start_time 

128 assert to_aware_datetime(res.end_time) == end_time 

129 # assert res.timezone == "UTC" 

130 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

131 assert not res.organizer 

132 assert not res.subscriber 

133 assert res.going_count == 1 

134 assert res.organizer_count == 1 

135 assert res.subscriber_count == 1 

136 assert res.owner_user_id == user1.id 

137 assert not res.owner_community_id 

138 assert not res.owner_group_id 

139 assert res.thread.thread_id 

140 assert res.can_edit 

141 assert res.can_moderate 

142 

143 with events_session(token3) as api: 

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

145 

146 assert res.is_next 

147 assert res.title == "Dummy Title" 

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

149 assert res.content == "Dummy content." 

150 assert not res.photo_url 

151 assert res.HasField("location") 

152 assert res.location.lat == 0.1 

153 assert res.location.lng == 0.2 

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

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

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

157 assert res.creator_user_id == user1.id 

158 assert to_aware_datetime(res.start_time) == start_time 

159 assert to_aware_datetime(res.end_time) == end_time 

160 # assert res.timezone == "UTC" 

161 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

162 assert not res.organizer 

163 assert not res.subscriber 

164 assert res.going_count == 1 

165 assert res.organizer_count == 1 

166 assert res.subscriber_count == 1 

167 assert res.owner_user_id == user1.id 

168 assert not res.owner_community_id 

169 assert not res.owner_group_id 

170 assert res.thread.thread_id 

171 assert not res.can_edit 

172 assert not res.can_moderate 

173 

174 # Failure cases 

175 with events_session(token1) as api: 

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

177 api.CreateEvent( 

178 events_pb2.CreateEventReq( 

179 # title="Dummy Title", 

180 content="Dummy content.", 

181 photo_key=None, 

182 location=events_pb2.EventLocation( 

183 address="Near Null Island", 

184 lat=0.1, 

185 lng=0.2, 

186 ), 

187 start_time=Timestamp_from_datetime(start_time), 

188 end_time=Timestamp_from_datetime(end_time), 

189 timezone="UTC", 

190 ) 

191 ) 

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

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

194 

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

196 api.CreateEvent( 

197 events_pb2.CreateEventReq( 

198 title="Dummy Title", 

199 # content="Dummy content.", 

200 photo_key=None, 

201 location=events_pb2.EventLocation( 

202 address="Near Null Island", 

203 lat=0.1, 

204 lng=0.2, 

205 ), 

206 start_time=Timestamp_from_datetime(start_time), 

207 end_time=Timestamp_from_datetime(end_time), 

208 timezone="UTC", 

209 ) 

210 ) 

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

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

213 

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

215 api.CreateEvent( 

216 events_pb2.CreateEventReq( 

217 title="Dummy Title", 

218 content="Dummy content.", 

219 photo_key="nonexistent", 

220 location=events_pb2.EventLocation( 

221 address="Near Null Island", 

222 lat=0.1, 

223 lng=0.2, 

224 ), 

225 start_time=Timestamp_from_datetime(start_time), 

226 end_time=Timestamp_from_datetime(end_time), 

227 timezone="UTC", 

228 ) 

229 ) 

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

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

232 

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

234 api.CreateEvent( 

235 events_pb2.CreateEventReq( 

236 title="Dummy Title", 

237 content="Dummy content.", 

238 location=events_pb2.EventLocation( 

239 address="Near Null Island", 

240 ), 

241 start_time=Timestamp_from_datetime(start_time), 

242 end_time=Timestamp_from_datetime(end_time), 

243 timezone="UTC", 

244 ) 

245 ) 

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

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

248 

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

250 api.CreateEvent( 

251 events_pb2.CreateEventReq( 

252 title="Dummy Title", 

253 content="Dummy content.", 

254 location=events_pb2.EventLocation( 

255 lat=0.1, 

256 lng=0.1, 

257 ), 

258 start_time=Timestamp_from_datetime(start_time), 

259 end_time=Timestamp_from_datetime(end_time), 

260 timezone="UTC", 

261 ) 

262 ) 

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

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

265 

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

267 api.CreateEvent( 

268 events_pb2.CreateEventReq( 

269 title="Dummy Title", 

270 content="Dummy content.", 

271 location=events_pb2.EventLocation( 

272 address="Near Null Island", 

273 lat=0.1, 

274 lng=0.2, 

275 ), 

276 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)), 

277 end_time=Timestamp_from_datetime(end_time), 

278 timezone="UTC", 

279 ) 

280 ) 

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

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

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_time=Timestamp_from_datetime(end_time), 

295 end_time=Timestamp_from_datetime(start_time), 

296 timezone="UTC", 

297 ) 

298 ) 

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

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

301 

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

303 api.CreateEvent( 

304 events_pb2.CreateEventReq( 

305 title="Dummy Title", 

306 content="Dummy content.", 

307 location=events_pb2.EventLocation( 

308 address="Near Null Island", 

309 lat=0.1, 

310 lng=0.2, 

311 ), 

312 start_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)), 

313 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)), 

314 timezone="UTC", 

315 ) 

316 ) 

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

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

319 

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

321 api.CreateEvent( 

322 events_pb2.CreateEventReq( 

323 title="Dummy Title", 

324 content="Dummy content.", 

325 location=events_pb2.EventLocation( 

326 address="Near Null Island", 

327 lat=0.1, 

328 lng=0.2, 

329 ), 

330 start_time=Timestamp_from_datetime(start_time), 

331 end_time=Timestamp_from_datetime(now() + timedelta(days=100)), 

332 timezone="UTC", 

333 ) 

334 ) 

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

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

337 

338 

339def test_CreateEvent_incomplete_profile(db): 

340 user1, token1 = generate_user(complete_profile=False) 

341 user2, token2 = generate_user() 

342 

343 with session_scope() as session: 

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

345 

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

347 end_time = start_time + timedelta(hours=3) 

348 

349 with events_session(token1) as api: 

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

351 api.CreateEvent( 

352 events_pb2.CreateEventReq( 

353 title="Dummy Title", 

354 content="Dummy content.", 

355 photo_key=None, 

356 location=events_pb2.EventLocation( 

357 address="Near Null Island", 

358 lat=0.1, 

359 lng=0.2, 

360 ), 

361 start_time=Timestamp_from_datetime(start_time), 

362 end_time=Timestamp_from_datetime(end_time), 

363 timezone="UTC", 

364 ) 

365 ) 

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

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

368 

369 

370def test_ScheduleEvent(db): 

371 # test cases: 

372 # can schedule a new event occurrence 

373 

374 user, token = generate_user() 

375 

376 with session_scope() as session: 

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

378 

379 time_before = now() 

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

381 end_time = start_time + timedelta(hours=3) 

382 

383 with events_session(token) as api: 

384 res = api.CreateEvent( 

385 events_pb2.CreateEventReq( 

386 title="Dummy Title", 

387 content="Dummy content.", 

388 parent_community_id=c_id, 

389 location=events_pb2.EventLocation( 

390 address="Near Null Island", 

391 lat=0.1, 

392 lng=0.2, 

393 ), 

394 start_time=Timestamp_from_datetime(start_time), 

395 end_time=Timestamp_from_datetime(end_time), 

396 timezone="UTC", 

397 ) 

398 ) 

399 

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

401 new_end_time = new_start_time + timedelta(hours=2) 

402 

403 res = api.ScheduleEvent( 

404 events_pb2.ScheduleEventReq( 

405 event_id=res.event_id, 

406 content="New event occurrence", 

407 location=events_pb2.EventLocation( 

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

409 lat=0.3, 

410 lng=0.2, 

411 ), 

412 start_time=Timestamp_from_datetime(new_start_time), 

413 end_time=Timestamp_from_datetime(new_end_time), 

414 timezone="UTC", 

415 ) 

416 ) 

417 

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

419 

420 assert not res.is_next 

421 assert res.title == "Dummy Title" 

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

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

424 assert not res.photo_url 

425 assert res.HasField("location") 

426 assert res.location.lat == 0.3 

427 assert res.location.lng == 0.2 

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

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

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

431 assert res.creator_user_id == user.id 

432 assert to_aware_datetime(res.start_time) == new_start_time 

433 assert to_aware_datetime(res.end_time) == new_end_time 

434 # assert res.timezone == "UTC" 

435 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

436 assert res.organizer 

437 assert res.subscriber 

438 assert res.going_count == 1 

439 assert res.organizer_count == 1 

440 assert res.subscriber_count == 1 

441 assert res.owner_user_id == user.id 

442 assert not res.owner_community_id 

443 assert not res.owner_group_id 

444 assert res.thread.thread_id 

445 assert res.can_edit 

446 assert res.can_moderate 

447 

448 

449def test_cannot_overlap_occurrences_schedule(db): 

450 user, token = generate_user() 

451 

452 with session_scope() as session: 

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

454 

455 start = now() 

456 

457 with events_session(token) as api: 

458 res = api.CreateEvent( 

459 events_pb2.CreateEventReq( 

460 title="Dummy Title", 

461 content="Dummy content.", 

462 parent_community_id=c_id, 

463 location=events_pb2.EventLocation( 

464 address="Near Null Island", 

465 lat=0.1, 

466 lng=0.2, 

467 ), 

468 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

469 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

470 timezone="UTC", 

471 ) 

472 ) 

473 

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

475 api.ScheduleEvent( 

476 events_pb2.ScheduleEventReq( 

477 event_id=res.event_id, 

478 content="New event occurrence", 

479 location=events_pb2.EventLocation( 

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

481 lat=0.3, 

482 lng=0.2, 

483 ), 

484 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

485 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

486 timezone="UTC", 

487 ) 

488 ) 

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

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

491 

492 

493def test_cannot_overlap_occurrences_update(db): 

494 user, token = generate_user() 

495 

496 with session_scope() as session: 

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

498 

499 start = now() 

500 

501 with events_session(token) as api: 

502 res = api.CreateEvent( 

503 events_pb2.CreateEventReq( 

504 title="Dummy Title", 

505 content="Dummy content.", 

506 parent_community_id=c_id, 

507 location=events_pb2.EventLocation( 

508 address="Near Null Island", 

509 lat=0.1, 

510 lng=0.2, 

511 ), 

512 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

513 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

514 timezone="UTC", 

515 ) 

516 ) 

517 

518 event_id = api.ScheduleEvent( 

519 events_pb2.ScheduleEventReq( 

520 event_id=res.event_id, 

521 content="New event occurrence", 

522 location=events_pb2.EventLocation( 

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

524 lat=0.3, 

525 lng=0.2, 

526 ), 

527 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

528 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

529 timezone="UTC", 

530 ) 

531 ).event_id 

532 

533 # can overlap with this current existing occurrence 

534 api.UpdateEvent( 

535 events_pb2.UpdateEventReq( 

536 event_id=event_id, 

537 start_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

538 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

539 ) 

540 ) 

541 

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

543 api.UpdateEvent( 

544 events_pb2.UpdateEventReq( 

545 event_id=event_id, 

546 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

547 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

548 ) 

549 ) 

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

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

552 

553 

554def test_UpdateEvent_single(db, moderator: Moderator): 

555 # test cases: 

556 # owner can update 

557 # community owner can update 

558 # notifies attendees 

559 

560 # event creator 

561 user1, token1 = generate_user() 

562 # community moderator 

563 user2, token2 = generate_user() 

564 # third parties 

565 user3, token3 = generate_user() 

566 user4, token4 = generate_user() 

567 user5, token5 = generate_user() 

568 user6, token6 = generate_user() 

569 

570 with session_scope() as session: 

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

572 

573 time_before = now() 

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

575 end_time = start_time + timedelta(hours=3) 

576 

577 with events_session(token1) as api: 

578 res = api.CreateEvent( 

579 events_pb2.CreateEventReq( 

580 title="Dummy Title", 

581 content="Dummy content.", 

582 parent_community_id=c_id, 

583 location=events_pb2.EventLocation( 

584 address="Near Null Island", 

585 lat=0.1, 

586 lng=0.2, 

587 ), 

588 start_time=Timestamp_from_datetime(start_time), 

589 end_time=Timestamp_from_datetime(end_time), 

590 timezone="UTC", 

591 ) 

592 ) 

593 

594 event_id = res.event_id 

595 

596 moderator.approve_event_occurrence(event_id) 

597 

598 with events_session(token4) as api: 

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

600 

601 with events_session(token5) as api: 

602 api.SetEventAttendance( 

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

604 ) 

605 

606 with events_session(token6) as api: 

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

608 

609 time_before_update = now() 

610 

611 with events_session(token1) as api: 

612 res = api.UpdateEvent( 

613 events_pb2.UpdateEventReq( 

614 event_id=event_id, 

615 ) 

616 ) 

617 

618 with events_session(token1) as api: 

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

620 

621 assert res.is_next 

622 assert res.title == "Dummy Title" 

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

624 assert res.content == "Dummy content." 

625 assert not res.photo_url 

626 assert res.HasField("location") 

627 assert res.location.lat == 0.1 

628 assert res.location.lng == 0.2 

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

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

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

632 assert res.creator_user_id == user1.id 

633 assert to_aware_datetime(res.start_time) == start_time 

634 assert to_aware_datetime(res.end_time) == end_time 

635 # assert res.timezone == "UTC" 

636 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

637 assert res.organizer 

638 assert res.subscriber 

639 assert res.going_count == 2 

640 assert res.organizer_count == 1 

641 assert res.subscriber_count == 3 

642 assert res.owner_user_id == user1.id 

643 assert not res.owner_community_id 

644 assert not res.owner_group_id 

645 assert res.thread.thread_id 

646 assert res.can_edit 

647 assert not res.can_moderate 

648 

649 with events_session(token2) as api: 

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

651 

652 assert res.is_next 

653 assert res.title == "Dummy Title" 

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

655 assert res.content == "Dummy content." 

656 assert not res.photo_url 

657 assert res.HasField("location") 

658 assert res.location.lat == 0.1 

659 assert res.location.lng == 0.2 

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

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

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

663 assert res.creator_user_id == user1.id 

664 assert to_aware_datetime(res.start_time) == start_time 

665 assert to_aware_datetime(res.end_time) == end_time 

666 # assert res.timezone == "UTC" 

667 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

668 assert not res.organizer 

669 assert not res.subscriber 

670 assert res.going_count == 2 

671 assert res.organizer_count == 1 

672 assert res.subscriber_count == 3 

673 assert res.owner_user_id == user1.id 

674 assert not res.owner_community_id 

675 assert not res.owner_group_id 

676 assert res.thread.thread_id 

677 assert res.can_edit 

678 assert res.can_moderate 

679 

680 with events_session(token3) as api: 

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

682 

683 assert res.is_next 

684 assert res.title == "Dummy Title" 

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

686 assert res.content == "Dummy content." 

687 assert not res.photo_url 

688 assert res.HasField("location") 

689 assert res.location.lat == 0.1 

690 assert res.location.lng == 0.2 

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

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

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

694 assert res.creator_user_id == user1.id 

695 assert to_aware_datetime(res.start_time) == start_time 

696 assert to_aware_datetime(res.end_time) == end_time 

697 # assert res.timezone == "UTC" 

698 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

699 assert not res.organizer 

700 assert not res.subscriber 

701 assert res.going_count == 2 

702 assert res.organizer_count == 1 

703 assert res.subscriber_count == 3 

704 assert res.owner_user_id == user1.id 

705 assert not res.owner_community_id 

706 assert not res.owner_group_id 

707 assert res.thread.thread_id 

708 assert not res.can_edit 

709 assert not res.can_moderate 

710 

711 with events_session(token1) as api: 

712 res = api.UpdateEvent( 

713 events_pb2.UpdateEventReq( 

714 event_id=event_id, 

715 location=events_pb2.EventLocation( 

716 address="Nearer Null Island", 

717 lat=0.01, 

718 lng=0.02, 

719 ), 

720 ) 

721 ) 

722 

723 with events_session(token3) as api: 

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

725 

726 assert res.HasField("location") 

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

728 assert res.location.lat == 0.01 

729 assert res.location.lng == 0.02 

730 

731 

732def test_UpdateEvent_all(db, moderator: Moderator): 

733 # event creator 

734 user1, token1 = generate_user() 

735 # community moderator 

736 user2, token2 = generate_user() 

737 # third parties 

738 user3, token3 = generate_user() 

739 user4, token4 = generate_user() 

740 user5, token5 = generate_user() 

741 user6, token6 = generate_user() 

742 

743 with session_scope() as session: 

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

745 

746 time_before = now() 

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

748 end_time = start_time + timedelta(hours=1.5) 

749 

750 event_ids = [] 

751 

752 with events_session(token1) as api: 

753 res = api.CreateEvent( 

754 events_pb2.CreateEventReq( 

755 title="Dummy Title", 

756 content="0th occurrence", 

757 location=events_pb2.EventLocation( 

758 address="Near Null Island", 

759 lat=0.1, 

760 lng=0.2, 

761 ), 

762 start_time=Timestamp_from_datetime(start_time), 

763 end_time=Timestamp_from_datetime(end_time), 

764 timezone="UTC", 

765 ) 

766 ) 

767 

768 event_id = res.event_id 

769 event_ids.append(event_id) 

770 

771 moderator.approve_event_occurrence(event_id) 

772 

773 with events_session(token4) as api: 

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

775 

776 with events_session(token5) as api: 

777 api.SetEventAttendance( 

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

779 ) 

780 

781 with events_session(token6) as api: 

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

783 

784 with events_session(token1) as api: 

785 for i in range(5): 

786 res = api.ScheduleEvent( 

787 events_pb2.ScheduleEventReq( 

788 event_id=event_ids[-1], 

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

790 location=events_pb2.EventLocation( 

791 address="Near Null Island", 

792 lat=0.1, 

793 lng=0.2, 

794 ), 

795 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)), 

796 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)), 

797 timezone="UTC", 

798 ) 

799 ) 

800 

801 event_ids.append(res.event_id) 

802 

803 # Approve all scheduled occurrences 

804 for eid in event_ids[1:]: 

805 moderator.approve_event_occurrence(eid) 

806 

807 updated_event_id = event_ids[3] 

808 

809 time_before_update = now() 

810 

811 with events_session(token1) as api: 

812 res = api.UpdateEvent( 

813 events_pb2.UpdateEventReq( 

814 event_id=updated_event_id, 

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

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

817 location=events_pb2.EventLocation( 

818 lat=0.2, 

819 lng=0.2, 

820 ), 

821 update_all_future=True, 

822 ) 

823 ) 

824 

825 time_after_update = now() 

826 

827 with events_session(token2) as api: 

828 for i in range(3): 

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

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

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

832 

833 for i in range(3, 6): 

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

835 assert res.content == "New content." 

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

837 

838 

839def test_GetEvent(db, moderator: Moderator): 

840 # event creator 

841 user1, token1 = generate_user() 

842 # community moderator 

843 user2, token2 = generate_user() 

844 # third parties 

845 user3, token3 = generate_user() 

846 user4, token4 = generate_user() 

847 user5, token5 = generate_user() 

848 user6, token6 = generate_user() 

849 

850 with session_scope() as session: 

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

852 

853 time_before = now() 

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

855 end_time = start_time + timedelta(hours=3) 

856 

857 with events_session(token1) as api: 

858 # in person event 

859 res = api.CreateEvent( 

860 events_pb2.CreateEventReq( 

861 title="Dummy Title", 

862 content="Dummy content.", 

863 location=events_pb2.EventLocation( 

864 address="Near Null Island", 

865 lat=0.1, 

866 lng=0.2, 

867 ), 

868 start_time=Timestamp_from_datetime(start_time), 

869 end_time=Timestamp_from_datetime(end_time), 

870 timezone="UTC", 

871 ) 

872 ) 

873 

874 event_id = res.event_id 

875 

876 moderator.approve_event_occurrence(event_id) 

877 

878 with events_session(token4) as api: 

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

880 

881 with events_session(token5) as api: 

882 api.SetEventAttendance( 

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

884 ) 

885 

886 with events_session(token6) as api: 

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

888 

889 with events_session(token1) as api: 

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

891 

892 assert res.is_next 

893 assert res.title == "Dummy Title" 

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

895 assert res.content == "Dummy content." 

896 assert not res.photo_url 

897 assert res.HasField("location") 

898 assert res.location.lat == 0.1 

899 assert res.location.lng == 0.2 

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

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

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

903 assert res.creator_user_id == user1.id 

904 assert to_aware_datetime(res.start_time) == start_time 

905 assert to_aware_datetime(res.end_time) == end_time 

906 # assert res.timezone == "UTC" 

907 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

908 assert res.organizer 

909 assert res.subscriber 

910 assert res.going_count == 2 

911 assert res.organizer_count == 1 

912 assert res.subscriber_count == 3 

913 assert res.owner_user_id == user1.id 

914 assert not res.owner_community_id 

915 assert not res.owner_group_id 

916 assert res.thread.thread_id 

917 assert res.can_edit 

918 assert not res.can_moderate 

919 

920 with events_session(token2) as api: 

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

922 

923 assert res.is_next 

924 assert res.title == "Dummy Title" 

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

926 assert res.content == "Dummy content." 

927 assert not res.photo_url 

928 assert res.HasField("location") 

929 assert res.location.lat == 0.1 

930 assert res.location.lng == 0.2 

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

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

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

934 assert res.creator_user_id == user1.id 

935 assert to_aware_datetime(res.start_time) == start_time 

936 assert to_aware_datetime(res.end_time) == end_time 

937 # assert res.timezone == "UTC" 

938 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

939 assert not res.organizer 

940 assert not res.subscriber 

941 assert res.going_count == 2 

942 assert res.organizer_count == 1 

943 assert res.subscriber_count == 3 

944 assert res.owner_user_id == user1.id 

945 assert not res.owner_community_id 

946 assert not res.owner_group_id 

947 assert res.thread.thread_id 

948 assert res.can_edit 

949 assert res.can_moderate 

950 

951 with events_session(token3) as api: 

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

953 

954 assert res.is_next 

955 assert res.title == "Dummy Title" 

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

957 assert res.content == "Dummy content." 

958 assert not res.photo_url 

959 assert res.HasField("location") 

960 assert res.location.lat == 0.1 

961 assert res.location.lng == 0.2 

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

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

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

965 assert res.creator_user_id == user1.id 

966 assert to_aware_datetime(res.start_time) == start_time 

967 assert to_aware_datetime(res.end_time) == end_time 

968 # assert res.timezone == "UTC" 

969 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

970 assert not res.organizer 

971 assert not res.subscriber 

972 assert res.going_count == 2 

973 assert res.organizer_count == 1 

974 assert res.subscriber_count == 3 

975 assert res.owner_user_id == user1.id 

976 assert not res.owner_community_id 

977 assert not res.owner_group_id 

978 assert res.thread.thread_id 

979 assert not res.can_edit 

980 assert not res.can_moderate 

981 

982 

983def test_CancelEvent(db, moderator: Moderator): 

984 # event creator 

985 user1, token1 = generate_user() 

986 # community moderator 

987 user2, token2 = generate_user() 

988 # third parties 

989 user3, token3 = generate_user() 

990 user4, token4 = generate_user() 

991 user5, token5 = generate_user() 

992 user6, token6 = generate_user() 

993 

994 with session_scope() as session: 

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

996 

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

998 end_time = start_time + timedelta(hours=3) 

999 

1000 with events_session(token1) as api: 

1001 res = api.CreateEvent( 

1002 events_pb2.CreateEventReq( 

1003 title="Dummy Title", 

1004 content="Dummy content.", 

1005 location=events_pb2.EventLocation( 

1006 address="Near Null Island", 

1007 lat=0.1, 

1008 lng=0.2, 

1009 ), 

1010 start_time=Timestamp_from_datetime(start_time), 

1011 end_time=Timestamp_from_datetime(end_time), 

1012 timezone="UTC", 

1013 ) 

1014 ) 

1015 

1016 event_id = res.event_id 

1017 

1018 moderator.approve_event_occurrence(event_id) 

1019 

1020 with events_session(token4) as api: 

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

1022 

1023 with events_session(token5) as api: 

1024 api.SetEventAttendance( 

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

1026 ) 

1027 

1028 with events_session(token6) as api: 

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

1030 

1031 with events_session(token1) as api: 

1032 res = api.CancelEvent( 

1033 events_pb2.CancelEventReq( 

1034 event_id=event_id, 

1035 ) 

1036 ) 

1037 

1038 with events_session(token1) as api: 

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

1040 assert res.is_cancelled 

1041 

1042 with events_session(token1) as api: 

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

1044 api.UpdateEvent( 

1045 events_pb2.UpdateEventReq( 

1046 event_id=event_id, 

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

1048 ) 

1049 ) 

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

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

1052 

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

1054 api.InviteEventOrganizer( 

1055 events_pb2.InviteEventOrganizerReq( 

1056 event_id=event_id, 

1057 user_id=user3.id, 

1058 ) 

1059 ) 

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

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

1062 

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

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

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

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

1067 

1068 with events_session(token3) as api: 

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

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

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.SetEventAttendance( 

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

1077 ) 

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

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

1080 

1081 with events_session(token1) as api: 

1082 for include_cancelled in [True, False]: 

1083 res = api.ListEventOccurrences( 

1084 events_pb2.ListEventOccurrencesReq( 

1085 event_id=event_id, 

1086 include_cancelled=include_cancelled, 

1087 ) 

1088 ) 

1089 if include_cancelled: 

1090 assert len(res.events) > 0 

1091 else: 

1092 assert len(res.events) == 0 

1093 

1094 res = api.ListMyEvents( 

1095 events_pb2.ListMyEventsReq( 

1096 include_cancelled=include_cancelled, 

1097 ) 

1098 ) 

1099 if include_cancelled: 

1100 assert len(res.events) > 0 

1101 else: 

1102 assert len(res.events) == 0 

1103 

1104 

1105def test_ListEventAttendees(db, moderator: Moderator): 

1106 # event creator 

1107 user1, token1 = generate_user() 

1108 # others 

1109 user2, token2 = generate_user() 

1110 user3, token3 = generate_user() 

1111 user4, token4 = generate_user() 

1112 user5, token5 = generate_user() 

1113 user6, token6 = generate_user() 

1114 

1115 with session_scope() as session: 

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

1117 

1118 with events_session(token1) as api: 

1119 event_id = api.CreateEvent( 

1120 events_pb2.CreateEventReq( 

1121 title="Dummy Title", 

1122 content="Dummy content.", 

1123 location=events_pb2.EventLocation( 

1124 address="Near Null Island", 

1125 lat=0.1, 

1126 lng=0.2, 

1127 ), 

1128 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1129 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1130 timezone="UTC", 

1131 ) 

1132 ).event_id 

1133 

1134 moderator.approve_event_occurrence(event_id) 

1135 

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

1137 with events_session(token) as api: 

1138 api.SetEventAttendance( 

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

1140 ) 

1141 

1142 with events_session(token6) as api: 

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

1144 

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

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

1147 

1148 res = api.ListEventAttendees( 

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

1150 ) 

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

1152 

1153 res = api.ListEventAttendees( 

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

1155 ) 

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

1157 assert not res.next_page_token 

1158 

1159 

1160def test_ListEventSubscribers(db, moderator: Moderator): 

1161 # event creator 

1162 user1, token1 = generate_user() 

1163 # others 

1164 user2, token2 = generate_user() 

1165 user3, token3 = generate_user() 

1166 user4, token4 = generate_user() 

1167 user5, token5 = generate_user() 

1168 user6, token6 = generate_user() 

1169 

1170 with session_scope() as session: 

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

1172 

1173 with events_session(token1) as api: 

1174 event_id = api.CreateEvent( 

1175 events_pb2.CreateEventReq( 

1176 title="Dummy Title", 

1177 content="Dummy content.", 

1178 location=events_pb2.EventLocation( 

1179 address="Near Null Island", 

1180 lat=0.1, 

1181 lng=0.2, 

1182 ), 

1183 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1184 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1185 timezone="UTC", 

1186 ) 

1187 ).event_id 

1188 

1189 moderator.approve_event_occurrence(event_id) 

1190 

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

1192 with events_session(token) as api: 

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

1194 

1195 with events_session(token6) as api: 

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

1197 

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

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

1200 

1201 res = api.ListEventSubscribers( 

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

1203 ) 

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

1205 

1206 res = api.ListEventSubscribers( 

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

1208 ) 

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

1210 assert not res.next_page_token 

1211 

1212 

1213def test_ListEventOrganizers(db, moderator: Moderator): 

1214 # event creator 

1215 user1, token1 = generate_user() 

1216 # others 

1217 user2, token2 = generate_user() 

1218 user3, token3 = generate_user() 

1219 user4, token4 = generate_user() 

1220 user5, token5 = generate_user() 

1221 user6, token6 = generate_user() 

1222 

1223 with session_scope() as session: 

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

1225 

1226 with events_session(token1) as api: 

1227 event_id = api.CreateEvent( 

1228 events_pb2.CreateEventReq( 

1229 title="Dummy Title", 

1230 content="Dummy content.", 

1231 location=events_pb2.EventLocation( 

1232 address="Near Null Island", 

1233 lat=0.1, 

1234 lng=0.2, 

1235 ), 

1236 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1237 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1238 timezone="UTC", 

1239 ) 

1240 ).event_id 

1241 

1242 moderator.approve_event_occurrence(event_id) 

1243 

1244 with events_session(token1) as api: 

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

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

1247 

1248 with events_session(token6) as api: 

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

1250 

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

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

1253 

1254 res = api.ListEventOrganizers( 

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

1256 ) 

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

1258 

1259 res = api.ListEventOrganizers( 

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

1261 ) 

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

1263 assert not res.next_page_token 

1264 

1265 

1266def test_TransferEvent(db): 

1267 user1, token1 = generate_user() 

1268 user2, token2 = generate_user() 

1269 user3, token3 = generate_user() 

1270 user4, token4 = generate_user() 

1271 

1272 with session_scope() as session: 

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

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

1275 c_id = c.id 

1276 h_id = h.id 

1277 

1278 with events_session(token1) as api: 

1279 event_id = api.CreateEvent( 

1280 events_pb2.CreateEventReq( 

1281 title="Dummy Title", 

1282 content="Dummy content.", 

1283 location=events_pb2.EventLocation( 

1284 address="Near Null Island", 

1285 lat=0.1, 

1286 lng=0.2, 

1287 ), 

1288 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1289 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1290 timezone="UTC", 

1291 ) 

1292 ).event_id 

1293 

1294 api.TransferEvent( 

1295 events_pb2.TransferEventReq( 

1296 event_id=event_id, 

1297 new_owner_community_id=c_id, 

1298 ) 

1299 ) 

1300 

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

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

1303 

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

1305 api.TransferEvent( 

1306 events_pb2.TransferEventReq( 

1307 event_id=event_id, 

1308 new_owner_group_id=h_id, 

1309 ) 

1310 ) 

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

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

1313 

1314 event_id = api.CreateEvent( 

1315 events_pb2.CreateEventReq( 

1316 title="Dummy Title", 

1317 content="Dummy content.", 

1318 location=events_pb2.EventLocation( 

1319 address="Near Null Island", 

1320 lat=0.1, 

1321 lng=0.2, 

1322 ), 

1323 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1324 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1325 timezone="UTC", 

1326 ) 

1327 ).event_id 

1328 

1329 api.TransferEvent( 

1330 events_pb2.TransferEventReq( 

1331 event_id=event_id, 

1332 new_owner_group_id=h_id, 

1333 ) 

1334 ) 

1335 

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

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

1338 

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

1340 api.TransferEvent( 

1341 events_pb2.TransferEventReq( 

1342 event_id=event_id, 

1343 new_owner_community_id=c_id, 

1344 ) 

1345 ) 

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

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

1348 

1349 

1350def test_SetEventSubscription(db, moderator: Moderator): 

1351 user1, token1 = generate_user() 

1352 user2, token2 = generate_user() 

1353 

1354 with session_scope() as session: 

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

1356 

1357 with events_session(token1) as api: 

1358 event_id = api.CreateEvent( 

1359 events_pb2.CreateEventReq( 

1360 title="Dummy Title", 

1361 content="Dummy content.", 

1362 location=events_pb2.EventLocation( 

1363 address="Near Null Island", 

1364 lat=0.1, 

1365 lng=0.2, 

1366 ), 

1367 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1368 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1369 timezone="UTC", 

1370 ) 

1371 ).event_id 

1372 

1373 moderator.approve_event_occurrence(event_id) 

1374 

1375 with events_session(token2) as api: 

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

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

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

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

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

1381 

1382 

1383def test_SetEventAttendance(db, moderator: Moderator): 

1384 user1, token1 = generate_user() 

1385 user2, token2 = generate_user() 

1386 

1387 with session_scope() as session: 

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

1389 

1390 with events_session(token1) as api: 

1391 event_id = api.CreateEvent( 

1392 events_pb2.CreateEventReq( 

1393 title="Dummy Title", 

1394 content="Dummy content.", 

1395 location=events_pb2.EventLocation( 

1396 address="Near Null Island", 

1397 lat=0.1, 

1398 lng=0.2, 

1399 ), 

1400 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1401 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1402 timezone="UTC", 

1403 ) 

1404 ).event_id 

1405 

1406 moderator.approve_event_occurrence(event_id) 

1407 

1408 with events_session(token2) as api: 

1409 assert ( 

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

1411 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1412 ) 

1413 api.SetEventAttendance( 

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

1415 ) 

1416 assert ( 

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

1418 == events_pb2.ATTENDANCE_STATE_GOING 

1419 ) 

1420 api.SetEventAttendance( 

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

1422 ) 

1423 assert ( 

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

1425 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1426 ) 

1427 

1428 

1429def test_InviteEventOrganizer(db, moderator: Moderator): 

1430 user1, token1 = generate_user() 

1431 user2, token2 = generate_user() 

1432 

1433 with session_scope() as session: 

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

1435 

1436 with events_session(token1) as api: 

1437 event_id = api.CreateEvent( 

1438 events_pb2.CreateEventReq( 

1439 title="Dummy Title", 

1440 content="Dummy content.", 

1441 location=events_pb2.EventLocation( 

1442 address="Near Null Island", 

1443 lat=0.1, 

1444 lng=0.2, 

1445 ), 

1446 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1447 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1448 timezone="UTC", 

1449 ) 

1450 ).event_id 

1451 

1452 moderator.approve_event_occurrence(event_id) 

1453 

1454 with events_session(token2) as api: 

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

1456 

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

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

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

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

1461 

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

1463 

1464 with events_session(token1) as api: 

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

1466 

1467 with events_session(token2) as api: 

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

1469 

1470 

1471def test_ListEventOccurrences(db): 

1472 user1, token1 = generate_user() 

1473 user2, token2 = generate_user() 

1474 user3, token3 = generate_user() 

1475 

1476 with session_scope() as session: 

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

1478 

1479 start = now() 

1480 

1481 event_ids = [] 

1482 

1483 with events_session(token1) as api: 

1484 res = api.CreateEvent( 

1485 events_pb2.CreateEventReq( 

1486 title="First occurrence", 

1487 content="Dummy content.", 

1488 parent_community_id=c_id, 

1489 location=events_pb2.EventLocation( 

1490 address="Near Null Island", 

1491 lat=0.1, 

1492 lng=0.2, 

1493 ), 

1494 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

1495 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)), 

1496 timezone="UTC", 

1497 ) 

1498 ) 

1499 

1500 event_ids.append(res.event_id) 

1501 

1502 for i in range(5): 

1503 res = api.ScheduleEvent( 

1504 events_pb2.ScheduleEventReq( 

1505 event_id=event_ids[-1], 

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

1507 location=events_pb2.EventLocation( 

1508 address="Near Null Island", 

1509 lat=0.1, 

1510 lng=0.2, 

1511 ), 

1512 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)), 

1513 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)), 

1514 timezone="UTC", 

1515 ) 

1516 ) 

1517 

1518 event_ids.append(res.event_id) 

1519 

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

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

1522 

1523 res = api.ListEventOccurrences( 

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

1525 ) 

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

1527 

1528 res = api.ListEventOccurrences( 

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

1530 ) 

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

1532 assert not res.next_page_token 

1533 

1534 

1535def test_ListMyEvents(db, moderator: Moderator): 

1536 user1, token1 = generate_user() 

1537 user2, token2 = generate_user() 

1538 user3, token3 = generate_user() 

1539 user4, token4 = generate_user() 

1540 user5, token5 = generate_user() 

1541 

1542 with session_scope() as session: 

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

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

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

1546 c_id = global_community.id 

1547 macroregion_community = create_community( 

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

1549 ) 

1550 region_community = create_community( 

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

1552 ) 

1553 subregion_community = create_community( 

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

1555 ) 

1556 c2_id = subregion_community.id 

1557 

1558 start = now() 

1559 

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

1561 return events_pb2.CreateEventReq( 

1562 title="Dummy Title", 

1563 content="Dummy content.", 

1564 location=events_pb2.EventLocation( 

1565 address="Near Null Island", 

1566 lat=0.1, 

1567 lng=0.2, 

1568 ), 

1569 parent_community_id=community_id, 

1570 timezone="UTC", 

1571 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)), 

1572 end_time=Timestamp_from_datetime(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 timezone="UTC", 

1745 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)), 

1746 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)), 

1747 ) 

1748 

1749 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1750 with events_session(token1) as api: 

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

1752 

1753 with events_session(token2) as api: 

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

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

1756 # e_both: user1 will be both organizer and attendee 

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

1758 

1759 moderator.approve_event_occurrence(e_own) 

1760 moderator.approve_event_occurrence(e_attending) 

1761 moderator.approve_event_occurrence(e_community_only) 

1762 moderator.approve_event_occurrence(e_both) 

1763 

1764 # invite user1 as organizer of e_both 

1765 with events_session(token2) as api: 

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

1767 

1768 # user1 RSVPs to e_attending and e_both 

1769 with events_session(token1) as api: 

1770 api.SetEventAttendance( 

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

1772 ) 

1773 api.SetEventAttendance( 

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

1775 ) 

1776 

1777 with events_session(token1) as api: 

1778 # baseline: all four community events visible 

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

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

1781 

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

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

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

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

1786 

1787 # exclude_attending with attending=True: invalid combination 

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

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

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

1791 

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

1793 with events_session(token2) as api: 

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

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

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

1797 

1798 

1799def test_RemoveEventOrganizer(db, moderator: Moderator): 

1800 user1, token1 = generate_user() 

1801 user2, token2 = generate_user() 

1802 

1803 with session_scope() as session: 

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

1805 

1806 with events_session(token1) as api: 

1807 event_id = api.CreateEvent( 

1808 events_pb2.CreateEventReq( 

1809 title="Dummy Title", 

1810 content="Dummy content.", 

1811 location=events_pb2.EventLocation( 

1812 address="Near Null Island", 

1813 lat=0.1, 

1814 lng=0.2, 

1815 ), 

1816 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1817 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1818 timezone="UTC", 

1819 ) 

1820 ).event_id 

1821 

1822 moderator.approve_event_occurrence(event_id) 

1823 

1824 with events_session(token2) as api: 

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

1826 

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

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

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

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

1831 

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

1833 

1834 with events_session(token1) as api: 

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

1836 

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

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

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

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

1841 

1842 with events_session(token2) as api: 

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

1844 assert res.organizer 

1845 assert res.organizer_count == 2 

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

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

1848 

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

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

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

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

1853 

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

1855 assert not res.organizer 

1856 assert res.organizer_count == 1 

1857 

1858 # Test that event owner can remove co-organizers 

1859 with events_session(token1) as api: 

1860 # Add user2 back as organizer 

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

1862 

1863 # Verify user2 is now an organizer 

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

1865 assert res.organizer_count == 2 

1866 

1867 # Event owner can remove co-organizer 

1868 api.RemoveEventOrganizer( 

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

1870 ) 

1871 

1872 # Verify user2 is no longer an organizer 

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

1874 assert res.organizer_count == 1 

1875 

1876 # Test that non-organizers cannot remove other organizers 

1877 with events_session(token2) as api: 

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

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

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

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

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

1883 

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

1885 with events_session(token1) as api: 

1886 # Add user2 back as organizer 

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

1888 

1889 

1890def test_ListEventAttendees_regression(db): 

1891 # see issue #1617: 

1892 # 

1893 # 1. Create an event 

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

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

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

1897 # 

1898 # **Expected behaviour** 

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

1900 # 

1901 # **Actual/current behaviour** 

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

1903 

1904 user1, token1 = generate_user() 

1905 user2, token2 = generate_user() 

1906 user3, token3 = generate_user() 

1907 user4, token4 = generate_user() 

1908 user5, token5 = generate_user() 

1909 

1910 with session_scope() as session: 

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

1912 

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

1914 end_time = start_time + timedelta(hours=3) 

1915 

1916 with events_session(token1) as api: 

1917 res = api.CreateEvent( 

1918 events_pb2.CreateEventReq( 

1919 title="Dummy Title", 

1920 content="Dummy content.", 

1921 location=events_pb2.EventLocation( 

1922 address="Near Null Island", 

1923 lat=0.1, 

1924 lng=0.2, 

1925 ), 

1926 parent_community_id=c_id, 

1927 start_time=Timestamp_from_datetime(start_time), 

1928 end_time=Timestamp_from_datetime(end_time), 

1929 timezone="UTC", 

1930 ) 

1931 ) 

1932 

1933 res = api.TransferEvent( 

1934 events_pb2.TransferEventReq( 

1935 event_id=res.event_id, 

1936 new_owner_community_id=c_id, 

1937 ) 

1938 ) 

1939 

1940 event_id = res.event_id 

1941 

1942 api.SetEventAttendance( 

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

1944 ) 

1945 api.SetEventAttendance( 

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

1947 ) 

1948 

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

1950 assert len(res.attendee_user_ids) == 1 

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

1952 

1953 

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

1955 user1, token1 = generate_user() 

1956 user2, token2 = generate_user() 

1957 user3, token3 = generate_user() 

1958 user4, token4 = generate_user() 

1959 

1960 with session_scope() as session: 

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

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

1963 c_id = c.id 

1964 h_id = h.id 

1965 user4_id = user4.id 

1966 

1967 with events_session(token1) as api: 

1968 event = api.CreateEvent( 

1969 events_pb2.CreateEventReq( 

1970 title="Dummy Title", 

1971 content="Dummy content.", 

1972 location=events_pb2.EventLocation( 

1973 address="Near Null Island", 

1974 lat=0.1, 

1975 lng=0.2, 

1976 ), 

1977 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1978 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1979 timezone="UTC", 

1980 ) 

1981 ) 

1982 

1983 moderator.approve_event_occurrence(event.event_id) 

1984 

1985 with threads_session(token2) as api: 

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

1987 

1988 moderator.approve_thread_post(reply_id) 

1989 

1990 with events_session(token3) as api: 

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

1992 assert res.thread.num_responses == 1 

1993 

1994 with threads_session(token3) as api: 

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

1996 assert len(ret.replies) == 1 

1997 assert not ret.next_page_token 

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

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

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

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

2002 

2003 nested_reply_id = api.PostReply( 

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

2005 ).thread_id 

2006 

2007 moderator.approve_thread_post(nested_reply_id) 

2008 

2009 process_jobs() 

2010 

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

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

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

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

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

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

2017 

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

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

2020 

2021 assert push_collector.count_for_user(user4_id) == 0 

2022 

2023 

2024def test_can_overlap_other_events_schedule_regression(db): 

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

2026 user, token = generate_user() 

2027 

2028 with session_scope() as session: 

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

2030 

2031 start = now() 

2032 

2033 with events_session(token) as api: 

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

2035 api.CreateEvent( 

2036 events_pb2.CreateEventReq( 

2037 title="Dummy Title", 

2038 content="Dummy content.", 

2039 parent_community_id=c_id, 

2040 location=events_pb2.EventLocation( 

2041 address="Near Null Island", 

2042 lat=0.1, 

2043 lng=0.2, 

2044 ), 

2045 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2046 end_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

2047 timezone="UTC", 

2048 ) 

2049 ) 

2050 

2051 # this event 

2052 res = api.CreateEvent( 

2053 events_pb2.CreateEventReq( 

2054 title="Dummy Title", 

2055 content="Dummy content.", 

2056 parent_community_id=c_id, 

2057 location=events_pb2.EventLocation( 

2058 address="Near Null Island", 

2059 lat=0.1, 

2060 lng=0.2, 

2061 ), 

2062 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2063 end_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

2064 timezone="UTC", 

2065 ) 

2066 ) 

2067 

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

2069 api.ScheduleEvent( 

2070 events_pb2.ScheduleEventReq( 

2071 event_id=res.event_id, 

2072 content="New event occurrence", 

2073 location=events_pb2.EventLocation( 

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

2075 lat=0.3, 

2076 lng=0.2, 

2077 ), 

2078 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2079 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2080 timezone="UTC", 

2081 ) 

2082 ) 

2083 

2084 

2085def test_can_overlap_other_events_update_regression(db): 

2086 user, token = generate_user() 

2087 

2088 with session_scope() as session: 

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

2090 

2091 start = now() 

2092 

2093 with events_session(token) as api: 

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

2095 api.CreateEvent( 

2096 events_pb2.CreateEventReq( 

2097 title="Dummy Title", 

2098 content="Dummy content.", 

2099 parent_community_id=c_id, 

2100 location=events_pb2.EventLocation( 

2101 address="Near Null Island", 

2102 lat=0.1, 

2103 lng=0.2, 

2104 ), 

2105 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2106 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2107 timezone="UTC", 

2108 ) 

2109 ) 

2110 

2111 res = api.CreateEvent( 

2112 events_pb2.CreateEventReq( 

2113 title="Dummy Title", 

2114 content="Dummy content.", 

2115 parent_community_id=c_id, 

2116 location=events_pb2.EventLocation( 

2117 address="Near Null Island", 

2118 lat=0.1, 

2119 lng=0.2, 

2120 ), 

2121 start_time=Timestamp_from_datetime(start + timedelta(hours=7)), 

2122 end_time=Timestamp_from_datetime(start + timedelta(hours=8)), 

2123 timezone="UTC", 

2124 ) 

2125 ) 

2126 

2127 event_id = api.ScheduleEvent( 

2128 events_pb2.ScheduleEventReq( 

2129 event_id=res.event_id, 

2130 content="New event occurrence", 

2131 location=events_pb2.EventLocation( 

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

2133 lat=0.3, 

2134 lng=0.2, 

2135 ), 

2136 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2137 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2138 timezone="UTC", 

2139 ) 

2140 ).event_id 

2141 

2142 # can overlap with this current existing occurrence 

2143 api.UpdateEvent( 

2144 events_pb2.UpdateEventReq( 

2145 event_id=event_id, 

2146 start_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

2147 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2148 ) 

2149 ) 

2150 

2151 api.UpdateEvent( 

2152 events_pb2.UpdateEventReq( 

2153 event_id=event_id, 

2154 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

2155 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2156 ) 

2157 ) 

2158 

2159 

2160def test_list_past_events_regression(db): 

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

2162 user, token = generate_user() 

2163 

2164 with session_scope() as session: 

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

2166 

2167 start = now() 

2168 

2169 with events_session(token) as api: 

2170 api.CreateEvent( 

2171 events_pb2.CreateEventReq( 

2172 title="Dummy Title", 

2173 content="Dummy content.", 

2174 parent_community_id=c_id, 

2175 location=events_pb2.EventLocation( 

2176 address="Near Null Island", 

2177 lat=0.1, 

2178 lng=0.2, 

2179 ), 

2180 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2181 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2182 timezone="UTC", 

2183 ) 

2184 ) 

2185 

2186 with session_scope() as session: 

2187 session.execute( 

2188 update(EventOccurrence).values( 

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

2190 ) 

2191 ) 

2192 

2193 with events_session(token) as api: 

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

2195 assert len(res.events) == 1 

2196 

2197 

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

2199 user1, token1 = generate_user(complete_profile=True) 

2200 user2, token2 = generate_user() 

2201 user3, token3 = generate_user() 

2202 user4, token4 = generate_user() 

2203 user5, token5 = generate_user(is_superuser=True) 

2204 

2205 with session_scope() as session: 

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

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

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

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

2210 

2211 enforce_community_memberships() 

2212 

2213 with events_session(token1) as api: 

2214 res = api.CreateEvent( 

2215 events_pb2.CreateEventReq( 

2216 title="Dummy Title", 

2217 content="Dummy content.", 

2218 parent_community_id=c_id, 

2219 location=events_pb2.EventLocation( 

2220 address="Near Null Island", 

2221 lat=0.1, 

2222 lng=0.2, 

2223 ), 

2224 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)), 

2225 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)), 

2226 timezone="UTC", 

2227 ) 

2228 ) 

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

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

2231 

2232 event_id = res.event_id 

2233 

2234 moderator.approve_event_occurrence(event_id) 

2235 

2236 with events_session(token1) as api: 

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

2238 

2239 email = email_collector.pop_for_mods(last=True) 

2240 

2241 assert user_url in email.plain 

2242 assert event_url in email.plain 

2243 

2244 # can't send another req 

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

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

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

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

2249 

2250 # another user can send one though 

2251 with events_session(token3) as api: 

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

2253 

2254 # but not a non-admin 

2255 with events_session(token2) as api: 

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

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

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

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

2260 

2261 with real_editor_session(token5) as editor: 

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

2263 assert len(res.requests) == 2 

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

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

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

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

2268 

2269 editor.DecideEventCommunityInviteRequest( 

2270 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2272 approve=False, 

2273 ) 

2274 ) 

2275 

2276 editor.DecideEventCommunityInviteRequest( 

2277 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2279 approve=True, 

2280 ) 

2281 ) 

2282 

2283 # not after approve 

2284 with events_session(token4) as api: 

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

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

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

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

2289 

2290 

2291def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator): 

2292 # Regression: users who already RSVP'd (or organize the event) must not get the 

2293 # community invite notification when it is approved. 

2294 organizer, organizer_token = generate_user() 

2295 attendee, attendee_token = generate_user() 

2296 member, _ = generate_user() 

2297 superuser, superuser_token = generate_user(is_superuser=True) 

2298 

2299 with session_scope() as session: 

2300 w = create_community(session, 0, 2, "World Community", [superuser], [], None) 

2301 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w) 

2302 r = create_community(session, 0, 2, "Region", [superuser], [], mr) 

2303 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id 

2304 

2305 enforce_community_memberships() 

2306 

2307 with events_session(organizer_token) as api: 

2308 event_id = api.CreateEvent( 

2309 events_pb2.CreateEventReq( 

2310 title="Dummy Title", 

2311 content="Dummy content.", 

2312 parent_community_id=c_id, 

2313 location=events_pb2.EventLocation( 

2314 address="Near Null Island", 

2315 lat=0.1, 

2316 lng=0.2, 

2317 ), 

2318 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)), 

2319 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)), 

2320 timezone="UTC", 

2321 ) 

2322 ).event_id 

2323 

2324 moderator.approve_event_occurrence(event_id) 

2325 

2326 # the attendee RSVPs before the community invite is approved 

2327 with events_session(attendee_token) as api: 

2328 api.SetEventAttendance( 

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

2330 ) 

2331 

2332 with events_session(organizer_token) as api: 

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

2334 

2335 with real_editor_session(superuser_token) as editor: 

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

2337 editor.DecideEventCommunityInviteRequest( 

2338 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2340 approve=True, 

2341 ) 

2342 ) 

2343 

2344 process_jobs() 

2345 

2346 with session_scope() as session: 

2347 

2348 def invite_notification_count(user_id: int) -> int: 

2349 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all() 

2350 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved]) 

2351 

2352 # a plain community member gets the invite... 

2353 assert invite_notification_count(member.id) == 1 

2354 # ...but the attendee and the organizer don't 

2355 assert invite_notification_count(attendee.id) == 0 

2356 assert invite_notification_count(organizer.id) == 0 

2357 

2358 

2359def test_update_event_should_notify_queues_job(): 

2360 user, token = generate_user() 

2361 start = now() 

2362 

2363 with session_scope() as session: 

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

2365 

2366 # create an event 

2367 with events_session(token) as api: 

2368 create_res = api.CreateEvent( 

2369 events_pb2.CreateEventReq( 

2370 title="Dummy Title", 

2371 content="Dummy content.", 

2372 parent_community_id=c_id, 

2373 location=events_pb2.EventLocation( 

2374 address="Near Null Island", 

2375 lat=1.0, 

2376 lng=2.0, 

2377 ), 

2378 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2379 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2380 timezone="UTC", 

2381 ) 

2382 ) 

2383 

2384 event_id = create_res.event_id 

2385 

2386 # measure initial background job queue length 

2387 with session_scope() as session: 

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

2389 job_length_before_update = len(jobs) 

2390 

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

2392 api.UpdateEvent( 

2393 events_pb2.UpdateEventReq( 

2394 event_id=event_id, 

2395 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2396 should_notify=False, 

2397 ) 

2398 ) 

2399 

2400 with session_scope() as session: 

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

2402 assert len(jobs) == job_length_before_update 

2403 

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

2405 api.UpdateEvent( 

2406 events_pb2.UpdateEventReq( 

2407 event_id=event_id, 

2408 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2409 should_notify=True, 

2410 ) 

2411 ) 

2412 

2413 with session_scope() as session: 

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

2415 assert len(jobs) == job_length_before_update + 1 

2416 

2417 

2418def test_event_photo_key(db): 

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

2420 user, token = generate_user() 

2421 

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

2423 end_time = start_time + timedelta(hours=3) 

2424 

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

2426 with session_scope() as session: 

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

2428 upload = Upload( 

2429 key="test_event_photo_key_123", 

2430 filename="test_event_photo_key_123.jpg", 

2431 creator_user_id=user.id, 

2432 ) 

2433 session.add(upload) 

2434 

2435 with events_session(token) as api: 

2436 # Create event without photo 

2437 res = api.CreateEvent( 

2438 events_pb2.CreateEventReq( 

2439 title="Event Without Photo", 

2440 content="No photo content.", 

2441 photo_key=None, 

2442 location=events_pb2.EventLocation( 

2443 address="Near Null Island", 

2444 lat=0.1, 

2445 lng=0.2, 

2446 ), 

2447 start_time=Timestamp_from_datetime(start_time), 

2448 end_time=Timestamp_from_datetime(end_time), 

2449 timezone="UTC", 

2450 ) 

2451 ) 

2452 

2453 assert res.photo_key == "" 

2454 assert res.photo_url == "" 

2455 

2456 # Create event with photo 

2457 res_with_photo = api.CreateEvent( 

2458 events_pb2.CreateEventReq( 

2459 title="Event With Photo", 

2460 content="Has photo content.", 

2461 photo_key="test_event_photo_key_123", 

2462 location=events_pb2.EventLocation( 

2463 address="Near Null Island", 

2464 lat=0.1, 

2465 lng=0.2, 

2466 ), 

2467 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)), 

2468 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)), 

2469 timezone="UTC", 

2470 ) 

2471 ) 

2472 

2473 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2474 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2475 

2476 event_id = res_with_photo.event_id 

2477 

2478 # Verify photo_key is returned when getting the event 

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

2480 assert get_res.photo_key == "test_event_photo_key_123" 

2481 assert "test_event_photo_key_123" in get_res.photo_url 

2482 

2483 

2484def test_event_created_with_shadowed_visibility(db): 

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

2486 user, token = generate_user() 

2487 

2488 with session_scope() as session: 

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

2490 

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

2492 end_time = start_time + timedelta(hours=3) 

2493 

2494 with events_session(token) as api: 

2495 res = api.CreateEvent( 

2496 events_pb2.CreateEventReq( 

2497 title="Test UMS Event", 

2498 content="UMS content.", 

2499 location=events_pb2.EventLocation( 

2500 address="Near Null Island", 

2501 lat=0.1, 

2502 lng=0.2, 

2503 ), 

2504 start_time=Timestamp_from_datetime(start_time), 

2505 end_time=Timestamp_from_datetime(end_time), 

2506 timezone="UTC", 

2507 ) 

2508 ) 

2509 event_id = res.event_id 

2510 

2511 with session_scope() as session: 

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

2513 mod_state = session.execute( 

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

2515 ).scalar_one() 

2516 assert mod_state.visibility == ModerationVisibility.shadowed 

2517 

2518 

2519def test_shadowed_event_visible_to_creator_only(db): 

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

2521 user1, token1 = generate_user() 

2522 user2, token2 = generate_user() 

2523 

2524 with session_scope() as session: 

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

2526 

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

2528 end_time = start_time + timedelta(hours=3) 

2529 

2530 with events_session(token1) as api: 

2531 res = api.CreateEvent( 

2532 events_pb2.CreateEventReq( 

2533 title="Shadowed Event", 

2534 content="Content.", 

2535 location=events_pb2.EventLocation( 

2536 address="Near Null Island", 

2537 lat=0.1, 

2538 lng=0.2, 

2539 ), 

2540 start_time=Timestamp_from_datetime(start_time), 

2541 end_time=Timestamp_from_datetime(end_time), 

2542 timezone="UTC", 

2543 ) 

2544 ) 

2545 event_id = res.event_id 

2546 

2547 # Creator can see it 

2548 with events_session(token1) as api: 

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

2550 assert res.title == "Shadowed Event" 

2551 

2552 # Other user cannot 

2553 with events_session(token2) as api: 

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

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

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

2557 

2558 

2559def test_event_visible_after_approval(db, moderator: Moderator): 

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

2561 user1, token1 = generate_user() 

2562 user2, token2 = generate_user() 

2563 

2564 with session_scope() as session: 

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

2566 

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

2568 end_time = start_time + timedelta(hours=3) 

2569 

2570 with events_session(token1) as api: 

2571 res = api.CreateEvent( 

2572 events_pb2.CreateEventReq( 

2573 title="Approved Event", 

2574 content="Content.", 

2575 location=events_pb2.EventLocation( 

2576 address="Near Null Island", 

2577 lat=0.1, 

2578 lng=0.2, 

2579 ), 

2580 start_time=Timestamp_from_datetime(start_time), 

2581 end_time=Timestamp_from_datetime(end_time), 

2582 timezone="UTC", 

2583 ) 

2584 ) 

2585 event_id = res.event_id 

2586 

2587 # Other user cannot see it yet 

2588 with events_session(token2) as api: 

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

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

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

2592 

2593 # Approve the event 

2594 moderator.approve_event_occurrence(event_id) 

2595 

2596 # Now other user can see it 

2597 with events_session(token2) as api: 

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

2599 assert res.title == "Approved Event" 

2600 

2601 

2602def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2604 user1, token1 = generate_user() 

2605 user2, token2 = generate_user() 

2606 

2607 with session_scope() as session: 

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

2609 

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

2611 end_time = start_time + timedelta(hours=3) 

2612 

2613 with events_session(token1) as api: 

2614 res = api.CreateEvent( 

2615 events_pb2.CreateEventReq( 

2616 title="List Test Event", 

2617 content="Content.", 

2618 location=events_pb2.EventLocation( 

2619 address="Near Null Island", 

2620 lat=0.1, 

2621 lng=0.2, 

2622 ), 

2623 start_time=Timestamp_from_datetime(start_time), 

2624 end_time=Timestamp_from_datetime(end_time), 

2625 timezone="UTC", 

2626 ) 

2627 ) 

2628 event_id = res.event_id 

2629 

2630 # Creator can see their own SHADOWED event in lists 

2631 with events_session(token1) as api: 

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

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

2634 assert event_id in event_ids 

2635 

2636 # Other user cannot see the SHADOWED event in lists 

2637 with events_session(token2) as api: 

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

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

2640 assert event_id not in event_ids 

2641 

2642 # After approval, other user can see it 

2643 moderator.approve_event_occurrence(event_id) 

2644 

2645 with events_session(token2) as api: 

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

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

2648 assert event_id in event_ids 

2649 

2650 

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

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

2653 user1, token1 = generate_user() 

2654 user2, token2 = generate_user() 

2655 

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

2657 with session_scope() as session: 

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

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

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

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

2662 

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

2664 end_time = start_time + timedelta(hours=3) 

2665 

2666 with events_session(token1) as api: 

2667 res = api.CreateEvent( 

2668 events_pb2.CreateEventReq( 

2669 title="Deferred Event", 

2670 content="Content.", 

2671 location=events_pb2.EventLocation( 

2672 address="Near Null Island", 

2673 lat=0.1, 

2674 lng=0.2, 

2675 ), 

2676 start_time=Timestamp_from_datetime(start_time), 

2677 end_time=Timestamp_from_datetime(end_time), 

2678 timezone="UTC", 

2679 ) 

2680 ) 

2681 event_id = res.event_id 

2682 

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

2684 process_jobs() 

2685 

2686 with session_scope() as session: 

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

2688 # Notification was created with moderation_state_id for deferral 

2689 assert notif.moderation_state_id is not None 

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

2691 delivery_count = session.execute( 

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

2693 ).scalar_one_or_none() 

2694 assert delivery_count is None 

2695 

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

2697 moderator.approve_event_occurrence(event_id) 

2698 

2699 # Verify handle_notification job was queued 

2700 with session_scope() as session: 

2701 pending_jobs = ( 

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

2703 .scalars() 

2704 .all() 

2705 ) 

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

2707 

2708 

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

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

2711 user1, token1 = generate_user() 

2712 user2, token2 = generate_user() 

2713 

2714 with session_scope() as session: 

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

2716 

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

2718 end_time = start_time + timedelta(hours=3) 

2719 

2720 with events_session(token1) as api: 

2721 res = api.CreateEvent( 

2722 events_pb2.CreateEventReq( 

2723 title="Update Test", 

2724 content="Content.", 

2725 location=events_pb2.EventLocation( 

2726 address="Near Null Island", 

2727 lat=0.1, 

2728 lng=0.2, 

2729 ), 

2730 start_time=Timestamp_from_datetime(start_time), 

2731 end_time=Timestamp_from_datetime(end_time), 

2732 timezone="UTC", 

2733 ) 

2734 ) 

2735 event_id = res.event_id 

2736 

2737 moderator.approve_event_occurrence(event_id) 

2738 process_jobs() 

2739 # Clear any create notifications 

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

2741 push_collector.pop_for_user(user2.id) 

2742 

2743 # User2 subscribes to the event 

2744 with events_session(token2) as api: 

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

2746 

2747 # User1 updates the event with should_notify=True 

2748 with events_session(token1) as api: 

2749 api.UpdateEvent( 

2750 events_pb2.UpdateEventReq( 

2751 event_id=event_id, 

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

2753 should_notify=True, 

2754 ) 

2755 ) 

2756 

2757 process_jobs() 

2758 

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

2760 with session_scope() as session: 

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

2762 

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

2764 # Find the update notification (most recent one) 

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

2766 assert len(update_notifs) == 1 

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

2768 

2769 

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

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

2772 user1, token1 = generate_user() 

2773 user2, token2 = generate_user() 

2774 

2775 with session_scope() as session: 

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

2777 

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

2779 end_time = start_time + timedelta(hours=3) 

2780 

2781 with events_session(token1) as api: 

2782 res = api.CreateEvent( 

2783 events_pb2.CreateEventReq( 

2784 title="Cancel Test", 

2785 content="Content.", 

2786 location=events_pb2.EventLocation( 

2787 address="Near Null Island", 

2788 lat=0.1, 

2789 lng=0.2, 

2790 ), 

2791 start_time=Timestamp_from_datetime(start_time), 

2792 end_time=Timestamp_from_datetime(end_time), 

2793 timezone="UTC", 

2794 ) 

2795 ) 

2796 event_id = res.event_id 

2797 

2798 moderator.approve_event_occurrence(event_id) 

2799 process_jobs() 

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

2801 push_collector.pop_for_user(user2.id) 

2802 

2803 # User2 subscribes 

2804 with events_session(token2) as api: 

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

2806 

2807 # User1 cancels the event 

2808 with events_session(token1) as api: 

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

2810 

2811 process_jobs() 

2812 

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

2814 with session_scope() as session: 

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

2816 

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

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

2819 assert len(cancel_notifs) == 1 

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

2821 

2822 

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

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

2825 user1, token1 = generate_user() 

2826 user2, token2 = generate_user() 

2827 

2828 with session_scope() as session: 

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

2830 

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

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

2833 end_time = start_time + timedelta(hours=1) 

2834 

2835 with events_session(token1) as api: 

2836 res = api.CreateEvent( 

2837 events_pb2.CreateEventReq( 

2838 title="Reminder Test", 

2839 content="Content.", 

2840 location=events_pb2.EventLocation( 

2841 address="Near Null Island", 

2842 lat=0.1, 

2843 lng=0.2, 

2844 ), 

2845 start_time=Timestamp_from_datetime(start_time), 

2846 end_time=Timestamp_from_datetime(end_time), 

2847 timezone="UTC", 

2848 ) 

2849 ) 

2850 event_id = res.event_id 

2851 

2852 moderator.approve_event_occurrence(event_id) 

2853 process_jobs() 

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

2855 push_collector.pop_for_user(user2.id) 

2856 

2857 # User2 marks attendance 

2858 with events_session(token2) as api: 

2859 api.SetEventAttendance( 

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

2861 ) 

2862 

2863 # Run the event reminder handler 

2864 send_event_reminders(empty_pb2.Empty()) 

2865 process_jobs() 

2866 

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

2868 with session_scope() as session: 

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

2870 

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

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

2873 assert len(reminder_notifs) == 1 

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

2875 

2876 

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

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

2879 user1, token1 = generate_user() 

2880 user2, token2 = generate_user() 

2881 

2882 with session_scope() as session: 

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

2884 

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

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

2887 end_time = start_time + timedelta(hours=1) 

2888 

2889 with events_session(token1) as api: 

2890 res = api.CreateEvent( 

2891 events_pb2.CreateEventReq( 

2892 title="Cancelled Reminder Test", 

2893 content="Content.", 

2894 location=events_pb2.EventLocation( 

2895 address="Near Null Island", 

2896 lat=0.1, 

2897 lng=0.2, 

2898 ), 

2899 start_time=Timestamp_from_datetime(start_time), 

2900 end_time=Timestamp_from_datetime(end_time), 

2901 timezone="UTC", 

2902 ) 

2903 ) 

2904 event_id = res.event_id 

2905 

2906 moderator.approve_event_occurrence(event_id) 

2907 process_jobs() 

2908 

2909 # User2 marks attendance 

2910 with events_session(token2) as api: 

2911 api.SetEventAttendance( 

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

2913 ) 

2914 

2915 # User1 cancels the event 

2916 with events_session(token1) as api: 

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

2918 

2919 process_jobs() 

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

2921 while push_collector.count_for_user(user2.id): 

2922 push_collector.pop_for_user(user2.id) 

2923 

2924 # Run the event reminder handler 

2925 send_event_reminders(empty_pb2.Empty()) 

2926 process_jobs() 

2927 

2928 # Verify that no reminder notification was sent for user2 

2929 with session_scope() as session: 

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

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

2932 assert len(reminder_notifs) == 0 

2933 

2934 

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

2936def test_event_reminder_not_sent_for_invisible_attendee( 

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

2938): 

2939 user1, token1 = generate_user() 

2940 user2, token2 = generate_user() 

2941 

2942 with session_scope() as session: 

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

2944 

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

2946 end_time = start_time + timedelta(hours=1) 

2947 

2948 with events_session(token1) as api: 

2949 res = api.CreateEvent( 

2950 events_pb2.CreateEventReq( 

2951 title="Invisible Attendee Reminder Test", 

2952 content="Content.", 

2953 location=events_pb2.EventLocation( 

2954 address="Near Null Island", 

2955 lat=0.1, 

2956 lng=0.2, 

2957 ), 

2958 start_time=Timestamp_from_datetime(start_time), 

2959 end_time=Timestamp_from_datetime(end_time), 

2960 timezone="UTC", 

2961 ) 

2962 ) 

2963 event_id = res.event_id 

2964 

2965 moderator.approve_event_occurrence(event_id) 

2966 process_jobs() 

2967 

2968 with events_session(token2) as api: 

2969 api.SetEventAttendance( 

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

2971 ) 

2972 

2973 with session_scope() as session: 

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

2975 

2976 send_event_reminders(empty_pb2.Empty()) 

2977 process_jobs() 

2978 

2979 with session_scope() as session: 

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

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

2982 assert len(reminder_notifs) == 0 

2983 

2984 

2985def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

2987 user1, token1 = generate_user() 

2988 user2, token2 = generate_user() 

2989 

2990 with session_scope() as session: 

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

2992 

2993 start = now() 

2994 

2995 # User1 creates event A with 3 occurrences 

2996 event_a_ids = [] 

2997 with events_session(token1) as api: 

2998 res = api.CreateEvent( 

2999 events_pb2.CreateEventReq( 

3000 title="Event A", 

3001 content="Content A.", 

3002 parent_community_id=c_id, 

3003 location=events_pb2.EventLocation( 

3004 address="Near Null Island", 

3005 lat=0.1, 

3006 lng=0.2, 

3007 ), 

3008 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

3009 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)), 

3010 timezone="UTC", 

3011 ) 

3012 ) 

3013 event_a_ids.append(res.event_id) 

3014 for i in range(2): 

3015 res = api.ScheduleEvent( 

3016 events_pb2.ScheduleEventReq( 

3017 event_id=event_a_ids[-1], 

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

3019 location=events_pb2.EventLocation( 

3020 address="Near Null Island", 

3021 lat=0.1, 

3022 lng=0.2, 

3023 ), 

3024 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)), 

3025 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)), 

3026 timezone="UTC", 

3027 ) 

3028 ) 

3029 event_a_ids.append(res.event_id) 

3030 

3031 # User2 creates event B with 2 occurrences 

3032 event_b_ids = [] 

3033 with events_session(token2) as api: 

3034 res = api.CreateEvent( 

3035 events_pb2.CreateEventReq( 

3036 title="Event B", 

3037 content="Content B.", 

3038 parent_community_id=c_id, 

3039 location=events_pb2.EventLocation( 

3040 address="Near Null Island", 

3041 lat=0.1, 

3042 lng=0.2, 

3043 ), 

3044 start_time=Timestamp_from_datetime(start + timedelta(hours=10)), 

3045 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)), 

3046 timezone="UTC", 

3047 ) 

3048 ) 

3049 event_b_ids.append(res.event_id) 

3050 res = api.ScheduleEvent( 

3051 events_pb2.ScheduleEventReq( 

3052 event_id=event_b_ids[-1], 

3053 content="B occurrence 1", 

3054 location=events_pb2.EventLocation( 

3055 address="Near Null Island", 

3056 lat=0.1, 

3057 lng=0.2, 

3058 ), 

3059 start_time=Timestamp_from_datetime(start + timedelta(hours=11)), 

3060 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)), 

3061 timezone="UTC", 

3062 ) 

3063 ) 

3064 event_b_ids.append(res.event_id) 

3065 

3066 moderator.approve_event_occurrence(event_a_ids[0]) 

3067 moderator.approve_event_occurrence(event_b_ids[0]) 

3068 

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

3070 with events_session(token1) as api: 

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

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

3073 assert sorted(returned_ids) == sorted(event_a_ids) 

3074 

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

3076 with events_session(token2) as api: 

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

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

3079 assert sorted(returned_ids) == sorted(event_b_ids) 

3080 

3081 

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

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

3084 user1, token1 = generate_user() 

3085 user2, token2 = generate_user() 

3086 

3087 with session_scope() as session: 

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

3089 

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

3091 end_time = start_time + timedelta(hours=3) 

3092 

3093 with events_session(token1) as api: 

3094 res = api.CreateEvent( 

3095 events_pb2.CreateEventReq( 

3096 title="Comment Test", 

3097 content="Content.", 

3098 parent_community_id=c_id, 

3099 location=events_pb2.EventLocation( 

3100 address="Near Null Island", 

3101 lat=0.1, 

3102 lng=0.2, 

3103 ), 

3104 start_time=Timestamp_from_datetime(start_time), 

3105 end_time=Timestamp_from_datetime(end_time), 

3106 timezone="UTC", 

3107 ) 

3108 ) 

3109 event_id = res.event_id 

3110 thread_id = res.thread.thread_id 

3111 

3112 moderator.approve_event_occurrence(event_id) 

3113 process_jobs() 

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

3115 push_collector.pop_for_user(user1.id) 

3116 

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

3118 with events_session(token1) as api: 

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

3120 

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

3122 with threads_session(token2) as api: 

3123 comment_thread_id = api.PostReply( 

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

3125 ).thread_id 

3126 

3127 process_jobs() 

3128 

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

3130 comment_db_id = comment_thread_id // 10 

3131 with session_scope() as session: 

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

3133 

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

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

3136 assert len(comment_notifs) == 1 

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

3138 

3139 

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

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

3142 user1, token1 = generate_user() 

3143 user2, token2 = generate_user() 

3144 user3, token3 = generate_user() 

3145 

3146 with session_scope() as session: 

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

3148 

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

3150 end_time = start_time + timedelta(hours=3) 

3151 

3152 with events_session(token1) as api: 

3153 res = api.CreateEvent( 

3154 events_pb2.CreateEventReq( 

3155 title="Reply Test", 

3156 content="Content.", 

3157 location=events_pb2.EventLocation( 

3158 address="Near Null Island", 

3159 lat=0.1, 

3160 lng=0.2, 

3161 ), 

3162 start_time=Timestamp_from_datetime(start_time), 

3163 end_time=Timestamp_from_datetime(end_time), 

3164 timezone="UTC", 

3165 ) 

3166 ) 

3167 event_id = res.event_id 

3168 thread_id = res.thread.thread_id 

3169 

3170 moderator.approve_event_occurrence(event_id) 

3171 process_jobs() 

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

3173 push_collector.pop_for_user(user1.id) 

3174 

3175 # User2 posts a top-level comment 

3176 with threads_session(token2) as api: 

3177 comment_thread_id = api.PostReply( 

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

3179 ).thread_id 

3180 

3181 process_jobs() 

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

3183 push_collector.pop_for_user(user1.id) 

3184 

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

3186 with threads_session(token3) as api: 

3187 nested_reply_thread_id = api.PostReply( 

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

3189 ).thread_id 

3190 

3191 process_jobs() 

3192 

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

3194 nested_reply_db_id = nested_reply_thread_id // 10 

3195 with session_scope() as session: 

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

3197 

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

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

3200 assert len(reply_notifs) == 1 

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