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

1638 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +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 

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

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

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

41 

42 

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

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

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

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

47 

48 

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

50 # test cases: 

51 # can create event 

52 # cannot create event with missing details 

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

54 # can create in different timezones 

55 

56 # event creator 

57 user1, token1 = generate_user() 

58 # community moderator 

59 user2, token2 = generate_user() 

60 # third party 

61 user3, token3 = generate_user() 

62 

63 with session_scope() as session: 

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

65 

66 time_before = now() 

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

68 end_time = start_time + timedelta(hours=3) 

69 

70 # Can create an event 

71 with events_session(token1) as api: 

72 res = api.CreateEvent( 

73 events_pb2.CreateEventReq( 

74 title="Dummy Title", 

75 content="Dummy content.", 

76 photo_key=None, 

77 location=events_pb2.EventLocation( 

78 address="Near Null Island", 

79 lat=0.1, 

80 lng=0.2, 

81 ), 

82 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

83 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

84 ) 

85 ) 

86 

87 assert res.is_next 

88 assert res.title == "Dummy Title" 

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

90 assert res.content == "Dummy content." 

91 assert not res.photo_url 

92 assert res.HasField("location") 

93 assert res.location.lat == 0.1 

94 assert res.location.lng == 0.2 

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

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

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

98 assert res.creator_user_id == user1.id 

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

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

101 assert is_utc_or_gmt(res.timezone) 

102 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

103 assert res.organizer 

104 assert res.subscriber 

105 assert res.going_count == 1 

106 assert res.organizer_count == 1 

107 assert res.subscriber_count == 1 

108 assert res.owner_user_id == user1.id 

109 assert not res.owner_community_id 

110 assert not res.owner_group_id 

111 assert res.thread.thread_id 

112 assert res.can_edit 

113 assert not res.can_moderate 

114 

115 event_id = res.event_id 

116 

117 # Approve the event so other users can see it 

118 moderator.approve_event_occurrence(event_id) 

119 

120 with events_session(token2) as api: 

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

122 

123 assert res.is_next 

124 assert res.title == "Dummy Title" 

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

126 assert res.content == "Dummy content." 

127 assert not res.photo_url 

128 assert res.HasField("location") 

129 assert res.location.lat == 0.1 

130 assert res.location.lng == 0.2 

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

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

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

134 assert res.creator_user_id == user1.id 

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

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

137 assert is_utc_or_gmt(res.timezone) 

138 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

139 assert not res.organizer 

140 assert not res.subscriber 

141 assert res.going_count == 1 

142 assert res.organizer_count == 1 

143 assert res.subscriber_count == 1 

144 assert res.owner_user_id == user1.id 

145 assert not res.owner_community_id 

146 assert not res.owner_group_id 

147 assert res.thread.thread_id 

148 assert res.can_edit 

149 assert res.can_moderate 

150 

151 with events_session(token3) as api: 

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

153 

154 assert res.is_next 

155 assert res.title == "Dummy Title" 

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

157 assert res.content == "Dummy content." 

158 assert not res.photo_url 

159 assert res.HasField("location") 

160 assert res.location.lat == 0.1 

161 assert res.location.lng == 0.2 

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

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

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

165 assert res.creator_user_id == user1.id 

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

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

168 assert is_utc_or_gmt(res.timezone) 

169 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

170 assert not res.organizer 

171 assert not res.subscriber 

172 assert res.going_count == 1 

173 assert res.organizer_count == 1 

174 assert res.subscriber_count == 1 

175 assert res.owner_user_id == user1.id 

176 assert not res.owner_community_id 

177 assert not res.owner_group_id 

178 assert res.thread.thread_id 

179 assert not res.can_edit 

180 assert not res.can_moderate 

181 

182 # Failure cases 

183 with events_session(token1) as api: 

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

185 api.CreateEvent( 

186 events_pb2.CreateEventReq( 

187 # title="Dummy Title", 

188 content="Dummy content.", 

189 photo_key=None, 

190 location=events_pb2.EventLocation( 

191 address="Near Null Island", 

192 lat=0.1, 

193 lng=0.2, 

194 ), 

195 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

196 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

197 ) 

198 ) 

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

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

201 

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

203 api.CreateEvent( 

204 events_pb2.CreateEventReq( 

205 title="Dummy Title", 

206 # content="Dummy content.", 

207 photo_key=None, 

208 location=events_pb2.EventLocation( 

209 address="Near Null Island", 

210 lat=0.1, 

211 lng=0.2, 

212 ), 

213 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

214 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

215 ) 

216 ) 

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

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

219 

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

221 api.CreateEvent( 

222 events_pb2.CreateEventReq( 

223 title="Dummy Title", 

224 content="Dummy content.", 

225 photo_key="nonexistent", 

226 location=events_pb2.EventLocation( 

227 address="Near Null Island", 

228 lat=0.1, 

229 lng=0.2, 

230 ), 

231 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

232 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

233 ) 

234 ) 

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

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

237 

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

239 api.CreateEvent( 

240 events_pb2.CreateEventReq( 

241 title="Dummy Title", 

242 content="Dummy content.", 

243 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

244 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

245 ) 

246 ) 

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

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

249 

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

251 api.CreateEvent( 

252 events_pb2.CreateEventReq( 

253 title="Dummy Title", 

254 content="Dummy content.", 

255 location=events_pb2.EventLocation( 

256 address="Near Null Island", 

257 ), 

258 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

259 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

260 ) 

261 ) 

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

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

264 

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

266 api.CreateEvent( 

267 events_pb2.CreateEventReq( 

268 title="Dummy Title", 

269 content="Dummy content.", 

270 location=events_pb2.EventLocation( 

271 lat=0.1, 

272 lng=0.1, 

273 ), 

274 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

275 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

276 ) 

277 ) 

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

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

280 

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

282 api.CreateEvent( 

283 events_pb2.CreateEventReq( 

284 title="Dummy Title", 

285 content="Dummy content.", 

286 location=events_pb2.EventLocation( 

287 address="Near Null Island", 

288 lat=0.1, 

289 lng=0.2, 

290 ), 

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

292 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

293 ) 

294 ) 

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

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

297 

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

299 api.CreateEvent( 

300 events_pb2.CreateEventReq( 

301 title="Dummy Title", 

302 content="Dummy content.", 

303 location=events_pb2.EventLocation( 

304 address="Near Null Island", 

305 lat=0.1, 

306 lng=0.2, 

307 ), 

308 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

309 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

310 ) 

311 ) 

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

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

314 

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

316 api.CreateEvent( 

317 events_pb2.CreateEventReq( 

318 title="Dummy Title", 

319 content="Dummy content.", 

320 location=events_pb2.EventLocation( 

321 address="Near Null Island", 

322 lat=0.1, 

323 lng=0.2, 

324 ), 

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

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

327 ) 

328 ) 

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

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

331 

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

333 api.CreateEvent( 

334 events_pb2.CreateEventReq( 

335 title="Dummy Title", 

336 content="Dummy content.", 

337 location=events_pb2.EventLocation( 

338 address="Near Null Island", 

339 lat=0.1, 

340 lng=0.2, 

341 ), 

342 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

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

344 ) 

345 ) 

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

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

348 

349 

350def test_CreateEvent_incomplete_profile(db): 

351 user1, token1 = generate_user(complete_profile=False) 

352 user2, token2 = generate_user() 

353 

354 with session_scope() as session: 

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

356 

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

358 end_time = start_time + timedelta(hours=3) 

359 

360 with events_session(token1) as api: 

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

362 api.CreateEvent( 

363 events_pb2.CreateEventReq( 

364 title="Dummy Title", 

365 content="Dummy content.", 

366 photo_key=None, 

367 location=events_pb2.EventLocation( 

368 address="Near Null Island", 

369 lat=0.1, 

370 lng=0.2, 

371 ), 

372 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

373 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

374 ) 

375 ) 

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

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

378 

379 

380def test_ScheduleEvent(db, frozen_timewarp): 

381 # test cases: 

382 # can schedule a new event occurrence 

383 

384 user, token = generate_user() 

385 

386 with session_scope() as session: 

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

388 

389 time_before = now() 

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

391 end_time = start_time + timedelta(hours=3) 

392 

393 with events_session(token) as api: 

394 create_res = api.CreateEvent( 

395 events_pb2.CreateEventReq( 

396 title="Dummy Title", 

397 content="Dummy content.", 

398 parent_community_id=c_id, 

399 location=events_pb2.EventLocation( 

400 address="Near Null Island", 

401 lat=0.1, 

402 lng=0.2, 

403 ), 

404 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

405 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

406 ) 

407 ) 

408 

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

410 new_end_time = new_start_time + timedelta(hours=2) 

411 

412 schedule_res = api.ScheduleEvent( 

413 events_pb2.ScheduleEventReq( 

414 event_id=create_res.event_id, 

415 content="New event occurrence", 

416 location=events_pb2.EventLocation( 

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

418 lat=0.3, 

419 lng=0.2, 

420 ), 

421 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time), 

422 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time), 

423 ) 

424 ) 

425 

426 # Each occurrence is independent and has an independent thread 

427 assert schedule_res.event_id != create_res.event_id 

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

429 

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

431 

432 assert not res.is_next 

433 assert res.title == "Dummy Title" 

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

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

436 assert not res.photo_url 

437 assert res.HasField("location") 

438 assert res.location.lat == 0.3 

439 assert res.location.lng == 0.2 

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

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

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

443 assert res.creator_user_id == user.id 

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

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

446 assert is_utc_or_gmt(res.timezone) 

447 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

448 assert res.organizer 

449 assert res.subscriber 

450 assert res.going_count == 1 

451 assert res.organizer_count == 1 

452 assert res.subscriber_count == 1 

453 assert res.owner_user_id == user.id 

454 assert not res.owner_community_id 

455 assert not res.owner_group_id 

456 assert res.thread.thread_id 

457 assert res.can_edit 

458 assert res.can_moderate 

459 

460 

461def test_cannot_overlap_occurrences_schedule(db): 

462 user, token = generate_user() 

463 

464 with session_scope() as session: 

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

466 

467 start = now() 

468 

469 with events_session(token) as api: 

470 res = api.CreateEvent( 

471 events_pb2.CreateEventReq( 

472 title="Dummy Title", 

473 content="Dummy content.", 

474 parent_community_id=c_id, 

475 location=events_pb2.EventLocation( 

476 address="Near Null Island", 

477 lat=0.1, 

478 lng=0.2, 

479 ), 

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

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

482 ) 

483 ) 

484 

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

486 api.ScheduleEvent( 

487 events_pb2.ScheduleEventReq( 

488 event_id=res.event_id, 

489 content="New event occurrence", 

490 location=events_pb2.EventLocation( 

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

492 lat=0.3, 

493 lng=0.2, 

494 ), 

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

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

497 ) 

498 ) 

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

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

501 

502 

503def test_cannot_overlap_occurrences_update(db): 

504 user, token = generate_user() 

505 

506 with session_scope() as session: 

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

508 

509 start = now() 

510 

511 with events_session(token) as api: 

512 res = api.CreateEvent( 

513 events_pb2.CreateEventReq( 

514 title="Dummy Title", 

515 content="Dummy content.", 

516 parent_community_id=c_id, 

517 location=events_pb2.EventLocation( 

518 address="Near Null Island", 

519 lat=0.1, 

520 lng=0.2, 

521 ), 

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

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

524 ) 

525 ) 

526 

527 event_id = api.ScheduleEvent( 

528 events_pb2.ScheduleEventReq( 

529 event_id=res.event_id, 

530 content="New event occurrence", 

531 location=events_pb2.EventLocation( 

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

533 lat=0.3, 

534 lng=0.2, 

535 ), 

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

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

538 ) 

539 ).event_id 

540 

541 # can overlap with this current existing occurrence 

542 api.UpdateEvent( 

543 events_pb2.UpdateEventReq( 

544 event_id=event_id, 

545 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

547 ), 

548 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

550 ), 

551 ) 

552 ) 

553 

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

555 api.UpdateEvent( 

556 events_pb2.UpdateEventReq( 

557 event_id=event_id, 

558 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

560 ), 

561 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

563 ), 

564 ) 

565 ) 

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

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

568 

569 

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

571 # test cases: 

572 # owner can update 

573 # community owner can update 

574 # notifies attendees 

575 

576 # event creator 

577 user1, token1 = generate_user() 

578 # community moderator 

579 user2, token2 = generate_user() 

580 # third parties 

581 user3, token3 = generate_user() 

582 user4, token4 = generate_user() 

583 user5, token5 = generate_user() 

584 user6, token6 = generate_user() 

585 

586 with session_scope() as session: 

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

588 

589 time_before = now() 

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

591 end_time = start_time + timedelta(hours=3) 

592 

593 with events_session(token1) as api: 

594 res = api.CreateEvent( 

595 events_pb2.CreateEventReq( 

596 title="Dummy Title", 

597 content="Dummy content.", 

598 parent_community_id=c_id, 

599 location=events_pb2.EventLocation( 

600 address="Near Null Island", 

601 lat=0.1, 

602 lng=0.2, 

603 ), 

604 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

605 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

606 ) 

607 ) 

608 

609 event_id = res.event_id 

610 

611 moderator.approve_event_occurrence(event_id) 

612 

613 with events_session(token4) as api: 

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

615 

616 with events_session(token5) as api: 

617 api.SetEventAttendance( 

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

619 ) 

620 

621 with events_session(token6) as api: 

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

623 

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

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

626 time_before_update = now() 

627 

628 with events_session(token1) as api: 

629 res = api.UpdateEvent( 

630 events_pb2.UpdateEventReq( 

631 event_id=event_id, 

632 ) 

633 ) 

634 

635 with events_session(token1) as api: 

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

637 

638 assert res.is_next 

639 assert res.title == "Dummy Title" 

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

641 assert res.content == "Dummy content." 

642 assert not res.photo_url 

643 assert res.HasField("location") 

644 assert res.location.lat == 0.1 

645 assert res.location.lng == 0.2 

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

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

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

649 assert res.creator_user_id == user1.id 

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

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

652 assert is_utc_or_gmt(res.timezone) 

653 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

654 assert res.organizer 

655 assert res.subscriber 

656 assert res.going_count == 2 

657 assert res.organizer_count == 1 

658 assert res.subscriber_count == 3 

659 assert res.owner_user_id == user1.id 

660 assert not res.owner_community_id 

661 assert not res.owner_group_id 

662 assert res.thread.thread_id 

663 assert res.can_edit 

664 assert not res.can_moderate 

665 

666 with events_session(token2) as api: 

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

668 

669 assert res.is_next 

670 assert res.title == "Dummy Title" 

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

672 assert res.content == "Dummy content." 

673 assert not res.photo_url 

674 assert res.HasField("location") 

675 assert res.location.lat == 0.1 

676 assert res.location.lng == 0.2 

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

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

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

680 assert res.creator_user_id == user1.id 

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

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

683 assert is_utc_or_gmt(res.timezone) 

684 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

685 assert not res.organizer 

686 assert not res.subscriber 

687 assert res.going_count == 2 

688 assert res.organizer_count == 1 

689 assert res.subscriber_count == 3 

690 assert res.owner_user_id == user1.id 

691 assert not res.owner_community_id 

692 assert not res.owner_group_id 

693 assert res.thread.thread_id 

694 assert res.can_edit 

695 assert res.can_moderate 

696 

697 with events_session(token3) as api: 

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

699 

700 assert res.is_next 

701 assert res.title == "Dummy Title" 

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

703 assert res.content == "Dummy content." 

704 assert not res.photo_url 

705 assert res.HasField("location") 

706 assert res.location.lat == 0.1 

707 assert res.location.lng == 0.2 

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

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

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

711 assert res.creator_user_id == user1.id 

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

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

714 assert is_utc_or_gmt(res.timezone) 

715 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

716 assert not res.organizer 

717 assert not res.subscriber 

718 assert res.going_count == 2 

719 assert res.organizer_count == 1 

720 assert res.subscriber_count == 3 

721 assert res.owner_user_id == user1.id 

722 assert not res.owner_community_id 

723 assert not res.owner_group_id 

724 assert res.thread.thread_id 

725 assert not res.can_edit 

726 assert not res.can_moderate 

727 

728 with events_session(token1) as api: 

729 res = api.UpdateEvent( 

730 events_pb2.UpdateEventReq( 

731 event_id=event_id, 

732 location=events_pb2.EventLocation( 

733 address="Nearer Null Island", 

734 lat=0.01, 

735 lng=0.02, 

736 ), 

737 ) 

738 ) 

739 

740 with events_session(token3) as api: 

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

742 

743 assert res.HasField("location") 

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

745 assert res.location.lat == 0.01 

746 assert res.location.lng == 0.02 

747 

748 

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

750 # event creator 

751 user1, token1 = generate_user() 

752 # community moderator 

753 user2, token2 = generate_user() 

754 # third parties 

755 user3, token3 = generate_user() 

756 user4, token4 = generate_user() 

757 user5, token5 = generate_user() 

758 user6, token6 = generate_user() 

759 

760 with session_scope() as session: 

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

762 

763 time_before = now() 

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

765 end_time = start_time + timedelta(hours=1.5) 

766 

767 event_ids = [] 

768 

769 with events_session(token1) as api: 

770 res = api.CreateEvent( 

771 events_pb2.CreateEventReq( 

772 title="Dummy Title", 

773 content="0th occurrence", 

774 location=events_pb2.EventLocation( 

775 address="Near Null Island", 

776 lat=0.1, 

777 lng=0.2, 

778 ), 

779 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

780 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

781 ) 

782 ) 

783 

784 event_id = res.event_id 

785 event_ids.append(event_id) 

786 

787 moderator.approve_event_occurrence(event_id) 

788 

789 with events_session(token4) as api: 

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

791 

792 with events_session(token5) as api: 

793 api.SetEventAttendance( 

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

795 ) 

796 

797 with events_session(token6) as api: 

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

799 

800 with events_session(token1) as api: 

801 for i in range(5): 

802 res = api.ScheduleEvent( 

803 events_pb2.ScheduleEventReq( 

804 event_id=event_ids[-1], 

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

806 location=events_pb2.EventLocation( 

807 address="Near Null Island", 

808 lat=0.1, 

809 lng=0.2, 

810 ), 

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

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

813 ) 

814 ) 

815 

816 event_ids.append(res.event_id) 

817 

818 # Approve all scheduled occurrences 

819 for eid in event_ids[1:]: 

820 moderator.approve_event_occurrence(eid) 

821 

822 updated_event_id = event_ids[3] 

823 

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

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

826 time_before_update = now() 

827 

828 with events_session(token1) as api: 

829 res = api.UpdateEvent( 

830 events_pb2.UpdateEventReq( 

831 event_id=updated_event_id, 

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

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

834 location=events_pb2.EventLocation( 

835 address="Not so near Null Island", 

836 lat=0.2, 

837 lng=0.2, 

838 ), 

839 update_all_future=True, 

840 ) 

841 ) 

842 

843 time_after_update = now() 

844 

845 with events_session(token2) as api: 

846 for i in range(3): 

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

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

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

850 

851 for i in range(3, 6): 

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

853 assert res.content == "New content." 

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

855 

856 

857def test_UpdateEvent_all_leaves_other_events_alone(db, moderator: Moderator): 

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

859 # creator of the event that gets edited 

860 user1, token1 = generate_user() 

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

862 user2, token2 = generate_user() 

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

864 user3, token3 = generate_user() 

865 

866 with session_scope() as session: 

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

868 

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

870 

871 with events_session(token1) as api: 

872 edited_id = api.CreateEvent( 

873 events_pb2.CreateEventReq( 

874 title="Edited Event", 

875 content="0th occurrence", 

876 location=events_pb2.EventLocation( 

877 address="Near Null Island", 

878 lat=0.1, 

879 lng=0.2, 

880 ), 

881 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

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

883 ) 

884 ).event_id 

885 

886 second_id = api.ScheduleEvent( 

887 events_pb2.ScheduleEventReq( 

888 event_id=edited_id, 

889 content="1th occurrence", 

890 location=events_pb2.EventLocation( 

891 address="Near Null Island", 

892 lat=0.1, 

893 lng=0.2, 

894 ), 

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

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

897 ) 

898 ).event_id 

899 

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

901 with events_session(token2) as api: 

902 other_id = api.CreateEvent( 

903 events_pb2.CreateEventReq( 

904 title="Other Event", 

905 content="Other content.", 

906 location=events_pb2.EventLocation( 

907 address="Somewhere else entirely", 

908 lat=0.5, 

909 lng=0.5, 

910 ), 

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

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

913 ) 

914 ).event_id 

915 

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

917 moderator.approve_event_occurrence(occurrence_id) 

918 

919 with events_session(token2) as api: 

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

921 

922 with events_session(token1) as api: 

923 api.UpdateEvent( 

924 events_pb2.UpdateEventReq( 

925 event_id=edited_id, 

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

927 location=events_pb2.EventLocation( 

928 address="Not so near Null Island", 

929 lat=0.2, 

930 lng=0.2, 

931 ), 

932 update_all_future=True, 

933 ) 

934 ) 

935 

936 with events_session(token3) as api: 

937 for occurrence_id in (edited_id, second_id): 

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

939 assert res.content == "New content." 

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

941 

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

943 assert res.content == "Other content." 

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

945 assert res.location.lat == 0.5 

946 assert res.location.lng == 0.5 

947 assert res.timezone == other_before.timezone 

948 assert res.last_edited == other_before.last_edited 

949 

950 

951def test_UpdateEvent_all_cant_change_times(db, moderator: Moderator): 

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

953 user1, token1 = generate_user() 

954 user2, token2 = generate_user() 

955 

956 with session_scope() as session: 

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

958 

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

960 

961 with events_session(token1) as api: 

962 event_id = api.CreateEvent( 

963 events_pb2.CreateEventReq( 

964 title="Dummy Title", 

965 content="0th occurrence", 

966 location=events_pb2.EventLocation( 

967 address="Near Null Island", 

968 lat=0.1, 

969 lng=0.2, 

970 ), 

971 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

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

973 ) 

974 ).event_id 

975 

976 api.ScheduleEvent( 

977 events_pb2.ScheduleEventReq( 

978 event_id=event_id, 

979 content="1th occurrence", 

980 location=events_pb2.EventLocation( 

981 address="Near Null Island", 

982 lat=0.1, 

983 lng=0.2, 

984 ), 

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

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

987 ) 

988 ) 

989 

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

991 api.UpdateEvent( 

992 events_pb2.UpdateEventReq( 

993 event_id=event_id, 

994 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

996 ), 

997 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

999 ), 

1000 update_all_future=True, 

1001 ) 

1002 ) 

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

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

1005 

1006 

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

1008 # event creator 

1009 user1, token1 = generate_user() 

1010 # community moderator 

1011 user2, token2 = generate_user() 

1012 # third parties 

1013 user3, token3 = generate_user() 

1014 user4, token4 = generate_user() 

1015 user5, token5 = generate_user() 

1016 user6, token6 = generate_user() 

1017 

1018 with session_scope() as session: 

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

1020 

1021 time_before = now() 

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

1023 end_time = start_time + timedelta(hours=3) 

1024 

1025 with events_session(token1) as api: 

1026 # in person event 

1027 res = api.CreateEvent( 

1028 events_pb2.CreateEventReq( 

1029 title="Dummy Title", 

1030 content="Dummy content.", 

1031 location=events_pb2.EventLocation( 

1032 address="Near Null Island", 

1033 lat=0.1, 

1034 lng=0.2, 

1035 ), 

1036 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1037 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1038 ) 

1039 ) 

1040 

1041 event_id = res.event_id 

1042 

1043 moderator.approve_event_occurrence(event_id) 

1044 

1045 with events_session(token4) as api: 

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

1047 

1048 with events_session(token5) as api: 

1049 api.SetEventAttendance( 

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

1051 ) 

1052 

1053 with events_session(token6) as api: 

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

1055 

1056 with events_session(token1) as api: 

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

1058 

1059 assert res.is_next 

1060 assert res.title == "Dummy Title" 

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

1062 assert res.content == "Dummy content." 

1063 assert not res.photo_url 

1064 assert res.HasField("location") 

1065 assert res.location.lat == 0.1 

1066 assert res.location.lng == 0.2 

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

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

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

1070 assert res.creator_user_id == user1.id 

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

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

1073 assert is_utc_or_gmt(res.timezone) 

1074 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

1075 assert res.organizer 

1076 assert res.subscriber 

1077 assert res.going_count == 2 

1078 assert res.organizer_count == 1 

1079 assert res.subscriber_count == 3 

1080 assert res.owner_user_id == user1.id 

1081 assert not res.owner_community_id 

1082 assert not res.owner_group_id 

1083 assert res.thread.thread_id 

1084 assert res.can_edit 

1085 assert not res.can_moderate 

1086 

1087 with events_session(token2) as api: 

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

1089 

1090 assert res.is_next 

1091 assert res.title == "Dummy Title" 

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

1093 assert res.content == "Dummy content." 

1094 assert not res.photo_url 

1095 assert res.HasField("location") 

1096 assert res.location.lat == 0.1 

1097 assert res.location.lng == 0.2 

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

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

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

1101 assert res.creator_user_id == user1.id 

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

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

1104 assert is_utc_or_gmt(res.timezone) 

1105 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1106 assert not res.organizer 

1107 assert not res.subscriber 

1108 assert res.going_count == 2 

1109 assert res.organizer_count == 1 

1110 assert res.subscriber_count == 3 

1111 assert res.owner_user_id == user1.id 

1112 assert not res.owner_community_id 

1113 assert not res.owner_group_id 

1114 assert res.thread.thread_id 

1115 assert res.can_edit 

1116 assert res.can_moderate 

1117 

1118 with events_session(token3) as api: 

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

1120 

1121 assert res.is_next 

1122 assert res.title == "Dummy Title" 

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

1124 assert res.content == "Dummy content." 

1125 assert not res.photo_url 

1126 assert res.HasField("location") 

1127 assert res.location.lat == 0.1 

1128 assert res.location.lng == 0.2 

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

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

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

1132 assert res.creator_user_id == user1.id 

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

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

1135 assert is_utc_or_gmt(res.timezone) 

1136 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1137 assert not res.organizer 

1138 assert not res.subscriber 

1139 assert res.going_count == 2 

1140 assert res.organizer_count == 1 

1141 assert res.subscriber_count == 3 

1142 assert res.owner_user_id == user1.id 

1143 assert not res.owner_community_id 

1144 assert not res.owner_group_id 

1145 assert res.thread.thread_id 

1146 assert not res.can_edit 

1147 assert not res.can_moderate 

1148 

1149 

1150def test_CancelEvent(db, moderator: Moderator): 

1151 # event creator 

1152 user1, token1 = generate_user() 

1153 # community moderator 

1154 user2, token2 = generate_user() 

1155 # third parties 

1156 user3, token3 = generate_user() 

1157 user4, token4 = generate_user() 

1158 user5, token5 = generate_user() 

1159 user6, token6 = generate_user() 

1160 

1161 with session_scope() as session: 

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

1163 

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

1165 end_time = start_time + timedelta(hours=3) 

1166 

1167 with events_session(token1) as api: 

1168 res = api.CreateEvent( 

1169 events_pb2.CreateEventReq( 

1170 title="Dummy Title", 

1171 content="Dummy content.", 

1172 location=events_pb2.EventLocation( 

1173 address="Near Null Island", 

1174 lat=0.1, 

1175 lng=0.2, 

1176 ), 

1177 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1178 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1179 ) 

1180 ) 

1181 

1182 event_id = res.event_id 

1183 

1184 moderator.approve_event_occurrence(event_id) 

1185 

1186 with events_session(token4) as api: 

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

1188 

1189 with events_session(token5) as api: 

1190 api.SetEventAttendance( 

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

1192 ) 

1193 

1194 with events_session(token6) as api: 

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

1196 

1197 with events_session(token1) as api: 

1198 res = api.CancelEvent( 

1199 events_pb2.CancelEventReq( 

1200 event_id=event_id, 

1201 ) 

1202 ) 

1203 

1204 with events_session(token1) as api: 

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

1206 assert res.is_cancelled 

1207 

1208 with events_session(token1) as api: 

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

1210 api.UpdateEvent( 

1211 events_pb2.UpdateEventReq( 

1212 event_id=event_id, 

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

1214 ) 

1215 ) 

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

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

1218 

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

1220 api.InviteEventOrganizer( 

1221 events_pb2.InviteEventOrganizerReq( 

1222 event_id=event_id, 

1223 user_id=user3.id, 

1224 ) 

1225 ) 

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

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

1228 

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

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

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 events_session(token3) as api: 

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

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

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

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

1239 

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

1241 api.SetEventAttendance( 

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

1243 ) 

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

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

1246 

1247 with events_session(token1) as api: 

1248 for include_cancelled in [True, False]: 

1249 res = api.ListEventOccurrences( 

1250 events_pb2.ListEventOccurrencesReq( 

1251 event_id=event_id, 

1252 include_cancelled=include_cancelled, 

1253 ) 

1254 ) 

1255 if include_cancelled: 

1256 assert len(res.events) > 0 

1257 else: 

1258 assert len(res.events) == 0 

1259 

1260 res = api.ListMyEvents( 

1261 events_pb2.ListMyEventsReq( 

1262 include_cancelled=include_cancelled, 

1263 ) 

1264 ) 

1265 if include_cancelled: 

1266 assert len(res.events) > 0 

1267 else: 

1268 assert len(res.events) == 0 

1269 

1270 

1271def test_ListEventAttendees(db, moderator: Moderator): 

1272 # event creator 

1273 user1, token1 = generate_user() 

1274 # others 

1275 user2, token2 = generate_user() 

1276 user3, token3 = generate_user() 

1277 user4, token4 = generate_user() 

1278 user5, token5 = generate_user() 

1279 user6, token6 = generate_user() 

1280 

1281 with session_scope() as session: 

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

1283 

1284 with events_session(token1) as api: 

1285 event_id = api.CreateEvent( 

1286 events_pb2.CreateEventReq( 

1287 title="Dummy Title", 

1288 content="Dummy content.", 

1289 location=events_pb2.EventLocation( 

1290 address="Near Null Island", 

1291 lat=0.1, 

1292 lng=0.2, 

1293 ), 

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

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

1296 ) 

1297 ).event_id 

1298 

1299 moderator.approve_event_occurrence(event_id) 

1300 

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

1302 with events_session(token) as api: 

1303 api.SetEventAttendance( 

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

1305 ) 

1306 

1307 with events_session(token6) as api: 

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

1309 

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

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

1312 

1313 res = api.ListEventAttendees( 

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

1315 ) 

1316 assert res.attendee_user_ids == [user3.id, user4.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 == [user5.id] 

1322 assert not res.next_page_token 

1323 

1324 

1325def test_ListEventSubscribers(db, moderator: Moderator): 

1326 # event creator 

1327 user1, token1 = generate_user() 

1328 # others 

1329 user2, token2 = generate_user() 

1330 user3, token3 = generate_user() 

1331 user4, token4 = generate_user() 

1332 user5, token5 = generate_user() 

1333 user6, token6 = generate_user() 

1334 

1335 with session_scope() as session: 

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

1337 

1338 with events_session(token1) as api: 

1339 event_id = api.CreateEvent( 

1340 events_pb2.CreateEventReq( 

1341 title="Dummy Title", 

1342 content="Dummy content.", 

1343 location=events_pb2.EventLocation( 

1344 address="Near Null Island", 

1345 lat=0.1, 

1346 lng=0.2, 

1347 ), 

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

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

1350 ) 

1351 ).event_id 

1352 

1353 moderator.approve_event_occurrence(event_id) 

1354 

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

1356 with events_session(token) as api: 

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

1358 

1359 with events_session(token6) as api: 

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

1361 

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

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

1364 

1365 res = api.ListEventSubscribers( 

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

1367 ) 

1368 assert res.subscriber_user_ids == [user3.id, user4.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 == [user5.id] 

1374 assert not res.next_page_token 

1375 

1376 

1377def test_ListEventOrganizers(db, moderator: Moderator): 

1378 # event creator 

1379 user1, token1 = generate_user() 

1380 # others 

1381 user2, token2 = generate_user() 

1382 user3, token3 = generate_user() 

1383 user4, token4 = generate_user() 

1384 user5, token5 = generate_user() 

1385 user6, token6 = generate_user() 

1386 

1387 with session_scope() as session: 

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

1389 

1390 with events_session(token1) as api: 

1391 event_id = api.CreateEvent( 

1392 events_pb2.CreateEventReq( 

1393 title="Dummy Title", 

1394 content="Dummy content.", 

1395 location=events_pb2.EventLocation( 

1396 address="Near Null Island", 

1397 lat=0.1, 

1398 lng=0.2, 

1399 ), 

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

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

1402 ) 

1403 ).event_id 

1404 

1405 moderator.approve_event_occurrence(event_id) 

1406 

1407 with events_session(token1) as api: 

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

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

1410 

1411 with events_session(token6) as api: 

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

1413 

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

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

1416 

1417 res = api.ListEventOrganizers( 

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

1419 ) 

1420 assert res.organizer_user_ids == [user3.id, user4.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 == [user5.id] 

1426 assert not res.next_page_token 

1427 

1428 

1429def test_TransferEvent(db): 

1430 user1, token1 = generate_user() 

1431 user2, token2 = generate_user() 

1432 user3, token3 = generate_user() 

1433 user4, token4 = generate_user() 

1434 

1435 with session_scope() as session: 

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

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

1438 c_id = c.id 

1439 h_id = h.id 

1440 

1441 with events_session(token1) as api: 

1442 event_id = api.CreateEvent( 

1443 events_pb2.CreateEventReq( 

1444 title="Dummy Title", 

1445 content="Dummy content.", 

1446 location=events_pb2.EventLocation( 

1447 address="Near Null Island", 

1448 lat=0.1, 

1449 lng=0.2, 

1450 ), 

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

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

1453 ) 

1454 ).event_id 

1455 

1456 api.TransferEvent( 

1457 events_pb2.TransferEventReq( 

1458 event_id=event_id, 

1459 new_owner_community_id=c_id, 

1460 ) 

1461 ) 

1462 

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

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

1465 

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

1467 api.TransferEvent( 

1468 events_pb2.TransferEventReq( 

1469 event_id=event_id, 

1470 new_owner_group_id=h_id, 

1471 ) 

1472 ) 

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

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

1475 

1476 event_id = api.CreateEvent( 

1477 events_pb2.CreateEventReq( 

1478 title="Dummy Title", 

1479 content="Dummy content.", 

1480 location=events_pb2.EventLocation( 

1481 address="Near Null Island", 

1482 lat=0.1, 

1483 lng=0.2, 

1484 ), 

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

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

1487 ) 

1488 ).event_id 

1489 

1490 api.TransferEvent( 

1491 events_pb2.TransferEventReq( 

1492 event_id=event_id, 

1493 new_owner_group_id=h_id, 

1494 ) 

1495 ) 

1496 

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

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

1499 

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

1501 api.TransferEvent( 

1502 events_pb2.TransferEventReq( 

1503 event_id=event_id, 

1504 new_owner_community_id=c_id, 

1505 ) 

1506 ) 

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

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

1509 

1510 

1511def test_SetEventSubscription(db, moderator: Moderator): 

1512 user1, token1 = generate_user() 

1513 user2, token2 = generate_user() 

1514 

1515 with session_scope() as session: 

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

1517 

1518 with events_session(token1) as api: 

1519 event_id = api.CreateEvent( 

1520 events_pb2.CreateEventReq( 

1521 title="Dummy Title", 

1522 content="Dummy content.", 

1523 location=events_pb2.EventLocation( 

1524 address="Near Null Island", 

1525 lat=0.1, 

1526 lng=0.2, 

1527 ), 

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

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

1530 ) 

1531 ).event_id 

1532 

1533 moderator.approve_event_occurrence(event_id) 

1534 

1535 with events_session(token2) as api: 

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

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

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

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

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

1541 

1542 

1543def test_SetEventAttendance(db, moderator: Moderator): 

1544 user1, token1 = generate_user() 

1545 user2, token2 = generate_user() 

1546 

1547 with session_scope() as session: 

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

1549 

1550 with events_session(token1) as api: 

1551 event_id = api.CreateEvent( 

1552 events_pb2.CreateEventReq( 

1553 title="Dummy Title", 

1554 content="Dummy content.", 

1555 location=events_pb2.EventLocation( 

1556 address="Near Null Island", 

1557 lat=0.1, 

1558 lng=0.2, 

1559 ), 

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

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

1562 ) 

1563 ).event_id 

1564 

1565 moderator.approve_event_occurrence(event_id) 

1566 

1567 with events_session(token2) as api: 

1568 assert ( 

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

1570 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1571 ) 

1572 api.SetEventAttendance( 

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

1574 ) 

1575 assert ( 

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

1577 == events_pb2.ATTENDANCE_STATE_GOING 

1578 ) 

1579 api.SetEventAttendance( 

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

1581 ) 

1582 assert ( 

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

1584 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1585 ) 

1586 

1587 

1588def test_InviteEventOrganizer(db, moderator: Moderator): 

1589 user1, token1 = generate_user() 

1590 user2, token2 = generate_user() 

1591 

1592 with session_scope() as session: 

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

1594 

1595 with events_session(token1) as api: 

1596 event_id = api.CreateEvent( 

1597 events_pb2.CreateEventReq( 

1598 title="Dummy Title", 

1599 content="Dummy content.", 

1600 location=events_pb2.EventLocation( 

1601 address="Near Null Island", 

1602 lat=0.1, 

1603 lng=0.2, 

1604 ), 

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

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

1607 ) 

1608 ).event_id 

1609 

1610 moderator.approve_event_occurrence(event_id) 

1611 

1612 with events_session(token2) as api: 

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

1614 

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

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

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

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

1619 

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

1621 

1622 with events_session(token1) as api: 

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

1624 

1625 with events_session(token2) as api: 

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

1627 

1628 

1629def test_ListEventOccurrences(db): 

1630 user1, token1 = generate_user() 

1631 user2, token2 = generate_user() 

1632 user3, token3 = generate_user() 

1633 

1634 with session_scope() as session: 

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

1636 

1637 start = now() 

1638 

1639 event_ids = [] 

1640 

1641 with events_session(token1) as api: 

1642 res = api.CreateEvent( 

1643 events_pb2.CreateEventReq( 

1644 title="First occurrence", 

1645 content="Dummy content.", 

1646 parent_community_id=c_id, 

1647 location=events_pb2.EventLocation( 

1648 address="Near Null Island", 

1649 lat=0.1, 

1650 lng=0.2, 

1651 ), 

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

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

1654 ) 

1655 ) 

1656 

1657 event_ids.append(res.event_id) 

1658 

1659 for i in range(5): 

1660 res = api.ScheduleEvent( 

1661 events_pb2.ScheduleEventReq( 

1662 event_id=event_ids[-1], 

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

1664 location=events_pb2.EventLocation( 

1665 address="Near Null Island", 

1666 lat=0.1, 

1667 lng=0.2, 

1668 ), 

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

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

1671 ) 

1672 ) 

1673 

1674 event_ids.append(res.event_id) 

1675 

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

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

1678 

1679 res = api.ListEventOccurrences( 

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

1681 ) 

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

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[4:6] 

1688 assert not res.next_page_token 

1689 

1690 

1691def test_ListMyEvents(db, moderator: Moderator): 

1692 user1, token1 = generate_user() 

1693 user2, token2 = generate_user() 

1694 user3, token3 = generate_user() 

1695 user4, token4 = generate_user() 

1696 user5, token5 = generate_user() 

1697 

1698 with session_scope() as session: 

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

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

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

1702 c_id = global_community.id 

1703 macroregion_community = create_community( 

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

1705 ) 

1706 region_community = create_community( 

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

1708 ) 

1709 subregion_community = create_community( 

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

1711 ) 

1712 c2_id = subregion_community.id 

1713 

1714 start = now() 

1715 

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

1717 return events_pb2.CreateEventReq( 

1718 title="Dummy Title", 

1719 content="Dummy content.", 

1720 location=events_pb2.EventLocation( 

1721 address="Near Null Island", 

1722 lat=0.1, 

1723 lng=0.2, 

1724 ), 

1725 parent_community_id=community_id, 

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

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

1728 ) 

1729 

1730 with events_session(token1) as api: 

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

1732 

1733 moderator.approve_event_occurrence(e2) 

1734 

1735 with events_session(token2) as api: 

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

1737 

1738 moderator.approve_event_occurrence(e1) 

1739 

1740 with events_session(token1) as api: 

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

1742 

1743 moderator.approve_event_occurrence(e3) 

1744 

1745 with events_session(token2) as api: 

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

1747 

1748 moderator.approve_event_occurrence(e5) 

1749 

1750 with events_session(token3) as api: 

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

1752 

1753 moderator.approve_event_occurrence(e4) 

1754 

1755 with events_session(token4) as api: 

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

1757 

1758 moderator.approve_event_occurrence(e6) 

1759 

1760 with events_session(token1) as api: 

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

1762 

1763 with events_session(token1) as api: 

1764 api.SetEventAttendance( 

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

1766 ) 

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

1768 

1769 with events_session(token2) as api: 

1770 api.SetEventAttendance( 

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

1772 ) 

1773 

1774 with events_session(token3) as api: 

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

1776 

1777 with events_session(token1) as api: 

1778 # test pagination with token first 

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

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

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

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

1783 assert not res.next_page_token 

1784 

1785 res = api.ListMyEvents( 

1786 events_pb2.ListMyEventsReq( 

1787 subscribed=True, 

1788 attending=True, 

1789 organizing=True, 

1790 ) 

1791 ) 

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

1793 

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

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

1796 

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

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

1799 

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

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

1802 

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

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

1805 

1806 with events_session(token1) as api: 

1807 # Test pagination with page_number and verify total_items 

1808 res = api.ListMyEvents( 

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

1810 ) 

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

1812 assert res.total_items == 4 

1813 

1814 res = api.ListMyEvents( 

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

1816 ) 

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

1818 assert res.total_items == 4 

1819 

1820 # Verify no more pages 

1821 res = api.ListMyEvents( 

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

1823 ) 

1824 assert not res.events 

1825 assert res.total_items == 4 

1826 

1827 with events_session(token2) as api: 

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

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

1830 

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

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

1833 

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

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

1836 

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

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

1839 

1840 with events_session(token3) as api: 

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

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

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

1844 

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

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

1847 

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

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

1850 

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

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

1853 

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

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

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

1857 

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

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

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

1861 

1862 # my_communities_exclude_global works independently of my_communities flag 

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

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

1865 

1866 # my_communities_exclude_global filters organizing results too 

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

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

1869 

1870 # my_communities_exclude_global filters subscribed results too 

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

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

1873 

1874 with events_session(token5) as api: 

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

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

1877 

1878 

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

1880 event_ids = [] 

1881 page_token = "" 

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

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

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

1885 page_token = res.next_page_token 

1886 if not page_token: 

1887 return event_ids 

1888 raise AssertionError("pagination did not terminate") 

1889 

1890 

1891def test_ListMyEvents_pagination_overlapping_durations(db, moderator: Moderator): 

1892 user, token = generate_user() 

1893 

1894 with session_scope() as session: 

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

1896 

1897 start = now() 

1898 

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

1900 return events_pb2.CreateEventReq( 

1901 title="Dummy Title", 

1902 content="Dummy content.", 

1903 location=events_pb2.EventLocation( 

1904 address="Near Null Island", 

1905 lat=0.1, 

1906 lng=0.2, 

1907 ), 

1908 parent_community_id=c_id, 

1909 start_datetime_iso8601_local=datetime_to_iso8601_local(start + start_offset), 

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

1911 ) 

1912 

1913 with events_session(token) as api: 

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

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

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

1917 short_events = [ 

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

1919 ] 

1920 

1921 for event_id in [long_event, *short_events]: 

1922 moderator.approve_event_occurrence(event_id) 

1923 

1924 with events_session(token) as api: 

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

1926 

1927 

1928def test_ListMyEvents_pagination_identical_start_times(db, moderator: Moderator): 

1929 user, token = generate_user() 

1930 

1931 with session_scope() as session: 

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

1933 

1934 start = now() 

1935 

1936 with events_session(token) as api: 

1937 event_ids = [ 

1938 api.CreateEvent( 

1939 events_pb2.CreateEventReq( 

1940 title="Dummy Title", 

1941 content="Dummy content.", 

1942 location=events_pb2.EventLocation( 

1943 address="Near Null Island", 

1944 lat=0.1, 

1945 lng=0.2, 

1946 ), 

1947 parent_community_id=c_id, 

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

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

1950 ) 

1951 ).event_id 

1952 for _ in range(5) 

1953 ] 

1954 

1955 for event_id in event_ids: 

1956 moderator.approve_event_occurrence(event_id) 

1957 

1958 with events_session(token) as api: 

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

1960 

1961 

1962def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1963 user1, token1 = generate_user() 

1964 user2, token2 = generate_user() 

1965 

1966 with session_scope() as session: 

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

1968 c_id = c.id 

1969 

1970 start = now() 

1971 

1972 def make_event(hours): 

1973 return events_pb2.CreateEventReq( 

1974 title="Test Event", 

1975 content="Test content.", 

1976 location=events_pb2.EventLocation( 

1977 address="Near Null Island", 

1978 lat=0.1, 

1979 lng=0.2, 

1980 ), 

1981 parent_community_id=c_id, 

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

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

1984 ) 

1985 

1986 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1987 with events_session(token1) as api: 

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

1989 

1990 with events_session(token2) as api: 

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

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

1993 # e_both: user1 will be both organizer and attendee 

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

1995 

1996 moderator.approve_event_occurrence(e_own) 

1997 moderator.approve_event_occurrence(e_attending) 

1998 moderator.approve_event_occurrence(e_community_only) 

1999 moderator.approve_event_occurrence(e_both) 

2000 

2001 # invite user1 as organizer of e_both 

2002 with events_session(token2) as api: 

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

2004 

2005 # user1 RSVPs to e_attending and e_both 

2006 with events_session(token1) as api: 

2007 api.SetEventAttendance( 

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

2009 ) 

2010 api.SetEventAttendance( 

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

2012 ) 

2013 

2014 with events_session(token1) as api: 

2015 # baseline: all four community events visible 

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

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

2018 

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

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

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

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

2023 

2024 # exclude_attending with attending=True: invalid combination 

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

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

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

2028 

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

2030 with events_session(token2) as api: 

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

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

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

2034 

2035 

2036def test_RemoveEventOrganizer(db, moderator: Moderator): 

2037 user1, token1 = generate_user() 

2038 user2, token2 = generate_user() 

2039 

2040 with session_scope() as session: 

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

2042 

2043 with events_session(token1) as api: 

2044 event_id = api.CreateEvent( 

2045 events_pb2.CreateEventReq( 

2046 title="Dummy Title", 

2047 content="Dummy content.", 

2048 location=events_pb2.EventLocation( 

2049 address="Near Null Island", 

2050 lat=0.1, 

2051 lng=0.2, 

2052 ), 

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

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

2055 ) 

2056 ).event_id 

2057 

2058 moderator.approve_event_occurrence(event_id) 

2059 

2060 with events_session(token2) as api: 

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

2062 

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

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

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

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

2067 

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

2069 

2070 with events_session(token1) as api: 

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

2072 

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

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

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

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

2077 

2078 with events_session(token2) as api: 

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

2080 assert res.organizer 

2081 assert res.organizer_count == 2 

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

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

2084 

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

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

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

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

2089 

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

2091 assert not res.organizer 

2092 assert res.organizer_count == 1 

2093 

2094 # Test that event owner can remove co-organizers 

2095 with events_session(token1) as api: 

2096 # Add user2 back as organizer 

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

2098 

2099 # Verify user2 is now an organizer 

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

2101 assert res.organizer_count == 2 

2102 

2103 # Event owner can remove co-organizer 

2104 api.RemoveEventOrganizer( 

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

2106 ) 

2107 

2108 # Verify user2 is no longer an organizer 

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

2110 assert res.organizer_count == 1 

2111 

2112 # Test that non-organizers cannot remove other organizers 

2113 with events_session(token2) as api: 

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

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

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

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

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

2119 

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

2121 with events_session(token1) as api: 

2122 # Add user2 back as organizer 

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

2124 

2125 

2126def test_ListEventAttendees_regression(db): 

2127 # see issue #1617: 

2128 # 

2129 # 1. Create an event 

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

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

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

2133 # 

2134 # **Expected behaviour** 

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

2136 # 

2137 # **Actual/current behaviour** 

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

2139 

2140 user1, token1 = generate_user() 

2141 user2, token2 = generate_user() 

2142 user3, token3 = generate_user() 

2143 user4, token4 = generate_user() 

2144 user5, token5 = generate_user() 

2145 

2146 with session_scope() as session: 

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

2148 

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

2150 end_time = start_time + timedelta(hours=3) 

2151 

2152 with events_session(token1) as api: 

2153 res = api.CreateEvent( 

2154 events_pb2.CreateEventReq( 

2155 title="Dummy Title", 

2156 content="Dummy content.", 

2157 location=events_pb2.EventLocation( 

2158 address="Near Null Island", 

2159 lat=0.1, 

2160 lng=0.2, 

2161 ), 

2162 parent_community_id=c_id, 

2163 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2164 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2165 ) 

2166 ) 

2167 

2168 res = api.TransferEvent( 

2169 events_pb2.TransferEventReq( 

2170 event_id=res.event_id, 

2171 new_owner_community_id=c_id, 

2172 ) 

2173 ) 

2174 

2175 event_id = res.event_id 

2176 

2177 api.SetEventAttendance( 

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

2179 ) 

2180 api.SetEventAttendance( 

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

2182 ) 

2183 

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

2185 assert len(res.attendee_user_ids) == 1 

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

2187 

2188 

2189def test_GetEventCalendarFile(db, moderator: Moderator): 

2190 user1, token1 = generate_user() 

2191 user2, token2 = generate_user() 

2192 

2193 with session_scope() as session: 

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

2195 

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

2197 end_time = start_time + timedelta(hours=3) 

2198 

2199 with events_session(token1) as api: 

2200 created_event: events_pb2.Event = api.CreateEvent( 

2201 events_pb2.CreateEventReq( 

2202 title="Dummy Title", 

2203 content="Dummy content.", 

2204 parent_community_id=c_id, 

2205 location=events_pb2.EventLocation( 

2206 address="Near Null Island", 

2207 lat=0.1, 

2208 lng=0.2, 

2209 ), 

2210 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2211 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2212 ) 

2213 ) 

2214 event_id = created_event.event_id 

2215 

2216 moderator.approve_event_occurrence(event_id) 

2217 

2218 with events_session(token1) as api: 

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

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

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

2222 assert "SUMMARY:Dummy Title" in ics_string 

2223 assert "DESCRIPTION:Dummy content." in ics_string 

2224 assert "LOCATION:Near Null Island" in ics_string 

2225 assert "STATUS:CANCELLED" not in ics_string 

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

2227 assert pre_cancel_match is not None 

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

2229 

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

2231 

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

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

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

2235 assert "STATUS:CANCELLED" in ics_string 

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

2237 assert post_cancel_match is not None 

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

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

2240 assert post_cancel_sequence >= pre_cancel_sequence 

2241 

2242 

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

2244 user1, token1 = generate_user() 

2245 user2, token2 = generate_user() 

2246 user3, token3 = generate_user() 

2247 user4, token4 = generate_user() 

2248 

2249 with session_scope() as session: 

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

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

2252 c_id = c.id 

2253 h_id = h.id 

2254 user4_id = user4.id 

2255 

2256 with events_session(token1) as api: 

2257 event = api.CreateEvent( 

2258 events_pb2.CreateEventReq( 

2259 title="Dummy Title", 

2260 content="Dummy content.", 

2261 location=events_pb2.EventLocation( 

2262 address="Near Null Island", 

2263 lat=0.1, 

2264 lng=0.2, 

2265 ), 

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

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

2268 ) 

2269 ) 

2270 

2271 moderator.approve_event_occurrence(event.event_id) 

2272 

2273 with threads_session(token2) as api: 

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

2275 

2276 moderator.approve_thread_post(reply_id) 

2277 

2278 with events_session(token3) as api: 

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

2280 assert res.thread.num_responses == 1 

2281 

2282 with threads_session(token3) as api: 

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

2284 assert len(ret.replies) == 1 

2285 assert not ret.next_page_token 

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

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

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

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

2290 

2291 nested_reply_id = api.PostReply( 

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

2293 ).thread_id 

2294 

2295 moderator.approve_thread_post(nested_reply_id) 

2296 

2297 process_jobs() 

2298 

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

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

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

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

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

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

2305 

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

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

2308 

2309 assert push_collector.count_for_user(user4_id) == 0 

2310 

2311 

2312def test_can_overlap_other_events_schedule_regression(db): 

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

2314 user, token = generate_user() 

2315 

2316 with session_scope() as session: 

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

2318 

2319 start = now() 

2320 

2321 with events_session(token) as api: 

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

2323 api.CreateEvent( 

2324 events_pb2.CreateEventReq( 

2325 title="Dummy Title", 

2326 content="Dummy content.", 

2327 parent_community_id=c_id, 

2328 location=events_pb2.EventLocation( 

2329 address="Near Null Island", 

2330 lat=0.1, 

2331 lng=0.2, 

2332 ), 

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

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

2335 ) 

2336 ) 

2337 

2338 # this event 

2339 res = api.CreateEvent( 

2340 events_pb2.CreateEventReq( 

2341 title="Dummy Title", 

2342 content="Dummy content.", 

2343 parent_community_id=c_id, 

2344 location=events_pb2.EventLocation( 

2345 address="Near Null Island", 

2346 lat=0.1, 

2347 lng=0.2, 

2348 ), 

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

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

2351 ) 

2352 ) 

2353 

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

2355 api.ScheduleEvent( 

2356 events_pb2.ScheduleEventReq( 

2357 event_id=res.event_id, 

2358 content="New event occurrence", 

2359 location=events_pb2.EventLocation( 

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

2361 lat=0.3, 

2362 lng=0.2, 

2363 ), 

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

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

2366 ) 

2367 ) 

2368 

2369 

2370def test_can_overlap_other_events_update_regression(db): 

2371 user, token = generate_user() 

2372 

2373 with session_scope() as session: 

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

2375 

2376 start = now() 

2377 

2378 with events_session(token) as api: 

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

2380 api.CreateEvent( 

2381 events_pb2.CreateEventReq( 

2382 title="Dummy Title", 

2383 content="Dummy content.", 

2384 parent_community_id=c_id, 

2385 location=events_pb2.EventLocation( 

2386 address="Near Null Island", 

2387 lat=0.1, 

2388 lng=0.2, 

2389 ), 

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

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

2392 ) 

2393 ) 

2394 

2395 res = api.CreateEvent( 

2396 events_pb2.CreateEventReq( 

2397 title="Dummy Title", 

2398 content="Dummy content.", 

2399 parent_community_id=c_id, 

2400 location=events_pb2.EventLocation( 

2401 address="Near Null Island", 

2402 lat=0.1, 

2403 lng=0.2, 

2404 ), 

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

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

2407 ) 

2408 ) 

2409 

2410 event_id = api.ScheduleEvent( 

2411 events_pb2.ScheduleEventReq( 

2412 event_id=res.event_id, 

2413 content="New event occurrence", 

2414 location=events_pb2.EventLocation( 

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

2416 lat=0.3, 

2417 lng=0.2, 

2418 ), 

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

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

2421 ) 

2422 ).event_id 

2423 

2424 # can overlap with this current existing occurrence 

2425 api.UpdateEvent( 

2426 events_pb2.UpdateEventReq( 

2427 event_id=event_id, 

2428 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2430 ), 

2431 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2433 ), 

2434 ) 

2435 ) 

2436 

2437 api.UpdateEvent( 

2438 events_pb2.UpdateEventReq( 

2439 event_id=event_id, 

2440 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2442 ), 

2443 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2445 ), 

2446 ) 

2447 ) 

2448 

2449 

2450def test_list_past_events_regression(db): 

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

2452 user, token = generate_user() 

2453 

2454 with session_scope() as session: 

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

2456 

2457 start = now() 

2458 

2459 with events_session(token) as api: 

2460 api.CreateEvent( 

2461 events_pb2.CreateEventReq( 

2462 title="Dummy Title", 

2463 content="Dummy content.", 

2464 parent_community_id=c_id, 

2465 location=events_pb2.EventLocation( 

2466 address="Near Null Island", 

2467 lat=0.1, 

2468 lng=0.2, 

2469 ), 

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

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

2472 ) 

2473 ) 

2474 

2475 with session_scope() as session: 

2476 session.execute( 

2477 update(EventOccurrence).values( 

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

2479 ) 

2480 ) 

2481 

2482 with events_session(token) as api: 

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

2484 assert len(res.events) == 1 

2485 

2486 

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

2488 user1, token1 = generate_user(complete_profile=True) 

2489 user2, token2 = generate_user() 

2490 user3, token3 = generate_user() 

2491 user4, token4 = generate_user() 

2492 user5, token5 = generate_user(is_superuser=True) 

2493 

2494 with session_scope() as session: 

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

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

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

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

2499 

2500 enforce_community_memberships() 

2501 

2502 with events_session(token1) as api: 

2503 res = api.CreateEvent( 

2504 events_pb2.CreateEventReq( 

2505 title="Dummy Title", 

2506 content="Dummy content.", 

2507 parent_community_id=c_id, 

2508 location=events_pb2.EventLocation( 

2509 address="Near Null Island", 

2510 lat=0.1, 

2511 lng=0.2, 

2512 ), 

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

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

2515 ) 

2516 ) 

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

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

2519 

2520 event_id = res.event_id 

2521 

2522 moderator.approve_event_occurrence(event_id) 

2523 

2524 with events_session(token1) as api: 

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

2526 

2527 email = email_collector.pop_for_mods(last=True) 

2528 

2529 assert user_url in email.plain 

2530 assert event_url in email.plain 

2531 

2532 # can't send another req 

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

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

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

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

2537 

2538 # another user can send one though 

2539 with events_session(token3) as api: 

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

2541 

2542 # but not a non-admin 

2543 with events_session(token2) as api: 

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

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

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

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

2548 

2549 with real_editor_session(token5) as editor: 

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

2551 assert len(res.requests) == 2 

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

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

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

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

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

2557 

2558 editor.DecideEventCommunityInviteRequest( 

2559 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2561 approve=False, 

2562 ) 

2563 ) 

2564 

2565 editor.DecideEventCommunityInviteRequest( 

2566 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2568 approve=True, 

2569 ) 

2570 ) 

2571 

2572 # not after approve 

2573 with events_session(token4) as api: 

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

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

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

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

2578 

2579 

2580def test_list_decided_community_invite_requests(db, moderator: Moderator): 

2581 user1, token1 = generate_user() 

2582 user2, token2 = generate_user() 

2583 user3, token3 = generate_user() 

2584 superuser, superuser_token = generate_user(is_superuser=True) 

2585 

2586 with session_scope() as session: 

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

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

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

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

2591 

2592 enforce_community_memberships() 

2593 

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

2595 with events_session(token) as api: 

2596 res = api.CreateEvent( 

2597 events_pb2.CreateEventReq( 

2598 title=title, 

2599 content="Dummy content.", 

2600 parent_community_id=c_id, 

2601 location=events_pb2.EventLocation( 

2602 address="Near Null Island", 

2603 lat=0.1, 

2604 lng=0.2, 

2605 ), 

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

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

2608 ) 

2609 ) 

2610 moderator.approve_event_occurrence(res.event_id) 

2611 with events_session(token) as api: 

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

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

2614 

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

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

2617 # this one stays pending 

2618 create_event_and_request_invite(token3, "Pending Event") 

2619 

2620 with real_editor_session(superuser_token) as editor: 

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

2622 assert len(pending.requests) == 3 

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

2624 

2625 # nothing decided yet 

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

2627 assert len(res.requests) == 0 

2628 assert not res.next_page_token 

2629 

2630 editor.DecideEventCommunityInviteRequest( 

2631 editor_pb2.DecideEventCommunityInviteRequestReq( 

2632 event_community_invite_request_id=by_user[user1.id], 

2633 approve=True, 

2634 ) 

2635 ) 

2636 editor.DecideEventCommunityInviteRequest( 

2637 editor_pb2.DecideEventCommunityInviteRequestReq( 

2638 event_community_invite_request_id=by_user[user2.id], 

2639 approve=False, 

2640 ) 

2641 ) 

2642 

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

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

2645 assert len(res.requests) == 2 

2646 assert not res.next_page_token 

2647 

2648 declined, approved = res.requests 

2649 

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

2651 assert declined.user_id == user2.id 

2652 assert declined.event_url == event2_url 

2653 assert declined.community_id == c_id 

2654 assert declined.decided_by_user_id == superuser.id 

2655 assert not declined.approved 

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

2657 

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

2659 assert approved.user_id == user1.id 

2660 assert approved.event_url == event1_url 

2661 assert approved.community_id == c_id 

2662 assert approved.decided_by_user_id == superuser.id 

2663 assert approved.approved 

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

2665 

2666 # filtering 

2667 res = editor.ListDecidedEventCommunityInviteRequests( 

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

2669 ) 

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

2671 

2672 res = editor.ListDecidedEventCommunityInviteRequests( 

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

2674 ) 

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

2676 

2677 # pagination 

2678 res = editor.ListDecidedEventCommunityInviteRequests( 

2679 editor_pb2.ListDecidedEventCommunityInviteRequestsReq(page_size=1) 

2680 ) 

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

2682 assert res.next_page_token 

2683 

2684 res = editor.ListDecidedEventCommunityInviteRequests( 

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

2686 ) 

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

2688 assert not res.next_page_token 

2689 

2690 

2691def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator): 

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

2693 # community invite notification when it is approved. 

2694 organizer, organizer_token = generate_user() 

2695 attendee, attendee_token = generate_user() 

2696 member, _ = generate_user() 

2697 superuser, superuser_token = generate_user(is_superuser=True) 

2698 

2699 with session_scope() as session: 

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

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

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

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

2704 

2705 enforce_community_memberships() 

2706 

2707 with events_session(organizer_token) as api: 

2708 event_id = api.CreateEvent( 

2709 events_pb2.CreateEventReq( 

2710 title="Dummy Title", 

2711 content="Dummy content.", 

2712 parent_community_id=c_id, 

2713 location=events_pb2.EventLocation( 

2714 address="Near Null Island", 

2715 lat=0.1, 

2716 lng=0.2, 

2717 ), 

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

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

2720 ) 

2721 ).event_id 

2722 

2723 moderator.approve_event_occurrence(event_id) 

2724 

2725 # the attendee RSVPs before the community invite is approved 

2726 with events_session(attendee_token) as api: 

2727 api.SetEventAttendance( 

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

2729 ) 

2730 

2731 with events_session(organizer_token) as api: 

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

2733 

2734 with real_editor_session(superuser_token) as editor: 

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

2736 editor.DecideEventCommunityInviteRequest( 

2737 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2739 approve=True, 

2740 ) 

2741 ) 

2742 

2743 process_jobs() 

2744 

2745 with session_scope() as session: 

2746 

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

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

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

2750 

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

2752 assert invite_notification_count(member.id) == 1 

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

2754 assert invite_notification_count(attendee.id) == 0 

2755 assert invite_notification_count(organizer.id) == 0 

2756 

2757 

2758def test_update_event_should_notify_queues_job(): 

2759 user, token = generate_user() 

2760 start = now() 

2761 

2762 with session_scope() as session: 

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

2764 

2765 # create an event 

2766 with events_session(token) as api: 

2767 create_res = api.CreateEvent( 

2768 events_pb2.CreateEventReq( 

2769 title="Dummy Title", 

2770 content="Dummy content.", 

2771 parent_community_id=c_id, 

2772 location=events_pb2.EventLocation( 

2773 address="Near Null Island", 

2774 lat=1.0, 

2775 lng=2.0, 

2776 ), 

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

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

2779 ) 

2780 ) 

2781 

2782 event_id = create_res.event_id 

2783 

2784 # measure initial background job queue length 

2785 with session_scope() as session: 

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

2787 job_length_before_update = len(jobs) 

2788 

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

2790 api.UpdateEvent( 

2791 events_pb2.UpdateEventReq( 

2792 event_id=event_id, 

2793 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2795 ), 

2796 should_notify=False, 

2797 ) 

2798 ) 

2799 

2800 with session_scope() as session: 

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

2802 assert len(jobs) == job_length_before_update 

2803 

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

2805 api.UpdateEvent( 

2806 events_pb2.UpdateEventReq( 

2807 event_id=event_id, 

2808 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2810 ), 

2811 should_notify=True, 

2812 ) 

2813 ) 

2814 

2815 with session_scope() as session: 

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

2817 assert len(jobs) == job_length_before_update + 1 

2818 

2819 

2820def test_event_photo_key(db): 

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

2822 user, token = generate_user() 

2823 

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

2825 end_time = start_time + timedelta(hours=3) 

2826 

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

2828 with session_scope() as session: 

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

2830 upload = Upload( 

2831 key="test_event_photo_key_123", 

2832 filename="test_event_photo_key_123.jpg", 

2833 creator_user_id=user.id, 

2834 ) 

2835 session.add(upload) 

2836 

2837 with events_session(token) as api: 

2838 # Create event without photo 

2839 res = api.CreateEvent( 

2840 events_pb2.CreateEventReq( 

2841 title="Event Without Photo", 

2842 content="No photo content.", 

2843 photo_key=None, 

2844 location=events_pb2.EventLocation( 

2845 address="Near Null Island", 

2846 lat=0.1, 

2847 lng=0.2, 

2848 ), 

2849 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2850 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2851 ) 

2852 ) 

2853 

2854 assert res.photo_key == "" 

2855 assert res.photo_url == "" 

2856 

2857 # Create event with photo 

2858 res_with_photo = api.CreateEvent( 

2859 events_pb2.CreateEventReq( 

2860 title="Event With Photo", 

2861 content="Has photo content.", 

2862 photo_key="test_event_photo_key_123", 

2863 location=events_pb2.EventLocation( 

2864 address="Near Null Island", 

2865 lat=0.1, 

2866 lng=0.2, 

2867 ), 

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

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

2870 ) 

2871 ) 

2872 

2873 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2874 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2875 

2876 event_id = res_with_photo.event_id 

2877 

2878 # Verify photo_key is returned when getting the event 

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

2880 assert get_res.photo_key == "test_event_photo_key_123" 

2881 assert "test_event_photo_key_123" in get_res.photo_url 

2882 

2883 

2884def test_event_timezone(db): 

2885 user, token = generate_user() 

2886 

2887 with session_scope() as session: 

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

2889 

2890 # Midnight future day, UTC timezone 

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

2892 end_time = start_time + timedelta(days=1) 

2893 

2894 with events_session(token) as api: 

2895 create_res: events_pb2.Event = api.CreateEvent( 

2896 events_pb2.CreateEventReq( 

2897 title="Dummy Title", 

2898 content="Dummy content.", 

2899 photo_key=None, 

2900 parent_community_id=c_id, 

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

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

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

2904 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2905 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2906 ) 

2907 ) 

2908 

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

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

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

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

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

2914 

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

2916 update_res: events_pb2.Event = api.UpdateEvent( 

2917 events_pb2.UpdateEventReq( 

2918 event_id=create_res.event_id, 

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

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

2921 ) 

2922 ) 

2923 

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

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

2926 # but now in the New York timezone. 

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

2928 assert update_res.start_time != create_res.start_time 

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

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

2931 

2932 # Also validate GetEvent 

2933 get_res: events_pb2.Event = api.GetEvent( 

2934 events_pb2.GetEventReq( 

2935 event_id=create_res.event_id, 

2936 ) 

2937 ) 

2938 

2939 assert get_res.timezone == update_res.timezone 

2940 assert get_res.start_time == update_res.start_time 

2941 

2942 

2943def test_event_created_with_shadowed_visibility(db): 

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

2945 user, token = generate_user() 

2946 

2947 with session_scope() as session: 

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

2949 

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

2951 end_time = start_time + timedelta(hours=3) 

2952 

2953 with events_session(token) as api: 

2954 res = api.CreateEvent( 

2955 events_pb2.CreateEventReq( 

2956 title="Test UMS Event", 

2957 content="UMS content.", 

2958 location=events_pb2.EventLocation( 

2959 address="Near Null Island", 

2960 lat=0.1, 

2961 lng=0.2, 

2962 ), 

2963 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2964 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2965 ) 

2966 ) 

2967 event_id = res.event_id 

2968 

2969 with session_scope() as session: 

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

2971 mod_state = session.execute( 

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

2973 ).scalar_one() 

2974 assert mod_state.visibility == ModerationVisibility.shadowed 

2975 

2976 

2977def test_shadowed_event_visible_to_creator_only(db): 

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

2979 user1, token1 = generate_user() 

2980 user2, token2 = generate_user() 

2981 

2982 with session_scope() as session: 

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

2984 

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

2986 end_time = start_time + timedelta(hours=3) 

2987 

2988 with events_session(token1) as api: 

2989 res = api.CreateEvent( 

2990 events_pb2.CreateEventReq( 

2991 title="Shadowed Event", 

2992 content="Content.", 

2993 location=events_pb2.EventLocation( 

2994 address="Near Null Island", 

2995 lat=0.1, 

2996 lng=0.2, 

2997 ), 

2998 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2999 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3000 ) 

3001 ) 

3002 event_id = res.event_id 

3003 

3004 # Creator can see it 

3005 with events_session(token1) as api: 

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

3007 assert res.title == "Shadowed Event" 

3008 

3009 # Other user cannot 

3010 with events_session(token2) as api: 

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

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

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

3014 

3015 

3016def test_event_visible_after_approval(db, moderator: Moderator): 

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

3018 user1, token1 = generate_user() 

3019 user2, token2 = generate_user() 

3020 

3021 with session_scope() as session: 

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

3023 

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

3025 end_time = start_time + timedelta(hours=3) 

3026 

3027 with events_session(token1) as api: 

3028 res = api.CreateEvent( 

3029 events_pb2.CreateEventReq( 

3030 title="Approved Event", 

3031 content="Content.", 

3032 location=events_pb2.EventLocation( 

3033 address="Near Null Island", 

3034 lat=0.1, 

3035 lng=0.2, 

3036 ), 

3037 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3038 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3039 ) 

3040 ) 

3041 event_id = res.event_id 

3042 

3043 # Other user cannot see it yet 

3044 with events_session(token2) as api: 

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

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

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

3048 

3049 # Approve the event 

3050 moderator.approve_event_occurrence(event_id) 

3051 

3052 # Now other user can see it 

3053 with events_session(token2) as api: 

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

3055 assert res.title == "Approved Event" 

3056 

3057 

3058def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

3060 user1, token1 = generate_user() 

3061 user2, token2 = generate_user() 

3062 

3063 with session_scope() as session: 

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

3065 

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

3067 end_time = start_time + timedelta(hours=3) 

3068 

3069 with events_session(token1) as api: 

3070 res = api.CreateEvent( 

3071 events_pb2.CreateEventReq( 

3072 title="List Test Event", 

3073 content="Content.", 

3074 location=events_pb2.EventLocation( 

3075 address="Near Null Island", 

3076 lat=0.1, 

3077 lng=0.2, 

3078 ), 

3079 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3080 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3081 ) 

3082 ) 

3083 event_id = res.event_id 

3084 

3085 # Creator can see their own SHADOWED event in lists 

3086 with events_session(token1) as api: 

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

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

3089 assert event_id in event_ids 

3090 

3091 # Other user cannot see the SHADOWED event in lists 

3092 with events_session(token2) as api: 

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

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

3095 assert event_id not in event_ids 

3096 

3097 # After approval, other user can see it 

3098 moderator.approve_event_occurrence(event_id) 

3099 

3100 with events_session(token2) as api: 

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

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

3103 assert event_id in event_ids 

3104 

3105 

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

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

3108 user1, token1 = generate_user() 

3109 user2, token2 = generate_user() 

3110 

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

3112 with session_scope() as session: 

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

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

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

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

3117 

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

3119 end_time = start_time + timedelta(hours=3) 

3120 

3121 with events_session(token1) as api: 

3122 res = api.CreateEvent( 

3123 events_pb2.CreateEventReq( 

3124 title="Deferred Event", 

3125 content="Content.", 

3126 location=events_pb2.EventLocation( 

3127 address="Near Null Island", 

3128 lat=0.1, 

3129 lng=0.2, 

3130 ), 

3131 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3132 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3133 ) 

3134 ) 

3135 event_id = res.event_id 

3136 

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

3138 process_jobs() 

3139 

3140 with session_scope() as session: 

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

3142 # Notification was created with moderation_state_id for deferral 

3143 assert notif.moderation_state_id is not None 

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

3145 delivery_count = session.execute( 

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

3147 ).scalar_one_or_none() 

3148 assert delivery_count is None 

3149 

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

3151 moderator.approve_event_occurrence(event_id) 

3152 

3153 # Verify handle_notification job was queued 

3154 with session_scope() as session: 

3155 pending_jobs = ( 

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

3157 .scalars() 

3158 .all() 

3159 ) 

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

3161 

3162 

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

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

3165 user1, token1 = generate_user() 

3166 user2, token2 = generate_user() 

3167 

3168 with session_scope() as session: 

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

3170 

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

3172 end_time = start_time + timedelta(hours=3) 

3173 

3174 with events_session(token1) as api: 

3175 res = api.CreateEvent( 

3176 events_pb2.CreateEventReq( 

3177 title="Update Test", 

3178 content="Content.", 

3179 location=events_pb2.EventLocation( 

3180 address="Near Null Island", 

3181 lat=0.1, 

3182 lng=0.2, 

3183 ), 

3184 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3185 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3186 ) 

3187 ) 

3188 event_id = res.event_id 

3189 

3190 moderator.approve_event_occurrence(event_id) 

3191 process_jobs() 

3192 # Clear any create notifications 

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

3194 push_collector.pop_for_user(user2.id) 

3195 

3196 # User2 subscribes to the event 

3197 with events_session(token2) as api: 

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

3199 

3200 # User1 updates the event with should_notify=True 

3201 with events_session(token1) as api: 

3202 api.UpdateEvent( 

3203 events_pb2.UpdateEventReq( 

3204 event_id=event_id, 

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

3206 should_notify=True, 

3207 ) 

3208 ) 

3209 

3210 process_jobs() 

3211 

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

3213 with session_scope() as session: 

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

3215 

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

3217 # Find the update notification (most recent one) 

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

3219 assert len(update_notifs) == 1 

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

3221 

3222 

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

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

3225 user1, token1 = generate_user() 

3226 user2, token2 = generate_user() 

3227 

3228 with session_scope() as session: 

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

3230 

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

3232 end_time = start_time + timedelta(hours=3) 

3233 

3234 with events_session(token1) as api: 

3235 res = api.CreateEvent( 

3236 events_pb2.CreateEventReq( 

3237 title="Cancel Test", 

3238 content="Content.", 

3239 location=events_pb2.EventLocation( 

3240 address="Near Null Island", 

3241 lat=0.1, 

3242 lng=0.2, 

3243 ), 

3244 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3245 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3246 ) 

3247 ) 

3248 event_id = res.event_id 

3249 

3250 moderator.approve_event_occurrence(event_id) 

3251 process_jobs() 

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

3253 push_collector.pop_for_user(user2.id) 

3254 

3255 # User2 subscribes 

3256 with events_session(token2) as api: 

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

3258 

3259 # User1 cancels the event 

3260 with events_session(token1) as api: 

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

3262 

3263 process_jobs() 

3264 

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

3266 with session_scope() as session: 

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

3268 

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

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

3271 assert len(cancel_notifs) == 1 

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

3273 

3274 

3275def test_event_update_and_cancel_notifications_not_sent_to_actor( 

3276 db, push_collector: PushCollector, moderator: Moderator 

3277): 

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

3279 organizer_user, organizer_token = generate_user() 

3280 attendee_user, attendee_token = generate_user() 

3281 

3282 with session_scope() as session: 

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

3284 

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

3286 end_time = start_time + timedelta(hours=3) 

3287 

3288 with events_session(organizer_token) as api: 

3289 res = api.CreateEvent( 

3290 events_pb2.CreateEventReq( 

3291 title="Actor Test", 

3292 content="Content.", 

3293 location=events_pb2.EventLocation( 

3294 address="Near Null Island", 

3295 lat=0.1, 

3296 lng=0.2, 

3297 ), 

3298 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3299 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3300 ) 

3301 ) 

3302 event_id = res.event_id 

3303 

3304 moderator.approve_event_occurrence(event_id) 

3305 process_jobs() 

3306 

3307 # The attendee subscribes to notifications 

3308 with events_session(attendee_token) as api: 

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

3310 

3311 # The organizer updates and then cancels their own event 

3312 with events_session(organizer_token) as api: 

3313 api.UpdateEvent( 

3314 events_pb2.UpdateEventReq( 

3315 event_id=event_id, 

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

3317 should_notify=True, 

3318 ) 

3319 ) 

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

3321 

3322 process_jobs() 

3323 

3324 # The organizer should not receive any notifications 

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

3326 

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

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

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

3330 

3331 

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

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

3334 user1, token1 = generate_user() 

3335 user2, token2 = generate_user() 

3336 

3337 with session_scope() as session: 

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

3339 

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

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

3342 end_time = start_time + timedelta(hours=1) 

3343 

3344 with events_session(token1) as api: 

3345 res = api.CreateEvent( 

3346 events_pb2.CreateEventReq( 

3347 title="Reminder Test", 

3348 content="Content.", 

3349 location=events_pb2.EventLocation( 

3350 address="Near Null Island", 

3351 lat=0.1, 

3352 lng=0.2, 

3353 ), 

3354 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3355 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3356 ) 

3357 ) 

3358 event_id = res.event_id 

3359 

3360 moderator.approve_event_occurrence(event_id) 

3361 process_jobs() 

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

3363 push_collector.pop_for_user(user2.id) 

3364 

3365 # User2 marks attendance 

3366 with events_session(token2) as api: 

3367 api.SetEventAttendance( 

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

3369 ) 

3370 

3371 # Run the event reminder handler 

3372 send_event_reminders(empty_pb2.Empty()) 

3373 process_jobs() 

3374 

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

3376 with session_scope() as session: 

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

3378 

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

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

3381 assert len(reminder_notifs) == 1 

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

3383 

3384 

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

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

3387 user1, token1 = generate_user() 

3388 user2, token2 = generate_user() 

3389 

3390 with session_scope() as session: 

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

3392 

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

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

3395 end_time = start_time + timedelta(hours=1) 

3396 

3397 with events_session(token1) as api: 

3398 res = api.CreateEvent( 

3399 events_pb2.CreateEventReq( 

3400 title="Cancelled Reminder Test", 

3401 content="Content.", 

3402 location=events_pb2.EventLocation( 

3403 address="Near Null Island", 

3404 lat=0.1, 

3405 lng=0.2, 

3406 ), 

3407 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3408 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3409 ) 

3410 ) 

3411 event_id = res.event_id 

3412 

3413 moderator.approve_event_occurrence(event_id) 

3414 process_jobs() 

3415 

3416 # User2 marks attendance 

3417 with events_session(token2) as api: 

3418 api.SetEventAttendance( 

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

3420 ) 

3421 

3422 # User1 cancels the event 

3423 with events_session(token1) as api: 

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

3425 

3426 process_jobs() 

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

3428 while push_collector.count_for_user(user2.id): 

3429 push_collector.pop_for_user(user2.id) 

3430 

3431 # Run the event reminder handler 

3432 send_event_reminders(empty_pb2.Empty()) 

3433 process_jobs() 

3434 

3435 # Verify that no reminder notification was sent for user2 

3436 with session_scope() as session: 

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

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

3439 assert len(reminder_notifs) == 0 

3440 

3441 

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

3443def test_event_reminder_not_sent_for_invisible_attendee( 

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

3445): 

3446 user1, token1 = generate_user() 

3447 user2, token2 = generate_user() 

3448 

3449 with session_scope() as session: 

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

3451 

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

3453 end_time = start_time + timedelta(hours=1) 

3454 

3455 with events_session(token1) as api: 

3456 res = api.CreateEvent( 

3457 events_pb2.CreateEventReq( 

3458 title="Invisible Attendee Reminder Test", 

3459 content="Content.", 

3460 location=events_pb2.EventLocation( 

3461 address="Near Null Island", 

3462 lat=0.1, 

3463 lng=0.2, 

3464 ), 

3465 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3466 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3467 ) 

3468 ) 

3469 event_id = res.event_id 

3470 

3471 moderator.approve_event_occurrence(event_id) 

3472 process_jobs() 

3473 

3474 with events_session(token2) as api: 

3475 api.SetEventAttendance( 

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

3477 ) 

3478 

3479 with session_scope() as session: 

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

3481 

3482 send_event_reminders(empty_pb2.Empty()) 

3483 process_jobs() 

3484 

3485 with session_scope() as session: 

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

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

3488 assert len(reminder_notifs) == 0 

3489 

3490 

3491def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3493 user1, token1 = generate_user() 

3494 user2, token2 = generate_user() 

3495 

3496 with session_scope() as session: 

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

3498 

3499 start = now() 

3500 

3501 # User1 creates event A with 3 occurrences 

3502 event_a_ids = [] 

3503 with events_session(token1) as api: 

3504 res = api.CreateEvent( 

3505 events_pb2.CreateEventReq( 

3506 title="Event A", 

3507 content="Content A.", 

3508 parent_community_id=c_id, 

3509 location=events_pb2.EventLocation( 

3510 address="Near Null Island", 

3511 lat=0.1, 

3512 lng=0.2, 

3513 ), 

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

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

3516 ) 

3517 ) 

3518 event_a_ids.append(res.event_id) 

3519 for i in range(2): 

3520 res = api.ScheduleEvent( 

3521 events_pb2.ScheduleEventReq( 

3522 event_id=event_a_ids[-1], 

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

3524 location=events_pb2.EventLocation( 

3525 address="Near Null Island", 

3526 lat=0.1, 

3527 lng=0.2, 

3528 ), 

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

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

3531 ) 

3532 ) 

3533 event_a_ids.append(res.event_id) 

3534 

3535 # User2 creates event B with 2 occurrences 

3536 event_b_ids = [] 

3537 with events_session(token2) as api: 

3538 res = api.CreateEvent( 

3539 events_pb2.CreateEventReq( 

3540 title="Event B", 

3541 content="Content B.", 

3542 parent_community_id=c_id, 

3543 location=events_pb2.EventLocation( 

3544 address="Near Null Island", 

3545 lat=0.1, 

3546 lng=0.2, 

3547 ), 

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

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

3550 ) 

3551 ) 

3552 event_b_ids.append(res.event_id) 

3553 res = api.ScheduleEvent( 

3554 events_pb2.ScheduleEventReq( 

3555 event_id=event_b_ids[-1], 

3556 content="B occurrence 1", 

3557 location=events_pb2.EventLocation( 

3558 address="Near Null Island", 

3559 lat=0.1, 

3560 lng=0.2, 

3561 ), 

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

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

3564 ) 

3565 ) 

3566 event_b_ids.append(res.event_id) 

3567 

3568 moderator.approve_event_occurrence(event_a_ids[0]) 

3569 moderator.approve_event_occurrence(event_b_ids[0]) 

3570 

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

3572 with events_session(token1) as api: 

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

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

3575 assert sorted(returned_ids) == sorted(event_a_ids) 

3576 

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

3578 with events_session(token2) as api: 

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

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

3581 assert sorted(returned_ids) == sorted(event_b_ids) 

3582 

3583 

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

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

3586 user1, token1 = generate_user() 

3587 user2, token2 = generate_user() 

3588 

3589 with session_scope() as session: 

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

3591 

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

3593 end_time = start_time + timedelta(hours=3) 

3594 

3595 with events_session(token1) as api: 

3596 res = api.CreateEvent( 

3597 events_pb2.CreateEventReq( 

3598 title="Comment Test", 

3599 content="Content.", 

3600 parent_community_id=c_id, 

3601 location=events_pb2.EventLocation( 

3602 address="Near Null Island", 

3603 lat=0.1, 

3604 lng=0.2, 

3605 ), 

3606 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3607 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3608 ) 

3609 ) 

3610 event_id = res.event_id 

3611 thread_id = res.thread.thread_id 

3612 

3613 moderator.approve_event_occurrence(event_id) 

3614 process_jobs() 

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

3616 push_collector.pop_for_user(user1.id) 

3617 

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

3619 with events_session(token1) as api: 

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

3621 

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

3623 with threads_session(token2) as api: 

3624 comment_thread_id = api.PostReply( 

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

3626 ).thread_id 

3627 

3628 process_jobs() 

3629 

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

3631 comment_db_id = comment_thread_id // 10 

3632 with session_scope() as session: 

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

3634 

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

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

3637 assert len(comment_notifs) == 1 

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

3639 

3640 

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

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

3643 user1, token1 = generate_user() 

3644 user2, token2 = generate_user() 

3645 user3, token3 = generate_user() 

3646 

3647 with session_scope() as session: 

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

3649 

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

3651 end_time = start_time + timedelta(hours=3) 

3652 

3653 with events_session(token1) as api: 

3654 res = api.CreateEvent( 

3655 events_pb2.CreateEventReq( 

3656 title="Reply Test", 

3657 content="Content.", 

3658 location=events_pb2.EventLocation( 

3659 address="Near Null Island", 

3660 lat=0.1, 

3661 lng=0.2, 

3662 ), 

3663 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3664 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3665 ) 

3666 ) 

3667 event_id = res.event_id 

3668 thread_id = res.thread.thread_id 

3669 

3670 moderator.approve_event_occurrence(event_id) 

3671 process_jobs() 

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

3673 push_collector.pop_for_user(user1.id) 

3674 

3675 # User2 posts a top-level comment 

3676 with threads_session(token2) as api: 

3677 comment_thread_id = api.PostReply( 

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

3679 ).thread_id 

3680 

3681 process_jobs() 

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

3683 push_collector.pop_for_user(user1.id) 

3684 

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

3686 with threads_session(token3) as api: 

3687 nested_reply_thread_id = api.PostReply( 

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

3689 ).thread_id 

3690 

3691 process_jobs() 

3692 

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

3694 nested_reply_db_id = nested_reply_thread_id // 10 

3695 with session_scope() as session: 

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

3697 

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

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

3700 assert len(reply_notifs) == 1 

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