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

1380 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-22 19:54 +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_update_event_should_notify_queues_job(): 

2292 user, token = generate_user() 

2293 start = now() 

2294 

2295 with session_scope() as session: 

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

2297 

2298 # create an event 

2299 with events_session(token) as api: 

2300 create_res = api.CreateEvent( 

2301 events_pb2.CreateEventReq( 

2302 title="Dummy Title", 

2303 content="Dummy content.", 

2304 parent_community_id=c_id, 

2305 location=events_pb2.EventLocation( 

2306 address="Near Null Island", 

2307 lat=1.0, 

2308 lng=2.0, 

2309 ), 

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

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

2312 timezone="UTC", 

2313 ) 

2314 ) 

2315 

2316 event_id = create_res.event_id 

2317 

2318 # measure initial background job queue length 

2319 with session_scope() as session: 

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

2321 job_length_before_update = len(jobs) 

2322 

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

2324 api.UpdateEvent( 

2325 events_pb2.UpdateEventReq( 

2326 event_id=event_id, 

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

2328 should_notify=False, 

2329 ) 

2330 ) 

2331 

2332 with session_scope() as session: 

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

2334 assert len(jobs) == job_length_before_update 

2335 

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

2337 api.UpdateEvent( 

2338 events_pb2.UpdateEventReq( 

2339 event_id=event_id, 

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

2341 should_notify=True, 

2342 ) 

2343 ) 

2344 

2345 with session_scope() as session: 

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

2347 assert len(jobs) == job_length_before_update + 1 

2348 

2349 

2350def test_event_photo_key(db): 

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

2352 user, token = generate_user() 

2353 

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

2355 end_time = start_time + timedelta(hours=3) 

2356 

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

2358 with session_scope() as session: 

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

2360 upload = Upload( 

2361 key="test_event_photo_key_123", 

2362 filename="test_event_photo_key_123.jpg", 

2363 creator_user_id=user.id, 

2364 ) 

2365 session.add(upload) 

2366 

2367 with events_session(token) as api: 

2368 # Create event without photo 

2369 res = api.CreateEvent( 

2370 events_pb2.CreateEventReq( 

2371 title="Event Without Photo", 

2372 content="No photo content.", 

2373 photo_key=None, 

2374 location=events_pb2.EventLocation( 

2375 address="Near Null Island", 

2376 lat=0.1, 

2377 lng=0.2, 

2378 ), 

2379 start_time=Timestamp_from_datetime(start_time), 

2380 end_time=Timestamp_from_datetime(end_time), 

2381 timezone="UTC", 

2382 ) 

2383 ) 

2384 

2385 assert res.photo_key == "" 

2386 assert res.photo_url == "" 

2387 

2388 # Create event with photo 

2389 res_with_photo = api.CreateEvent( 

2390 events_pb2.CreateEventReq( 

2391 title="Event With Photo", 

2392 content="Has photo content.", 

2393 photo_key="test_event_photo_key_123", 

2394 location=events_pb2.EventLocation( 

2395 address="Near Null Island", 

2396 lat=0.1, 

2397 lng=0.2, 

2398 ), 

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

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

2401 timezone="UTC", 

2402 ) 

2403 ) 

2404 

2405 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2406 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2407 

2408 event_id = res_with_photo.event_id 

2409 

2410 # Verify photo_key is returned when getting the event 

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

2412 assert get_res.photo_key == "test_event_photo_key_123" 

2413 assert "test_event_photo_key_123" in get_res.photo_url 

2414 

2415 

2416def test_event_created_with_shadowed_visibility(db): 

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

2418 user, token = generate_user() 

2419 

2420 with session_scope() as session: 

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

2422 

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

2424 end_time = start_time + timedelta(hours=3) 

2425 

2426 with events_session(token) as api: 

2427 res = api.CreateEvent( 

2428 events_pb2.CreateEventReq( 

2429 title="Test UMS Event", 

2430 content="UMS content.", 

2431 location=events_pb2.EventLocation( 

2432 address="Near Null Island", 

2433 lat=0.1, 

2434 lng=0.2, 

2435 ), 

2436 start_time=Timestamp_from_datetime(start_time), 

2437 end_time=Timestamp_from_datetime(end_time), 

2438 timezone="UTC", 

2439 ) 

2440 ) 

2441 event_id = res.event_id 

2442 

2443 with session_scope() as session: 

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

2445 mod_state = session.execute( 

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

2447 ).scalar_one() 

2448 assert mod_state.visibility == ModerationVisibility.shadowed 

2449 

2450 

2451def test_shadowed_event_visible_to_creator_only(db): 

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

2453 user1, token1 = generate_user() 

2454 user2, token2 = generate_user() 

2455 

2456 with session_scope() as session: 

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

2458 

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

2460 end_time = start_time + timedelta(hours=3) 

2461 

2462 with events_session(token1) as api: 

2463 res = api.CreateEvent( 

2464 events_pb2.CreateEventReq( 

2465 title="Shadowed Event", 

2466 content="Content.", 

2467 location=events_pb2.EventLocation( 

2468 address="Near Null Island", 

2469 lat=0.1, 

2470 lng=0.2, 

2471 ), 

2472 start_time=Timestamp_from_datetime(start_time), 

2473 end_time=Timestamp_from_datetime(end_time), 

2474 timezone="UTC", 

2475 ) 

2476 ) 

2477 event_id = res.event_id 

2478 

2479 # Creator can see it 

2480 with events_session(token1) as api: 

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

2482 assert res.title == "Shadowed Event" 

2483 

2484 # Other user cannot 

2485 with events_session(token2) as api: 

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

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

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

2489 

2490 

2491def test_event_visible_after_approval(db, moderator: Moderator): 

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

2493 user1, token1 = generate_user() 

2494 user2, token2 = generate_user() 

2495 

2496 with session_scope() as session: 

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

2498 

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

2500 end_time = start_time + timedelta(hours=3) 

2501 

2502 with events_session(token1) as api: 

2503 res = api.CreateEvent( 

2504 events_pb2.CreateEventReq( 

2505 title="Approved Event", 

2506 content="Content.", 

2507 location=events_pb2.EventLocation( 

2508 address="Near Null Island", 

2509 lat=0.1, 

2510 lng=0.2, 

2511 ), 

2512 start_time=Timestamp_from_datetime(start_time), 

2513 end_time=Timestamp_from_datetime(end_time), 

2514 timezone="UTC", 

2515 ) 

2516 ) 

2517 event_id = res.event_id 

2518 

2519 # Other user cannot see it yet 

2520 with events_session(token2) as api: 

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

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

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

2524 

2525 # Approve the event 

2526 moderator.approve_event_occurrence(event_id) 

2527 

2528 # Now other user can see it 

2529 with events_session(token2) as api: 

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

2531 assert res.title == "Approved Event" 

2532 

2533 

2534def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2536 user1, token1 = generate_user() 

2537 user2, token2 = generate_user() 

2538 

2539 with session_scope() as session: 

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

2541 

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

2543 end_time = start_time + timedelta(hours=3) 

2544 

2545 with events_session(token1) as api: 

2546 res = api.CreateEvent( 

2547 events_pb2.CreateEventReq( 

2548 title="List Test Event", 

2549 content="Content.", 

2550 location=events_pb2.EventLocation( 

2551 address="Near Null Island", 

2552 lat=0.1, 

2553 lng=0.2, 

2554 ), 

2555 start_time=Timestamp_from_datetime(start_time), 

2556 end_time=Timestamp_from_datetime(end_time), 

2557 timezone="UTC", 

2558 ) 

2559 ) 

2560 event_id = res.event_id 

2561 

2562 # Creator can see their own SHADOWED event in lists 

2563 with events_session(token1) as api: 

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

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

2566 assert event_id in event_ids 

2567 

2568 # Other user cannot see the SHADOWED event in lists 

2569 with events_session(token2) as api: 

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

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

2572 assert event_id not in event_ids 

2573 

2574 # After approval, other user can see it 

2575 moderator.approve_event_occurrence(event_id) 

2576 

2577 with events_session(token2) as api: 

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

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

2580 assert event_id in event_ids 

2581 

2582 

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

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

2585 user1, token1 = generate_user() 

2586 user2, token2 = generate_user() 

2587 

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

2589 with session_scope() as session: 

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

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

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

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

2594 

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

2596 end_time = start_time + timedelta(hours=3) 

2597 

2598 with events_session(token1) as api: 

2599 res = api.CreateEvent( 

2600 events_pb2.CreateEventReq( 

2601 title="Deferred Event", 

2602 content="Content.", 

2603 location=events_pb2.EventLocation( 

2604 address="Near Null Island", 

2605 lat=0.1, 

2606 lng=0.2, 

2607 ), 

2608 start_time=Timestamp_from_datetime(start_time), 

2609 end_time=Timestamp_from_datetime(end_time), 

2610 timezone="UTC", 

2611 ) 

2612 ) 

2613 event_id = res.event_id 

2614 

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

2616 process_jobs() 

2617 

2618 with session_scope() as session: 

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

2620 # Notification was created with moderation_state_id for deferral 

2621 assert notif.moderation_state_id is not None 

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

2623 delivery_count = session.execute( 

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

2625 ).scalar_one_or_none() 

2626 assert delivery_count is None 

2627 

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

2629 moderator.approve_event_occurrence(event_id) 

2630 

2631 # Verify handle_notification job was queued 

2632 with session_scope() as session: 

2633 pending_jobs = ( 

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

2635 .scalars() 

2636 .all() 

2637 ) 

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

2639 

2640 

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

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

2643 user1, token1 = generate_user() 

2644 user2, token2 = generate_user() 

2645 

2646 with session_scope() as session: 

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

2648 

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

2650 end_time = start_time + timedelta(hours=3) 

2651 

2652 with events_session(token1) as api: 

2653 res = api.CreateEvent( 

2654 events_pb2.CreateEventReq( 

2655 title="Update Test", 

2656 content="Content.", 

2657 location=events_pb2.EventLocation( 

2658 address="Near Null Island", 

2659 lat=0.1, 

2660 lng=0.2, 

2661 ), 

2662 start_time=Timestamp_from_datetime(start_time), 

2663 end_time=Timestamp_from_datetime(end_time), 

2664 timezone="UTC", 

2665 ) 

2666 ) 

2667 event_id = res.event_id 

2668 

2669 moderator.approve_event_occurrence(event_id) 

2670 process_jobs() 

2671 # Clear any create notifications 

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

2673 push_collector.pop_for_user(user2.id) 

2674 

2675 # User2 subscribes to the event 

2676 with events_session(token2) as api: 

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

2678 

2679 # User1 updates the event with should_notify=True 

2680 with events_session(token1) as api: 

2681 api.UpdateEvent( 

2682 events_pb2.UpdateEventReq( 

2683 event_id=event_id, 

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

2685 should_notify=True, 

2686 ) 

2687 ) 

2688 

2689 process_jobs() 

2690 

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

2692 with session_scope() as session: 

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

2694 

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

2696 # Find the update notification (most recent one) 

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

2698 assert len(update_notifs) == 1 

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

2700 

2701 

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

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

2704 user1, token1 = generate_user() 

2705 user2, token2 = generate_user() 

2706 

2707 with session_scope() as session: 

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

2709 

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

2711 end_time = start_time + timedelta(hours=3) 

2712 

2713 with events_session(token1) as api: 

2714 res = api.CreateEvent( 

2715 events_pb2.CreateEventReq( 

2716 title="Cancel Test", 

2717 content="Content.", 

2718 location=events_pb2.EventLocation( 

2719 address="Near Null Island", 

2720 lat=0.1, 

2721 lng=0.2, 

2722 ), 

2723 start_time=Timestamp_from_datetime(start_time), 

2724 end_time=Timestamp_from_datetime(end_time), 

2725 timezone="UTC", 

2726 ) 

2727 ) 

2728 event_id = res.event_id 

2729 

2730 moderator.approve_event_occurrence(event_id) 

2731 process_jobs() 

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

2733 push_collector.pop_for_user(user2.id) 

2734 

2735 # User2 subscribes 

2736 with events_session(token2) as api: 

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

2738 

2739 # User1 cancels the event 

2740 with events_session(token1) as api: 

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

2742 

2743 process_jobs() 

2744 

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

2746 with session_scope() as session: 

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

2748 

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

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

2751 assert len(cancel_notifs) == 1 

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

2753 

2754 

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

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

2757 user1, token1 = generate_user() 

2758 user2, token2 = generate_user() 

2759 

2760 with session_scope() as session: 

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

2762 

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

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

2765 end_time = start_time + timedelta(hours=1) 

2766 

2767 with events_session(token1) as api: 

2768 res = api.CreateEvent( 

2769 events_pb2.CreateEventReq( 

2770 title="Reminder Test", 

2771 content="Content.", 

2772 location=events_pb2.EventLocation( 

2773 address="Near Null Island", 

2774 lat=0.1, 

2775 lng=0.2, 

2776 ), 

2777 start_time=Timestamp_from_datetime(start_time), 

2778 end_time=Timestamp_from_datetime(end_time), 

2779 timezone="UTC", 

2780 ) 

2781 ) 

2782 event_id = res.event_id 

2783 

2784 moderator.approve_event_occurrence(event_id) 

2785 process_jobs() 

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

2787 push_collector.pop_for_user(user2.id) 

2788 

2789 # User2 marks attendance 

2790 with events_session(token2) as api: 

2791 api.SetEventAttendance( 

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

2793 ) 

2794 

2795 # Run the event reminder handler 

2796 send_event_reminders(empty_pb2.Empty()) 

2797 process_jobs() 

2798 

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

2800 with session_scope() as session: 

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

2802 

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

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

2805 assert len(reminder_notifs) == 1 

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

2807 

2808 

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

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

2811 user1, token1 = generate_user() 

2812 user2, token2 = generate_user() 

2813 

2814 with session_scope() as session: 

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

2816 

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

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

2819 end_time = start_time + timedelta(hours=1) 

2820 

2821 with events_session(token1) as api: 

2822 res = api.CreateEvent( 

2823 events_pb2.CreateEventReq( 

2824 title="Cancelled Reminder Test", 

2825 content="Content.", 

2826 location=events_pb2.EventLocation( 

2827 address="Near Null Island", 

2828 lat=0.1, 

2829 lng=0.2, 

2830 ), 

2831 start_time=Timestamp_from_datetime(start_time), 

2832 end_time=Timestamp_from_datetime(end_time), 

2833 timezone="UTC", 

2834 ) 

2835 ) 

2836 event_id = res.event_id 

2837 

2838 moderator.approve_event_occurrence(event_id) 

2839 process_jobs() 

2840 

2841 # User2 marks attendance 

2842 with events_session(token2) as api: 

2843 api.SetEventAttendance( 

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

2845 ) 

2846 

2847 # User1 cancels the event 

2848 with events_session(token1) as api: 

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

2850 

2851 process_jobs() 

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

2853 while push_collector.count_for_user(user2.id): 

2854 push_collector.pop_for_user(user2.id) 

2855 

2856 # Run the event reminder handler 

2857 send_event_reminders(empty_pb2.Empty()) 

2858 process_jobs() 

2859 

2860 # Verify that no reminder notification was sent for user2 

2861 with session_scope() as session: 

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

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

2864 assert len(reminder_notifs) == 0 

2865 

2866 

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

2868def test_event_reminder_not_sent_for_invisible_attendee( 

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

2870): 

2871 user1, token1 = generate_user() 

2872 user2, token2 = generate_user() 

2873 

2874 with session_scope() as session: 

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

2876 

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

2878 end_time = start_time + timedelta(hours=1) 

2879 

2880 with events_session(token1) as api: 

2881 res = api.CreateEvent( 

2882 events_pb2.CreateEventReq( 

2883 title="Invisible Attendee Reminder Test", 

2884 content="Content.", 

2885 location=events_pb2.EventLocation( 

2886 address="Near Null Island", 

2887 lat=0.1, 

2888 lng=0.2, 

2889 ), 

2890 start_time=Timestamp_from_datetime(start_time), 

2891 end_time=Timestamp_from_datetime(end_time), 

2892 timezone="UTC", 

2893 ) 

2894 ) 

2895 event_id = res.event_id 

2896 

2897 moderator.approve_event_occurrence(event_id) 

2898 process_jobs() 

2899 

2900 with events_session(token2) as api: 

2901 api.SetEventAttendance( 

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

2903 ) 

2904 

2905 with session_scope() as session: 

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

2907 

2908 send_event_reminders(empty_pb2.Empty()) 

2909 process_jobs() 

2910 

2911 with session_scope() as session: 

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

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

2914 assert len(reminder_notifs) == 0 

2915 

2916 

2917def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

2919 user1, token1 = generate_user() 

2920 user2, token2 = generate_user() 

2921 

2922 with session_scope() as session: 

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

2924 

2925 start = now() 

2926 

2927 # User1 creates event A with 3 occurrences 

2928 event_a_ids = [] 

2929 with events_session(token1) as api: 

2930 res = api.CreateEvent( 

2931 events_pb2.CreateEventReq( 

2932 title="Event A", 

2933 content="Content A.", 

2934 parent_community_id=c_id, 

2935 location=events_pb2.EventLocation( 

2936 address="Near Null Island", 

2937 lat=0.1, 

2938 lng=0.2, 

2939 ), 

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

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

2942 timezone="UTC", 

2943 ) 

2944 ) 

2945 event_a_ids.append(res.event_id) 

2946 for i in range(2): 

2947 res = api.ScheduleEvent( 

2948 events_pb2.ScheduleEventReq( 

2949 event_id=event_a_ids[-1], 

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

2951 location=events_pb2.EventLocation( 

2952 address="Near Null Island", 

2953 lat=0.1, 

2954 lng=0.2, 

2955 ), 

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

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

2958 timezone="UTC", 

2959 ) 

2960 ) 

2961 event_a_ids.append(res.event_id) 

2962 

2963 # User2 creates event B with 2 occurrences 

2964 event_b_ids = [] 

2965 with events_session(token2) as api: 

2966 res = api.CreateEvent( 

2967 events_pb2.CreateEventReq( 

2968 title="Event B", 

2969 content="Content B.", 

2970 parent_community_id=c_id, 

2971 location=events_pb2.EventLocation( 

2972 address="Near Null Island", 

2973 lat=0.1, 

2974 lng=0.2, 

2975 ), 

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

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

2978 timezone="UTC", 

2979 ) 

2980 ) 

2981 event_b_ids.append(res.event_id) 

2982 res = api.ScheduleEvent( 

2983 events_pb2.ScheduleEventReq( 

2984 event_id=event_b_ids[-1], 

2985 content="B occurrence 1", 

2986 location=events_pb2.EventLocation( 

2987 address="Near Null Island", 

2988 lat=0.1, 

2989 lng=0.2, 

2990 ), 

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

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

2993 timezone="UTC", 

2994 ) 

2995 ) 

2996 event_b_ids.append(res.event_id) 

2997 

2998 moderator.approve_event_occurrence(event_a_ids[0]) 

2999 moderator.approve_event_occurrence(event_b_ids[0]) 

3000 

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

3002 with events_session(token1) as api: 

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

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

3005 assert sorted(returned_ids) == sorted(event_a_ids) 

3006 

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

3008 with events_session(token2) as api: 

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

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

3011 assert sorted(returned_ids) == sorted(event_b_ids) 

3012 

3013 

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

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

3016 user1, token1 = generate_user() 

3017 user2, token2 = generate_user() 

3018 

3019 with session_scope() as session: 

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

3021 

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

3023 end_time = start_time + timedelta(hours=3) 

3024 

3025 with events_session(token1) as api: 

3026 res = api.CreateEvent( 

3027 events_pb2.CreateEventReq( 

3028 title="Comment Test", 

3029 content="Content.", 

3030 parent_community_id=c_id, 

3031 location=events_pb2.EventLocation( 

3032 address="Near Null Island", 

3033 lat=0.1, 

3034 lng=0.2, 

3035 ), 

3036 start_time=Timestamp_from_datetime(start_time), 

3037 end_time=Timestamp_from_datetime(end_time), 

3038 timezone="UTC", 

3039 ) 

3040 ) 

3041 event_id = res.event_id 

3042 thread_id = res.thread.thread_id 

3043 

3044 moderator.approve_event_occurrence(event_id) 

3045 process_jobs() 

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

3047 push_collector.pop_for_user(user1.id) 

3048 

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

3050 with events_session(token1) as api: 

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

3052 

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

3054 with threads_session(token2) as api: 

3055 comment_thread_id = api.PostReply( 

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

3057 ).thread_id 

3058 

3059 process_jobs() 

3060 

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

3062 comment_db_id = comment_thread_id // 10 

3063 with session_scope() as session: 

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

3065 

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

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

3068 assert len(comment_notifs) == 1 

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

3070 

3071 

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

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

3074 user1, token1 = generate_user() 

3075 user2, token2 = generate_user() 

3076 user3, token3 = generate_user() 

3077 

3078 with session_scope() as session: 

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

3080 

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

3082 end_time = start_time + timedelta(hours=3) 

3083 

3084 with events_session(token1) as api: 

3085 res = api.CreateEvent( 

3086 events_pb2.CreateEventReq( 

3087 title="Reply Test", 

3088 content="Content.", 

3089 location=events_pb2.EventLocation( 

3090 address="Near Null Island", 

3091 lat=0.1, 

3092 lng=0.2, 

3093 ), 

3094 start_time=Timestamp_from_datetime(start_time), 

3095 end_time=Timestamp_from_datetime(end_time), 

3096 timezone="UTC", 

3097 ) 

3098 ) 

3099 event_id = res.event_id 

3100 thread_id = res.thread.thread_id 

3101 

3102 moderator.approve_event_occurrence(event_id) 

3103 process_jobs() 

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

3105 push_collector.pop_for_user(user1.id) 

3106 

3107 # User2 posts a top-level comment 

3108 with threads_session(token2) as api: 

3109 comment_thread_id = api.PostReply( 

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

3111 ).thread_id 

3112 

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 # User3 replies to user2's comment (depth=2 reply) 

3118 with threads_session(token3) as api: 

3119 nested_reply_thread_id = api.PostReply( 

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

3121 ).thread_id 

3122 

3123 process_jobs() 

3124 

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

3126 nested_reply_db_id = nested_reply_thread_id // 10 

3127 with session_scope() as session: 

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

3129 

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

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

3132 assert len(reply_notifs) == 1 

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