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

1593 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-04 22:32 +0000

1import re 

2from datetime import datetime, timedelta 

3from zoneinfo import ZoneInfo 

4 

5import grpc 

6import pytest 

7from google.protobuf import empty_pb2, wrappers_pb2 

8from psycopg.types.range import TimestamptzRange 

9from sqlalchemy import select 

10from sqlalchemy.sql.expression import update 

11 

12from couchers.db import session_scope 

13from couchers.jobs.handlers import send_event_reminders 

14from couchers.models import ( 

15 BackgroundJob, 

16 BackgroundJobState, 

17 Comment, 

18 EventOccurrence, 

19 ModerationState, 

20 ModerationVisibility, 

21 Notification, 

22 NotificationDelivery, 

23 NotificationTopicAction, 

24 Reply, 

25 Upload, 

26 User, 

27) 

28from couchers.proto import editor_pb2, events_pb2, threads_pb2 

29from couchers.tasks import enforce_community_memberships 

30from couchers.utils import datetime_to_iso8601_local, now, to_aware_datetime 

31from tests.fixtures.db import generate_user 

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

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

34from tests.test_communities import create_community, create_group 

35 

36 

37@pytest.fixture(autouse=True) 

38def _(testconfig): 

39 pass 

40 

41 

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

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

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

45 

46 

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

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

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

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

51 

52 

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

54 # test cases: 

55 # can create event 

56 # cannot create event with missing details 

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

58 # can create in different timezones 

59 

60 # event creator 

61 user1, token1 = generate_user() 

62 # community moderator 

63 user2, token2 = generate_user() 

64 # third party 

65 user3, token3 = generate_user() 

66 

67 with session_scope() as session: 

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

69 

70 time_before = now() 

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

72 end_time = start_time + timedelta(hours=3) 

73 

74 # Can create an event 

75 with events_session(token1) as api: 

76 res = api.CreateEvent( 

77 events_pb2.CreateEventReq( 

78 title="Dummy Title", 

79 content="Dummy content.", 

80 photo_key=None, 

81 location=events_pb2.EventLocation( 

82 address="Near Null Island", 

83 lat=0.1, 

84 lng=0.2, 

85 ), 

86 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

87 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

88 ) 

89 ) 

90 

91 assert res.is_next 

92 assert res.title == "Dummy Title" 

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

94 assert res.content == "Dummy content." 

95 assert not res.photo_url 

96 assert res.HasField("location") 

97 assert res.location.lat == 0.1 

98 assert res.location.lng == 0.2 

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

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

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

102 assert res.creator_user_id == user1.id 

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

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

105 assert is_utc_or_gmt(res.timezone) 

106 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

107 assert res.organizer 

108 assert res.subscriber 

109 assert res.going_count == 1 

110 assert res.organizer_count == 1 

111 assert res.subscriber_count == 1 

112 assert res.owner_user_id == user1.id 

113 assert not res.owner_community_id 

114 assert not res.owner_group_id 

115 assert res.thread.thread_id 

116 assert res.can_edit 

117 assert not res.can_moderate 

118 

119 event_id = res.event_id 

120 

121 # Approve the event so other users can see it 

122 moderator.approve_event_occurrence(event_id) 

123 

124 with events_session(token2) as api: 

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

126 

127 assert res.is_next 

128 assert res.title == "Dummy Title" 

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

130 assert res.content == "Dummy content." 

131 assert not res.photo_url 

132 assert res.HasField("location") 

133 assert res.location.lat == 0.1 

134 assert res.location.lng == 0.2 

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

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

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

138 assert res.creator_user_id == user1.id 

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

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

141 assert is_utc_or_gmt(res.timezone) 

142 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

143 assert not res.organizer 

144 assert not res.subscriber 

145 assert res.going_count == 1 

146 assert res.organizer_count == 1 

147 assert res.subscriber_count == 1 

148 assert res.owner_user_id == user1.id 

149 assert not res.owner_community_id 

150 assert not res.owner_group_id 

151 assert res.thread.thread_id 

152 assert res.can_edit 

153 assert res.can_moderate 

154 

155 with events_session(token3) as api: 

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

157 

158 assert res.is_next 

159 assert res.title == "Dummy Title" 

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

161 assert res.content == "Dummy content." 

162 assert not res.photo_url 

163 assert res.HasField("location") 

164 assert res.location.lat == 0.1 

165 assert res.location.lng == 0.2 

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

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

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

169 assert res.creator_user_id == user1.id 

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

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

172 assert is_utc_or_gmt(res.timezone) 

173 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

174 assert not res.organizer 

175 assert not res.subscriber 

176 assert res.going_count == 1 

177 assert res.organizer_count == 1 

178 assert res.subscriber_count == 1 

179 assert res.owner_user_id == user1.id 

180 assert not res.owner_community_id 

181 assert not res.owner_group_id 

182 assert res.thread.thread_id 

183 assert not res.can_edit 

184 assert not res.can_moderate 

185 

186 # Failure cases 

187 with events_session(token1) as api: 

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

189 api.CreateEvent( 

190 events_pb2.CreateEventReq( 

191 # title="Dummy Title", 

192 content="Dummy content.", 

193 photo_key=None, 

194 location=events_pb2.EventLocation( 

195 address="Near Null Island", 

196 lat=0.1, 

197 lng=0.2, 

198 ), 

199 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

200 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

201 ) 

202 ) 

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

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

205 

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

207 api.CreateEvent( 

208 events_pb2.CreateEventReq( 

209 title="Dummy Title", 

210 # content="Dummy content.", 

211 photo_key=None, 

212 location=events_pb2.EventLocation( 

213 address="Near Null Island", 

214 lat=0.1, 

215 lng=0.2, 

216 ), 

217 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

218 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

219 ) 

220 ) 

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

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

223 

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

225 api.CreateEvent( 

226 events_pb2.CreateEventReq( 

227 title="Dummy Title", 

228 content="Dummy content.", 

229 photo_key="nonexistent", 

230 location=events_pb2.EventLocation( 

231 address="Near Null Island", 

232 lat=0.1, 

233 lng=0.2, 

234 ), 

235 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

236 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

237 ) 

238 ) 

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

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

241 

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

243 api.CreateEvent( 

244 events_pb2.CreateEventReq( 

245 title="Dummy Title", 

246 content="Dummy content.", 

247 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

248 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

249 ) 

250 ) 

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

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

253 

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

255 api.CreateEvent( 

256 events_pb2.CreateEventReq( 

257 title="Dummy Title", 

258 content="Dummy content.", 

259 location=events_pb2.EventLocation( 

260 address="Near Null Island", 

261 ), 

262 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

263 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

264 ) 

265 ) 

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

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

268 

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

270 api.CreateEvent( 

271 events_pb2.CreateEventReq( 

272 title="Dummy Title", 

273 content="Dummy content.", 

274 location=events_pb2.EventLocation( 

275 lat=0.1, 

276 lng=0.1, 

277 ), 

278 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

279 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

280 ) 

281 ) 

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

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

284 

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

286 api.CreateEvent( 

287 events_pb2.CreateEventReq( 

288 title="Dummy Title", 

289 content="Dummy content.", 

290 location=events_pb2.EventLocation( 

291 address="Near Null Island", 

292 lat=0.1, 

293 lng=0.2, 

294 ), 

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

296 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

297 ) 

298 ) 

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

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

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_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

313 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

314 ) 

315 ) 

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

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

318 

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

320 api.CreateEvent( 

321 events_pb2.CreateEventReq( 

322 title="Dummy Title", 

323 content="Dummy content.", 

324 location=events_pb2.EventLocation( 

325 address="Near Null Island", 

326 lat=0.1, 

327 lng=0.2, 

328 ), 

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

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

331 ) 

332 ) 

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

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

335 

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

337 api.CreateEvent( 

338 events_pb2.CreateEventReq( 

339 title="Dummy Title", 

340 content="Dummy content.", 

341 location=events_pb2.EventLocation( 

342 address="Near Null Island", 

343 lat=0.1, 

344 lng=0.2, 

345 ), 

346 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

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

348 ) 

349 ) 

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

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

352 

353 

354def test_CreateEvent_incomplete_profile(db): 

355 user1, token1 = generate_user(complete_profile=False) 

356 user2, token2 = generate_user() 

357 

358 with session_scope() as session: 

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

360 

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

362 end_time = start_time + timedelta(hours=3) 

363 

364 with events_session(token1) as api: 

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

366 api.CreateEvent( 

367 events_pb2.CreateEventReq( 

368 title="Dummy Title", 

369 content="Dummy content.", 

370 photo_key=None, 

371 location=events_pb2.EventLocation( 

372 address="Near Null Island", 

373 lat=0.1, 

374 lng=0.2, 

375 ), 

376 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

377 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

378 ) 

379 ) 

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

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

382 

383 

384def test_ScheduleEvent(db): 

385 # test cases: 

386 # can schedule a new event occurrence 

387 

388 user, token = generate_user() 

389 

390 with session_scope() as session: 

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

392 

393 time_before = now() 

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

395 end_time = start_time + timedelta(hours=3) 

396 

397 with events_session(token) as api: 

398 res = api.CreateEvent( 

399 events_pb2.CreateEventReq( 

400 title="Dummy Title", 

401 content="Dummy content.", 

402 parent_community_id=c_id, 

403 location=events_pb2.EventLocation( 

404 address="Near Null Island", 

405 lat=0.1, 

406 lng=0.2, 

407 ), 

408 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

409 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

410 ) 

411 ) 

412 

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

414 new_end_time = new_start_time + timedelta(hours=2) 

415 

416 res = api.ScheduleEvent( 

417 events_pb2.ScheduleEventReq( 

418 event_id=res.event_id, 

419 content="New event occurrence", 

420 location=events_pb2.EventLocation( 

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

422 lat=0.3, 

423 lng=0.2, 

424 ), 

425 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time), 

426 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time), 

427 ) 

428 ) 

429 

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

431 

432 assert not res.is_next 

433 assert res.title == "Dummy Title" 

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

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

436 assert not res.photo_url 

437 assert res.HasField("location") 

438 assert res.location.lat == 0.3 

439 assert res.location.lng == 0.2 

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

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

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

443 assert res.creator_user_id == user.id 

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

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

446 assert is_utc_or_gmt(res.timezone) 

447 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

448 assert res.organizer 

449 assert res.subscriber 

450 assert res.going_count == 1 

451 assert res.organizer_count == 1 

452 assert res.subscriber_count == 1 

453 assert res.owner_user_id == user.id 

454 assert not res.owner_community_id 

455 assert not res.owner_group_id 

456 assert res.thread.thread_id 

457 assert res.can_edit 

458 assert res.can_moderate 

459 

460 

461def test_cannot_overlap_occurrences_schedule(db): 

462 user, token = generate_user() 

463 

464 with session_scope() as session: 

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

466 

467 start = now() 

468 

469 with events_session(token) as api: 

470 res = api.CreateEvent( 

471 events_pb2.CreateEventReq( 

472 title="Dummy Title", 

473 content="Dummy content.", 

474 parent_community_id=c_id, 

475 location=events_pb2.EventLocation( 

476 address="Near Null Island", 

477 lat=0.1, 

478 lng=0.2, 

479 ), 

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

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

482 ) 

483 ) 

484 

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

486 api.ScheduleEvent( 

487 events_pb2.ScheduleEventReq( 

488 event_id=res.event_id, 

489 content="New event occurrence", 

490 location=events_pb2.EventLocation( 

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

492 lat=0.3, 

493 lng=0.2, 

494 ), 

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

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

497 ) 

498 ) 

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

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

501 

502 

503def test_cannot_overlap_occurrences_update(db): 

504 user, token = generate_user() 

505 

506 with session_scope() as session: 

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

508 

509 start = now() 

510 

511 with events_session(token) as api: 

512 res = api.CreateEvent( 

513 events_pb2.CreateEventReq( 

514 title="Dummy Title", 

515 content="Dummy content.", 

516 parent_community_id=c_id, 

517 location=events_pb2.EventLocation( 

518 address="Near Null Island", 

519 lat=0.1, 

520 lng=0.2, 

521 ), 

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

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

524 ) 

525 ) 

526 

527 event_id = api.ScheduleEvent( 

528 events_pb2.ScheduleEventReq( 

529 event_id=res.event_id, 

530 content="New event occurrence", 

531 location=events_pb2.EventLocation( 

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

533 lat=0.3, 

534 lng=0.2, 

535 ), 

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

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

538 ) 

539 ).event_id 

540 

541 # can overlap with this current existing occurrence 

542 api.UpdateEvent( 

543 events_pb2.UpdateEventReq( 

544 event_id=event_id, 

545 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

547 ), 

548 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

550 ), 

551 ) 

552 ) 

553 

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

555 api.UpdateEvent( 

556 events_pb2.UpdateEventReq( 

557 event_id=event_id, 

558 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

560 ), 

561 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

563 ), 

564 ) 

565 ) 

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

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

568 

569 

570def test_UpdateEvent_single(db, moderator: Moderator): 

571 # test cases: 

572 # owner can update 

573 # community owner can update 

574 # notifies attendees 

575 

576 # event creator 

577 user1, token1 = generate_user() 

578 # community moderator 

579 user2, token2 = generate_user() 

580 # third parties 

581 user3, token3 = generate_user() 

582 user4, token4 = generate_user() 

583 user5, token5 = generate_user() 

584 user6, token6 = generate_user() 

585 

586 with session_scope() as session: 

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

588 

589 time_before = now() 

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

591 end_time = start_time + timedelta(hours=3) 

592 

593 with events_session(token1) as api: 

594 res = api.CreateEvent( 

595 events_pb2.CreateEventReq( 

596 title="Dummy Title", 

597 content="Dummy content.", 

598 parent_community_id=c_id, 

599 location=events_pb2.EventLocation( 

600 address="Near Null Island", 

601 lat=0.1, 

602 lng=0.2, 

603 ), 

604 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

605 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

606 ) 

607 ) 

608 

609 event_id = res.event_id 

610 

611 moderator.approve_event_occurrence(event_id) 

612 

613 with events_session(token4) as api: 

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

615 

616 with events_session(token5) as api: 

617 api.SetEventAttendance( 

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

619 ) 

620 

621 with events_session(token6) as api: 

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

623 

624 time_before_update = now() 

625 

626 with events_session(token1) as api: 

627 res = api.UpdateEvent( 

628 events_pb2.UpdateEventReq( 

629 event_id=event_id, 

630 ) 

631 ) 

632 

633 with events_session(token1) as api: 

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

635 

636 assert res.is_next 

637 assert res.title == "Dummy Title" 

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

639 assert res.content == "Dummy content." 

640 assert not res.photo_url 

641 assert res.HasField("location") 

642 assert res.location.lat == 0.1 

643 assert res.location.lng == 0.2 

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

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

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

647 assert res.creator_user_id == user1.id 

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

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

650 assert is_utc_or_gmt(res.timezone) 

651 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

652 assert res.organizer 

653 assert res.subscriber 

654 assert res.going_count == 2 

655 assert res.organizer_count == 1 

656 assert res.subscriber_count == 3 

657 assert res.owner_user_id == user1.id 

658 assert not res.owner_community_id 

659 assert not res.owner_group_id 

660 assert res.thread.thread_id 

661 assert res.can_edit 

662 assert not res.can_moderate 

663 

664 with events_session(token2) as api: 

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

666 

667 assert res.is_next 

668 assert res.title == "Dummy Title" 

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

670 assert res.content == "Dummy content." 

671 assert not res.photo_url 

672 assert res.HasField("location") 

673 assert res.location.lat == 0.1 

674 assert res.location.lng == 0.2 

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

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

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

678 assert res.creator_user_id == user1.id 

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

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

681 assert is_utc_or_gmt(res.timezone) 

682 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

683 assert not res.organizer 

684 assert not res.subscriber 

685 assert res.going_count == 2 

686 assert res.organizer_count == 1 

687 assert res.subscriber_count == 3 

688 assert res.owner_user_id == user1.id 

689 assert not res.owner_community_id 

690 assert not res.owner_group_id 

691 assert res.thread.thread_id 

692 assert res.can_edit 

693 assert res.can_moderate 

694 

695 with events_session(token3) as api: 

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

697 

698 assert res.is_next 

699 assert res.title == "Dummy Title" 

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

701 assert res.content == "Dummy content." 

702 assert not res.photo_url 

703 assert res.HasField("location") 

704 assert res.location.lat == 0.1 

705 assert res.location.lng == 0.2 

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

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

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

709 assert res.creator_user_id == user1.id 

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

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

712 assert is_utc_or_gmt(res.timezone) 

713 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

714 assert not res.organizer 

715 assert not res.subscriber 

716 assert res.going_count == 2 

717 assert res.organizer_count == 1 

718 assert res.subscriber_count == 3 

719 assert res.owner_user_id == user1.id 

720 assert not res.owner_community_id 

721 assert not res.owner_group_id 

722 assert res.thread.thread_id 

723 assert not res.can_edit 

724 assert not res.can_moderate 

725 

726 with events_session(token1) as api: 

727 res = api.UpdateEvent( 

728 events_pb2.UpdateEventReq( 

729 event_id=event_id, 

730 location=events_pb2.EventLocation( 

731 address="Nearer Null Island", 

732 lat=0.01, 

733 lng=0.02, 

734 ), 

735 ) 

736 ) 

737 

738 with events_session(token3) as api: 

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

740 

741 assert res.HasField("location") 

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

743 assert res.location.lat == 0.01 

744 assert res.location.lng == 0.02 

745 

746 

747def test_UpdateEvent_all(db, moderator: Moderator): 

748 # event creator 

749 user1, token1 = generate_user() 

750 # community moderator 

751 user2, token2 = generate_user() 

752 # third parties 

753 user3, token3 = generate_user() 

754 user4, token4 = generate_user() 

755 user5, token5 = generate_user() 

756 user6, token6 = generate_user() 

757 

758 with session_scope() as session: 

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

760 

761 time_before = now() 

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

763 end_time = start_time + timedelta(hours=1.5) 

764 

765 event_ids = [] 

766 

767 with events_session(token1) as api: 

768 res = api.CreateEvent( 

769 events_pb2.CreateEventReq( 

770 title="Dummy Title", 

771 content="0th occurrence", 

772 location=events_pb2.EventLocation( 

773 address="Near Null Island", 

774 lat=0.1, 

775 lng=0.2, 

776 ), 

777 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

778 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

779 ) 

780 ) 

781 

782 event_id = res.event_id 

783 event_ids.append(event_id) 

784 

785 moderator.approve_event_occurrence(event_id) 

786 

787 with events_session(token4) as api: 

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

789 

790 with events_session(token5) as api: 

791 api.SetEventAttendance( 

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

793 ) 

794 

795 with events_session(token6) as api: 

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

797 

798 with events_session(token1) as api: 

799 for i in range(5): 

800 res = api.ScheduleEvent( 

801 events_pb2.ScheduleEventReq( 

802 event_id=event_ids[-1], 

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

804 location=events_pb2.EventLocation( 

805 address="Near Null Island", 

806 lat=0.1, 

807 lng=0.2, 

808 ), 

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

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

811 ) 

812 ) 

813 

814 event_ids.append(res.event_id) 

815 

816 # Approve all scheduled occurrences 

817 for eid in event_ids[1:]: 

818 moderator.approve_event_occurrence(eid) 

819 

820 updated_event_id = event_ids[3] 

821 

822 time_before_update = now() 

823 

824 with events_session(token1) as api: 

825 res = api.UpdateEvent( 

826 events_pb2.UpdateEventReq( 

827 event_id=updated_event_id, 

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

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

830 location=events_pb2.EventLocation( 

831 address="Not so near Null Island", 

832 lat=0.2, 

833 lng=0.2, 

834 ), 

835 update_all_future=True, 

836 ) 

837 ) 

838 

839 time_after_update = now() 

840 

841 with events_session(token2) as api: 

842 for i in range(3): 

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

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

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

846 

847 for i in range(3, 6): 

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

849 assert res.content == "New content." 

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

851 

852 

853def test_GetEvent(db, moderator: Moderator): 

854 # event creator 

855 user1, token1 = generate_user() 

856 # community moderator 

857 user2, token2 = generate_user() 

858 # third parties 

859 user3, token3 = generate_user() 

860 user4, token4 = generate_user() 

861 user5, token5 = generate_user() 

862 user6, token6 = generate_user() 

863 

864 with session_scope() as session: 

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

866 

867 time_before = now() 

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

869 end_time = start_time + timedelta(hours=3) 

870 

871 with events_session(token1) as api: 

872 # in person event 

873 res = api.CreateEvent( 

874 events_pb2.CreateEventReq( 

875 title="Dummy Title", 

876 content="Dummy content.", 

877 location=events_pb2.EventLocation( 

878 address="Near Null Island", 

879 lat=0.1, 

880 lng=0.2, 

881 ), 

882 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

883 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

884 ) 

885 ) 

886 

887 event_id = res.event_id 

888 

889 moderator.approve_event_occurrence(event_id) 

890 

891 with events_session(token4) as api: 

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

893 

894 with events_session(token5) as api: 

895 api.SetEventAttendance( 

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

897 ) 

898 

899 with events_session(token6) as api: 

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

901 

902 with events_session(token1) as api: 

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

904 

905 assert res.is_next 

906 assert res.title == "Dummy Title" 

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

908 assert res.content == "Dummy content." 

909 assert not res.photo_url 

910 assert res.HasField("location") 

911 assert res.location.lat == 0.1 

912 assert res.location.lng == 0.2 

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

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

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

916 assert res.creator_user_id == user1.id 

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

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

919 assert is_utc_or_gmt(res.timezone) 

920 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

921 assert res.organizer 

922 assert res.subscriber 

923 assert res.going_count == 2 

924 assert res.organizer_count == 1 

925 assert res.subscriber_count == 3 

926 assert res.owner_user_id == user1.id 

927 assert not res.owner_community_id 

928 assert not res.owner_group_id 

929 assert res.thread.thread_id 

930 assert res.can_edit 

931 assert not res.can_moderate 

932 

933 with events_session(token2) as api: 

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

935 

936 assert res.is_next 

937 assert res.title == "Dummy Title" 

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

939 assert res.content == "Dummy content." 

940 assert not res.photo_url 

941 assert res.HasField("location") 

942 assert res.location.lat == 0.1 

943 assert res.location.lng == 0.2 

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

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

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

947 assert res.creator_user_id == user1.id 

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

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

950 assert is_utc_or_gmt(res.timezone) 

951 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

952 assert not res.organizer 

953 assert not res.subscriber 

954 assert res.going_count == 2 

955 assert res.organizer_count == 1 

956 assert res.subscriber_count == 3 

957 assert res.owner_user_id == user1.id 

958 assert not res.owner_community_id 

959 assert not res.owner_group_id 

960 assert res.thread.thread_id 

961 assert res.can_edit 

962 assert res.can_moderate 

963 

964 with events_session(token3) as api: 

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

966 

967 assert res.is_next 

968 assert res.title == "Dummy Title" 

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

970 assert res.content == "Dummy content." 

971 assert not res.photo_url 

972 assert res.HasField("location") 

973 assert res.location.lat == 0.1 

974 assert res.location.lng == 0.2 

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

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

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

978 assert res.creator_user_id == user1.id 

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

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

981 assert is_utc_or_gmt(res.timezone) 

982 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

983 assert not res.organizer 

984 assert not res.subscriber 

985 assert res.going_count == 2 

986 assert res.organizer_count == 1 

987 assert res.subscriber_count == 3 

988 assert res.owner_user_id == user1.id 

989 assert not res.owner_community_id 

990 assert not res.owner_group_id 

991 assert res.thread.thread_id 

992 assert not res.can_edit 

993 assert not res.can_moderate 

994 

995 

996def test_CancelEvent(db, moderator: Moderator): 

997 # event creator 

998 user1, token1 = generate_user() 

999 # community moderator 

1000 user2, token2 = generate_user() 

1001 # third parties 

1002 user3, token3 = generate_user() 

1003 user4, token4 = generate_user() 

1004 user5, token5 = generate_user() 

1005 user6, token6 = generate_user() 

1006 

1007 with session_scope() as session: 

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

1009 

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

1011 end_time = start_time + timedelta(hours=3) 

1012 

1013 with events_session(token1) as api: 

1014 res = api.CreateEvent( 

1015 events_pb2.CreateEventReq( 

1016 title="Dummy Title", 

1017 content="Dummy content.", 

1018 location=events_pb2.EventLocation( 

1019 address="Near Null Island", 

1020 lat=0.1, 

1021 lng=0.2, 

1022 ), 

1023 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1024 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1025 ) 

1026 ) 

1027 

1028 event_id = res.event_id 

1029 

1030 moderator.approve_event_occurrence(event_id) 

1031 

1032 with events_session(token4) as api: 

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

1034 

1035 with events_session(token5) as api: 

1036 api.SetEventAttendance( 

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

1038 ) 

1039 

1040 with events_session(token6) as api: 

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

1042 

1043 with events_session(token1) as api: 

1044 res = api.CancelEvent( 

1045 events_pb2.CancelEventReq( 

1046 event_id=event_id, 

1047 ) 

1048 ) 

1049 

1050 with events_session(token1) as api: 

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

1052 assert res.is_cancelled 

1053 

1054 with events_session(token1) as api: 

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

1056 api.UpdateEvent( 

1057 events_pb2.UpdateEventReq( 

1058 event_id=event_id, 

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

1060 ) 

1061 ) 

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

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

1064 

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

1066 api.InviteEventOrganizer( 

1067 events_pb2.InviteEventOrganizerReq( 

1068 event_id=event_id, 

1069 user_id=user3.id, 

1070 ) 

1071 ) 

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

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

1074 

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

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

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

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

1079 

1080 with events_session(token3) as api: 

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

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

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

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

1085 

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

1087 api.SetEventAttendance( 

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

1089 ) 

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

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

1092 

1093 with events_session(token1) as api: 

1094 for include_cancelled in [True, False]: 

1095 res = api.ListEventOccurrences( 

1096 events_pb2.ListEventOccurrencesReq( 

1097 event_id=event_id, 

1098 include_cancelled=include_cancelled, 

1099 ) 

1100 ) 

1101 if include_cancelled: 

1102 assert len(res.events) > 0 

1103 else: 

1104 assert len(res.events) == 0 

1105 

1106 res = api.ListMyEvents( 

1107 events_pb2.ListMyEventsReq( 

1108 include_cancelled=include_cancelled, 

1109 ) 

1110 ) 

1111 if include_cancelled: 

1112 assert len(res.events) > 0 

1113 else: 

1114 assert len(res.events) == 0 

1115 

1116 

1117def test_ListEventAttendees(db, moderator: Moderator): 

1118 # event creator 

1119 user1, token1 = generate_user() 

1120 # others 

1121 user2, token2 = generate_user() 

1122 user3, token3 = generate_user() 

1123 user4, token4 = generate_user() 

1124 user5, token5 = generate_user() 

1125 user6, token6 = generate_user() 

1126 

1127 with session_scope() as session: 

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

1129 

1130 with events_session(token1) as api: 

1131 event_id = api.CreateEvent( 

1132 events_pb2.CreateEventReq( 

1133 title="Dummy Title", 

1134 content="Dummy content.", 

1135 location=events_pb2.EventLocation( 

1136 address="Near Null Island", 

1137 lat=0.1, 

1138 lng=0.2, 

1139 ), 

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

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

1142 ) 

1143 ).event_id 

1144 

1145 moderator.approve_event_occurrence(event_id) 

1146 

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

1148 with events_session(token) as api: 

1149 api.SetEventAttendance( 

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

1151 ) 

1152 

1153 with events_session(token6) as api: 

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

1155 

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

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

1158 

1159 res = api.ListEventAttendees( 

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

1161 ) 

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

1163 

1164 res = api.ListEventAttendees( 

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

1166 ) 

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

1168 assert not res.next_page_token 

1169 

1170 

1171def test_ListEventSubscribers(db, moderator: Moderator): 

1172 # event creator 

1173 user1, token1 = generate_user() 

1174 # others 

1175 user2, token2 = generate_user() 

1176 user3, token3 = generate_user() 

1177 user4, token4 = generate_user() 

1178 user5, token5 = generate_user() 

1179 user6, token6 = generate_user() 

1180 

1181 with session_scope() as session: 

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

1183 

1184 with events_session(token1) as api: 

1185 event_id = api.CreateEvent( 

1186 events_pb2.CreateEventReq( 

1187 title="Dummy Title", 

1188 content="Dummy content.", 

1189 location=events_pb2.EventLocation( 

1190 address="Near Null Island", 

1191 lat=0.1, 

1192 lng=0.2, 

1193 ), 

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

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

1196 ) 

1197 ).event_id 

1198 

1199 moderator.approve_event_occurrence(event_id) 

1200 

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

1202 with events_session(token) as api: 

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

1204 

1205 with events_session(token6) as api: 

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

1207 

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

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

1210 

1211 res = api.ListEventSubscribers( 

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

1213 ) 

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

1215 

1216 res = api.ListEventSubscribers( 

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

1218 ) 

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

1220 assert not res.next_page_token 

1221 

1222 

1223def test_ListEventOrganizers(db, moderator: Moderator): 

1224 # event creator 

1225 user1, token1 = generate_user() 

1226 # others 

1227 user2, token2 = generate_user() 

1228 user3, token3 = generate_user() 

1229 user4, token4 = generate_user() 

1230 user5, token5 = generate_user() 

1231 user6, token6 = generate_user() 

1232 

1233 with session_scope() as session: 

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

1235 

1236 with events_session(token1) as api: 

1237 event_id = api.CreateEvent( 

1238 events_pb2.CreateEventReq( 

1239 title="Dummy Title", 

1240 content="Dummy content.", 

1241 location=events_pb2.EventLocation( 

1242 address="Near Null Island", 

1243 lat=0.1, 

1244 lng=0.2, 

1245 ), 

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

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

1248 ) 

1249 ).event_id 

1250 

1251 moderator.approve_event_occurrence(event_id) 

1252 

1253 with events_session(token1) as api: 

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

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

1256 

1257 with events_session(token6) as api: 

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

1259 

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

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

1262 

1263 res = api.ListEventOrganizers( 

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

1265 ) 

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

1267 

1268 res = api.ListEventOrganizers( 

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

1270 ) 

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

1272 assert not res.next_page_token 

1273 

1274 

1275def test_TransferEvent(db): 

1276 user1, token1 = generate_user() 

1277 user2, token2 = generate_user() 

1278 user3, token3 = generate_user() 

1279 user4, token4 = generate_user() 

1280 

1281 with session_scope() as session: 

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

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

1284 c_id = c.id 

1285 h_id = h.id 

1286 

1287 with events_session(token1) as api: 

1288 event_id = api.CreateEvent( 

1289 events_pb2.CreateEventReq( 

1290 title="Dummy Title", 

1291 content="Dummy content.", 

1292 location=events_pb2.EventLocation( 

1293 address="Near Null Island", 

1294 lat=0.1, 

1295 lng=0.2, 

1296 ), 

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

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

1299 ) 

1300 ).event_id 

1301 

1302 api.TransferEvent( 

1303 events_pb2.TransferEventReq( 

1304 event_id=event_id, 

1305 new_owner_community_id=c_id, 

1306 ) 

1307 ) 

1308 

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

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

1311 

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

1313 api.TransferEvent( 

1314 events_pb2.TransferEventReq( 

1315 event_id=event_id, 

1316 new_owner_group_id=h_id, 

1317 ) 

1318 ) 

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

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

1321 

1322 event_id = api.CreateEvent( 

1323 events_pb2.CreateEventReq( 

1324 title="Dummy Title", 

1325 content="Dummy content.", 

1326 location=events_pb2.EventLocation( 

1327 address="Near Null Island", 

1328 lat=0.1, 

1329 lng=0.2, 

1330 ), 

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

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

1333 ) 

1334 ).event_id 

1335 

1336 api.TransferEvent( 

1337 events_pb2.TransferEventReq( 

1338 event_id=event_id, 

1339 new_owner_group_id=h_id, 

1340 ) 

1341 ) 

1342 

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

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

1345 

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

1347 api.TransferEvent( 

1348 events_pb2.TransferEventReq( 

1349 event_id=event_id, 

1350 new_owner_community_id=c_id, 

1351 ) 

1352 ) 

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

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

1355 

1356 

1357def test_SetEventSubscription(db, moderator: Moderator): 

1358 user1, token1 = generate_user() 

1359 user2, token2 = generate_user() 

1360 

1361 with session_scope() as session: 

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

1363 

1364 with events_session(token1) as api: 

1365 event_id = api.CreateEvent( 

1366 events_pb2.CreateEventReq( 

1367 title="Dummy Title", 

1368 content="Dummy content.", 

1369 location=events_pb2.EventLocation( 

1370 address="Near Null Island", 

1371 lat=0.1, 

1372 lng=0.2, 

1373 ), 

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

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

1376 ) 

1377 ).event_id 

1378 

1379 moderator.approve_event_occurrence(event_id) 

1380 

1381 with events_session(token2) as api: 

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

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

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

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

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

1387 

1388 

1389def test_SetEventAttendance(db, moderator: Moderator): 

1390 user1, token1 = generate_user() 

1391 user2, token2 = generate_user() 

1392 

1393 with session_scope() as session: 

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

1395 

1396 with events_session(token1) as api: 

1397 event_id = api.CreateEvent( 

1398 events_pb2.CreateEventReq( 

1399 title="Dummy Title", 

1400 content="Dummy content.", 

1401 location=events_pb2.EventLocation( 

1402 address="Near Null Island", 

1403 lat=0.1, 

1404 lng=0.2, 

1405 ), 

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

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

1408 ) 

1409 ).event_id 

1410 

1411 moderator.approve_event_occurrence(event_id) 

1412 

1413 with events_session(token2) as api: 

1414 assert ( 

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

1416 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1417 ) 

1418 api.SetEventAttendance( 

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

1420 ) 

1421 assert ( 

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

1423 == events_pb2.ATTENDANCE_STATE_GOING 

1424 ) 

1425 api.SetEventAttendance( 

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

1427 ) 

1428 assert ( 

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

1430 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1431 ) 

1432 

1433 

1434def test_InviteEventOrganizer(db, moderator: Moderator): 

1435 user1, token1 = generate_user() 

1436 user2, token2 = generate_user() 

1437 

1438 with session_scope() as session: 

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

1440 

1441 with events_session(token1) as api: 

1442 event_id = api.CreateEvent( 

1443 events_pb2.CreateEventReq( 

1444 title="Dummy Title", 

1445 content="Dummy content.", 

1446 location=events_pb2.EventLocation( 

1447 address="Near Null Island", 

1448 lat=0.1, 

1449 lng=0.2, 

1450 ), 

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

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

1453 ) 

1454 ).event_id 

1455 

1456 moderator.approve_event_occurrence(event_id) 

1457 

1458 with events_session(token2) as api: 

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

1460 

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

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

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

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

1465 

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

1467 

1468 with events_session(token1) as api: 

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

1470 

1471 with events_session(token2) as api: 

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

1473 

1474 

1475def test_ListEventOccurrences(db): 

1476 user1, token1 = generate_user() 

1477 user2, token2 = generate_user() 

1478 user3, token3 = generate_user() 

1479 

1480 with session_scope() as session: 

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

1482 

1483 start = now() 

1484 

1485 event_ids = [] 

1486 

1487 with events_session(token1) as api: 

1488 res = api.CreateEvent( 

1489 events_pb2.CreateEventReq( 

1490 title="First occurrence", 

1491 content="Dummy content.", 

1492 parent_community_id=c_id, 

1493 location=events_pb2.EventLocation( 

1494 address="Near Null Island", 

1495 lat=0.1, 

1496 lng=0.2, 

1497 ), 

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

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

1500 ) 

1501 ) 

1502 

1503 event_ids.append(res.event_id) 

1504 

1505 for i in range(5): 

1506 res = api.ScheduleEvent( 

1507 events_pb2.ScheduleEventReq( 

1508 event_id=event_ids[-1], 

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

1510 location=events_pb2.EventLocation( 

1511 address="Near Null Island", 

1512 lat=0.1, 

1513 lng=0.2, 

1514 ), 

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

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

1517 ) 

1518 ) 

1519 

1520 event_ids.append(res.event_id) 

1521 

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

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

1524 

1525 res = api.ListEventOccurrences( 

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

1527 ) 

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

1529 

1530 res = api.ListEventOccurrences( 

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

1532 ) 

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

1534 assert not res.next_page_token 

1535 

1536 

1537def test_ListMyEvents(db, moderator: Moderator): 

1538 user1, token1 = generate_user() 

1539 user2, token2 = generate_user() 

1540 user3, token3 = generate_user() 

1541 user4, token4 = generate_user() 

1542 user5, token5 = generate_user() 

1543 

1544 with session_scope() as session: 

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

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

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

1548 c_id = global_community.id 

1549 macroregion_community = create_community( 

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

1551 ) 

1552 region_community = create_community( 

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

1554 ) 

1555 subregion_community = create_community( 

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

1557 ) 

1558 c2_id = subregion_community.id 

1559 

1560 start = now() 

1561 

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

1563 return events_pb2.CreateEventReq( 

1564 title="Dummy Title", 

1565 content="Dummy content.", 

1566 location=events_pb2.EventLocation( 

1567 address="Near Null Island", 

1568 lat=0.1, 

1569 lng=0.2, 

1570 ), 

1571 parent_community_id=community_id, 

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

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

1574 ) 

1575 

1576 with events_session(token1) as api: 

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

1578 

1579 moderator.approve_event_occurrence(e2) 

1580 

1581 with events_session(token2) as api: 

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

1583 

1584 moderator.approve_event_occurrence(e1) 

1585 

1586 with events_session(token1) as api: 

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

1588 

1589 moderator.approve_event_occurrence(e3) 

1590 

1591 with events_session(token2) as api: 

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

1593 

1594 moderator.approve_event_occurrence(e5) 

1595 

1596 with events_session(token3) as api: 

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

1598 

1599 moderator.approve_event_occurrence(e4) 

1600 

1601 with events_session(token4) as api: 

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

1603 

1604 moderator.approve_event_occurrence(e6) 

1605 

1606 with events_session(token1) as api: 

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

1608 

1609 with events_session(token1) as api: 

1610 api.SetEventAttendance( 

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

1612 ) 

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

1614 

1615 with events_session(token2) as api: 

1616 api.SetEventAttendance( 

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

1618 ) 

1619 

1620 with events_session(token3) as api: 

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

1622 

1623 with events_session(token1) as api: 

1624 # test pagination with token first 

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

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

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

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

1629 assert not res.next_page_token 

1630 

1631 res = api.ListMyEvents( 

1632 events_pb2.ListMyEventsReq( 

1633 subscribed=True, 

1634 attending=True, 

1635 organizing=True, 

1636 ) 

1637 ) 

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

1639 

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

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

1642 

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

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

1645 

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

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

1648 

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

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

1651 

1652 with events_session(token1) as api: 

1653 # Test pagination with page_number and verify total_items 

1654 res = api.ListMyEvents( 

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

1656 ) 

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

1658 assert res.total_items == 4 

1659 

1660 res = api.ListMyEvents( 

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

1662 ) 

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

1664 assert res.total_items == 4 

1665 

1666 # Verify no more pages 

1667 res = api.ListMyEvents( 

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

1669 ) 

1670 assert not res.events 

1671 assert res.total_items == 4 

1672 

1673 with events_session(token2) as api: 

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

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

1676 

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

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

1679 

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

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

1682 

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

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

1685 

1686 with events_session(token3) as api: 

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

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

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

1690 

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

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

1693 

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

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

1696 

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

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

1699 

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

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

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

1703 

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

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

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

1707 

1708 # my_communities_exclude_global works independently of my_communities flag 

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

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

1711 

1712 # my_communities_exclude_global filters organizing results too 

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

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

1715 

1716 # my_communities_exclude_global filters subscribed results too 

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

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

1719 

1720 with events_session(token5) as api: 

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

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

1723 

1724 

1725def _paginate_my_events(api, page_size: int) -> list[int]: 

1726 event_ids = [] 

1727 page_token = "" 

1728 for _ in range(10): 1728 ↛ 1734line 1728 didn't jump to line 1734 because the loop on line 1728 didn't complete

1729 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=page_size, page_token=page_token)) 

1730 event_ids += [event.event_id for event in res.events] 

1731 page_token = res.next_page_token 

1732 if not page_token: 

1733 return event_ids 

1734 raise AssertionError("pagination did not terminate") 

1735 

1736 

1737def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator): 

1738 user, token = generate_user() 

1739 

1740 with session_scope() as session: 

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

1742 

1743 start = now() 

1744 

1745 def new_event(start_offset: timedelta, duration: timedelta) -> events_pb2.CreateEventReq: 

1746 return events_pb2.CreateEventReq( 

1747 title="Dummy Title", 

1748 content="Dummy content.", 

1749 location=events_pb2.EventLocation( 

1750 address="Near Null Island", 

1751 lat=0.1, 

1752 lng=0.2, 

1753 ), 

1754 parent_community_id=c_id, 

1755 start_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset), 

1756 end_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset + duration), 

1757 ) 

1758 

1759 with events_session(token) as api: 

1760 # a multi-day event overlapping all the short events below: it ends last but starts first, 

1761 # so an end time based cursor would repeat it on every page and skip the short events 

1762 long_event = api.CreateEvent(new_event(timedelta(hours=1), timedelta(days=3))).event_id 

1763 short_events = [ 

1764 api.CreateEvent(new_event(timedelta(hours=2 + i), timedelta(hours=1))).event_id for i in range(4) 

1765 ] 

1766 

1767 for event_id in [long_event, *short_events]: 

1768 moderator.approve_event_occurrence(event_id) 

1769 

1770 with events_session(token) as api: 

1771 assert _paginate_my_events(api, page_size=2) == [long_event, *short_events] 

1772 

1773 

1774def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator): 

1775 user, token = generate_user() 

1776 

1777 with session_scope() as session: 

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

1779 

1780 start = now() 

1781 

1782 with events_session(token) as api: 

1783 event_ids = [ 

1784 api.CreateEvent( 

1785 events_pb2.CreateEventReq( 

1786 title="Dummy Title", 

1787 content="Dummy content.", 

1788 location=events_pb2.EventLocation( 

1789 address="Near Null Island", 

1790 lat=0.1, 

1791 lng=0.2, 

1792 ), 

1793 parent_community_id=c_id, 

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

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

1796 ) 

1797 ).event_id 

1798 for _ in range(5) 

1799 ] 

1800 

1801 for event_id in event_ids: 

1802 moderator.approve_event_occurrence(event_id) 

1803 

1804 with events_session(token) as api: 

1805 assert _paginate_my_events(api, page_size=2) == event_ids 

1806 

1807 

1808def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1809 user1, token1 = generate_user() 

1810 user2, token2 = generate_user() 

1811 

1812 with session_scope() as session: 

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

1814 c_id = c.id 

1815 

1816 start = now() 

1817 

1818 def make_event(hours): 

1819 return events_pb2.CreateEventReq( 

1820 title="Test Event", 

1821 content="Test content.", 

1822 location=events_pb2.EventLocation( 

1823 address="Near Null Island", 

1824 lat=0.1, 

1825 lng=0.2, 

1826 ), 

1827 parent_community_id=c_id, 

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

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

1830 ) 

1831 

1832 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1833 with events_session(token1) as api: 

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

1835 

1836 with events_session(token2) as api: 

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

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

1839 # e_both: user1 will be both organizer and attendee 

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

1841 

1842 moderator.approve_event_occurrence(e_own) 

1843 moderator.approve_event_occurrence(e_attending) 

1844 moderator.approve_event_occurrence(e_community_only) 

1845 moderator.approve_event_occurrence(e_both) 

1846 

1847 # invite user1 as organizer of e_both 

1848 with events_session(token2) as api: 

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

1850 

1851 # user1 RSVPs to e_attending and e_both 

1852 with events_session(token1) as api: 

1853 api.SetEventAttendance( 

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

1855 ) 

1856 api.SetEventAttendance( 

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

1858 ) 

1859 

1860 with events_session(token1) as api: 

1861 # baseline: all four community events visible 

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

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

1864 

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

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

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

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

1869 

1870 # exclude_attending with attending=True: invalid combination 

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

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

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

1874 

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

1876 with events_session(token2) as api: 

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

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

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

1880 

1881 

1882def test_RemoveEventOrganizer(db, moderator: Moderator): 

1883 user1, token1 = generate_user() 

1884 user2, token2 = generate_user() 

1885 

1886 with session_scope() as session: 

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

1888 

1889 with events_session(token1) as api: 

1890 event_id = api.CreateEvent( 

1891 events_pb2.CreateEventReq( 

1892 title="Dummy Title", 

1893 content="Dummy content.", 

1894 location=events_pb2.EventLocation( 

1895 address="Near Null Island", 

1896 lat=0.1, 

1897 lng=0.2, 

1898 ), 

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

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

1901 ) 

1902 ).event_id 

1903 

1904 moderator.approve_event_occurrence(event_id) 

1905 

1906 with events_session(token2) as api: 

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

1908 

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

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

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

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

1913 

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

1915 

1916 with events_session(token1) as api: 

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

1918 

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

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

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

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

1923 

1924 with events_session(token2) as api: 

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

1926 assert res.organizer 

1927 assert res.organizer_count == 2 

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

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

1930 

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

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

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

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

1935 

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

1937 assert not res.organizer 

1938 assert res.organizer_count == 1 

1939 

1940 # Test that event owner can remove co-organizers 

1941 with events_session(token1) as api: 

1942 # Add user2 back as organizer 

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

1944 

1945 # Verify user2 is now an organizer 

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

1947 assert res.organizer_count == 2 

1948 

1949 # Event owner can remove co-organizer 

1950 api.RemoveEventOrganizer( 

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

1952 ) 

1953 

1954 # Verify user2 is no longer an organizer 

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

1956 assert res.organizer_count == 1 

1957 

1958 # Test that non-organizers cannot remove other organizers 

1959 with events_session(token2) as api: 

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

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

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

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

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

1965 

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

1967 with events_session(token1) as api: 

1968 # Add user2 back as organizer 

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

1970 

1971 

1972def test_ListEventAttendees_regression(db): 

1973 # see issue #1617: 

1974 # 

1975 # 1. Create an event 

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

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

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

1979 # 

1980 # **Expected behaviour** 

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

1982 # 

1983 # **Actual/current behaviour** 

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

1985 

1986 user1, token1 = generate_user() 

1987 user2, token2 = generate_user() 

1988 user3, token3 = generate_user() 

1989 user4, token4 = generate_user() 

1990 user5, token5 = generate_user() 

1991 

1992 with session_scope() as session: 

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

1994 

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

1996 end_time = start_time + timedelta(hours=3) 

1997 

1998 with events_session(token1) as api: 

1999 res = api.CreateEvent( 

2000 events_pb2.CreateEventReq( 

2001 title="Dummy Title", 

2002 content="Dummy content.", 

2003 location=events_pb2.EventLocation( 

2004 address="Near Null Island", 

2005 lat=0.1, 

2006 lng=0.2, 

2007 ), 

2008 parent_community_id=c_id, 

2009 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2010 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2011 ) 

2012 ) 

2013 

2014 res = api.TransferEvent( 

2015 events_pb2.TransferEventReq( 

2016 event_id=res.event_id, 

2017 new_owner_community_id=c_id, 

2018 ) 

2019 ) 

2020 

2021 event_id = res.event_id 

2022 

2023 api.SetEventAttendance( 

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

2025 ) 

2026 api.SetEventAttendance( 

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

2028 ) 

2029 

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

2031 assert len(res.attendee_user_ids) == 1 

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

2033 

2034 

2035def test_GetEventCalendarFile(db, moderator: Moderator): 

2036 user1, token1 = generate_user() 

2037 user2, token2 = generate_user() 

2038 

2039 with session_scope() as session: 

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

2041 

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

2043 end_time = start_time + timedelta(hours=3) 

2044 

2045 with events_session(token1) as api: 

2046 created_event: events_pb2.Event = api.CreateEvent( 

2047 events_pb2.CreateEventReq( 

2048 title="Dummy Title", 

2049 content="Dummy content.", 

2050 parent_community_id=c_id, 

2051 location=events_pb2.EventLocation( 

2052 address="Near Null Island", 

2053 lat=0.1, 

2054 lng=0.2, 

2055 ), 

2056 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2057 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2058 ) 

2059 ) 

2060 event_id = created_event.event_id 

2061 

2062 moderator.approve_event_occurrence(event_id) 

2063 

2064 with events_session(token1) as api: 

2065 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id)) 

2066 assert file_res.content_type == "text/calendar" 

2067 ics_string = file_res.data.decode("utf-8") 

2068 assert "SUMMARY:Dummy Title" in ics_string 

2069 assert "DESCRIPTION:Dummy content." in ics_string 

2070 assert "LOCATION:Near Null Island" in ics_string 

2071 assert "STATUS:CANCELLED" not in ics_string 

2072 pre_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string) 

2073 assert pre_cancel_match is not None 

2074 pre_cancel_sequence = int(pre_cancel_match.group(1)) 

2075 

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

2077 

2078 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id)) 

2079 ics_string = file_res.data.decode("utf-8") 

2080 assert "SUMMARY:Cancelled: Dummy Title" in ics_string 

2081 assert "STATUS:CANCELLED" in ics_string 

2082 post_cancel_match = re.search(r"SEQUENCE:(\d+)", ics_string) 

2083 assert post_cancel_match is not None 

2084 post_cancel_sequence = int(post_cancel_match.group(1)) 

2085 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal. 

2086 assert post_cancel_sequence >= pre_cancel_sequence 

2087 

2088 

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

2090 user1, token1 = generate_user() 

2091 user2, token2 = generate_user() 

2092 user3, token3 = generate_user() 

2093 user4, token4 = generate_user() 

2094 

2095 with session_scope() as session: 

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

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

2098 c_id = c.id 

2099 h_id = h.id 

2100 user4_id = user4.id 

2101 

2102 with events_session(token1) as api: 

2103 event = api.CreateEvent( 

2104 events_pb2.CreateEventReq( 

2105 title="Dummy Title", 

2106 content="Dummy content.", 

2107 location=events_pb2.EventLocation( 

2108 address="Near Null Island", 

2109 lat=0.1, 

2110 lng=0.2, 

2111 ), 

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

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

2114 ) 

2115 ) 

2116 

2117 moderator.approve_event_occurrence(event.event_id) 

2118 

2119 with threads_session(token2) as api: 

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

2121 

2122 moderator.approve_thread_post(reply_id) 

2123 

2124 with events_session(token3) as api: 

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

2126 assert res.thread.num_responses == 1 

2127 

2128 with threads_session(token3) as api: 

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

2130 assert len(ret.replies) == 1 

2131 assert not ret.next_page_token 

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

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

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

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

2136 

2137 nested_reply_id = api.PostReply( 

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

2139 ).thread_id 

2140 

2141 moderator.approve_thread_post(nested_reply_id) 

2142 

2143 process_jobs() 

2144 

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

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

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

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

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

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

2151 

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

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

2154 

2155 assert push_collector.count_for_user(user4_id) == 0 

2156 

2157 

2158def test_can_overlap_other_events_schedule_regression(db): 

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

2160 user, token = generate_user() 

2161 

2162 with session_scope() as session: 

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

2164 

2165 start = now() 

2166 

2167 with events_session(token) as api: 

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

2169 api.CreateEvent( 

2170 events_pb2.CreateEventReq( 

2171 title="Dummy Title", 

2172 content="Dummy content.", 

2173 parent_community_id=c_id, 

2174 location=events_pb2.EventLocation( 

2175 address="Near Null Island", 

2176 lat=0.1, 

2177 lng=0.2, 

2178 ), 

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

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

2181 ) 

2182 ) 

2183 

2184 # this event 

2185 res = api.CreateEvent( 

2186 events_pb2.CreateEventReq( 

2187 title="Dummy Title", 

2188 content="Dummy content.", 

2189 parent_community_id=c_id, 

2190 location=events_pb2.EventLocation( 

2191 address="Near Null Island", 

2192 lat=0.1, 

2193 lng=0.2, 

2194 ), 

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

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

2197 ) 

2198 ) 

2199 

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

2201 api.ScheduleEvent( 

2202 events_pb2.ScheduleEventReq( 

2203 event_id=res.event_id, 

2204 content="New event occurrence", 

2205 location=events_pb2.EventLocation( 

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

2207 lat=0.3, 

2208 lng=0.2, 

2209 ), 

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

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

2212 ) 

2213 ) 

2214 

2215 

2216def test_can_overlap_other_events_update_regression(db): 

2217 user, token = generate_user() 

2218 

2219 with session_scope() as session: 

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

2221 

2222 start = now() 

2223 

2224 with events_session(token) as api: 

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

2226 api.CreateEvent( 

2227 events_pb2.CreateEventReq( 

2228 title="Dummy Title", 

2229 content="Dummy content.", 

2230 parent_community_id=c_id, 

2231 location=events_pb2.EventLocation( 

2232 address="Near Null Island", 

2233 lat=0.1, 

2234 lng=0.2, 

2235 ), 

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

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

2238 ) 

2239 ) 

2240 

2241 res = api.CreateEvent( 

2242 events_pb2.CreateEventReq( 

2243 title="Dummy Title", 

2244 content="Dummy content.", 

2245 parent_community_id=c_id, 

2246 location=events_pb2.EventLocation( 

2247 address="Near Null Island", 

2248 lat=0.1, 

2249 lng=0.2, 

2250 ), 

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

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

2253 ) 

2254 ) 

2255 

2256 event_id = api.ScheduleEvent( 

2257 events_pb2.ScheduleEventReq( 

2258 event_id=res.event_id, 

2259 content="New event occurrence", 

2260 location=events_pb2.EventLocation( 

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

2262 lat=0.3, 

2263 lng=0.2, 

2264 ), 

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

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

2267 ) 

2268 ).event_id 

2269 

2270 # can overlap with this current existing occurrence 

2271 api.UpdateEvent( 

2272 events_pb2.UpdateEventReq( 

2273 event_id=event_id, 

2274 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2276 ), 

2277 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2279 ), 

2280 ) 

2281 ) 

2282 

2283 api.UpdateEvent( 

2284 events_pb2.UpdateEventReq( 

2285 event_id=event_id, 

2286 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2288 ), 

2289 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2291 ), 

2292 ) 

2293 ) 

2294 

2295 

2296def test_list_past_events_regression(db): 

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

2298 user, token = generate_user() 

2299 

2300 with session_scope() as session: 

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

2302 

2303 start = now() 

2304 

2305 with events_session(token) as api: 

2306 api.CreateEvent( 

2307 events_pb2.CreateEventReq( 

2308 title="Dummy Title", 

2309 content="Dummy content.", 

2310 parent_community_id=c_id, 

2311 location=events_pb2.EventLocation( 

2312 address="Near Null Island", 

2313 lat=0.1, 

2314 lng=0.2, 

2315 ), 

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

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

2318 ) 

2319 ) 

2320 

2321 with session_scope() as session: 

2322 session.execute( 

2323 update(EventOccurrence).values( 

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

2325 ) 

2326 ) 

2327 

2328 with events_session(token) as api: 

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

2330 assert len(res.events) == 1 

2331 

2332 

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

2334 user1, token1 = generate_user(complete_profile=True) 

2335 user2, token2 = generate_user() 

2336 user3, token3 = generate_user() 

2337 user4, token4 = generate_user() 

2338 user5, token5 = generate_user(is_superuser=True) 

2339 

2340 with session_scope() as session: 

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

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

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

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

2345 

2346 enforce_community_memberships() 

2347 

2348 with events_session(token1) as api: 

2349 res = api.CreateEvent( 

2350 events_pb2.CreateEventReq( 

2351 title="Dummy Title", 

2352 content="Dummy content.", 

2353 parent_community_id=c_id, 

2354 location=events_pb2.EventLocation( 

2355 address="Near Null Island", 

2356 lat=0.1, 

2357 lng=0.2, 

2358 ), 

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

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

2361 ) 

2362 ) 

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

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

2365 

2366 event_id = res.event_id 

2367 

2368 moderator.approve_event_occurrence(event_id) 

2369 

2370 with events_session(token1) as api: 

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

2372 

2373 email = email_collector.pop_for_mods(last=True) 

2374 

2375 assert user_url in email.plain 

2376 assert event_url in email.plain 

2377 

2378 # can't send another req 

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

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

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

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

2383 

2384 # another user can send one though 

2385 with events_session(token3) as api: 

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

2387 

2388 # but not a non-admin 

2389 with events_session(token2) as api: 

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

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

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

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

2394 

2395 with real_editor_session(token5) as editor: 

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

2397 assert len(res.requests) == 2 

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

2399 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain) 

2400 assert res.requests[0].approx_users_to_notify == 2 

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

2402 assert res.requests[1].approx_users_to_notify == 2 

2403 

2404 editor.DecideEventCommunityInviteRequest( 

2405 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2407 approve=False, 

2408 ) 

2409 ) 

2410 

2411 editor.DecideEventCommunityInviteRequest( 

2412 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2414 approve=True, 

2415 ) 

2416 ) 

2417 

2418 # not after approve 

2419 with events_session(token4) as api: 

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

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

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

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

2424 

2425 

2426def test_list_decided_community_invite_requests(db, moderator: Moderator): 

2427 user1, token1 = generate_user() 

2428 user2, token2 = generate_user() 

2429 user3, token3 = generate_user() 

2430 superuser, superuser_token = generate_user(is_superuser=True) 

2431 

2432 with session_scope() as session: 

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

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

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

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

2437 

2438 enforce_community_memberships() 

2439 

2440 def create_event_and_request_invite(token: str, title: str) -> str: 

2441 with events_session(token) as api: 

2442 res = api.CreateEvent( 

2443 events_pb2.CreateEventReq( 

2444 title=title, 

2445 content="Dummy content.", 

2446 parent_community_id=c_id, 

2447 location=events_pb2.EventLocation( 

2448 address="Near Null Island", 

2449 lat=0.1, 

2450 lng=0.2, 

2451 ), 

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

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

2454 ) 

2455 ) 

2456 moderator.approve_event_occurrence(res.event_id) 

2457 with events_session(token) as api: 

2458 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=res.event_id)) 

2459 return f"http://localhost:3000/event/{res.event_id}/{res.slug}" 

2460 

2461 event1_url = create_event_and_request_invite(token1, "Approved Event") 

2462 event2_url = create_event_and_request_invite(token2, "Declined Event") 

2463 # this one stays pending 

2464 create_event_and_request_invite(token3, "Pending Event") 

2465 

2466 with real_editor_session(superuser_token) as editor: 

2467 pending = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq()) 

2468 assert len(pending.requests) == 3 

2469 by_user = {req.user_id: req.event_community_invite_request_id for req in pending.requests} 

2470 

2471 # nothing decided yet 

2472 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq()) 

2473 assert len(res.requests) == 0 

2474 assert not res.next_page_token 

2475 

2476 editor.DecideEventCommunityInviteRequest( 

2477 editor_pb2.DecideEventCommunityInviteRequestReq( 

2478 event_community_invite_request_id=by_user[user1.id], 

2479 approve=True, 

2480 ) 

2481 ) 

2482 editor.DecideEventCommunityInviteRequest( 

2483 editor_pb2.DecideEventCommunityInviteRequestReq( 

2484 event_community_invite_request_id=by_user[user2.id], 

2485 approve=False, 

2486 ) 

2487 ) 

2488 

2489 # the pending one is not returned, and the most recently decided comes first 

2490 res = editor.ListDecidedEventCommunityInviteRequests(editor_pb2.ListDecidedEventCommunityInviteRequestsReq()) 

2491 assert len(res.requests) == 2 

2492 assert not res.next_page_token 

2493 

2494 declined, approved = res.requests 

2495 

2496 assert declined.event_community_invite_request_id == by_user[user2.id] 

2497 assert declined.user_id == user2.id 

2498 assert declined.event_url == event2_url 

2499 assert declined.community_id == c_id 

2500 assert declined.decided_by_user_id == superuser.id 

2501 assert not declined.approved 

2502 assert declined.created.ToDatetime() <= declined.decided.ToDatetime() 

2503 

2504 assert approved.event_community_invite_request_id == by_user[user1.id] 

2505 assert approved.user_id == user1.id 

2506 assert approved.event_url == event1_url 

2507 assert approved.community_id == c_id 

2508 assert approved.decided_by_user_id == superuser.id 

2509 assert approved.approved 

2510 assert approved.decided.ToDatetime() <= declined.decided.ToDatetime() 

2511 

2512 # filtering 

2513 res = editor.ListDecidedEventCommunityInviteRequests( 

2514 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=True)) 

2515 ) 

2516 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]] 

2517 

2518 res = editor.ListDecidedEventCommunityInviteRequests( 

2519 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(approved=wrappers_pb2.BoolValue(value=False)) 

2520 ) 

2521 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]] 

2522 

2523 # pagination 

2524 res = editor.ListDecidedEventCommunityInviteRequests( 

2525 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1) 

2526 ) 

2527 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user2.id]] 

2528 assert res.next_page_token 

2529 

2530 res = editor.ListDecidedEventCommunityInviteRequests( 

2531 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1, page_token=res.next_page_token) 

2532 ) 

2533 assert [req.event_community_invite_request_id for req in res.requests] == [by_user[user1.id]] 

2534 assert not res.next_page_token 

2535 

2536 

2537def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator): 

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

2539 # community invite notification when it is approved. 

2540 organizer, organizer_token = generate_user() 

2541 attendee, attendee_token = generate_user() 

2542 member, _ = generate_user() 

2543 superuser, superuser_token = generate_user(is_superuser=True) 

2544 

2545 with session_scope() as session: 

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

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

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

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

2550 

2551 enforce_community_memberships() 

2552 

2553 with events_session(organizer_token) as api: 

2554 event_id = api.CreateEvent( 

2555 events_pb2.CreateEventReq( 

2556 title="Dummy Title", 

2557 content="Dummy content.", 

2558 parent_community_id=c_id, 

2559 location=events_pb2.EventLocation( 

2560 address="Near Null Island", 

2561 lat=0.1, 

2562 lng=0.2, 

2563 ), 

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

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

2566 ) 

2567 ).event_id 

2568 

2569 moderator.approve_event_occurrence(event_id) 

2570 

2571 # the attendee RSVPs before the community invite is approved 

2572 with events_session(attendee_token) as api: 

2573 api.SetEventAttendance( 

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

2575 ) 

2576 

2577 with events_session(organizer_token) as api: 

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

2579 

2580 with real_editor_session(superuser_token) as editor: 

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

2582 editor.DecideEventCommunityInviteRequest( 

2583 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2585 approve=True, 

2586 ) 

2587 ) 

2588 

2589 process_jobs() 

2590 

2591 with session_scope() as session: 

2592 

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

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

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

2596 

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

2598 assert invite_notification_count(member.id) == 1 

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

2600 assert invite_notification_count(attendee.id) == 0 

2601 assert invite_notification_count(organizer.id) == 0 

2602 

2603 

2604def test_update_event_should_notify_queues_job(): 

2605 user, token = generate_user() 

2606 start = now() 

2607 

2608 with session_scope() as session: 

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

2610 

2611 # create an event 

2612 with events_session(token) as api: 

2613 create_res = api.CreateEvent( 

2614 events_pb2.CreateEventReq( 

2615 title="Dummy Title", 

2616 content="Dummy content.", 

2617 parent_community_id=c_id, 

2618 location=events_pb2.EventLocation( 

2619 address="Near Null Island", 

2620 lat=1.0, 

2621 lng=2.0, 

2622 ), 

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

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

2625 ) 

2626 ) 

2627 

2628 event_id = create_res.event_id 

2629 

2630 # measure initial background job queue length 

2631 with session_scope() as session: 

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

2633 job_length_before_update = len(jobs) 

2634 

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

2636 api.UpdateEvent( 

2637 events_pb2.UpdateEventReq( 

2638 event_id=event_id, 

2639 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2641 ), 

2642 should_notify=False, 

2643 ) 

2644 ) 

2645 

2646 with session_scope() as session: 

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

2648 assert len(jobs) == job_length_before_update 

2649 

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

2651 api.UpdateEvent( 

2652 events_pb2.UpdateEventReq( 

2653 event_id=event_id, 

2654 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2656 ), 

2657 should_notify=True, 

2658 ) 

2659 ) 

2660 

2661 with session_scope() as session: 

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

2663 assert len(jobs) == job_length_before_update + 1 

2664 

2665 

2666def test_event_photo_key(db): 

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

2668 user, token = generate_user() 

2669 

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

2671 end_time = start_time + timedelta(hours=3) 

2672 

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

2674 with session_scope() as session: 

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

2676 upload = Upload( 

2677 key="test_event_photo_key_123", 

2678 filename="test_event_photo_key_123.jpg", 

2679 creator_user_id=user.id, 

2680 ) 

2681 session.add(upload) 

2682 

2683 with events_session(token) as api: 

2684 # Create event without photo 

2685 res = api.CreateEvent( 

2686 events_pb2.CreateEventReq( 

2687 title="Event Without Photo", 

2688 content="No photo content.", 

2689 photo_key=None, 

2690 location=events_pb2.EventLocation( 

2691 address="Near Null Island", 

2692 lat=0.1, 

2693 lng=0.2, 

2694 ), 

2695 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2696 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2697 ) 

2698 ) 

2699 

2700 assert res.photo_key == "" 

2701 assert res.photo_url == "" 

2702 

2703 # Create event with photo 

2704 res_with_photo = api.CreateEvent( 

2705 events_pb2.CreateEventReq( 

2706 title="Event With Photo", 

2707 content="Has photo content.", 

2708 photo_key="test_event_photo_key_123", 

2709 location=events_pb2.EventLocation( 

2710 address="Near Null Island", 

2711 lat=0.1, 

2712 lng=0.2, 

2713 ), 

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

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

2716 ) 

2717 ) 

2718 

2719 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2720 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2721 

2722 event_id = res_with_photo.event_id 

2723 

2724 # Verify photo_key is returned when getting the event 

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

2726 assert get_res.photo_key == "test_event_photo_key_123" 

2727 assert "test_event_photo_key_123" in get_res.photo_url 

2728 

2729 

2730def test_event_timezone(db): 

2731 user, token = generate_user() 

2732 

2733 with session_scope() as session: 

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

2735 

2736 # Midnight future day, UTC timezone 

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

2738 end_time = start_time + timedelta(days=1) 

2739 

2740 with events_session(token) as api: 

2741 create_res: events_pb2.Event = api.CreateEvent( 

2742 events_pb2.CreateEventReq( 

2743 title="Dummy Title", 

2744 content="Dummy content.", 

2745 photo_key=None, 

2746 parent_community_id=c_id, 

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

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

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

2750 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2751 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2752 ) 

2753 ) 

2754 

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

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

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

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

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

2760 

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

2762 update_res: events_pb2.Event = api.UpdateEvent( 

2763 events_pb2.UpdateEventReq( 

2764 event_id=create_res.event_id, 

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

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

2767 ) 

2768 ) 

2769 

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

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

2772 # but now in the New York timezone. 

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

2774 assert update_res.start_time != create_res.start_time 

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

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

2777 

2778 # Also validate GetEvent 

2779 get_res: events_pb2.Event = api.GetEvent( 

2780 events_pb2.GetEventReq( 

2781 event_id=create_res.event_id, 

2782 ) 

2783 ) 

2784 

2785 assert get_res.timezone == update_res.timezone 

2786 assert get_res.start_time == update_res.start_time 

2787 

2788 

2789def test_event_created_with_shadowed_visibility(db): 

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

2791 user, token = generate_user() 

2792 

2793 with session_scope() as session: 

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

2795 

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

2797 end_time = start_time + timedelta(hours=3) 

2798 

2799 with events_session(token) as api: 

2800 res = api.CreateEvent( 

2801 events_pb2.CreateEventReq( 

2802 title="Test UMS Event", 

2803 content="UMS content.", 

2804 location=events_pb2.EventLocation( 

2805 address="Near Null Island", 

2806 lat=0.1, 

2807 lng=0.2, 

2808 ), 

2809 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2810 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2811 ) 

2812 ) 

2813 event_id = res.event_id 

2814 

2815 with session_scope() as session: 

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

2817 mod_state = session.execute( 

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

2819 ).scalar_one() 

2820 assert mod_state.visibility == ModerationVisibility.shadowed 

2821 

2822 

2823def test_shadowed_event_visible_to_creator_only(db): 

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

2825 user1, token1 = generate_user() 

2826 user2, token2 = generate_user() 

2827 

2828 with session_scope() as session: 

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

2830 

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

2832 end_time = start_time + timedelta(hours=3) 

2833 

2834 with events_session(token1) as api: 

2835 res = api.CreateEvent( 

2836 events_pb2.CreateEventReq( 

2837 title="Shadowed Event", 

2838 content="Content.", 

2839 location=events_pb2.EventLocation( 

2840 address="Near Null Island", 

2841 lat=0.1, 

2842 lng=0.2, 

2843 ), 

2844 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2845 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2846 ) 

2847 ) 

2848 event_id = res.event_id 

2849 

2850 # Creator can see it 

2851 with events_session(token1) as api: 

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

2853 assert res.title == "Shadowed Event" 

2854 

2855 # Other user cannot 

2856 with events_session(token2) as api: 

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

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

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

2860 

2861 

2862def test_event_visible_after_approval(db, moderator: Moderator): 

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

2864 user1, token1 = generate_user() 

2865 user2, token2 = generate_user() 

2866 

2867 with session_scope() as session: 

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

2869 

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

2871 end_time = start_time + timedelta(hours=3) 

2872 

2873 with events_session(token1) as api: 

2874 res = api.CreateEvent( 

2875 events_pb2.CreateEventReq( 

2876 title="Approved Event", 

2877 content="Content.", 

2878 location=events_pb2.EventLocation( 

2879 address="Near Null Island", 

2880 lat=0.1, 

2881 lng=0.2, 

2882 ), 

2883 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2884 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2885 ) 

2886 ) 

2887 event_id = res.event_id 

2888 

2889 # Other user cannot see it yet 

2890 with events_session(token2) as api: 

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

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

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

2894 

2895 # Approve the event 

2896 moderator.approve_event_occurrence(event_id) 

2897 

2898 # Now other user can see it 

2899 with events_session(token2) as api: 

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

2901 assert res.title == "Approved Event" 

2902 

2903 

2904def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2906 user1, token1 = generate_user() 

2907 user2, token2 = generate_user() 

2908 

2909 with session_scope() as session: 

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

2911 

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

2913 end_time = start_time + timedelta(hours=3) 

2914 

2915 with events_session(token1) as api: 

2916 res = api.CreateEvent( 

2917 events_pb2.CreateEventReq( 

2918 title="List Test Event", 

2919 content="Content.", 

2920 location=events_pb2.EventLocation( 

2921 address="Near Null Island", 

2922 lat=0.1, 

2923 lng=0.2, 

2924 ), 

2925 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2926 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2927 ) 

2928 ) 

2929 event_id = res.event_id 

2930 

2931 # Creator can see their own SHADOWED event in lists 

2932 with events_session(token1) as api: 

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

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

2935 assert event_id in event_ids 

2936 

2937 # Other user cannot see the SHADOWED event in lists 

2938 with events_session(token2) as api: 

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

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

2941 assert event_id not in event_ids 

2942 

2943 # After approval, other user can see it 

2944 moderator.approve_event_occurrence(event_id) 

2945 

2946 with events_session(token2) as api: 

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

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

2949 assert event_id in event_ids 

2950 

2951 

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

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

2954 user1, token1 = generate_user() 

2955 user2, token2 = generate_user() 

2956 

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

2958 with session_scope() as session: 

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

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

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

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

2963 

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

2965 end_time = start_time + timedelta(hours=3) 

2966 

2967 with events_session(token1) as api: 

2968 res = api.CreateEvent( 

2969 events_pb2.CreateEventReq( 

2970 title="Deferred Event", 

2971 content="Content.", 

2972 location=events_pb2.EventLocation( 

2973 address="Near Null Island", 

2974 lat=0.1, 

2975 lng=0.2, 

2976 ), 

2977 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2978 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2979 ) 

2980 ) 

2981 event_id = res.event_id 

2982 

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

2984 process_jobs() 

2985 

2986 with session_scope() as session: 

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

2988 # Notification was created with moderation_state_id for deferral 

2989 assert notif.moderation_state_id is not None 

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

2991 delivery_count = session.execute( 

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

2993 ).scalar_one_or_none() 

2994 assert delivery_count is None 

2995 

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

2997 moderator.approve_event_occurrence(event_id) 

2998 

2999 # Verify handle_notification job was queued 

3000 with session_scope() as session: 

3001 pending_jobs = ( 

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

3003 .scalars() 

3004 .all() 

3005 ) 

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

3007 

3008 

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

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

3011 user1, token1 = generate_user() 

3012 user2, token2 = generate_user() 

3013 

3014 with session_scope() as session: 

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

3016 

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

3018 end_time = start_time + timedelta(hours=3) 

3019 

3020 with events_session(token1) as api: 

3021 res = api.CreateEvent( 

3022 events_pb2.CreateEventReq( 

3023 title="Update Test", 

3024 content="Content.", 

3025 location=events_pb2.EventLocation( 

3026 address="Near Null Island", 

3027 lat=0.1, 

3028 lng=0.2, 

3029 ), 

3030 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3031 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3032 ) 

3033 ) 

3034 event_id = res.event_id 

3035 

3036 moderator.approve_event_occurrence(event_id) 

3037 process_jobs() 

3038 # Clear any create notifications 

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

3040 push_collector.pop_for_user(user2.id) 

3041 

3042 # User2 subscribes to the event 

3043 with events_session(token2) as api: 

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

3045 

3046 # User1 updates the event with should_notify=True 

3047 with events_session(token1) as api: 

3048 api.UpdateEvent( 

3049 events_pb2.UpdateEventReq( 

3050 event_id=event_id, 

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

3052 should_notify=True, 

3053 ) 

3054 ) 

3055 

3056 process_jobs() 

3057 

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

3059 with session_scope() as session: 

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

3061 

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

3063 # Find the update notification (most recent one) 

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

3065 assert len(update_notifs) == 1 

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

3067 

3068 

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

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

3071 user1, token1 = generate_user() 

3072 user2, token2 = generate_user() 

3073 

3074 with session_scope() as session: 

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

3076 

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

3078 end_time = start_time + timedelta(hours=3) 

3079 

3080 with events_session(token1) as api: 

3081 res = api.CreateEvent( 

3082 events_pb2.CreateEventReq( 

3083 title="Cancel Test", 

3084 content="Content.", 

3085 location=events_pb2.EventLocation( 

3086 address="Near Null Island", 

3087 lat=0.1, 

3088 lng=0.2, 

3089 ), 

3090 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3091 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3092 ) 

3093 ) 

3094 event_id = res.event_id 

3095 

3096 moderator.approve_event_occurrence(event_id) 

3097 process_jobs() 

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

3099 push_collector.pop_for_user(user2.id) 

3100 

3101 # User2 subscribes 

3102 with events_session(token2) as api: 

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

3104 

3105 # User1 cancels the event 

3106 with events_session(token1) as api: 

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

3108 

3109 process_jobs() 

3110 

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

3112 with session_scope() as session: 

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

3114 

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

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

3117 assert len(cancel_notifs) == 1 

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

3119 

3120 

3121def test_event_update_and_cancel_notifications_not_sent_to_actor( 

3122 db, push_collector: PushCollector, moderator: Moderator 

3123): 

3124 """The user who updates or cancels an event shouldn't be notified about their own action.""" 

3125 organizer_user, organizer_token = generate_user() 

3126 attendee_user, attendee_token = generate_user() 

3127 

3128 with session_scope() as session: 

3129 create_community(session, 0, 2, "Community", [attendee_user], [], None) 

3130 

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

3132 end_time = start_time + timedelta(hours=3) 

3133 

3134 with events_session(organizer_token) as api: 

3135 res = api.CreateEvent( 

3136 events_pb2.CreateEventReq( 

3137 title="Actor Test", 

3138 content="Content.", 

3139 location=events_pb2.EventLocation( 

3140 address="Near Null Island", 

3141 lat=0.1, 

3142 lng=0.2, 

3143 ), 

3144 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3145 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3146 ) 

3147 ) 

3148 event_id = res.event_id 

3149 

3150 moderator.approve_event_occurrence(event_id) 

3151 process_jobs() 

3152 

3153 # The attendee subscribes to notifications 

3154 with events_session(attendee_token) as api: 

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

3156 

3157 # The organizer updates and then cancels their own event 

3158 with events_session(organizer_token) as api: 

3159 api.UpdateEvent( 

3160 events_pb2.UpdateEventReq( 

3161 event_id=event_id, 

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

3163 should_notify=True, 

3164 ) 

3165 ) 

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

3167 

3168 process_jobs() 

3169 

3170 # The organizer should not receive any notifications 

3171 assert push_collector.count_for_user(organizer_user.id) == 0 

3172 

3173 # But the attendee should receive both the update and cancel notifications 

3174 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__update.display 

3175 assert push_collector.pop_for_user(attendee_user.id).topic_action == NotificationTopicAction.event__cancel.display 

3176 

3177 

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

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

3180 user1, token1 = generate_user() 

3181 user2, token2 = generate_user() 

3182 

3183 with session_scope() as session: 

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

3185 

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

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

3188 end_time = start_time + timedelta(hours=1) 

3189 

3190 with events_session(token1) as api: 

3191 res = api.CreateEvent( 

3192 events_pb2.CreateEventReq( 

3193 title="Reminder Test", 

3194 content="Content.", 

3195 location=events_pb2.EventLocation( 

3196 address="Near Null Island", 

3197 lat=0.1, 

3198 lng=0.2, 

3199 ), 

3200 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3201 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3202 ) 

3203 ) 

3204 event_id = res.event_id 

3205 

3206 moderator.approve_event_occurrence(event_id) 

3207 process_jobs() 

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

3209 push_collector.pop_for_user(user2.id) 

3210 

3211 # User2 marks attendance 

3212 with events_session(token2) as api: 

3213 api.SetEventAttendance( 

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

3215 ) 

3216 

3217 # Run the event reminder handler 

3218 send_event_reminders(empty_pb2.Empty()) 

3219 process_jobs() 

3220 

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

3222 with session_scope() as session: 

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

3224 

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

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

3227 assert len(reminder_notifs) == 1 

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

3229 

3230 

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

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

3233 user1, token1 = generate_user() 

3234 user2, token2 = generate_user() 

3235 

3236 with session_scope() as session: 

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

3238 

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

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

3241 end_time = start_time + timedelta(hours=1) 

3242 

3243 with events_session(token1) as api: 

3244 res = api.CreateEvent( 

3245 events_pb2.CreateEventReq( 

3246 title="Cancelled Reminder Test", 

3247 content="Content.", 

3248 location=events_pb2.EventLocation( 

3249 address="Near Null Island", 

3250 lat=0.1, 

3251 lng=0.2, 

3252 ), 

3253 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3254 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3255 ) 

3256 ) 

3257 event_id = res.event_id 

3258 

3259 moderator.approve_event_occurrence(event_id) 

3260 process_jobs() 

3261 

3262 # User2 marks attendance 

3263 with events_session(token2) as api: 

3264 api.SetEventAttendance( 

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

3266 ) 

3267 

3268 # User1 cancels the event 

3269 with events_session(token1) as api: 

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

3271 

3272 process_jobs() 

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

3274 while push_collector.count_for_user(user2.id): 

3275 push_collector.pop_for_user(user2.id) 

3276 

3277 # Run the event reminder handler 

3278 send_event_reminders(empty_pb2.Empty()) 

3279 process_jobs() 

3280 

3281 # Verify that no reminder notification was sent for user2 

3282 with session_scope() as session: 

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

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

3285 assert len(reminder_notifs) == 0 

3286 

3287 

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

3289def test_event_reminder_not_sent_for_invisible_attendee( 

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

3291): 

3292 user1, token1 = generate_user() 

3293 user2, token2 = generate_user() 

3294 

3295 with session_scope() as session: 

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

3297 

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

3299 end_time = start_time + timedelta(hours=1) 

3300 

3301 with events_session(token1) as api: 

3302 res = api.CreateEvent( 

3303 events_pb2.CreateEventReq( 

3304 title="Invisible Attendee Reminder Test", 

3305 content="Content.", 

3306 location=events_pb2.EventLocation( 

3307 address="Near Null Island", 

3308 lat=0.1, 

3309 lng=0.2, 

3310 ), 

3311 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3312 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3313 ) 

3314 ) 

3315 event_id = res.event_id 

3316 

3317 moderator.approve_event_occurrence(event_id) 

3318 process_jobs() 

3319 

3320 with events_session(token2) as api: 

3321 api.SetEventAttendance( 

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

3323 ) 

3324 

3325 with session_scope() as session: 

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

3327 

3328 send_event_reminders(empty_pb2.Empty()) 

3329 process_jobs() 

3330 

3331 with session_scope() as session: 

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

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

3334 assert len(reminder_notifs) == 0 

3335 

3336 

3337def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3339 user1, token1 = generate_user() 

3340 user2, token2 = generate_user() 

3341 

3342 with session_scope() as session: 

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

3344 

3345 start = now() 

3346 

3347 # User1 creates event A with 3 occurrences 

3348 event_a_ids = [] 

3349 with events_session(token1) as api: 

3350 res = api.CreateEvent( 

3351 events_pb2.CreateEventReq( 

3352 title="Event A", 

3353 content="Content A.", 

3354 parent_community_id=c_id, 

3355 location=events_pb2.EventLocation( 

3356 address="Near Null Island", 

3357 lat=0.1, 

3358 lng=0.2, 

3359 ), 

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

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

3362 ) 

3363 ) 

3364 event_a_ids.append(res.event_id) 

3365 for i in range(2): 

3366 res = api.ScheduleEvent( 

3367 events_pb2.ScheduleEventReq( 

3368 event_id=event_a_ids[-1], 

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

3370 location=events_pb2.EventLocation( 

3371 address="Near Null Island", 

3372 lat=0.1, 

3373 lng=0.2, 

3374 ), 

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

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

3377 ) 

3378 ) 

3379 event_a_ids.append(res.event_id) 

3380 

3381 # User2 creates event B with 2 occurrences 

3382 event_b_ids = [] 

3383 with events_session(token2) as api: 

3384 res = api.CreateEvent( 

3385 events_pb2.CreateEventReq( 

3386 title="Event B", 

3387 content="Content B.", 

3388 parent_community_id=c_id, 

3389 location=events_pb2.EventLocation( 

3390 address="Near Null Island", 

3391 lat=0.1, 

3392 lng=0.2, 

3393 ), 

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

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

3396 ) 

3397 ) 

3398 event_b_ids.append(res.event_id) 

3399 res = api.ScheduleEvent( 

3400 events_pb2.ScheduleEventReq( 

3401 event_id=event_b_ids[-1], 

3402 content="B occurrence 1", 

3403 location=events_pb2.EventLocation( 

3404 address="Near Null Island", 

3405 lat=0.1, 

3406 lng=0.2, 

3407 ), 

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

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

3410 ) 

3411 ) 

3412 event_b_ids.append(res.event_id) 

3413 

3414 moderator.approve_event_occurrence(event_a_ids[0]) 

3415 moderator.approve_event_occurrence(event_b_ids[0]) 

3416 

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

3418 with events_session(token1) as api: 

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

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

3421 assert sorted(returned_ids) == sorted(event_a_ids) 

3422 

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

3424 with events_session(token2) as api: 

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

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

3427 assert sorted(returned_ids) == sorted(event_b_ids) 

3428 

3429 

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

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

3432 user1, token1 = generate_user() 

3433 user2, token2 = generate_user() 

3434 

3435 with session_scope() as session: 

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

3437 

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

3439 end_time = start_time + timedelta(hours=3) 

3440 

3441 with events_session(token1) as api: 

3442 res = api.CreateEvent( 

3443 events_pb2.CreateEventReq( 

3444 title="Comment Test", 

3445 content="Content.", 

3446 parent_community_id=c_id, 

3447 location=events_pb2.EventLocation( 

3448 address="Near Null Island", 

3449 lat=0.1, 

3450 lng=0.2, 

3451 ), 

3452 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3453 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3454 ) 

3455 ) 

3456 event_id = res.event_id 

3457 thread_id = res.thread.thread_id 

3458 

3459 moderator.approve_event_occurrence(event_id) 

3460 process_jobs() 

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

3462 push_collector.pop_for_user(user1.id) 

3463 

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

3465 with events_session(token1) as api: 

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

3467 

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

3469 with threads_session(token2) as api: 

3470 comment_thread_id = api.PostReply( 

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

3472 ).thread_id 

3473 

3474 process_jobs() 

3475 

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

3477 comment_db_id = comment_thread_id // 10 

3478 with session_scope() as session: 

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

3480 

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

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

3483 assert len(comment_notifs) == 1 

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

3485 

3486 

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

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

3489 user1, token1 = generate_user() 

3490 user2, token2 = generate_user() 

3491 user3, token3 = generate_user() 

3492 

3493 with session_scope() as session: 

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

3495 

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

3497 end_time = start_time + timedelta(hours=3) 

3498 

3499 with events_session(token1) as api: 

3500 res = api.CreateEvent( 

3501 events_pb2.CreateEventReq( 

3502 title="Reply Test", 

3503 content="Content.", 

3504 location=events_pb2.EventLocation( 

3505 address="Near Null Island", 

3506 lat=0.1, 

3507 lng=0.2, 

3508 ), 

3509 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3510 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3511 ) 

3512 ) 

3513 event_id = res.event_id 

3514 thread_id = res.thread.thread_id 

3515 

3516 moderator.approve_event_occurrence(event_id) 

3517 process_jobs() 

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

3519 push_collector.pop_for_user(user1.id) 

3520 

3521 # User2 posts a top-level comment 

3522 with threads_session(token2) as api: 

3523 comment_thread_id = api.PostReply( 

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

3525 ).thread_id 

3526 

3527 process_jobs() 

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

3529 push_collector.pop_for_user(user1.id) 

3530 

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

3532 with threads_session(token3) as api: 

3533 nested_reply_thread_id = api.PostReply( 

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

3535 ).thread_id 

3536 

3537 process_jobs() 

3538 

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

3540 nested_reply_db_id = nested_reply_thread_id // 10 

3541 with session_scope() as session: 

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

3543 

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

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

3546 assert len(reply_notifs) == 1 

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