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

1641 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-10 12:25 +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.fixtures.timewarp import FrozenTimewarp 

35from tests.test_communities import create_community, create_group 

36 

37 

38@pytest.fixture(autouse=True) 

39def _(testconfig): 

40 pass 

41 

42 

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

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

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

46 

47 

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

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

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

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

52 

53 

54def test_CreateEvent(db, frozen_timewarp, push_collector: PushCollector, moderator: Moderator): 

55 # test cases: 

56 # can create event 

57 # cannot create event with missing details 

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

59 # can create in different timezones 

60 

61 # event creator 

62 user1, token1 = generate_user() 

63 # community moderator 

64 user2, token2 = generate_user() 

65 # third party 

66 user3, token3 = generate_user() 

67 

68 with session_scope() as session: 

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

70 

71 time_before = now() 

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

73 end_time = start_time + timedelta(hours=3) 

74 

75 # Can create an event 

76 with events_session(token1) as api: 

77 res = api.CreateEvent( 

78 events_pb2.CreateEventReq( 

79 title="Dummy Title", 

80 content="Dummy content.", 

81 photo_key=None, 

82 location=events_pb2.EventLocation( 

83 address="Near Null Island", 

84 lat=0.1, 

85 lng=0.2, 

86 ), 

87 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

88 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

89 ) 

90 ) 

91 

92 assert res.is_next 

93 assert res.title == "Dummy Title" 

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

95 assert res.content == "Dummy content." 

96 assert not res.photo_url 

97 assert res.HasField("location") 

98 assert res.location.lat == 0.1 

99 assert res.location.lng == 0.2 

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

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

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

103 assert res.creator_user_id == user1.id 

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

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

106 assert is_utc_or_gmt(res.timezone) 

107 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

108 assert res.organizer 

109 assert res.subscriber 

110 assert res.going_count == 1 

111 assert res.organizer_count == 1 

112 assert res.subscriber_count == 1 

113 assert res.owner_user_id == user1.id 

114 assert not res.owner_community_id 

115 assert not res.owner_group_id 

116 assert res.thread.thread_id 

117 assert res.can_edit 

118 assert not res.can_moderate 

119 

120 event_id = res.event_id 

121 

122 # Approve the event so other users can see it 

123 moderator.approve_event_occurrence(event_id) 

124 

125 with events_session(token2) as api: 

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

127 

128 assert res.is_next 

129 assert res.title == "Dummy Title" 

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

131 assert res.content == "Dummy content." 

132 assert not res.photo_url 

133 assert res.HasField("location") 

134 assert res.location.lat == 0.1 

135 assert res.location.lng == 0.2 

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

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

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

139 assert res.creator_user_id == user1.id 

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

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

142 assert is_utc_or_gmt(res.timezone) 

143 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

144 assert not res.organizer 

145 assert not res.subscriber 

146 assert res.going_count == 1 

147 assert res.organizer_count == 1 

148 assert res.subscriber_count == 1 

149 assert res.owner_user_id == user1.id 

150 assert not res.owner_community_id 

151 assert not res.owner_group_id 

152 assert res.thread.thread_id 

153 assert res.can_edit 

154 assert res.can_moderate 

155 

156 with events_session(token3) as api: 

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

158 

159 assert res.is_next 

160 assert res.title == "Dummy Title" 

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

162 assert res.content == "Dummy content." 

163 assert not res.photo_url 

164 assert res.HasField("location") 

165 assert res.location.lat == 0.1 

166 assert res.location.lng == 0.2 

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

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

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

170 assert res.creator_user_id == user1.id 

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

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

173 assert is_utc_or_gmt(res.timezone) 

174 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

175 assert not res.organizer 

176 assert not res.subscriber 

177 assert res.going_count == 1 

178 assert res.organizer_count == 1 

179 assert res.subscriber_count == 1 

180 assert res.owner_user_id == user1.id 

181 assert not res.owner_community_id 

182 assert not res.owner_group_id 

183 assert res.thread.thread_id 

184 assert not res.can_edit 

185 assert not res.can_moderate 

186 

187 # Failure cases 

188 with events_session(token1) as api: 

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

190 api.CreateEvent( 

191 events_pb2.CreateEventReq( 

192 # title="Dummy Title", 

193 content="Dummy content.", 

194 photo_key=None, 

195 location=events_pb2.EventLocation( 

196 address="Near Null Island", 

197 lat=0.1, 

198 lng=0.2, 

199 ), 

200 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

201 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

202 ) 

203 ) 

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

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

206 

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

208 api.CreateEvent( 

209 events_pb2.CreateEventReq( 

210 title="Dummy Title", 

211 # content="Dummy content.", 

212 photo_key=None, 

213 location=events_pb2.EventLocation( 

214 address="Near Null Island", 

215 lat=0.1, 

216 lng=0.2, 

217 ), 

218 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

219 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

220 ) 

221 ) 

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

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

224 

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

226 api.CreateEvent( 

227 events_pb2.CreateEventReq( 

228 title="Dummy Title", 

229 content="Dummy content.", 

230 photo_key="nonexistent", 

231 location=events_pb2.EventLocation( 

232 address="Near Null Island", 

233 lat=0.1, 

234 lng=0.2, 

235 ), 

236 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

237 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

238 ) 

239 ) 

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

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

242 

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

244 api.CreateEvent( 

245 events_pb2.CreateEventReq( 

246 title="Dummy Title", 

247 content="Dummy content.", 

248 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

249 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

250 ) 

251 ) 

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

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

254 

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

256 api.CreateEvent( 

257 events_pb2.CreateEventReq( 

258 title="Dummy Title", 

259 content="Dummy content.", 

260 location=events_pb2.EventLocation( 

261 address="Near Null Island", 

262 ), 

263 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

264 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

265 ) 

266 ) 

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

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

269 

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

271 api.CreateEvent( 

272 events_pb2.CreateEventReq( 

273 title="Dummy Title", 

274 content="Dummy content.", 

275 location=events_pb2.EventLocation( 

276 lat=0.1, 

277 lng=0.1, 

278 ), 

279 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

280 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

281 ) 

282 ) 

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

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

285 

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

287 api.CreateEvent( 

288 events_pb2.CreateEventReq( 

289 title="Dummy Title", 

290 content="Dummy content.", 

291 location=events_pb2.EventLocation( 

292 address="Near Null Island", 

293 lat=0.1, 

294 lng=0.2, 

295 ), 

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

297 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

298 ) 

299 ) 

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

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

302 

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

304 api.CreateEvent( 

305 events_pb2.CreateEventReq( 

306 title="Dummy Title", 

307 content="Dummy content.", 

308 location=events_pb2.EventLocation( 

309 address="Near Null Island", 

310 lat=0.1, 

311 lng=0.2, 

312 ), 

313 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

314 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

315 ) 

316 ) 

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

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

319 

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

321 api.CreateEvent( 

322 events_pb2.CreateEventReq( 

323 title="Dummy Title", 

324 content="Dummy content.", 

325 location=events_pb2.EventLocation( 

326 address="Near Null Island", 

327 lat=0.1, 

328 lng=0.2, 

329 ), 

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

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

332 ) 

333 ) 

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

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

336 

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

338 api.CreateEvent( 

339 events_pb2.CreateEventReq( 

340 title="Dummy Title", 

341 content="Dummy content.", 

342 location=events_pb2.EventLocation( 

343 address="Near Null Island", 

344 lat=0.1, 

345 lng=0.2, 

346 ), 

347 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

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

349 ) 

350 ) 

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

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

353 

354 

355def test_CreateEvent_incomplete_profile(db): 

356 user1, token1 = generate_user(complete_profile=False) 

357 user2, token2 = generate_user() 

358 

359 with session_scope() as session: 

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

361 

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

363 end_time = start_time + timedelta(hours=3) 

364 

365 with events_session(token1) as api: 

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

367 api.CreateEvent( 

368 events_pb2.CreateEventReq( 

369 title="Dummy Title", 

370 content="Dummy content.", 

371 photo_key=None, 

372 location=events_pb2.EventLocation( 

373 address="Near Null Island", 

374 lat=0.1, 

375 lng=0.2, 

376 ), 

377 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

378 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

379 ) 

380 ) 

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

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

383 

384 

385def test_ScheduleEvent(db, frozen_timewarp): 

386 # test cases: 

387 # can schedule a new event occurrence 

388 

389 user, token = generate_user() 

390 

391 with session_scope() as session: 

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

393 

394 time_before = now() 

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

396 end_time = start_time + timedelta(hours=3) 

397 

398 with events_session(token) as api: 

399 create_res = api.CreateEvent( 

400 events_pb2.CreateEventReq( 

401 title="Dummy Title", 

402 content="Dummy content.", 

403 parent_community_id=c_id, 

404 location=events_pb2.EventLocation( 

405 address="Near Null Island", 

406 lat=0.1, 

407 lng=0.2, 

408 ), 

409 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

410 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

411 ) 

412 ) 

413 

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

415 new_end_time = new_start_time + timedelta(hours=2) 

416 

417 schedule_res = api.ScheduleEvent( 

418 events_pb2.ScheduleEventReq( 

419 event_id=create_res.event_id, 

420 content="New event occurrence", 

421 location=events_pb2.EventLocation( 

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

423 lat=0.3, 

424 lng=0.2, 

425 ), 

426 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time), 

427 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time), 

428 ) 

429 ) 

430 

431 # Each occurrence is independent and has an independent thread 

432 assert schedule_res.event_id != create_res.event_id 

433 assert schedule_res.thread.thread_id != create_res.thread.thread_id 

434 

435 res = api.GetEvent(events_pb2.GetEventReq(event_id=schedule_res.event_id)) 

436 

437 assert not res.is_next 

438 assert res.title == "Dummy Title" 

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

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

441 assert not res.photo_url 

442 assert res.HasField("location") 

443 assert res.location.lat == 0.3 

444 assert res.location.lng == 0.2 

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

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

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

448 assert res.creator_user_id == user.id 

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

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

451 assert is_utc_or_gmt(res.timezone) 

452 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

453 assert res.organizer 

454 assert res.subscriber 

455 assert res.going_count == 1 

456 assert res.organizer_count == 1 

457 assert res.subscriber_count == 1 

458 assert res.owner_user_id == user.id 

459 assert not res.owner_community_id 

460 assert not res.owner_group_id 

461 assert res.thread.thread_id 

462 assert res.can_edit 

463 assert res.can_moderate 

464 

465 

466def test_cannot_overlap_occurrences_schedule(db): 

467 user, token = generate_user() 

468 

469 with session_scope() as session: 

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

471 

472 start = now() 

473 

474 with events_session(token) as api: 

475 res = api.CreateEvent( 

476 events_pb2.CreateEventReq( 

477 title="Dummy Title", 

478 content="Dummy content.", 

479 parent_community_id=c_id, 

480 location=events_pb2.EventLocation( 

481 address="Near Null Island", 

482 lat=0.1, 

483 lng=0.2, 

484 ), 

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

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

487 ) 

488 ) 

489 

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

491 api.ScheduleEvent( 

492 events_pb2.ScheduleEventReq( 

493 event_id=res.event_id, 

494 content="New event occurrence", 

495 location=events_pb2.EventLocation( 

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

497 lat=0.3, 

498 lng=0.2, 

499 ), 

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

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

502 ) 

503 ) 

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

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

506 

507 

508def test_cannot_overlap_occurrences_update(db): 

509 user, token = generate_user() 

510 

511 with session_scope() as session: 

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

513 

514 start = now() 

515 

516 with events_session(token) as api: 

517 res = api.CreateEvent( 

518 events_pb2.CreateEventReq( 

519 title="Dummy Title", 

520 content="Dummy content.", 

521 parent_community_id=c_id, 

522 location=events_pb2.EventLocation( 

523 address="Near Null Island", 

524 lat=0.1, 

525 lng=0.2, 

526 ), 

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

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

529 ) 

530 ) 

531 

532 event_id = api.ScheduleEvent( 

533 events_pb2.ScheduleEventReq( 

534 event_id=res.event_id, 

535 content="New event occurrence", 

536 location=events_pb2.EventLocation( 

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

538 lat=0.3, 

539 lng=0.2, 

540 ), 

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

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

543 ) 

544 ).event_id 

545 

546 # can overlap with this current existing occurrence 

547 api.UpdateEvent( 

548 events_pb2.UpdateEventReq( 

549 event_id=event_id, 

550 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

552 ), 

553 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

555 ), 

556 ) 

557 ) 

558 

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

560 api.UpdateEvent( 

561 events_pb2.UpdateEventReq( 

562 event_id=event_id, 

563 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

565 ), 

566 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

568 ), 

569 ) 

570 ) 

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

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

573 

574 

575def test_UpdateEvent_single(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator): 

576 # test cases: 

577 # owner can update 

578 # community owner can update 

579 # notifies attendees 

580 

581 # event creator 

582 user1, token1 = generate_user() 

583 # community moderator 

584 user2, token2 = generate_user() 

585 # third parties 

586 user3, token3 = generate_user() 

587 user4, token4 = generate_user() 

588 user5, token5 = generate_user() 

589 user6, token6 = generate_user() 

590 

591 with session_scope() as session: 

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

593 

594 time_before = now() 

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

596 end_time = start_time + timedelta(hours=3) 

597 

598 with events_session(token1) as api: 

599 res = api.CreateEvent( 

600 events_pb2.CreateEventReq( 

601 title="Dummy Title", 

602 content="Dummy content.", 

603 parent_community_id=c_id, 

604 location=events_pb2.EventLocation( 

605 address="Near Null Island", 

606 lat=0.1, 

607 lng=0.2, 

608 ), 

609 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

610 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

611 ) 

612 ) 

613 

614 event_id = res.event_id 

615 

616 moderator.approve_event_occurrence(event_id) 

617 

618 with events_session(token4) as api: 

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

620 

621 with events_session(token5) as api: 

622 api.SetEventAttendance( 

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

624 ) 

625 

626 with events_session(token6) as api: 

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

628 

629 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change 

630 frozen_timewarp.advance(timedelta(minutes=1)) 

631 time_before_update = now() 

632 

633 with events_session(token1) as api: 

634 res = api.UpdateEvent( 

635 events_pb2.UpdateEventReq( 

636 event_id=event_id, 

637 ) 

638 ) 

639 

640 with events_session(token1) as api: 

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

642 

643 assert res.is_next 

644 assert res.title == "Dummy Title" 

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

646 assert res.content == "Dummy content." 

647 assert not res.photo_url 

648 assert res.HasField("location") 

649 assert res.location.lat == 0.1 

650 assert res.location.lng == 0.2 

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

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

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

654 assert res.creator_user_id == user1.id 

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

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

657 assert is_utc_or_gmt(res.timezone) 

658 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

659 assert res.organizer 

660 assert res.subscriber 

661 assert res.going_count == 2 

662 assert res.organizer_count == 1 

663 assert res.subscriber_count == 3 

664 assert res.owner_user_id == user1.id 

665 assert not res.owner_community_id 

666 assert not res.owner_group_id 

667 assert res.thread.thread_id 

668 assert res.can_edit 

669 assert not res.can_moderate 

670 

671 with events_session(token2) as api: 

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

673 

674 assert res.is_next 

675 assert res.title == "Dummy Title" 

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

677 assert res.content == "Dummy content." 

678 assert not res.photo_url 

679 assert res.HasField("location") 

680 assert res.location.lat == 0.1 

681 assert res.location.lng == 0.2 

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

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

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

685 assert res.creator_user_id == user1.id 

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

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

688 assert is_utc_or_gmt(res.timezone) 

689 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

690 assert not res.organizer 

691 assert not res.subscriber 

692 assert res.going_count == 2 

693 assert res.organizer_count == 1 

694 assert res.subscriber_count == 3 

695 assert res.owner_user_id == user1.id 

696 assert not res.owner_community_id 

697 assert not res.owner_group_id 

698 assert res.thread.thread_id 

699 assert res.can_edit 

700 assert res.can_moderate 

701 

702 with events_session(token3) as api: 

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

704 

705 assert res.is_next 

706 assert res.title == "Dummy Title" 

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

708 assert res.content == "Dummy content." 

709 assert not res.photo_url 

710 assert res.HasField("location") 

711 assert res.location.lat == 0.1 

712 assert res.location.lng == 0.2 

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

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

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

716 assert res.creator_user_id == user1.id 

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

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

719 assert is_utc_or_gmt(res.timezone) 

720 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

721 assert not res.organizer 

722 assert not res.subscriber 

723 assert res.going_count == 2 

724 assert res.organizer_count == 1 

725 assert res.subscriber_count == 3 

726 assert res.owner_user_id == user1.id 

727 assert not res.owner_community_id 

728 assert not res.owner_group_id 

729 assert res.thread.thread_id 

730 assert not res.can_edit 

731 assert not res.can_moderate 

732 

733 with events_session(token1) as api: 

734 res = api.UpdateEvent( 

735 events_pb2.UpdateEventReq( 

736 event_id=event_id, 

737 location=events_pb2.EventLocation( 

738 address="Nearer Null Island", 

739 lat=0.01, 

740 lng=0.02, 

741 ), 

742 ) 

743 ) 

744 

745 with events_session(token3) as api: 

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

747 

748 assert res.HasField("location") 

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

750 assert res.location.lat == 0.01 

751 assert res.location.lng == 0.02 

752 

753 

754def test_UpdateEvent_all(db, frozen_timewarp: FrozenTimewarp, moderator: Moderator): 

755 # event creator 

756 user1, token1 = generate_user() 

757 # community moderator 

758 user2, token2 = generate_user() 

759 # third parties 

760 user3, token3 = generate_user() 

761 user4, token4 = generate_user() 

762 user5, token5 = generate_user() 

763 user6, token6 = generate_user() 

764 

765 with session_scope() as session: 

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

767 

768 time_before = now() 

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

770 end_time = start_time + timedelta(hours=1.5) 

771 

772 event_ids = [] 

773 

774 with events_session(token1) as api: 

775 res = api.CreateEvent( 

776 events_pb2.CreateEventReq( 

777 title="Dummy Title", 

778 content="0th occurrence", 

779 location=events_pb2.EventLocation( 

780 address="Near Null Island", 

781 lat=0.1, 

782 lng=0.2, 

783 ), 

784 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

785 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

786 ) 

787 ) 

788 

789 event_id = res.event_id 

790 event_ids.append(event_id) 

791 

792 moderator.approve_event_occurrence(event_id) 

793 

794 with events_session(token4) as api: 

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

796 

797 with events_session(token5) as api: 

798 api.SetEventAttendance( 

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

800 ) 

801 

802 with events_session(token6) as api: 

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

804 

805 with events_session(token1) as api: 

806 for i in range(5): 

807 res = api.ScheduleEvent( 

808 events_pb2.ScheduleEventReq( 

809 event_id=event_ids[-1], 

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

811 location=events_pb2.EventLocation( 

812 address="Near Null Island", 

813 lat=0.1, 

814 lng=0.2, 

815 ), 

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

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

818 ) 

819 ) 

820 

821 event_ids.append(res.event_id) 

822 

823 # Approve all scheduled occurrences 

824 for eid in event_ids[1:]: 

825 moderator.approve_event_occurrence(eid) 

826 

827 updated_event_id = event_ids[3] 

828 

829 # the clock is stopped, so the edit below needs the test to move it on for last_edited to change 

830 frozen_timewarp.advance(timedelta(minutes=1)) 

831 time_before_update = now() 

832 

833 with events_session(token1) as api: 

834 res = api.UpdateEvent( 

835 events_pb2.UpdateEventReq( 

836 event_id=updated_event_id, 

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

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

839 location=events_pb2.EventLocation( 

840 address="Not so near Null Island", 

841 lat=0.2, 

842 lng=0.2, 

843 ), 

844 update_all_future=True, 

845 ) 

846 ) 

847 

848 time_after_update = now() 

849 

850 with events_session(token2) as api: 

851 for i in range(3): 

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

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

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

855 

856 for i in range(3, 6): 

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

858 assert res.content == "New content." 

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

860 

861 

862def test_UpdateEvent_all_leaves_other_events_alone(db, moderator: Moderator): 

863 """update_all_future must only touch the occurrences of the event being edited.""" 

864 # creator of the event that gets edited 

865 user1, token1 = generate_user() 

866 # creator of an unrelated event in the same time window 

867 user2, token2 = generate_user() 

868 # community moderator, so that neither creator has edit rights on the other's event 

869 user3, token3 = generate_user() 

870 

871 with session_scope() as session: 

872 create_community(session, 0, 2, "Community", [user3], [], None) 

873 

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

875 

876 with events_session(token1) as api: 

877 edited_id = api.CreateEvent( 

878 events_pb2.CreateEventReq( 

879 title="Edited Event", 

880 content="0th occurrence", 

881 location=events_pb2.EventLocation( 

882 address="Near Null Island", 

883 lat=0.1, 

884 lng=0.2, 

885 ), 

886 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

887 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=1)), 

888 ) 

889 ).event_id 

890 

891 second_id = api.ScheduleEvent( 

892 events_pb2.ScheduleEventReq( 

893 event_id=edited_id, 

894 content="1th occurrence", 

895 location=events_pb2.EventLocation( 

896 address="Near Null Island", 

897 lat=0.1, 

898 lng=0.2, 

899 ), 

900 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2)), 

901 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=3)), 

902 ) 

903 ).event_id 

904 

905 # an occurrence of a different event, starting after the edited one and ending well after the cutoff 

906 with events_session(token2) as api: 

907 other_id = api.CreateEvent( 

908 events_pb2.CreateEventReq( 

909 title="Other Event", 

910 content="Other content.", 

911 location=events_pb2.EventLocation( 

912 address="Somewhere else entirely", 

913 lat=0.5, 

914 lng=0.5, 

915 ), 

916 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=4)), 

917 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=5)), 

918 ) 

919 ).event_id 

920 

921 for occurrence_id in (edited_id, second_id, other_id): 

922 moderator.approve_event_occurrence(occurrence_id) 

923 

924 with events_session(token2) as api: 

925 other_before = api.GetEvent(events_pb2.GetEventReq(event_id=other_id)) 

926 

927 with events_session(token1) as api: 

928 api.UpdateEvent( 

929 events_pb2.UpdateEventReq( 

930 event_id=edited_id, 

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

932 location=events_pb2.EventLocation( 

933 address="Not so near Null Island", 

934 lat=0.2, 

935 lng=0.2, 

936 ), 

937 update_all_future=True, 

938 ) 

939 ) 

940 

941 with events_session(token3) as api: 

942 for occurrence_id in (edited_id, second_id): 

943 res = api.GetEvent(events_pb2.GetEventReq(event_id=occurrence_id)) 

944 assert res.content == "New content." 

945 assert res.location.address == "Not so near Null Island" 

946 

947 res = api.GetEvent(events_pb2.GetEventReq(event_id=other_id)) 

948 assert res.content == "Other content." 

949 assert res.location.address == "Somewhere else entirely" 

950 assert res.location.lat == 0.5 

951 assert res.location.lng == 0.5 

952 assert res.timezone == other_before.timezone 

953 assert res.last_edited == other_before.last_edited 

954 

955 

956def test_UpdateEvent_all_cant_change_times(db, moderator: Moderator): 

957 """Every future occurrence would get the same times, which the exclusion constraint forbids.""" 

958 user1, token1 = generate_user() 

959 user2, token2 = generate_user() 

960 

961 with session_scope() as session: 

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

963 

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

965 

966 with events_session(token1) as api: 

967 event_id = api.CreateEvent( 

968 events_pb2.CreateEventReq( 

969 title="Dummy Title", 

970 content="0th occurrence", 

971 location=events_pb2.EventLocation( 

972 address="Near Null Island", 

973 lat=0.1, 

974 lng=0.2, 

975 ), 

976 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

977 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=1)), 

978 ) 

979 ).event_id 

980 

981 api.ScheduleEvent( 

982 events_pb2.ScheduleEventReq( 

983 event_id=event_id, 

984 content="1th occurrence", 

985 location=events_pb2.EventLocation( 

986 address="Near Null Island", 

987 lat=0.1, 

988 lng=0.2, 

989 ), 

990 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2)), 

991 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=3)), 

992 ) 

993 ) 

994 

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

996 api.UpdateEvent( 

997 events_pb2.UpdateEventReq( 

998 event_id=event_id, 

999 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

1000 value=datetime_to_iso8601_local(start_time + timedelta(minutes=30)) 

1001 ), 

1002 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

1003 value=datetime_to_iso8601_local(start_time + timedelta(hours=1, minutes=30)) 

1004 ), 

1005 update_all_future=True, 

1006 ) 

1007 ) 

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

1009 assert e.value.details() == "You cannot update all events if you're modifying start or end times." 

1010 

1011 

1012def test_GetEvent(db, frozen_timewarp, moderator: Moderator): 

1013 # event creator 

1014 user1, token1 = generate_user() 

1015 # community moderator 

1016 user2, token2 = generate_user() 

1017 # third parties 

1018 user3, token3 = generate_user() 

1019 user4, token4 = generate_user() 

1020 user5, token5 = generate_user() 

1021 user6, token6 = generate_user() 

1022 

1023 with session_scope() as session: 

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

1025 

1026 time_before = now() 

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

1028 end_time = start_time + timedelta(hours=3) 

1029 

1030 with events_session(token1) as api: 

1031 # in person event 

1032 res = api.CreateEvent( 

1033 events_pb2.CreateEventReq( 

1034 title="Dummy Title", 

1035 content="Dummy content.", 

1036 location=events_pb2.EventLocation( 

1037 address="Near Null Island", 

1038 lat=0.1, 

1039 lng=0.2, 

1040 ), 

1041 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1042 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1043 ) 

1044 ) 

1045 

1046 event_id = res.event_id 

1047 

1048 moderator.approve_event_occurrence(event_id) 

1049 

1050 with events_session(token4) as api: 

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

1052 

1053 with events_session(token5) as api: 

1054 api.SetEventAttendance( 

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

1056 ) 

1057 

1058 with events_session(token6) as api: 

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

1060 

1061 with events_session(token1) as api: 

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

1063 

1064 assert res.is_next 

1065 assert res.title == "Dummy Title" 

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

1067 assert res.content == "Dummy content." 

1068 assert not res.photo_url 

1069 assert res.HasField("location") 

1070 assert res.location.lat == 0.1 

1071 assert res.location.lng == 0.2 

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

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

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

1075 assert res.creator_user_id == user1.id 

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

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

1078 assert is_utc_or_gmt(res.timezone) 

1079 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

1080 assert res.organizer 

1081 assert res.subscriber 

1082 assert res.going_count == 2 

1083 assert res.organizer_count == 1 

1084 assert res.subscriber_count == 3 

1085 assert res.owner_user_id == user1.id 

1086 assert not res.owner_community_id 

1087 assert not res.owner_group_id 

1088 assert res.thread.thread_id 

1089 assert res.can_edit 

1090 assert not res.can_moderate 

1091 

1092 with events_session(token2) as api: 

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

1094 

1095 assert res.is_next 

1096 assert res.title == "Dummy Title" 

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

1098 assert res.content == "Dummy content." 

1099 assert not res.photo_url 

1100 assert res.HasField("location") 

1101 assert res.location.lat == 0.1 

1102 assert res.location.lng == 0.2 

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

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

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

1106 assert res.creator_user_id == user1.id 

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

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

1109 assert is_utc_or_gmt(res.timezone) 

1110 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1111 assert not res.organizer 

1112 assert not res.subscriber 

1113 assert res.going_count == 2 

1114 assert res.organizer_count == 1 

1115 assert res.subscriber_count == 3 

1116 assert res.owner_user_id == user1.id 

1117 assert not res.owner_community_id 

1118 assert not res.owner_group_id 

1119 assert res.thread.thread_id 

1120 assert res.can_edit 

1121 assert res.can_moderate 

1122 

1123 with events_session(token3) as api: 

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

1125 

1126 assert res.is_next 

1127 assert res.title == "Dummy Title" 

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

1129 assert res.content == "Dummy content." 

1130 assert not res.photo_url 

1131 assert res.HasField("location") 

1132 assert res.location.lat == 0.1 

1133 assert res.location.lng == 0.2 

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

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

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

1137 assert res.creator_user_id == user1.id 

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

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

1140 assert is_utc_or_gmt(res.timezone) 

1141 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1142 assert not res.organizer 

1143 assert not res.subscriber 

1144 assert res.going_count == 2 

1145 assert res.organizer_count == 1 

1146 assert res.subscriber_count == 3 

1147 assert res.owner_user_id == user1.id 

1148 assert not res.owner_community_id 

1149 assert not res.owner_group_id 

1150 assert res.thread.thread_id 

1151 assert not res.can_edit 

1152 assert not res.can_moderate 

1153 

1154 

1155def test_CancelEvent(db, moderator: Moderator): 

1156 # event creator 

1157 user1, token1 = generate_user() 

1158 # community moderator 

1159 user2, token2 = generate_user() 

1160 # third parties 

1161 user3, token3 = generate_user() 

1162 user4, token4 = generate_user() 

1163 user5, token5 = generate_user() 

1164 user6, token6 = generate_user() 

1165 

1166 with session_scope() as session: 

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

1168 

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

1170 end_time = start_time + timedelta(hours=3) 

1171 

1172 with events_session(token1) as api: 

1173 res = api.CreateEvent( 

1174 events_pb2.CreateEventReq( 

1175 title="Dummy Title", 

1176 content="Dummy content.", 

1177 location=events_pb2.EventLocation( 

1178 address="Near Null Island", 

1179 lat=0.1, 

1180 lng=0.2, 

1181 ), 

1182 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1183 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1184 ) 

1185 ) 

1186 

1187 event_id = res.event_id 

1188 

1189 moderator.approve_event_occurrence(event_id) 

1190 

1191 with events_session(token4) as api: 

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

1193 

1194 with events_session(token5) as api: 

1195 api.SetEventAttendance( 

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

1197 ) 

1198 

1199 with events_session(token6) as api: 

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

1201 

1202 with events_session(token1) as api: 

1203 res = api.CancelEvent( 

1204 events_pb2.CancelEventReq( 

1205 event_id=event_id, 

1206 ) 

1207 ) 

1208 

1209 with events_session(token1) as api: 

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

1211 assert res.is_cancelled 

1212 

1213 with events_session(token1) as api: 

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

1215 api.UpdateEvent( 

1216 events_pb2.UpdateEventReq( 

1217 event_id=event_id, 

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

1219 ) 

1220 ) 

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

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

1223 

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

1225 api.InviteEventOrganizer( 

1226 events_pb2.InviteEventOrganizerReq( 

1227 event_id=event_id, 

1228 user_id=user3.id, 

1229 ) 

1230 ) 

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

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

1233 

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

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

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

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

1238 

1239 with events_session(token3) as api: 

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

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

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

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

1244 

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

1246 api.SetEventAttendance( 

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

1248 ) 

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

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

1251 

1252 with events_session(token1) as api: 

1253 for include_cancelled in [True, False]: 

1254 res = api.ListEventOccurrences( 

1255 events_pb2.ListEventOccurrencesReq( 

1256 event_id=event_id, 

1257 include_cancelled=include_cancelled, 

1258 ) 

1259 ) 

1260 if include_cancelled: 

1261 assert len(res.events) > 0 

1262 else: 

1263 assert len(res.events) == 0 

1264 

1265 res = api.ListMyEvents( 

1266 events_pb2.ListMyEventsReq( 

1267 include_cancelled=include_cancelled, 

1268 ) 

1269 ) 

1270 if include_cancelled: 

1271 assert len(res.events) > 0 

1272 else: 

1273 assert len(res.events) == 0 

1274 

1275 

1276def test_ListEventAttendees(db, moderator: Moderator): 

1277 # event creator 

1278 user1, token1 = generate_user() 

1279 # others 

1280 user2, token2 = generate_user() 

1281 user3, token3 = generate_user() 

1282 user4, token4 = generate_user() 

1283 user5, token5 = generate_user() 

1284 user6, token6 = generate_user() 

1285 

1286 with session_scope() as session: 

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

1288 

1289 with events_session(token1) as api: 

1290 event_id = api.CreateEvent( 

1291 events_pb2.CreateEventReq( 

1292 title="Dummy Title", 

1293 content="Dummy content.", 

1294 location=events_pb2.EventLocation( 

1295 address="Near Null Island", 

1296 lat=0.1, 

1297 lng=0.2, 

1298 ), 

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

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

1301 ) 

1302 ).event_id 

1303 

1304 moderator.approve_event_occurrence(event_id) 

1305 

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

1307 with events_session(token) as api: 

1308 api.SetEventAttendance( 

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

1310 ) 

1311 

1312 with events_session(token6) as api: 

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

1314 

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

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

1317 

1318 res = api.ListEventAttendees( 

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

1320 ) 

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

1322 

1323 res = api.ListEventAttendees( 

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

1325 ) 

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

1327 assert not res.next_page_token 

1328 

1329 

1330def test_ListEventSubscribers(db, moderator: Moderator): 

1331 # event creator 

1332 user1, token1 = generate_user() 

1333 # others 

1334 user2, token2 = generate_user() 

1335 user3, token3 = generate_user() 

1336 user4, token4 = generate_user() 

1337 user5, token5 = generate_user() 

1338 user6, token6 = generate_user() 

1339 

1340 with session_scope() as session: 

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

1342 

1343 with events_session(token1) as api: 

1344 event_id = api.CreateEvent( 

1345 events_pb2.CreateEventReq( 

1346 title="Dummy Title", 

1347 content="Dummy content.", 

1348 location=events_pb2.EventLocation( 

1349 address="Near Null Island", 

1350 lat=0.1, 

1351 lng=0.2, 

1352 ), 

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

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

1355 ) 

1356 ).event_id 

1357 

1358 moderator.approve_event_occurrence(event_id) 

1359 

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

1361 with events_session(token) as api: 

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

1363 

1364 with events_session(token6) as api: 

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

1366 

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

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

1369 

1370 res = api.ListEventSubscribers( 

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

1372 ) 

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

1374 

1375 res = api.ListEventSubscribers( 

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

1377 ) 

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

1379 assert not res.next_page_token 

1380 

1381 

1382def test_ListEventOrganizers(db, moderator: Moderator): 

1383 # event creator 

1384 user1, token1 = generate_user() 

1385 # others 

1386 user2, token2 = generate_user() 

1387 user3, token3 = generate_user() 

1388 user4, token4 = generate_user() 

1389 user5, token5 = generate_user() 

1390 user6, token6 = generate_user() 

1391 

1392 with session_scope() as session: 

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

1394 

1395 with events_session(token1) as api: 

1396 event_id = api.CreateEvent( 

1397 events_pb2.CreateEventReq( 

1398 title="Dummy Title", 

1399 content="Dummy content.", 

1400 location=events_pb2.EventLocation( 

1401 address="Near Null Island", 

1402 lat=0.1, 

1403 lng=0.2, 

1404 ), 

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

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

1407 ) 

1408 ).event_id 

1409 

1410 moderator.approve_event_occurrence(event_id) 

1411 

1412 with events_session(token1) as api: 

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

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

1415 

1416 with events_session(token6) as api: 

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

1418 

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

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

1421 

1422 res = api.ListEventOrganizers( 

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

1424 ) 

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

1426 

1427 res = api.ListEventOrganizers( 

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

1429 ) 

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

1431 assert not res.next_page_token 

1432 

1433 

1434def test_TransferEvent(db): 

1435 user1, token1 = generate_user() 

1436 user2, token2 = generate_user() 

1437 user3, token3 = generate_user() 

1438 user4, token4 = generate_user() 

1439 

1440 with session_scope() as session: 

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

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

1443 c_id = c.id 

1444 h_id = h.id 

1445 

1446 with events_session(token1) as api: 

1447 event_id = api.CreateEvent( 

1448 events_pb2.CreateEventReq( 

1449 title="Dummy Title", 

1450 content="Dummy content.", 

1451 location=events_pb2.EventLocation( 

1452 address="Near Null Island", 

1453 lat=0.1, 

1454 lng=0.2, 

1455 ), 

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

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

1458 ) 

1459 ).event_id 

1460 

1461 api.TransferEvent( 

1462 events_pb2.TransferEventReq( 

1463 event_id=event_id, 

1464 new_owner_community_id=c_id, 

1465 ) 

1466 ) 

1467 

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

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

1470 

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

1472 api.TransferEvent( 

1473 events_pb2.TransferEventReq( 

1474 event_id=event_id, 

1475 new_owner_group_id=h_id, 

1476 ) 

1477 ) 

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

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

1480 

1481 event_id = api.CreateEvent( 

1482 events_pb2.CreateEventReq( 

1483 title="Dummy Title", 

1484 content="Dummy content.", 

1485 location=events_pb2.EventLocation( 

1486 address="Near Null Island", 

1487 lat=0.1, 

1488 lng=0.2, 

1489 ), 

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

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

1492 ) 

1493 ).event_id 

1494 

1495 api.TransferEvent( 

1496 events_pb2.TransferEventReq( 

1497 event_id=event_id, 

1498 new_owner_group_id=h_id, 

1499 ) 

1500 ) 

1501 

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

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

1504 

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

1506 api.TransferEvent( 

1507 events_pb2.TransferEventReq( 

1508 event_id=event_id, 

1509 new_owner_community_id=c_id, 

1510 ) 

1511 ) 

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

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

1514 

1515 

1516def test_SetEventSubscription(db, moderator: Moderator): 

1517 user1, token1 = generate_user() 

1518 user2, token2 = generate_user() 

1519 

1520 with session_scope() as session: 

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

1522 

1523 with events_session(token1) as api: 

1524 event_id = api.CreateEvent( 

1525 events_pb2.CreateEventReq( 

1526 title="Dummy Title", 

1527 content="Dummy content.", 

1528 location=events_pb2.EventLocation( 

1529 address="Near Null Island", 

1530 lat=0.1, 

1531 lng=0.2, 

1532 ), 

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

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

1535 ) 

1536 ).event_id 

1537 

1538 moderator.approve_event_occurrence(event_id) 

1539 

1540 with events_session(token2) as api: 

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

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

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

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

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

1546 

1547 

1548def test_SetEventAttendance(db, moderator: Moderator): 

1549 user1, token1 = generate_user() 

1550 user2, token2 = generate_user() 

1551 

1552 with session_scope() as session: 

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

1554 

1555 with events_session(token1) as api: 

1556 event_id = api.CreateEvent( 

1557 events_pb2.CreateEventReq( 

1558 title="Dummy Title", 

1559 content="Dummy content.", 

1560 location=events_pb2.EventLocation( 

1561 address="Near Null Island", 

1562 lat=0.1, 

1563 lng=0.2, 

1564 ), 

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

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

1567 ) 

1568 ).event_id 

1569 

1570 moderator.approve_event_occurrence(event_id) 

1571 

1572 with events_session(token2) as api: 

1573 assert ( 

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

1575 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1576 ) 

1577 api.SetEventAttendance( 

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

1579 ) 

1580 assert ( 

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

1582 == events_pb2.ATTENDANCE_STATE_GOING 

1583 ) 

1584 api.SetEventAttendance( 

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

1586 ) 

1587 assert ( 

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

1589 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1590 ) 

1591 

1592 

1593def test_InviteEventOrganizer(db, moderator: Moderator): 

1594 user1, token1 = generate_user() 

1595 user2, token2 = generate_user() 

1596 

1597 with session_scope() as session: 

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

1599 

1600 with events_session(token1) as api: 

1601 event_id = api.CreateEvent( 

1602 events_pb2.CreateEventReq( 

1603 title="Dummy Title", 

1604 content="Dummy content.", 

1605 location=events_pb2.EventLocation( 

1606 address="Near Null Island", 

1607 lat=0.1, 

1608 lng=0.2, 

1609 ), 

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

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

1612 ) 

1613 ).event_id 

1614 

1615 moderator.approve_event_occurrence(event_id) 

1616 

1617 with events_session(token2) as api: 

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

1619 

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

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

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

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

1624 

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

1626 

1627 with events_session(token1) as api: 

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

1629 

1630 with events_session(token2) as api: 

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

1632 

1633 

1634def test_ListEventOccurrences(db): 

1635 user1, token1 = generate_user() 

1636 user2, token2 = generate_user() 

1637 user3, token3 = generate_user() 

1638 

1639 with session_scope() as session: 

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

1641 

1642 start = now() 

1643 

1644 event_ids = [] 

1645 

1646 with events_session(token1) as api: 

1647 res = api.CreateEvent( 

1648 events_pb2.CreateEventReq( 

1649 title="First occurrence", 

1650 content="Dummy content.", 

1651 parent_community_id=c_id, 

1652 location=events_pb2.EventLocation( 

1653 address="Near Null Island", 

1654 lat=0.1, 

1655 lng=0.2, 

1656 ), 

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

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

1659 ) 

1660 ) 

1661 

1662 event_ids.append(res.event_id) 

1663 

1664 for i in range(5): 

1665 res = api.ScheduleEvent( 

1666 events_pb2.ScheduleEventReq( 

1667 event_id=event_ids[-1], 

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

1669 location=events_pb2.EventLocation( 

1670 address="Near Null Island", 

1671 lat=0.1, 

1672 lng=0.2, 

1673 ), 

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

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

1676 ) 

1677 ) 

1678 

1679 event_ids.append(res.event_id) 

1680 

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

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

1683 

1684 res = api.ListEventOccurrences( 

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

1686 ) 

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

1688 

1689 res = api.ListEventOccurrences( 

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

1691 ) 

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

1693 assert not res.next_page_token 

1694 

1695 

1696def test_ListMyEvents(db, moderator: Moderator): 

1697 user1, token1 = generate_user() 

1698 user2, token2 = generate_user() 

1699 user3, token3 = generate_user() 

1700 user4, token4 = generate_user() 

1701 user5, token5 = generate_user() 

1702 

1703 with session_scope() as session: 

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

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

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

1707 c_id = global_community.id 

1708 macroregion_community = create_community( 

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

1710 ) 

1711 region_community = create_community( 

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

1713 ) 

1714 subregion_community = create_community( 

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

1716 ) 

1717 c2_id = subregion_community.id 

1718 

1719 start = now() 

1720 

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

1722 return events_pb2.CreateEventReq( 

1723 title="Dummy Title", 

1724 content="Dummy content.", 

1725 location=events_pb2.EventLocation( 

1726 address="Near Null Island", 

1727 lat=0.1, 

1728 lng=0.2, 

1729 ), 

1730 parent_community_id=community_id, 

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

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

1733 ) 

1734 

1735 with events_session(token1) as api: 

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

1737 

1738 moderator.approve_event_occurrence(e2) 

1739 

1740 with events_session(token2) as api: 

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

1742 

1743 moderator.approve_event_occurrence(e1) 

1744 

1745 with events_session(token1) as api: 

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

1747 

1748 moderator.approve_event_occurrence(e3) 

1749 

1750 with events_session(token2) as api: 

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

1752 

1753 moderator.approve_event_occurrence(e5) 

1754 

1755 with events_session(token3) as api: 

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

1757 

1758 moderator.approve_event_occurrence(e4) 

1759 

1760 with events_session(token4) as api: 

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

1762 

1763 moderator.approve_event_occurrence(e6) 

1764 

1765 with events_session(token1) as api: 

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

1767 

1768 with events_session(token1) as api: 

1769 api.SetEventAttendance( 

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

1771 ) 

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

1773 

1774 with events_session(token2) as api: 

1775 api.SetEventAttendance( 

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

1777 ) 

1778 

1779 with events_session(token3) as api: 

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

1781 

1782 with events_session(token1) as api: 

1783 # test pagination with token first 

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

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

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

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

1788 assert not res.next_page_token 

1789 

1790 res = api.ListMyEvents( 

1791 events_pb2.ListMyEventsReq( 

1792 subscribed=True, 

1793 attending=True, 

1794 organizing=True, 

1795 ) 

1796 ) 

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

1798 

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

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

1801 

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

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

1804 

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

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

1807 

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

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

1810 

1811 with events_session(token1) as api: 

1812 # Test pagination with page_number and verify total_items 

1813 res = api.ListMyEvents( 

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

1815 ) 

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

1817 assert res.total_items == 4 

1818 

1819 res = api.ListMyEvents( 

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

1821 ) 

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

1823 assert res.total_items == 4 

1824 

1825 # Verify no more pages 

1826 res = api.ListMyEvents( 

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

1828 ) 

1829 assert not res.events 

1830 assert res.total_items == 4 

1831 

1832 with events_session(token2) as api: 

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

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

1835 

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

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

1838 

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

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

1841 

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

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

1844 

1845 with events_session(token3) as api: 

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

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

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

1849 

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

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

1852 

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

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

1855 

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

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

1858 

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

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

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

1862 

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

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

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

1866 

1867 # my_communities_exclude_global works independently of my_communities flag 

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

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

1870 

1871 # my_communities_exclude_global filters organizing results too 

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

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

1874 

1875 # my_communities_exclude_global filters subscribed results too 

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

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

1878 

1879 with events_session(token5) as api: 

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

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

1882 

1883 

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

1885 event_ids = [] 

1886 page_token = "" 

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

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

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

1890 page_token = res.next_page_token 

1891 if not page_token: 

1892 return event_ids 

1893 raise AssertionError("pagination did not terminate") 

1894 

1895 

1896def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator): 

1897 user, token = generate_user() 

1898 

1899 with session_scope() as session: 

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

1901 

1902 start = now() 

1903 

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

1905 return events_pb2.CreateEventReq( 

1906 title="Dummy Title", 

1907 content="Dummy content.", 

1908 location=events_pb2.EventLocation( 

1909 address="Near Null Island", 

1910 lat=0.1, 

1911 lng=0.2, 

1912 ), 

1913 parent_community_id=c_id, 

1914 start_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset), 

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

1916 ) 

1917 

1918 with events_session(token) as api: 

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

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

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

1922 short_events = [ 

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

1924 ] 

1925 

1926 for event_id in [long_event, *short_events]: 

1927 moderator.approve_event_occurrence(event_id) 

1928 

1929 with events_session(token) as api: 

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

1931 

1932 

1933def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator): 

1934 user, token = generate_user() 

1935 

1936 with session_scope() as session: 

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

1938 

1939 start = now() 

1940 

1941 with events_session(token) as api: 

1942 event_ids = [ 

1943 api.CreateEvent( 

1944 events_pb2.CreateEventReq( 

1945 title="Dummy Title", 

1946 content="Dummy content.", 

1947 location=events_pb2.EventLocation( 

1948 address="Near Null Island", 

1949 lat=0.1, 

1950 lng=0.2, 

1951 ), 

1952 parent_community_id=c_id, 

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

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

1955 ) 

1956 ).event_id 

1957 for _ in range(5) 

1958 ] 

1959 

1960 for event_id in event_ids: 

1961 moderator.approve_event_occurrence(event_id) 

1962 

1963 with events_session(token) as api: 

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

1965 

1966 

1967def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1968 user1, token1 = generate_user() 

1969 user2, token2 = generate_user() 

1970 

1971 with session_scope() as session: 

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

1973 c_id = c.id 

1974 

1975 start = now() 

1976 

1977 def make_event(hours): 

1978 return events_pb2.CreateEventReq( 

1979 title="Test Event", 

1980 content="Test content.", 

1981 location=events_pb2.EventLocation( 

1982 address="Near Null Island", 

1983 lat=0.1, 

1984 lng=0.2, 

1985 ), 

1986 parent_community_id=c_id, 

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

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

1989 ) 

1990 

1991 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1992 with events_session(token1) as api: 

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

1994 

1995 with events_session(token2) as api: 

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

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

1998 # e_both: user1 will be both organizer and attendee 

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

2000 

2001 moderator.approve_event_occurrence(e_own) 

2002 moderator.approve_event_occurrence(e_attending) 

2003 moderator.approve_event_occurrence(e_community_only) 

2004 moderator.approve_event_occurrence(e_both) 

2005 

2006 # invite user1 as organizer of e_both 

2007 with events_session(token2) as api: 

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

2009 

2010 # user1 RSVPs to e_attending and e_both 

2011 with events_session(token1) as api: 

2012 api.SetEventAttendance( 

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

2014 ) 

2015 api.SetEventAttendance( 

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

2017 ) 

2018 

2019 with events_session(token1) as api: 

2020 # baseline: all four community events visible 

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

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

2023 

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

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

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

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

2028 

2029 # exclude_attending with attending=True: invalid combination 

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

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

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

2033 

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

2035 with events_session(token2) as api: 

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

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

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

2039 

2040 

2041def test_RemoveEventOrganizer(db, moderator: Moderator): 

2042 user1, token1 = generate_user() 

2043 user2, token2 = generate_user() 

2044 

2045 with session_scope() as session: 

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

2047 

2048 with events_session(token1) as api: 

2049 event_id = api.CreateEvent( 

2050 events_pb2.CreateEventReq( 

2051 title="Dummy Title", 

2052 content="Dummy content.", 

2053 location=events_pb2.EventLocation( 

2054 address="Near Null Island", 

2055 lat=0.1, 

2056 lng=0.2, 

2057 ), 

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

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

2060 ) 

2061 ).event_id 

2062 

2063 moderator.approve_event_occurrence(event_id) 

2064 

2065 with events_session(token2) as api: 

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

2067 

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

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

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

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

2072 

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

2074 

2075 with events_session(token1) as api: 

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

2077 

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

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

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

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

2082 

2083 with events_session(token2) as api: 

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

2085 assert res.organizer 

2086 assert res.organizer_count == 2 

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

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

2089 

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

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

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

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

2094 

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

2096 assert not res.organizer 

2097 assert res.organizer_count == 1 

2098 

2099 # Test that event owner can remove co-organizers 

2100 with events_session(token1) as api: 

2101 # Add user2 back as organizer 

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

2103 

2104 # Verify user2 is now an organizer 

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

2106 assert res.organizer_count == 2 

2107 

2108 # Event owner can remove co-organizer 

2109 api.RemoveEventOrganizer( 

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

2111 ) 

2112 

2113 # Verify user2 is no longer an organizer 

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

2115 assert res.organizer_count == 1 

2116 

2117 # Test that non-organizers cannot remove other organizers 

2118 with events_session(token2) as api: 

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

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

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

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

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

2124 

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

2126 with events_session(token1) as api: 

2127 # Add user2 back as organizer 

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

2129 

2130 

2131def test_ListEventAttendees_regression(db): 

2132 # see issue #1617: 

2133 # 

2134 # 1. Create an event 

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

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

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

2138 # 

2139 # **Expected behaviour** 

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

2141 # 

2142 # **Actual/current behaviour** 

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

2144 

2145 user1, token1 = generate_user() 

2146 user2, token2 = generate_user() 

2147 user3, token3 = generate_user() 

2148 user4, token4 = generate_user() 

2149 user5, token5 = generate_user() 

2150 

2151 with session_scope() as session: 

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

2153 

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

2155 end_time = start_time + timedelta(hours=3) 

2156 

2157 with events_session(token1) as api: 

2158 res = api.CreateEvent( 

2159 events_pb2.CreateEventReq( 

2160 title="Dummy Title", 

2161 content="Dummy content.", 

2162 location=events_pb2.EventLocation( 

2163 address="Near Null Island", 

2164 lat=0.1, 

2165 lng=0.2, 

2166 ), 

2167 parent_community_id=c_id, 

2168 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2169 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2170 ) 

2171 ) 

2172 

2173 res = api.TransferEvent( 

2174 events_pb2.TransferEventReq( 

2175 event_id=res.event_id, 

2176 new_owner_community_id=c_id, 

2177 ) 

2178 ) 

2179 

2180 event_id = res.event_id 

2181 

2182 api.SetEventAttendance( 

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

2184 ) 

2185 api.SetEventAttendance( 

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

2187 ) 

2188 

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

2190 assert len(res.attendee_user_ids) == 1 

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

2192 

2193 

2194def test_GetEventCalendarFile(db, moderator: Moderator): 

2195 user1, token1 = generate_user() 

2196 user2, token2 = generate_user() 

2197 

2198 with session_scope() as session: 

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

2200 

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

2202 end_time = start_time + timedelta(hours=3) 

2203 

2204 with events_session(token1) as api: 

2205 created_event: events_pb2.Event = api.CreateEvent( 

2206 events_pb2.CreateEventReq( 

2207 title="Dummy Title", 

2208 content="Dummy content.", 

2209 parent_community_id=c_id, 

2210 location=events_pb2.EventLocation( 

2211 address="Near Null Island", 

2212 lat=0.1, 

2213 lng=0.2, 

2214 ), 

2215 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2216 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2217 ) 

2218 ) 

2219 event_id = created_event.event_id 

2220 

2221 moderator.approve_event_occurrence(event_id) 

2222 

2223 with events_session(token1) as api: 

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

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

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

2227 assert "SUMMARY:Dummy Title" in ics_string 

2228 assert "DESCRIPTION:Dummy content." in ics_string 

2229 assert "LOCATION:Near Null Island" in ics_string 

2230 assert "STATUS:CANCELLED" not in ics_string 

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

2232 assert pre_cancel_match is not None 

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

2234 

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

2236 

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

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

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

2240 assert "STATUS:CANCELLED" in ics_string 

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

2242 assert post_cancel_match is not None 

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

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

2245 assert post_cancel_sequence >= pre_cancel_sequence 

2246 

2247 

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

2249 user1, token1 = generate_user() 

2250 user2, token2 = generate_user() 

2251 user3, token3 = generate_user() 

2252 user4, token4 = generate_user() 

2253 

2254 with session_scope() as session: 

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

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

2257 c_id = c.id 

2258 h_id = h.id 

2259 user4_id = user4.id 

2260 

2261 with events_session(token1) as api: 

2262 event = api.CreateEvent( 

2263 events_pb2.CreateEventReq( 

2264 title="Dummy Title", 

2265 content="Dummy content.", 

2266 location=events_pb2.EventLocation( 

2267 address="Near Null Island", 

2268 lat=0.1, 

2269 lng=0.2, 

2270 ), 

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

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

2273 ) 

2274 ) 

2275 

2276 moderator.approve_event_occurrence(event.event_id) 

2277 

2278 with threads_session(token2) as api: 

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

2280 

2281 moderator.approve_thread_post(reply_id) 

2282 

2283 with events_session(token3) as api: 

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

2285 assert res.thread.num_responses == 1 

2286 

2287 with threads_session(token3) as api: 

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

2289 assert len(ret.replies) == 1 

2290 assert not ret.next_page_token 

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

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

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

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

2295 

2296 nested_reply_id = api.PostReply( 

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

2298 ).thread_id 

2299 

2300 moderator.approve_thread_post(nested_reply_id) 

2301 

2302 process_jobs() 

2303 

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

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

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

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

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

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

2310 

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

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

2313 

2314 assert push_collector.count_for_user(user4_id) == 0 

2315 

2316 

2317def test_can_overlap_other_events_schedule_regression(db): 

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

2319 user, token = generate_user() 

2320 

2321 with session_scope() as session: 

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

2323 

2324 start = now() 

2325 

2326 with events_session(token) as api: 

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

2328 api.CreateEvent( 

2329 events_pb2.CreateEventReq( 

2330 title="Dummy Title", 

2331 content="Dummy content.", 

2332 parent_community_id=c_id, 

2333 location=events_pb2.EventLocation( 

2334 address="Near Null Island", 

2335 lat=0.1, 

2336 lng=0.2, 

2337 ), 

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

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

2340 ) 

2341 ) 

2342 

2343 # this event 

2344 res = api.CreateEvent( 

2345 events_pb2.CreateEventReq( 

2346 title="Dummy Title", 

2347 content="Dummy content.", 

2348 parent_community_id=c_id, 

2349 location=events_pb2.EventLocation( 

2350 address="Near Null Island", 

2351 lat=0.1, 

2352 lng=0.2, 

2353 ), 

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

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

2356 ) 

2357 ) 

2358 

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

2360 api.ScheduleEvent( 

2361 events_pb2.ScheduleEventReq( 

2362 event_id=res.event_id, 

2363 content="New event occurrence", 

2364 location=events_pb2.EventLocation( 

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

2366 lat=0.3, 

2367 lng=0.2, 

2368 ), 

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

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

2371 ) 

2372 ) 

2373 

2374 

2375def test_can_overlap_other_events_update_regression(db): 

2376 user, token = generate_user() 

2377 

2378 with session_scope() as session: 

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

2380 

2381 start = now() 

2382 

2383 with events_session(token) as api: 

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

2385 api.CreateEvent( 

2386 events_pb2.CreateEventReq( 

2387 title="Dummy Title", 

2388 content="Dummy content.", 

2389 parent_community_id=c_id, 

2390 location=events_pb2.EventLocation( 

2391 address="Near Null Island", 

2392 lat=0.1, 

2393 lng=0.2, 

2394 ), 

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

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

2397 ) 

2398 ) 

2399 

2400 res = api.CreateEvent( 

2401 events_pb2.CreateEventReq( 

2402 title="Dummy Title", 

2403 content="Dummy content.", 

2404 parent_community_id=c_id, 

2405 location=events_pb2.EventLocation( 

2406 address="Near Null Island", 

2407 lat=0.1, 

2408 lng=0.2, 

2409 ), 

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

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

2412 ) 

2413 ) 

2414 

2415 event_id = api.ScheduleEvent( 

2416 events_pb2.ScheduleEventReq( 

2417 event_id=res.event_id, 

2418 content="New event occurrence", 

2419 location=events_pb2.EventLocation( 

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

2421 lat=0.3, 

2422 lng=0.2, 

2423 ), 

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

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

2426 ) 

2427 ).event_id 

2428 

2429 # can overlap with this current existing occurrence 

2430 api.UpdateEvent( 

2431 events_pb2.UpdateEventReq( 

2432 event_id=event_id, 

2433 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2435 ), 

2436 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2438 ), 

2439 ) 

2440 ) 

2441 

2442 api.UpdateEvent( 

2443 events_pb2.UpdateEventReq( 

2444 event_id=event_id, 

2445 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2447 ), 

2448 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2450 ), 

2451 ) 

2452 ) 

2453 

2454 

2455def test_list_past_events_regression(db): 

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

2457 user, token = generate_user() 

2458 

2459 with session_scope() as session: 

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

2461 

2462 start = now() 

2463 

2464 with events_session(token) as api: 

2465 api.CreateEvent( 

2466 events_pb2.CreateEventReq( 

2467 title="Dummy Title", 

2468 content="Dummy content.", 

2469 parent_community_id=c_id, 

2470 location=events_pb2.EventLocation( 

2471 address="Near Null Island", 

2472 lat=0.1, 

2473 lng=0.2, 

2474 ), 

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

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

2477 ) 

2478 ) 

2479 

2480 with session_scope() as session: 

2481 session.execute( 

2482 update(EventOccurrence).values( 

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

2484 ) 

2485 ) 

2486 

2487 with events_session(token) as api: 

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

2489 assert len(res.events) == 1 

2490 

2491 

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

2493 user1, token1 = generate_user(complete_profile=True) 

2494 user2, token2 = generate_user() 

2495 user3, token3 = generate_user() 

2496 user4, token4 = generate_user() 

2497 user5, token5 = generate_user(is_superuser=True) 

2498 

2499 with session_scope() as session: 

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

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

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

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

2504 

2505 enforce_community_memberships() 

2506 

2507 with events_session(token1) as api: 

2508 res = api.CreateEvent( 

2509 events_pb2.CreateEventReq( 

2510 title="Dummy Title", 

2511 content="Dummy content.", 

2512 parent_community_id=c_id, 

2513 location=events_pb2.EventLocation( 

2514 address="Near Null Island", 

2515 lat=0.1, 

2516 lng=0.2, 

2517 ), 

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

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

2520 ) 

2521 ) 

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

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

2524 

2525 event_id = res.event_id 

2526 

2527 moderator.approve_event_occurrence(event_id) 

2528 

2529 with events_session(token1) as api: 

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

2531 

2532 email = email_collector.pop_for_mods(last=True) 

2533 

2534 assert user_url in email.plain 

2535 assert event_url in email.plain 

2536 

2537 # can't send another req 

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

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

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

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

2542 

2543 # another user can send one though 

2544 with events_session(token3) as api: 

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

2546 

2547 # but not a non-admin 

2548 with events_session(token2) as api: 

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

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

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

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

2553 

2554 with real_editor_session(token5) as editor: 

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

2556 assert len(res.requests) == 2 

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

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

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

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

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

2562 

2563 editor.DecideEventCommunityInviteRequest( 

2564 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2566 approve=False, 

2567 ) 

2568 ) 

2569 

2570 editor.DecideEventCommunityInviteRequest( 

2571 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2573 approve=True, 

2574 ) 

2575 ) 

2576 

2577 # not after approve 

2578 with events_session(token4) as api: 

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

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

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

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

2583 

2584 

2585def test_list_decided_community_invite_requests(db, moderator: Moderator): 

2586 user1, token1 = generate_user() 

2587 user2, token2 = generate_user() 

2588 user3, token3 = generate_user() 

2589 superuser, superuser_token = generate_user(is_superuser=True) 

2590 

2591 with session_scope() as session: 

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

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

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

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

2596 

2597 enforce_community_memberships() 

2598 

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

2600 with events_session(token) as api: 

2601 res = api.CreateEvent( 

2602 events_pb2.CreateEventReq( 

2603 title=title, 

2604 content="Dummy content.", 

2605 parent_community_id=c_id, 

2606 location=events_pb2.EventLocation( 

2607 address="Near Null Island", 

2608 lat=0.1, 

2609 lng=0.2, 

2610 ), 

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

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

2613 ) 

2614 ) 

2615 moderator.approve_event_occurrence(res.event_id) 

2616 with events_session(token) as api: 

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

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

2619 

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

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

2622 # this one stays pending 

2623 create_event_and_request_invite(token3, "Pending Event") 

2624 

2625 with real_editor_session(superuser_token) as editor: 

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

2627 assert len(pending.requests) == 3 

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

2629 

2630 # nothing decided yet 

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

2632 assert len(res.requests) == 0 

2633 assert not res.next_page_token 

2634 

2635 editor.DecideEventCommunityInviteRequest( 

2636 editor_pb2.DecideEventCommunityInviteRequestReq( 

2637 event_community_invite_request_id=by_user[user1.id], 

2638 approve=True, 

2639 ) 

2640 ) 

2641 editor.DecideEventCommunityInviteRequest( 

2642 editor_pb2.DecideEventCommunityInviteRequestReq( 

2643 event_community_invite_request_id=by_user[user2.id], 

2644 approve=False, 

2645 ) 

2646 ) 

2647 

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

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

2650 assert len(res.requests) == 2 

2651 assert not res.next_page_token 

2652 

2653 declined, approved = res.requests 

2654 

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

2656 assert declined.user_id == user2.id 

2657 assert declined.event_url == event2_url 

2658 assert declined.community_id == c_id 

2659 assert declined.decided_by_user_id == superuser.id 

2660 assert not declined.approved 

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

2662 

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

2664 assert approved.user_id == user1.id 

2665 assert approved.event_url == event1_url 

2666 assert approved.community_id == c_id 

2667 assert approved.decided_by_user_id == superuser.id 

2668 assert approved.approved 

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

2670 

2671 # filtering 

2672 res = editor.ListDecidedEventCommunityInviteRequests( 

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

2674 ) 

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

2676 

2677 res = editor.ListDecidedEventCommunityInviteRequests( 

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

2679 ) 

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

2681 

2682 # pagination 

2683 res = editor.ListDecidedEventCommunityInviteRequests( 

2684 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1) 

2685 ) 

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

2687 assert res.next_page_token 

2688 

2689 res = editor.ListDecidedEventCommunityInviteRequests( 

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

2691 ) 

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

2693 assert not res.next_page_token 

2694 

2695 

2696def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator): 

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

2698 # community invite notification when it is approved. 

2699 organizer, organizer_token = generate_user() 

2700 attendee, attendee_token = generate_user() 

2701 member, _ = generate_user() 

2702 superuser, superuser_token = generate_user(is_superuser=True) 

2703 

2704 with session_scope() as session: 

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

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

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

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

2709 

2710 enforce_community_memberships() 

2711 

2712 with events_session(organizer_token) as api: 

2713 event_id = api.CreateEvent( 

2714 events_pb2.CreateEventReq( 

2715 title="Dummy Title", 

2716 content="Dummy content.", 

2717 parent_community_id=c_id, 

2718 location=events_pb2.EventLocation( 

2719 address="Near Null Island", 

2720 lat=0.1, 

2721 lng=0.2, 

2722 ), 

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

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

2725 ) 

2726 ).event_id 

2727 

2728 moderator.approve_event_occurrence(event_id) 

2729 

2730 # the attendee RSVPs before the community invite is approved 

2731 with events_session(attendee_token) as api: 

2732 api.SetEventAttendance( 

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

2734 ) 

2735 

2736 with events_session(organizer_token) as api: 

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

2738 

2739 with real_editor_session(superuser_token) as editor: 

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

2741 editor.DecideEventCommunityInviteRequest( 

2742 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2744 approve=True, 

2745 ) 

2746 ) 

2747 

2748 process_jobs() 

2749 

2750 with session_scope() as session: 

2751 

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

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

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

2755 

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

2757 assert invite_notification_count(member.id) == 1 

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

2759 assert invite_notification_count(attendee.id) == 0 

2760 assert invite_notification_count(organizer.id) == 0 

2761 

2762 

2763def test_update_event_should_notify_queues_job(): 

2764 user, token = generate_user() 

2765 start = now() 

2766 

2767 with session_scope() as session: 

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

2769 

2770 # create an event 

2771 with events_session(token) as api: 

2772 create_res = api.CreateEvent( 

2773 events_pb2.CreateEventReq( 

2774 title="Dummy Title", 

2775 content="Dummy content.", 

2776 parent_community_id=c_id, 

2777 location=events_pb2.EventLocation( 

2778 address="Near Null Island", 

2779 lat=1.0, 

2780 lng=2.0, 

2781 ), 

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

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

2784 ) 

2785 ) 

2786 

2787 event_id = create_res.event_id 

2788 

2789 # measure initial background job queue length 

2790 with session_scope() as session: 

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

2792 job_length_before_update = len(jobs) 

2793 

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

2795 api.UpdateEvent( 

2796 events_pb2.UpdateEventReq( 

2797 event_id=event_id, 

2798 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2800 ), 

2801 should_notify=False, 

2802 ) 

2803 ) 

2804 

2805 with session_scope() as session: 

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

2807 assert len(jobs) == job_length_before_update 

2808 

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

2810 api.UpdateEvent( 

2811 events_pb2.UpdateEventReq( 

2812 event_id=event_id, 

2813 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2815 ), 

2816 should_notify=True, 

2817 ) 

2818 ) 

2819 

2820 with session_scope() as session: 

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

2822 assert len(jobs) == job_length_before_update + 1 

2823 

2824 

2825def test_event_photo_key(db): 

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

2827 user, token = generate_user() 

2828 

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

2830 end_time = start_time + timedelta(hours=3) 

2831 

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

2833 with session_scope() as session: 

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

2835 upload = Upload( 

2836 key="test_event_photo_key_123", 

2837 filename="test_event_photo_key_123.jpg", 

2838 creator_user_id=user.id, 

2839 ) 

2840 session.add(upload) 

2841 

2842 with events_session(token) as api: 

2843 # Create event without photo 

2844 res = api.CreateEvent( 

2845 events_pb2.CreateEventReq( 

2846 title="Event Without Photo", 

2847 content="No photo content.", 

2848 photo_key=None, 

2849 location=events_pb2.EventLocation( 

2850 address="Near Null Island", 

2851 lat=0.1, 

2852 lng=0.2, 

2853 ), 

2854 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2855 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2856 ) 

2857 ) 

2858 

2859 assert res.photo_key == "" 

2860 assert res.photo_url == "" 

2861 

2862 # Create event with photo 

2863 res_with_photo = api.CreateEvent( 

2864 events_pb2.CreateEventReq( 

2865 title="Event With Photo", 

2866 content="Has photo content.", 

2867 photo_key="test_event_photo_key_123", 

2868 location=events_pb2.EventLocation( 

2869 address="Near Null Island", 

2870 lat=0.1, 

2871 lng=0.2, 

2872 ), 

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

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

2875 ) 

2876 ) 

2877 

2878 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2879 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2880 

2881 event_id = res_with_photo.event_id 

2882 

2883 # Verify photo_key is returned when getting the event 

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

2885 assert get_res.photo_key == "test_event_photo_key_123" 

2886 assert "test_event_photo_key_123" in get_res.photo_url 

2887 

2888 

2889def test_event_timezone(db): 

2890 user, token = generate_user() 

2891 

2892 with session_scope() as session: 

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

2894 

2895 # Midnight future day, UTC timezone 

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

2897 end_time = start_time + timedelta(days=1) 

2898 

2899 with events_session(token) as api: 

2900 create_res: events_pb2.Event = api.CreateEvent( 

2901 events_pb2.CreateEventReq( 

2902 title="Dummy Title", 

2903 content="Dummy content.", 

2904 photo_key=None, 

2905 parent_community_id=c_id, 

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

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

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

2909 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2910 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2911 ) 

2912 ) 

2913 

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

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

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

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

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

2919 

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

2921 update_res: events_pb2.Event = api.UpdateEvent( 

2922 events_pb2.UpdateEventReq( 

2923 event_id=create_res.event_id, 

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

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

2926 ) 

2927 ) 

2928 

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

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

2931 # but now in the New York timezone. 

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

2933 assert update_res.start_time != create_res.start_time 

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

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

2936 

2937 # Also validate GetEvent 

2938 get_res: events_pb2.Event = api.GetEvent( 

2939 events_pb2.GetEventReq( 

2940 event_id=create_res.event_id, 

2941 ) 

2942 ) 

2943 

2944 assert get_res.timezone == update_res.timezone 

2945 assert get_res.start_time == update_res.start_time 

2946 

2947 

2948def test_event_created_with_shadowed_visibility(db): 

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

2950 user, token = generate_user() 

2951 

2952 with session_scope() as session: 

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

2954 

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

2956 end_time = start_time + timedelta(hours=3) 

2957 

2958 with events_session(token) as api: 

2959 res = api.CreateEvent( 

2960 events_pb2.CreateEventReq( 

2961 title="Test UMS Event", 

2962 content="UMS content.", 

2963 location=events_pb2.EventLocation( 

2964 address="Near Null Island", 

2965 lat=0.1, 

2966 lng=0.2, 

2967 ), 

2968 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2969 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2970 ) 

2971 ) 

2972 event_id = res.event_id 

2973 

2974 with session_scope() as session: 

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

2976 mod_state = session.execute( 

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

2978 ).scalar_one() 

2979 assert mod_state.visibility == ModerationVisibility.shadowed 

2980 

2981 

2982def test_shadowed_event_visible_to_creator_only(db): 

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

2984 user1, token1 = generate_user() 

2985 user2, token2 = generate_user() 

2986 

2987 with session_scope() as session: 

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

2989 

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

2991 end_time = start_time + timedelta(hours=3) 

2992 

2993 with events_session(token1) as api: 

2994 res = api.CreateEvent( 

2995 events_pb2.CreateEventReq( 

2996 title="Shadowed Event", 

2997 content="Content.", 

2998 location=events_pb2.EventLocation( 

2999 address="Near Null Island", 

3000 lat=0.1, 

3001 lng=0.2, 

3002 ), 

3003 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3004 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3005 ) 

3006 ) 

3007 event_id = res.event_id 

3008 

3009 # Creator can see it 

3010 with events_session(token1) as api: 

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

3012 assert res.title == "Shadowed Event" 

3013 

3014 # Other user cannot 

3015 with events_session(token2) as api: 

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

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

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

3019 

3020 

3021def test_event_visible_after_approval(db, moderator: Moderator): 

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

3023 user1, token1 = generate_user() 

3024 user2, token2 = generate_user() 

3025 

3026 with session_scope() as session: 

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

3028 

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

3030 end_time = start_time + timedelta(hours=3) 

3031 

3032 with events_session(token1) as api: 

3033 res = api.CreateEvent( 

3034 events_pb2.CreateEventReq( 

3035 title="Approved Event", 

3036 content="Content.", 

3037 location=events_pb2.EventLocation( 

3038 address="Near Null Island", 

3039 lat=0.1, 

3040 lng=0.2, 

3041 ), 

3042 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3043 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3044 ) 

3045 ) 

3046 event_id = res.event_id 

3047 

3048 # Other user cannot see it yet 

3049 with events_session(token2) as api: 

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

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

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

3053 

3054 # Approve the event 

3055 moderator.approve_event_occurrence(event_id) 

3056 

3057 # Now other user can see it 

3058 with events_session(token2) as api: 

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

3060 assert res.title == "Approved Event" 

3061 

3062 

3063def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

3065 user1, token1 = generate_user() 

3066 user2, token2 = generate_user() 

3067 

3068 with session_scope() as session: 

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

3070 

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

3072 end_time = start_time + timedelta(hours=3) 

3073 

3074 with events_session(token1) as api: 

3075 res = api.CreateEvent( 

3076 events_pb2.CreateEventReq( 

3077 title="List Test Event", 

3078 content="Content.", 

3079 location=events_pb2.EventLocation( 

3080 address="Near Null Island", 

3081 lat=0.1, 

3082 lng=0.2, 

3083 ), 

3084 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3085 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3086 ) 

3087 ) 

3088 event_id = res.event_id 

3089 

3090 # Creator can see their own SHADOWED event in lists 

3091 with events_session(token1) as api: 

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

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

3094 assert event_id in event_ids 

3095 

3096 # Other user cannot see the SHADOWED event in lists 

3097 with events_session(token2) as api: 

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

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

3100 assert event_id not in event_ids 

3101 

3102 # After approval, other user can see it 

3103 moderator.approve_event_occurrence(event_id) 

3104 

3105 with events_session(token2) as api: 

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

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

3108 assert event_id in event_ids 

3109 

3110 

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

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

3113 user1, token1 = generate_user() 

3114 user2, token2 = generate_user() 

3115 

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

3117 with session_scope() as session: 

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

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

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

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

3122 

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

3124 end_time = start_time + timedelta(hours=3) 

3125 

3126 with events_session(token1) as api: 

3127 res = api.CreateEvent( 

3128 events_pb2.CreateEventReq( 

3129 title="Deferred Event", 

3130 content="Content.", 

3131 location=events_pb2.EventLocation( 

3132 address="Near Null Island", 

3133 lat=0.1, 

3134 lng=0.2, 

3135 ), 

3136 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3137 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3138 ) 

3139 ) 

3140 event_id = res.event_id 

3141 

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

3143 process_jobs() 

3144 

3145 with session_scope() as session: 

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

3147 # Notification was created with moderation_state_id for deferral 

3148 assert notif.moderation_state_id is not None 

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

3150 delivery_count = session.execute( 

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

3152 ).scalar_one_or_none() 

3153 assert delivery_count is None 

3154 

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

3156 moderator.approve_event_occurrence(event_id) 

3157 

3158 # Verify handle_notification job was queued 

3159 with session_scope() as session: 

3160 pending_jobs = ( 

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

3162 .scalars() 

3163 .all() 

3164 ) 

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

3166 

3167 

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

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

3170 user1, token1 = generate_user() 

3171 user2, token2 = generate_user() 

3172 

3173 with session_scope() as session: 

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

3175 

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

3177 end_time = start_time + timedelta(hours=3) 

3178 

3179 with events_session(token1) as api: 

3180 res = api.CreateEvent( 

3181 events_pb2.CreateEventReq( 

3182 title="Update Test", 

3183 content="Content.", 

3184 location=events_pb2.EventLocation( 

3185 address="Near Null Island", 

3186 lat=0.1, 

3187 lng=0.2, 

3188 ), 

3189 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3190 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3191 ) 

3192 ) 

3193 event_id = res.event_id 

3194 

3195 moderator.approve_event_occurrence(event_id) 

3196 process_jobs() 

3197 # Clear any create notifications 

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

3199 push_collector.pop_for_user(user2.id) 

3200 

3201 # User2 subscribes to the event 

3202 with events_session(token2) as api: 

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

3204 

3205 # User1 updates the event with should_notify=True 

3206 with events_session(token1) as api: 

3207 api.UpdateEvent( 

3208 events_pb2.UpdateEventReq( 

3209 event_id=event_id, 

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

3211 should_notify=True, 

3212 ) 

3213 ) 

3214 

3215 process_jobs() 

3216 

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

3218 with session_scope() as session: 

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

3220 

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

3222 # Find the update notification (most recent one) 

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

3224 assert len(update_notifs) == 1 

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

3226 

3227 

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

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

3230 user1, token1 = generate_user() 

3231 user2, token2 = generate_user() 

3232 

3233 with session_scope() as session: 

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

3235 

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

3237 end_time = start_time + timedelta(hours=3) 

3238 

3239 with events_session(token1) as api: 

3240 res = api.CreateEvent( 

3241 events_pb2.CreateEventReq( 

3242 title="Cancel Test", 

3243 content="Content.", 

3244 location=events_pb2.EventLocation( 

3245 address="Near Null Island", 

3246 lat=0.1, 

3247 lng=0.2, 

3248 ), 

3249 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3250 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3251 ) 

3252 ) 

3253 event_id = res.event_id 

3254 

3255 moderator.approve_event_occurrence(event_id) 

3256 process_jobs() 

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

3258 push_collector.pop_for_user(user2.id) 

3259 

3260 # User2 subscribes 

3261 with events_session(token2) as api: 

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

3263 

3264 # User1 cancels the event 

3265 with events_session(token1) as api: 

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

3267 

3268 process_jobs() 

3269 

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

3271 with session_scope() as session: 

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

3273 

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

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

3276 assert len(cancel_notifs) == 1 

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

3278 

3279 

3280def test_event_update_and_cancel_notifications_not_sent_to_actor( 

3281 db, push_collector: PushCollector, moderator: Moderator 

3282): 

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

3284 organizer_user, organizer_token = generate_user() 

3285 attendee_user, attendee_token = generate_user() 

3286 

3287 with session_scope() as session: 

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

3289 

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

3291 end_time = start_time + timedelta(hours=3) 

3292 

3293 with events_session(organizer_token) as api: 

3294 res = api.CreateEvent( 

3295 events_pb2.CreateEventReq( 

3296 title="Actor Test", 

3297 content="Content.", 

3298 location=events_pb2.EventLocation( 

3299 address="Near Null Island", 

3300 lat=0.1, 

3301 lng=0.2, 

3302 ), 

3303 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3304 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3305 ) 

3306 ) 

3307 event_id = res.event_id 

3308 

3309 moderator.approve_event_occurrence(event_id) 

3310 process_jobs() 

3311 

3312 # The attendee subscribes to notifications 

3313 with events_session(attendee_token) as api: 

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

3315 

3316 # The organizer updates and then cancels their own event 

3317 with events_session(organizer_token) as api: 

3318 api.UpdateEvent( 

3319 events_pb2.UpdateEventReq( 

3320 event_id=event_id, 

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

3322 should_notify=True, 

3323 ) 

3324 ) 

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

3326 

3327 process_jobs() 

3328 

3329 # The organizer should not receive any notifications 

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

3331 

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

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

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

3335 

3336 

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

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

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", [user2], [], None).id 

3344 

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

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

3347 end_time = start_time + timedelta(hours=1) 

3348 

3349 with events_session(token1) as api: 

3350 res = api.CreateEvent( 

3351 events_pb2.CreateEventReq( 

3352 title="Reminder Test", 

3353 content="Content.", 

3354 location=events_pb2.EventLocation( 

3355 address="Near Null Island", 

3356 lat=0.1, 

3357 lng=0.2, 

3358 ), 

3359 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3360 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3361 ) 

3362 ) 

3363 event_id = res.event_id 

3364 

3365 moderator.approve_event_occurrence(event_id) 

3366 process_jobs() 

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

3368 push_collector.pop_for_user(user2.id) 

3369 

3370 # User2 marks attendance 

3371 with events_session(token2) as api: 

3372 api.SetEventAttendance( 

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

3374 ) 

3375 

3376 # Run the event reminder handler 

3377 send_event_reminders(empty_pb2.Empty()) 

3378 process_jobs() 

3379 

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

3381 with session_scope() as session: 

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

3383 

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

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

3386 assert len(reminder_notifs) == 1 

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

3388 

3389 

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

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

3392 user1, token1 = generate_user() 

3393 user2, token2 = generate_user() 

3394 

3395 with session_scope() as session: 

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

3397 

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

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

3400 end_time = start_time + timedelta(hours=1) 

3401 

3402 with events_session(token1) as api: 

3403 res = api.CreateEvent( 

3404 events_pb2.CreateEventReq( 

3405 title="Cancelled Reminder Test", 

3406 content="Content.", 

3407 location=events_pb2.EventLocation( 

3408 address="Near Null Island", 

3409 lat=0.1, 

3410 lng=0.2, 

3411 ), 

3412 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3413 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3414 ) 

3415 ) 

3416 event_id = res.event_id 

3417 

3418 moderator.approve_event_occurrence(event_id) 

3419 process_jobs() 

3420 

3421 # User2 marks attendance 

3422 with events_session(token2) as api: 

3423 api.SetEventAttendance( 

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

3425 ) 

3426 

3427 # User1 cancels the event 

3428 with events_session(token1) as api: 

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

3430 

3431 process_jobs() 

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

3433 while push_collector.count_for_user(user2.id): 

3434 push_collector.pop_for_user(user2.id) 

3435 

3436 # Run the event reminder handler 

3437 send_event_reminders(empty_pb2.Empty()) 

3438 process_jobs() 

3439 

3440 # Verify that no reminder notification was sent for user2 

3441 with session_scope() as session: 

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

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

3444 assert len(reminder_notifs) == 0 

3445 

3446 

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

3448def test_event_reminder_not_sent_for_invisible_attendee( 

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

3450): 

3451 user1, token1 = generate_user() 

3452 user2, token2 = generate_user() 

3453 

3454 with session_scope() as session: 

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

3456 

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

3458 end_time = start_time + timedelta(hours=1) 

3459 

3460 with events_session(token1) as api: 

3461 res = api.CreateEvent( 

3462 events_pb2.CreateEventReq( 

3463 title="Invisible Attendee Reminder Test", 

3464 content="Content.", 

3465 location=events_pb2.EventLocation( 

3466 address="Near Null Island", 

3467 lat=0.1, 

3468 lng=0.2, 

3469 ), 

3470 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3471 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3472 ) 

3473 ) 

3474 event_id = res.event_id 

3475 

3476 moderator.approve_event_occurrence(event_id) 

3477 process_jobs() 

3478 

3479 with events_session(token2) as api: 

3480 api.SetEventAttendance( 

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

3482 ) 

3483 

3484 with session_scope() as session: 

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

3486 

3487 send_event_reminders(empty_pb2.Empty()) 

3488 process_jobs() 

3489 

3490 with session_scope() as session: 

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

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

3493 assert len(reminder_notifs) == 0 

3494 

3495 

3496def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3498 user1, token1 = generate_user() 

3499 user2, token2 = generate_user() 

3500 

3501 with session_scope() as session: 

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

3503 

3504 start = now() 

3505 

3506 # User1 creates event A with 3 occurrences 

3507 event_a_ids = [] 

3508 with events_session(token1) as api: 

3509 res = api.CreateEvent( 

3510 events_pb2.CreateEventReq( 

3511 title="Event A", 

3512 content="Content A.", 

3513 parent_community_id=c_id, 

3514 location=events_pb2.EventLocation( 

3515 address="Near Null Island", 

3516 lat=0.1, 

3517 lng=0.2, 

3518 ), 

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

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

3521 ) 

3522 ) 

3523 event_a_ids.append(res.event_id) 

3524 for i in range(2): 

3525 res = api.ScheduleEvent( 

3526 events_pb2.ScheduleEventReq( 

3527 event_id=event_a_ids[-1], 

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

3529 location=events_pb2.EventLocation( 

3530 address="Near Null Island", 

3531 lat=0.1, 

3532 lng=0.2, 

3533 ), 

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

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

3536 ) 

3537 ) 

3538 event_a_ids.append(res.event_id) 

3539 

3540 # User2 creates event B with 2 occurrences 

3541 event_b_ids = [] 

3542 with events_session(token2) as api: 

3543 res = api.CreateEvent( 

3544 events_pb2.CreateEventReq( 

3545 title="Event B", 

3546 content="Content B.", 

3547 parent_community_id=c_id, 

3548 location=events_pb2.EventLocation( 

3549 address="Near Null Island", 

3550 lat=0.1, 

3551 lng=0.2, 

3552 ), 

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

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

3555 ) 

3556 ) 

3557 event_b_ids.append(res.event_id) 

3558 res = api.ScheduleEvent( 

3559 events_pb2.ScheduleEventReq( 

3560 event_id=event_b_ids[-1], 

3561 content="B occurrence 1", 

3562 location=events_pb2.EventLocation( 

3563 address="Near Null Island", 

3564 lat=0.1, 

3565 lng=0.2, 

3566 ), 

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

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

3569 ) 

3570 ) 

3571 event_b_ids.append(res.event_id) 

3572 

3573 moderator.approve_event_occurrence(event_a_ids[0]) 

3574 moderator.approve_event_occurrence(event_b_ids[0]) 

3575 

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

3577 with events_session(token1) as api: 

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

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

3580 assert sorted(returned_ids) == sorted(event_a_ids) 

3581 

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

3583 with events_session(token2) as api: 

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

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

3586 assert sorted(returned_ids) == sorted(event_b_ids) 

3587 

3588 

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

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

3591 user1, token1 = generate_user() 

3592 user2, token2 = generate_user() 

3593 

3594 with session_scope() as session: 

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

3596 

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

3598 end_time = start_time + timedelta(hours=3) 

3599 

3600 with events_session(token1) as api: 

3601 res = api.CreateEvent( 

3602 events_pb2.CreateEventReq( 

3603 title="Comment Test", 

3604 content="Content.", 

3605 parent_community_id=c_id, 

3606 location=events_pb2.EventLocation( 

3607 address="Near Null Island", 

3608 lat=0.1, 

3609 lng=0.2, 

3610 ), 

3611 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3612 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3613 ) 

3614 ) 

3615 event_id = res.event_id 

3616 thread_id = res.thread.thread_id 

3617 

3618 moderator.approve_event_occurrence(event_id) 

3619 process_jobs() 

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

3621 push_collector.pop_for_user(user1.id) 

3622 

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

3624 with events_session(token1) as api: 

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

3626 

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

3628 with threads_session(token2) as api: 

3629 comment_thread_id = api.PostReply( 

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

3631 ).thread_id 

3632 

3633 process_jobs() 

3634 

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

3636 comment_db_id = comment_thread_id // 10 

3637 with session_scope() as session: 

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

3639 

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

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

3642 assert len(comment_notifs) == 1 

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

3644 

3645 

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

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

3648 user1, token1 = generate_user() 

3649 user2, token2 = generate_user() 

3650 user3, token3 = generate_user() 

3651 

3652 with session_scope() as session: 

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

3654 

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

3656 end_time = start_time + timedelta(hours=3) 

3657 

3658 with events_session(token1) as api: 

3659 res = api.CreateEvent( 

3660 events_pb2.CreateEventReq( 

3661 title="Reply Test", 

3662 content="Content.", 

3663 location=events_pb2.EventLocation( 

3664 address="Near Null Island", 

3665 lat=0.1, 

3666 lng=0.2, 

3667 ), 

3668 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3669 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3670 ) 

3671 ) 

3672 event_id = res.event_id 

3673 thread_id = res.thread.thread_id 

3674 

3675 moderator.approve_event_occurrence(event_id) 

3676 process_jobs() 

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

3678 push_collector.pop_for_user(user1.id) 

3679 

3680 # User2 posts a top-level comment 

3681 with threads_session(token2) as api: 

3682 comment_thread_id = api.PostReply( 

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

3684 ).thread_id 

3685 

3686 process_jobs() 

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

3688 push_collector.pop_for_user(user1.id) 

3689 

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

3691 with threads_session(token3) as api: 

3692 nested_reply_thread_id = api.PostReply( 

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

3694 ).thread_id 

3695 

3696 process_jobs() 

3697 

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

3699 nested_reply_db_id = nested_reply_thread_id // 10 

3700 with session_scope() as session: 

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

3702 

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

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

3705 assert len(reply_notifs) == 1 

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