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

1475 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-26 11:50 +0000

1import re 

2from datetime import datetime, timedelta 

3from zoneinfo import ZoneInfo 

4 

5import grpc 

6import pytest 

7from google.protobuf import empty_pb2, wrappers_pb2 

8from psycopg.types.range import TimestamptzRange 

9from sqlalchemy import select 

10from sqlalchemy.sql.expression import update 

11 

12from couchers.db import session_scope 

13from couchers.jobs.handlers import send_event_reminders 

14from couchers.models import ( 

15 BackgroundJob, 

16 BackgroundJobState, 

17 Comment, 

18 EventOccurrence, 

19 ModerationState, 

20 ModerationVisibility, 

21 Notification, 

22 NotificationDelivery, 

23 NotificationTopicAction, 

24 Reply, 

25 Upload, 

26 User, 

27) 

28from couchers.proto import editor_pb2, events_pb2, threads_pb2 

29from couchers.tasks import enforce_community_memberships 

30from couchers.utils import datetime_to_iso8601_local, now, to_aware_datetime 

31from tests.fixtures.db import generate_user 

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

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

34from tests.test_communities import create_community, create_group 

35 

36 

37@pytest.fixture(autouse=True) 

38def _(testconfig): 

39 pass 

40 

41 

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

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

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

45 

46 

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

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

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

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

51 

52 

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

54 # test cases: 

55 # can create event 

56 # cannot create event with missing details 

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

58 # can create in different timezones 

59 

60 # event creator 

61 user1, token1 = generate_user() 

62 # community moderator 

63 user2, token2 = generate_user() 

64 # third party 

65 user3, token3 = generate_user() 

66 

67 with session_scope() as session: 

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

69 

70 time_before = now() 

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

72 end_time = start_time + timedelta(hours=3) 

73 

74 # Can create an event 

75 with events_session(token1) as api: 

76 res = api.CreateEvent( 

77 events_pb2.CreateEventReq( 

78 title="Dummy Title", 

79 content="Dummy content.", 

80 photo_key=None, 

81 location=events_pb2.EventLocation( 

82 address="Near Null Island", 

83 lat=0.1, 

84 lng=0.2, 

85 ), 

86 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

87 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

88 ) 

89 ) 

90 

91 assert res.is_next 

92 assert res.title == "Dummy Title" 

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

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

95 assert not res.photo_url 

96 assert res.HasField("location") 

97 assert res.location.lat == 0.1 

98 assert res.location.lng == 0.2 

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

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

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

102 assert res.creator_user_id == user1.id 

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

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

105 assert is_utc_or_gmt(res.timezone) 

106 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

107 assert res.organizer 

108 assert res.subscriber 

109 assert res.going_count == 1 

110 assert res.organizer_count == 1 

111 assert res.subscriber_count == 1 

112 assert res.owner_user_id == user1.id 

113 assert not res.owner_community_id 

114 assert not res.owner_group_id 

115 assert res.thread.thread_id 

116 assert res.can_edit 

117 assert not res.can_moderate 

118 

119 event_id = res.event_id 

120 

121 # Approve the event so other users can see it 

122 moderator.approve_event_occurrence(event_id) 

123 

124 with events_session(token2) as api: 

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

126 

127 assert res.is_next 

128 assert res.title == "Dummy Title" 

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

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

131 assert not res.photo_url 

132 assert res.HasField("location") 

133 assert res.location.lat == 0.1 

134 assert res.location.lng == 0.2 

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

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

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

138 assert res.creator_user_id == user1.id 

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

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

141 assert is_utc_or_gmt(res.timezone) 

142 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

143 assert not res.organizer 

144 assert not res.subscriber 

145 assert res.going_count == 1 

146 assert res.organizer_count == 1 

147 assert res.subscriber_count == 1 

148 assert res.owner_user_id == user1.id 

149 assert not res.owner_community_id 

150 assert not res.owner_group_id 

151 assert res.thread.thread_id 

152 assert res.can_edit 

153 assert res.can_moderate 

154 

155 with events_session(token3) as api: 

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

157 

158 assert res.is_next 

159 assert res.title == "Dummy Title" 

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

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

162 assert not res.photo_url 

163 assert res.HasField("location") 

164 assert res.location.lat == 0.1 

165 assert res.location.lng == 0.2 

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

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

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

169 assert res.creator_user_id == user1.id 

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

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

172 assert is_utc_or_gmt(res.timezone) 

173 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

174 assert not res.organizer 

175 assert not res.subscriber 

176 assert res.going_count == 1 

177 assert res.organizer_count == 1 

178 assert res.subscriber_count == 1 

179 assert res.owner_user_id == user1.id 

180 assert not res.owner_community_id 

181 assert not res.owner_group_id 

182 assert res.thread.thread_id 

183 assert not res.can_edit 

184 assert not res.can_moderate 

185 

186 # Failure cases 

187 with events_session(token1) as api: 

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

189 api.CreateEvent( 

190 events_pb2.CreateEventReq( 

191 # title="Dummy Title", 

192 content="Dummy content.", 

193 photo_key=None, 

194 location=events_pb2.EventLocation( 

195 address="Near Null Island", 

196 lat=0.1, 

197 lng=0.2, 

198 ), 

199 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

200 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

201 ) 

202 ) 

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

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

205 

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

207 api.CreateEvent( 

208 events_pb2.CreateEventReq( 

209 title="Dummy Title", 

210 # content="Dummy content.", 

211 photo_key=None, 

212 location=events_pb2.EventLocation( 

213 address="Near Null Island", 

214 lat=0.1, 

215 lng=0.2, 

216 ), 

217 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

218 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

219 ) 

220 ) 

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

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

223 

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

225 api.CreateEvent( 

226 events_pb2.CreateEventReq( 

227 title="Dummy Title", 

228 content="Dummy content.", 

229 photo_key="nonexistent", 

230 location=events_pb2.EventLocation( 

231 address="Near Null Island", 

232 lat=0.1, 

233 lng=0.2, 

234 ), 

235 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

236 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

237 ) 

238 ) 

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

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

241 

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

243 api.CreateEvent( 

244 events_pb2.CreateEventReq( 

245 title="Dummy Title", 

246 content="Dummy content.", 

247 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

248 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

249 ) 

250 ) 

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

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

253 

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

255 api.CreateEvent( 

256 events_pb2.CreateEventReq( 

257 title="Dummy Title", 

258 content="Dummy content.", 

259 location=events_pb2.EventLocation( 

260 address="Near Null Island", 

261 ), 

262 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

263 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

264 ) 

265 ) 

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

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

268 

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

270 api.CreateEvent( 

271 events_pb2.CreateEventReq( 

272 title="Dummy Title", 

273 content="Dummy content.", 

274 location=events_pb2.EventLocation( 

275 lat=0.1, 

276 lng=0.1, 

277 ), 

278 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

279 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

280 ) 

281 ) 

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

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

284 

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

286 api.CreateEvent( 

287 events_pb2.CreateEventReq( 

288 title="Dummy Title", 

289 content="Dummy content.", 

290 location=events_pb2.EventLocation( 

291 address="Near Null Island", 

292 lat=0.1, 

293 lng=0.2, 

294 ), 

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

296 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

297 ) 

298 ) 

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

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

301 

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

303 api.CreateEvent( 

304 events_pb2.CreateEventReq( 

305 title="Dummy Title", 

306 content="Dummy content.", 

307 location=events_pb2.EventLocation( 

308 address="Near Null Island", 

309 lat=0.1, 

310 lng=0.2, 

311 ), 

312 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

313 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

314 ) 

315 ) 

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

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

318 

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

320 api.CreateEvent( 

321 events_pb2.CreateEventReq( 

322 title="Dummy Title", 

323 content="Dummy content.", 

324 location=events_pb2.EventLocation( 

325 address="Near Null Island", 

326 lat=0.1, 

327 lng=0.2, 

328 ), 

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

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

331 ) 

332 ) 

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

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

335 

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

337 api.CreateEvent( 

338 events_pb2.CreateEventReq( 

339 title="Dummy Title", 

340 content="Dummy content.", 

341 location=events_pb2.EventLocation( 

342 address="Near Null Island", 

343 lat=0.1, 

344 lng=0.2, 

345 ), 

346 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

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

348 ) 

349 ) 

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

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

352 

353 

354def test_CreateEvent_incomplete_profile(db): 

355 user1, token1 = generate_user(complete_profile=False) 

356 user2, token2 = generate_user() 

357 

358 with session_scope() as session: 

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

360 

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

362 end_time = start_time + timedelta(hours=3) 

363 

364 with events_session(token1) as api: 

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

366 api.CreateEvent( 

367 events_pb2.CreateEventReq( 

368 title="Dummy Title", 

369 content="Dummy content.", 

370 photo_key=None, 

371 location=events_pb2.EventLocation( 

372 address="Near Null Island", 

373 lat=0.1, 

374 lng=0.2, 

375 ), 

376 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

377 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

378 ) 

379 ) 

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

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

382 

383 

384def test_ScheduleEvent(db): 

385 # test cases: 

386 # can schedule a new event occurrence 

387 

388 user, token = generate_user() 

389 

390 with session_scope() as session: 

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

392 

393 time_before = now() 

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

395 end_time = start_time + timedelta(hours=3) 

396 

397 with events_session(token) as api: 

398 res = api.CreateEvent( 

399 events_pb2.CreateEventReq( 

400 title="Dummy Title", 

401 content="Dummy content.", 

402 parent_community_id=c_id, 

403 location=events_pb2.EventLocation( 

404 address="Near Null Island", 

405 lat=0.1, 

406 lng=0.2, 

407 ), 

408 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

409 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

410 ) 

411 ) 

412 

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

414 new_end_time = new_start_time + timedelta(hours=2) 

415 

416 res = api.ScheduleEvent( 

417 events_pb2.ScheduleEventReq( 

418 event_id=res.event_id, 

419 content="New event occurrence", 

420 location=events_pb2.EventLocation( 

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

422 lat=0.3, 

423 lng=0.2, 

424 ), 

425 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time), 

426 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time), 

427 ) 

428 ) 

429 

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

431 

432 assert not res.is_next 

433 assert res.title == "Dummy Title" 

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

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

436 assert not res.photo_url 

437 assert res.HasField("location") 

438 assert res.location.lat == 0.3 

439 assert res.location.lng == 0.2 

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

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

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

443 assert res.creator_user_id == user.id 

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

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

446 assert is_utc_or_gmt(res.timezone) 

447 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

448 assert res.organizer 

449 assert res.subscriber 

450 assert res.going_count == 1 

451 assert res.organizer_count == 1 

452 assert res.subscriber_count == 1 

453 assert res.owner_user_id == user.id 

454 assert not res.owner_community_id 

455 assert not res.owner_group_id 

456 assert res.thread.thread_id 

457 assert res.can_edit 

458 assert res.can_moderate 

459 

460 

461def test_cannot_overlap_occurrences_schedule(db): 

462 user, token = generate_user() 

463 

464 with session_scope() as session: 

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

466 

467 start = now() 

468 

469 with events_session(token) as api: 

470 res = api.CreateEvent( 

471 events_pb2.CreateEventReq( 

472 title="Dummy Title", 

473 content="Dummy content.", 

474 parent_community_id=c_id, 

475 location=events_pb2.EventLocation( 

476 address="Near Null Island", 

477 lat=0.1, 

478 lng=0.2, 

479 ), 

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

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

482 ) 

483 ) 

484 

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

486 api.ScheduleEvent( 

487 events_pb2.ScheduleEventReq( 

488 event_id=res.event_id, 

489 content="New event occurrence", 

490 location=events_pb2.EventLocation( 

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

492 lat=0.3, 

493 lng=0.2, 

494 ), 

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

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

497 ) 

498 ) 

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

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

501 

502 

503def test_cannot_overlap_occurrences_update(db): 

504 user, token = generate_user() 

505 

506 with session_scope() as session: 

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

508 

509 start = now() 

510 

511 with events_session(token) as api: 

512 res = api.CreateEvent( 

513 events_pb2.CreateEventReq( 

514 title="Dummy Title", 

515 content="Dummy content.", 

516 parent_community_id=c_id, 

517 location=events_pb2.EventLocation( 

518 address="Near Null Island", 

519 lat=0.1, 

520 lng=0.2, 

521 ), 

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

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

524 ) 

525 ) 

526 

527 event_id = api.ScheduleEvent( 

528 events_pb2.ScheduleEventReq( 

529 event_id=res.event_id, 

530 content="New event occurrence", 

531 location=events_pb2.EventLocation( 

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

533 lat=0.3, 

534 lng=0.2, 

535 ), 

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

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

538 ) 

539 ).event_id 

540 

541 # can overlap with this current existing occurrence 

542 api.UpdateEvent( 

543 events_pb2.UpdateEventReq( 

544 event_id=event_id, 

545 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

547 ), 

548 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

550 ), 

551 ) 

552 ) 

553 

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

555 api.UpdateEvent( 

556 events_pb2.UpdateEventReq( 

557 event_id=event_id, 

558 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

560 ), 

561 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

563 ), 

564 ) 

565 ) 

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

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

568 

569 

570def test_UpdateEvent_single(db, moderator: Moderator): 

571 # test cases: 

572 # owner can update 

573 # community owner can update 

574 # notifies attendees 

575 

576 # event creator 

577 user1, token1 = generate_user() 

578 # community moderator 

579 user2, token2 = generate_user() 

580 # third parties 

581 user3, token3 = generate_user() 

582 user4, token4 = generate_user() 

583 user5, token5 = generate_user() 

584 user6, token6 = generate_user() 

585 

586 with session_scope() as session: 

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

588 

589 time_before = now() 

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

591 end_time = start_time + timedelta(hours=3) 

592 

593 with events_session(token1) as api: 

594 res = api.CreateEvent( 

595 events_pb2.CreateEventReq( 

596 title="Dummy Title", 

597 content="Dummy content.", 

598 parent_community_id=c_id, 

599 location=events_pb2.EventLocation( 

600 address="Near Null Island", 

601 lat=0.1, 

602 lng=0.2, 

603 ), 

604 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

605 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

606 ) 

607 ) 

608 

609 event_id = res.event_id 

610 

611 moderator.approve_event_occurrence(event_id) 

612 

613 with events_session(token4) as api: 

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

615 

616 with events_session(token5) as api: 

617 api.SetEventAttendance( 

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

619 ) 

620 

621 with events_session(token6) as api: 

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

623 

624 time_before_update = now() 

625 

626 with events_session(token1) as api: 

627 res = api.UpdateEvent( 

628 events_pb2.UpdateEventReq( 

629 event_id=event_id, 

630 ) 

631 ) 

632 

633 with events_session(token1) as api: 

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

635 

636 assert res.is_next 

637 assert res.title == "Dummy Title" 

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

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

640 assert not res.photo_url 

641 assert res.HasField("location") 

642 assert res.location.lat == 0.1 

643 assert res.location.lng == 0.2 

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

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

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

647 assert res.creator_user_id == user1.id 

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

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

650 assert is_utc_or_gmt(res.timezone) 

651 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

652 assert res.organizer 

653 assert res.subscriber 

654 assert res.going_count == 2 

655 assert res.organizer_count == 1 

656 assert res.subscriber_count == 3 

657 assert res.owner_user_id == user1.id 

658 assert not res.owner_community_id 

659 assert not res.owner_group_id 

660 assert res.thread.thread_id 

661 assert res.can_edit 

662 assert not res.can_moderate 

663 

664 with events_session(token2) as api: 

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

666 

667 assert res.is_next 

668 assert res.title == "Dummy Title" 

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

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

671 assert not res.photo_url 

672 assert res.HasField("location") 

673 assert res.location.lat == 0.1 

674 assert res.location.lng == 0.2 

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

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

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

678 assert res.creator_user_id == user1.id 

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

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

681 assert is_utc_or_gmt(res.timezone) 

682 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

683 assert not res.organizer 

684 assert not res.subscriber 

685 assert res.going_count == 2 

686 assert res.organizer_count == 1 

687 assert res.subscriber_count == 3 

688 assert res.owner_user_id == user1.id 

689 assert not res.owner_community_id 

690 assert not res.owner_group_id 

691 assert res.thread.thread_id 

692 assert res.can_edit 

693 assert res.can_moderate 

694 

695 with events_session(token3) as api: 

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

697 

698 assert res.is_next 

699 assert res.title == "Dummy Title" 

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

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

702 assert not res.photo_url 

703 assert res.HasField("location") 

704 assert res.location.lat == 0.1 

705 assert res.location.lng == 0.2 

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

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

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

709 assert res.creator_user_id == user1.id 

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

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

712 assert is_utc_or_gmt(res.timezone) 

713 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

714 assert not res.organizer 

715 assert not res.subscriber 

716 assert res.going_count == 2 

717 assert res.organizer_count == 1 

718 assert res.subscriber_count == 3 

719 assert res.owner_user_id == user1.id 

720 assert not res.owner_community_id 

721 assert not res.owner_group_id 

722 assert res.thread.thread_id 

723 assert not res.can_edit 

724 assert not res.can_moderate 

725 

726 with events_session(token1) as api: 

727 res = api.UpdateEvent( 

728 events_pb2.UpdateEventReq( 

729 event_id=event_id, 

730 location=events_pb2.EventLocation( 

731 address="Nearer Null Island", 

732 lat=0.01, 

733 lng=0.02, 

734 ), 

735 ) 

736 ) 

737 

738 with events_session(token3) as api: 

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

740 

741 assert res.HasField("location") 

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

743 assert res.location.lat == 0.01 

744 assert res.location.lng == 0.02 

745 

746 

747def test_UpdateEvent_all(db, moderator: Moderator): 

748 # event creator 

749 user1, token1 = generate_user() 

750 # community moderator 

751 user2, token2 = generate_user() 

752 # third parties 

753 user3, token3 = generate_user() 

754 user4, token4 = generate_user() 

755 user5, token5 = generate_user() 

756 user6, token6 = generate_user() 

757 

758 with session_scope() as session: 

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

760 

761 time_before = now() 

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

763 end_time = start_time + timedelta(hours=1.5) 

764 

765 event_ids = [] 

766 

767 with events_session(token1) as api: 

768 res = api.CreateEvent( 

769 events_pb2.CreateEventReq( 

770 title="Dummy Title", 

771 content="0th occurrence", 

772 location=events_pb2.EventLocation( 

773 address="Near Null Island", 

774 lat=0.1, 

775 lng=0.2, 

776 ), 

777 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

778 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

779 ) 

780 ) 

781 

782 event_id = res.event_id 

783 event_ids.append(event_id) 

784 

785 moderator.approve_event_occurrence(event_id) 

786 

787 with events_session(token4) as api: 

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

789 

790 with events_session(token5) as api: 

791 api.SetEventAttendance( 

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

793 ) 

794 

795 with events_session(token6) as api: 

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

797 

798 with events_session(token1) as api: 

799 for i in range(5): 

800 res = api.ScheduleEvent( 

801 events_pb2.ScheduleEventReq( 

802 event_id=event_ids[-1], 

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

804 location=events_pb2.EventLocation( 

805 address="Near Null Island", 

806 lat=0.1, 

807 lng=0.2, 

808 ), 

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

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

811 ) 

812 ) 

813 

814 event_ids.append(res.event_id) 

815 

816 # Approve all scheduled occurrences 

817 for eid in event_ids[1:]: 

818 moderator.approve_event_occurrence(eid) 

819 

820 updated_event_id = event_ids[3] 

821 

822 time_before_update = now() 

823 

824 with events_session(token1) as api: 

825 res = api.UpdateEvent( 

826 events_pb2.UpdateEventReq( 

827 event_id=updated_event_id, 

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

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

830 location=events_pb2.EventLocation( 

831 address="Not so near Null Island", 

832 lat=0.2, 

833 lng=0.2, 

834 ), 

835 update_all_future=True, 

836 ) 

837 ) 

838 

839 time_after_update = now() 

840 

841 with events_session(token2) as api: 

842 for i in range(3): 

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

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

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

846 

847 for i in range(3, 6): 

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

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

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

851 

852 

853def test_GetEvent(db, moderator: Moderator): 

854 # event creator 

855 user1, token1 = generate_user() 

856 # community moderator 

857 user2, token2 = generate_user() 

858 # third parties 

859 user3, token3 = generate_user() 

860 user4, token4 = generate_user() 

861 user5, token5 = generate_user() 

862 user6, token6 = generate_user() 

863 

864 with session_scope() as session: 

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

866 

867 time_before = now() 

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

869 end_time = start_time + timedelta(hours=3) 

870 

871 with events_session(token1) as api: 

872 # in person event 

873 res = api.CreateEvent( 

874 events_pb2.CreateEventReq( 

875 title="Dummy Title", 

876 content="Dummy content.", 

877 location=events_pb2.EventLocation( 

878 address="Near Null Island", 

879 lat=0.1, 

880 lng=0.2, 

881 ), 

882 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

883 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

884 ) 

885 ) 

886 

887 event_id = res.event_id 

888 

889 moderator.approve_event_occurrence(event_id) 

890 

891 with events_session(token4) as api: 

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

893 

894 with events_session(token5) as api: 

895 api.SetEventAttendance( 

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

897 ) 

898 

899 with events_session(token6) as api: 

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

901 

902 with events_session(token1) as api: 

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

904 

905 assert res.is_next 

906 assert res.title == "Dummy Title" 

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

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

909 assert not res.photo_url 

910 assert res.HasField("location") 

911 assert res.location.lat == 0.1 

912 assert res.location.lng == 0.2 

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

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

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

916 assert res.creator_user_id == user1.id 

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

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

919 assert is_utc_or_gmt(res.timezone) 

920 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

921 assert res.organizer 

922 assert res.subscriber 

923 assert res.going_count == 2 

924 assert res.organizer_count == 1 

925 assert res.subscriber_count == 3 

926 assert res.owner_user_id == user1.id 

927 assert not res.owner_community_id 

928 assert not res.owner_group_id 

929 assert res.thread.thread_id 

930 assert res.can_edit 

931 assert not res.can_moderate 

932 

933 with events_session(token2) as api: 

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

935 

936 assert res.is_next 

937 assert res.title == "Dummy Title" 

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

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

940 assert not res.photo_url 

941 assert res.HasField("location") 

942 assert res.location.lat == 0.1 

943 assert res.location.lng == 0.2 

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

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

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

947 assert res.creator_user_id == user1.id 

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

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

950 assert is_utc_or_gmt(res.timezone) 

951 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

952 assert not res.organizer 

953 assert not res.subscriber 

954 assert res.going_count == 2 

955 assert res.organizer_count == 1 

956 assert res.subscriber_count == 3 

957 assert res.owner_user_id == user1.id 

958 assert not res.owner_community_id 

959 assert not res.owner_group_id 

960 assert res.thread.thread_id 

961 assert res.can_edit 

962 assert res.can_moderate 

963 

964 with events_session(token3) as api: 

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

966 

967 assert res.is_next 

968 assert res.title == "Dummy Title" 

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

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

971 assert not res.photo_url 

972 assert res.HasField("location") 

973 assert res.location.lat == 0.1 

974 assert res.location.lng == 0.2 

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

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

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

978 assert res.creator_user_id == user1.id 

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

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

981 assert is_utc_or_gmt(res.timezone) 

982 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

983 assert not res.organizer 

984 assert not res.subscriber 

985 assert res.going_count == 2 

986 assert res.organizer_count == 1 

987 assert res.subscriber_count == 3 

988 assert res.owner_user_id == user1.id 

989 assert not res.owner_community_id 

990 assert not res.owner_group_id 

991 assert res.thread.thread_id 

992 assert not res.can_edit 

993 assert not res.can_moderate 

994 

995 

996def test_CancelEvent(db, moderator: Moderator): 

997 # event creator 

998 user1, token1 = generate_user() 

999 # community moderator 

1000 user2, token2 = generate_user() 

1001 # third parties 

1002 user3, token3 = generate_user() 

1003 user4, token4 = generate_user() 

1004 user5, token5 = generate_user() 

1005 user6, token6 = generate_user() 

1006 

1007 with session_scope() as session: 

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

1009 

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

1011 end_time = start_time + timedelta(hours=3) 

1012 

1013 with events_session(token1) as api: 

1014 res = api.CreateEvent( 

1015 events_pb2.CreateEventReq( 

1016 title="Dummy Title", 

1017 content="Dummy content.", 

1018 location=events_pb2.EventLocation( 

1019 address="Near Null Island", 

1020 lat=0.1, 

1021 lng=0.2, 

1022 ), 

1023 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1024 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1025 ) 

1026 ) 

1027 

1028 event_id = res.event_id 

1029 

1030 moderator.approve_event_occurrence(event_id) 

1031 

1032 with events_session(token4) as api: 

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

1034 

1035 with events_session(token5) as api: 

1036 api.SetEventAttendance( 

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

1038 ) 

1039 

1040 with events_session(token6) as api: 

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

1042 

1043 with events_session(token1) as api: 

1044 res = api.CancelEvent( 

1045 events_pb2.CancelEventReq( 

1046 event_id=event_id, 

1047 ) 

1048 ) 

1049 

1050 with events_session(token1) as api: 

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

1052 assert res.is_cancelled 

1053 

1054 with events_session(token1) as api: 

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

1056 api.UpdateEvent( 

1057 events_pb2.UpdateEventReq( 

1058 event_id=event_id, 

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

1060 ) 

1061 ) 

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

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

1064 

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

1066 api.InviteEventOrganizer( 

1067 events_pb2.InviteEventOrganizerReq( 

1068 event_id=event_id, 

1069 user_id=user3.id, 

1070 ) 

1071 ) 

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

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

1074 

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

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

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

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

1079 

1080 with events_session(token3) as api: 

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

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

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

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

1085 

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

1087 api.SetEventAttendance( 

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

1089 ) 

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

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

1092 

1093 with events_session(token1) as api: 

1094 for include_cancelled in [True, False]: 

1095 res = api.ListEventOccurrences( 

1096 events_pb2.ListEventOccurrencesReq( 

1097 event_id=event_id, 

1098 include_cancelled=include_cancelled, 

1099 ) 

1100 ) 

1101 if include_cancelled: 

1102 assert len(res.events) > 0 

1103 else: 

1104 assert len(res.events) == 0 

1105 

1106 res = api.ListMyEvents( 

1107 events_pb2.ListMyEventsReq( 

1108 include_cancelled=include_cancelled, 

1109 ) 

1110 ) 

1111 if include_cancelled: 

1112 assert len(res.events) > 0 

1113 else: 

1114 assert len(res.events) == 0 

1115 

1116 

1117def test_ListEventAttendees(db, moderator: Moderator): 

1118 # event creator 

1119 user1, token1 = generate_user() 

1120 # others 

1121 user2, token2 = generate_user() 

1122 user3, token3 = generate_user() 

1123 user4, token4 = generate_user() 

1124 user5, token5 = generate_user() 

1125 user6, token6 = generate_user() 

1126 

1127 with session_scope() as session: 

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

1129 

1130 with events_session(token1) as api: 

1131 event_id = api.CreateEvent( 

1132 events_pb2.CreateEventReq( 

1133 title="Dummy Title", 

1134 content="Dummy content.", 

1135 location=events_pb2.EventLocation( 

1136 address="Near Null Island", 

1137 lat=0.1, 

1138 lng=0.2, 

1139 ), 

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

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

1142 ) 

1143 ).event_id 

1144 

1145 moderator.approve_event_occurrence(event_id) 

1146 

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

1148 with events_session(token) as api: 

1149 api.SetEventAttendance( 

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

1151 ) 

1152 

1153 with events_session(token6) as api: 

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

1155 

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

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

1158 

1159 res = api.ListEventAttendees( 

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

1161 ) 

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

1163 

1164 res = api.ListEventAttendees( 

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

1166 ) 

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

1168 assert not res.next_page_token 

1169 

1170 

1171def test_ListEventSubscribers(db, moderator: Moderator): 

1172 # event creator 

1173 user1, token1 = generate_user() 

1174 # others 

1175 user2, token2 = generate_user() 

1176 user3, token3 = generate_user() 

1177 user4, token4 = generate_user() 

1178 user5, token5 = generate_user() 

1179 user6, token6 = generate_user() 

1180 

1181 with session_scope() as session: 

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

1183 

1184 with events_session(token1) as api: 

1185 event_id = api.CreateEvent( 

1186 events_pb2.CreateEventReq( 

1187 title="Dummy Title", 

1188 content="Dummy content.", 

1189 location=events_pb2.EventLocation( 

1190 address="Near Null Island", 

1191 lat=0.1, 

1192 lng=0.2, 

1193 ), 

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

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

1196 ) 

1197 ).event_id 

1198 

1199 moderator.approve_event_occurrence(event_id) 

1200 

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

1202 with events_session(token) as api: 

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

1204 

1205 with events_session(token6) as api: 

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

1207 

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

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

1210 

1211 res = api.ListEventSubscribers( 

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

1213 ) 

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

1215 

1216 res = api.ListEventSubscribers( 

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

1218 ) 

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

1220 assert not res.next_page_token 

1221 

1222 

1223def test_ListEventOrganizers(db, moderator: Moderator): 

1224 # event creator 

1225 user1, token1 = generate_user() 

1226 # others 

1227 user2, token2 = generate_user() 

1228 user3, token3 = generate_user() 

1229 user4, token4 = generate_user() 

1230 user5, token5 = generate_user() 

1231 user6, token6 = generate_user() 

1232 

1233 with session_scope() as session: 

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

1235 

1236 with events_session(token1) as api: 

1237 event_id = api.CreateEvent( 

1238 events_pb2.CreateEventReq( 

1239 title="Dummy Title", 

1240 content="Dummy content.", 

1241 location=events_pb2.EventLocation( 

1242 address="Near Null Island", 

1243 lat=0.1, 

1244 lng=0.2, 

1245 ), 

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

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

1248 ) 

1249 ).event_id 

1250 

1251 moderator.approve_event_occurrence(event_id) 

1252 

1253 with events_session(token1) as api: 

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

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

1256 

1257 with events_session(token6) as api: 

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

1259 

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

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

1262 

1263 res = api.ListEventOrganizers( 

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

1265 ) 

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

1267 

1268 res = api.ListEventOrganizers( 

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

1270 ) 

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

1272 assert not res.next_page_token 

1273 

1274 

1275def test_TransferEvent(db): 

1276 user1, token1 = generate_user() 

1277 user2, token2 = generate_user() 

1278 user3, token3 = generate_user() 

1279 user4, token4 = generate_user() 

1280 

1281 with session_scope() as session: 

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

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

1284 c_id = c.id 

1285 h_id = h.id 

1286 

1287 with events_session(token1) as api: 

1288 event_id = api.CreateEvent( 

1289 events_pb2.CreateEventReq( 

1290 title="Dummy Title", 

1291 content="Dummy content.", 

1292 location=events_pb2.EventLocation( 

1293 address="Near Null Island", 

1294 lat=0.1, 

1295 lng=0.2, 

1296 ), 

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

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

1299 ) 

1300 ).event_id 

1301 

1302 api.TransferEvent( 

1303 events_pb2.TransferEventReq( 

1304 event_id=event_id, 

1305 new_owner_community_id=c_id, 

1306 ) 

1307 ) 

1308 

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

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

1311 

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

1313 api.TransferEvent( 

1314 events_pb2.TransferEventReq( 

1315 event_id=event_id, 

1316 new_owner_group_id=h_id, 

1317 ) 

1318 ) 

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

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

1321 

1322 event_id = api.CreateEvent( 

1323 events_pb2.CreateEventReq( 

1324 title="Dummy Title", 

1325 content="Dummy content.", 

1326 location=events_pb2.EventLocation( 

1327 address="Near Null Island", 

1328 lat=0.1, 

1329 lng=0.2, 

1330 ), 

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

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

1333 ) 

1334 ).event_id 

1335 

1336 api.TransferEvent( 

1337 events_pb2.TransferEventReq( 

1338 event_id=event_id, 

1339 new_owner_group_id=h_id, 

1340 ) 

1341 ) 

1342 

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

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

1345 

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

1347 api.TransferEvent( 

1348 events_pb2.TransferEventReq( 

1349 event_id=event_id, 

1350 new_owner_community_id=c_id, 

1351 ) 

1352 ) 

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

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

1355 

1356 

1357def test_SetEventSubscription(db, moderator: Moderator): 

1358 user1, token1 = generate_user() 

1359 user2, token2 = generate_user() 

1360 

1361 with session_scope() as session: 

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

1363 

1364 with events_session(token1) as api: 

1365 event_id = api.CreateEvent( 

1366 events_pb2.CreateEventReq( 

1367 title="Dummy Title", 

1368 content="Dummy content.", 

1369 location=events_pb2.EventLocation( 

1370 address="Near Null Island", 

1371 lat=0.1, 

1372 lng=0.2, 

1373 ), 

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

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

1376 ) 

1377 ).event_id 

1378 

1379 moderator.approve_event_occurrence(event_id) 

1380 

1381 with events_session(token2) as api: 

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

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

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

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

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

1387 

1388 

1389def test_SetEventAttendance(db, moderator: Moderator): 

1390 user1, token1 = generate_user() 

1391 user2, token2 = generate_user() 

1392 

1393 with session_scope() as session: 

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

1395 

1396 with events_session(token1) as api: 

1397 event_id = api.CreateEvent( 

1398 events_pb2.CreateEventReq( 

1399 title="Dummy Title", 

1400 content="Dummy content.", 

1401 location=events_pb2.EventLocation( 

1402 address="Near Null Island", 

1403 lat=0.1, 

1404 lng=0.2, 

1405 ), 

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

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

1408 ) 

1409 ).event_id 

1410 

1411 moderator.approve_event_occurrence(event_id) 

1412 

1413 with events_session(token2) as api: 

1414 assert ( 

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

1416 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1417 ) 

1418 api.SetEventAttendance( 

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

1420 ) 

1421 assert ( 

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

1423 == events_pb2.ATTENDANCE_STATE_GOING 

1424 ) 

1425 api.SetEventAttendance( 

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

1427 ) 

1428 assert ( 

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

1430 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1431 ) 

1432 

1433 

1434def test_InviteEventOrganizer(db, moderator: Moderator): 

1435 user1, token1 = generate_user() 

1436 user2, token2 = generate_user() 

1437 

1438 with session_scope() as session: 

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

1440 

1441 with events_session(token1) as api: 

1442 event_id = api.CreateEvent( 

1443 events_pb2.CreateEventReq( 

1444 title="Dummy Title", 

1445 content="Dummy content.", 

1446 location=events_pb2.EventLocation( 

1447 address="Near Null Island", 

1448 lat=0.1, 

1449 lng=0.2, 

1450 ), 

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

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

1453 ) 

1454 ).event_id 

1455 

1456 moderator.approve_event_occurrence(event_id) 

1457 

1458 with events_session(token2) as api: 

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

1460 

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

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

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

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

1465 

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

1467 

1468 with events_session(token1) as api: 

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

1470 

1471 with events_session(token2) as api: 

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

1473 

1474 

1475def test_ListEventOccurrences(db): 

1476 user1, token1 = generate_user() 

1477 user2, token2 = generate_user() 

1478 user3, token3 = generate_user() 

1479 

1480 with session_scope() as session: 

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

1482 

1483 start = now() 

1484 

1485 event_ids = [] 

1486 

1487 with events_session(token1) as api: 

1488 res = api.CreateEvent( 

1489 events_pb2.CreateEventReq( 

1490 title="First occurrence", 

1491 content="Dummy content.", 

1492 parent_community_id=c_id, 

1493 location=events_pb2.EventLocation( 

1494 address="Near Null Island", 

1495 lat=0.1, 

1496 lng=0.2, 

1497 ), 

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

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

1500 ) 

1501 ) 

1502 

1503 event_ids.append(res.event_id) 

1504 

1505 for i in range(5): 

1506 res = api.ScheduleEvent( 

1507 events_pb2.ScheduleEventReq( 

1508 event_id=event_ids[-1], 

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

1510 location=events_pb2.EventLocation( 

1511 address="Near Null Island", 

1512 lat=0.1, 

1513 lng=0.2, 

1514 ), 

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

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

1517 ) 

1518 ) 

1519 

1520 event_ids.append(res.event_id) 

1521 

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

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

1524 

1525 res = api.ListEventOccurrences( 

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

1527 ) 

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

1529 

1530 res = api.ListEventOccurrences( 

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

1532 ) 

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

1534 assert not res.next_page_token 

1535 

1536 

1537def test_ListMyEvents(db, moderator: Moderator): 

1538 user1, token1 = generate_user() 

1539 user2, token2 = generate_user() 

1540 user3, token3 = generate_user() 

1541 user4, token4 = generate_user() 

1542 user5, token5 = generate_user() 

1543 

1544 with session_scope() as session: 

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

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

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

1548 c_id = global_community.id 

1549 macroregion_community = create_community( 

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

1551 ) 

1552 region_community = create_community( 

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

1554 ) 

1555 subregion_community = create_community( 

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

1557 ) 

1558 c2_id = subregion_community.id 

1559 

1560 start = now() 

1561 

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

1563 return events_pb2.CreateEventReq( 

1564 title="Dummy Title", 

1565 content="Dummy content.", 

1566 location=events_pb2.EventLocation( 

1567 address="Near Null Island", 

1568 lat=0.1, 

1569 lng=0.2, 

1570 ), 

1571 parent_community_id=community_id, 

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

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

1574 ) 

1575 

1576 with events_session(token1) as api: 

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

1578 

1579 moderator.approve_event_occurrence(e2) 

1580 

1581 with events_session(token2) as api: 

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

1583 

1584 moderator.approve_event_occurrence(e1) 

1585 

1586 with events_session(token1) as api: 

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

1588 

1589 moderator.approve_event_occurrence(e3) 

1590 

1591 with events_session(token2) as api: 

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

1593 

1594 moderator.approve_event_occurrence(e5) 

1595 

1596 with events_session(token3) as api: 

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

1598 

1599 moderator.approve_event_occurrence(e4) 

1600 

1601 with events_session(token4) as api: 

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

1603 

1604 moderator.approve_event_occurrence(e6) 

1605 

1606 with events_session(token1) as api: 

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

1608 

1609 with events_session(token1) as api: 

1610 api.SetEventAttendance( 

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

1612 ) 

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

1614 

1615 with events_session(token2) as api: 

1616 api.SetEventAttendance( 

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

1618 ) 

1619 

1620 with events_session(token3) as api: 

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

1622 

1623 with events_session(token1) as api: 

1624 # test pagination with token first 

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

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

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

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

1629 assert not res.next_page_token 

1630 

1631 res = api.ListMyEvents( 

1632 events_pb2.ListMyEventsReq( 

1633 subscribed=True, 

1634 attending=True, 

1635 organizing=True, 

1636 ) 

1637 ) 

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

1639 

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

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

1642 

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

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

1645 

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

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

1648 

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

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

1651 

1652 with events_session(token1) as api: 

1653 # Test pagination with page_number and verify total_items 

1654 res = api.ListMyEvents( 

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

1656 ) 

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

1658 assert res.total_items == 4 

1659 

1660 res = api.ListMyEvents( 

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

1662 ) 

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

1664 assert res.total_items == 4 

1665 

1666 # Verify no more pages 

1667 res = api.ListMyEvents( 

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

1669 ) 

1670 assert not res.events 

1671 assert res.total_items == 4 

1672 

1673 with events_session(token2) as api: 

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

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

1676 

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

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

1679 

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

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

1682 

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

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

1685 

1686 with events_session(token3) as api: 

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

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

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

1690 

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

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

1693 

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

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

1696 

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

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

1699 

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

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

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

1703 

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

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

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

1707 

1708 # my_communities_exclude_global works independently of my_communities flag 

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

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

1711 

1712 # my_communities_exclude_global filters organizing results too 

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

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

1715 

1716 # my_communities_exclude_global filters subscribed results too 

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

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

1719 

1720 with events_session(token5) as api: 

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

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

1723 

1724 

1725def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1726 user1, token1 = generate_user() 

1727 user2, token2 = generate_user() 

1728 

1729 with session_scope() as session: 

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

1731 c_id = c.id 

1732 

1733 start = now() 

1734 

1735 def make_event(hours): 

1736 return events_pb2.CreateEventReq( 

1737 title="Test Event", 

1738 content="Test content.", 

1739 location=events_pb2.EventLocation( 

1740 address="Near Null Island", 

1741 lat=0.1, 

1742 lng=0.2, 

1743 ), 

1744 parent_community_id=c_id, 

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

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

1747 ) 

1748 

1749 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1750 with events_session(token1) as api: 

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

1752 

1753 with events_session(token2) as api: 

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

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

1756 # e_both: user1 will be both organizer and attendee 

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

1758 

1759 moderator.approve_event_occurrence(e_own) 

1760 moderator.approve_event_occurrence(e_attending) 

1761 moderator.approve_event_occurrence(e_community_only) 

1762 moderator.approve_event_occurrence(e_both) 

1763 

1764 # invite user1 as organizer of e_both 

1765 with events_session(token2) as api: 

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

1767 

1768 # user1 RSVPs to e_attending and e_both 

1769 with events_session(token1) as api: 

1770 api.SetEventAttendance( 

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

1772 ) 

1773 api.SetEventAttendance( 

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

1775 ) 

1776 

1777 with events_session(token1) as api: 

1778 # baseline: all four community events visible 

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

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

1781 

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

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

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

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

1786 

1787 # exclude_attending with attending=True: invalid combination 

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

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

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

1791 

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

1793 with events_session(token2) as api: 

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

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

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

1797 

1798 

1799def test_RemoveEventOrganizer(db, moderator: Moderator): 

1800 user1, token1 = generate_user() 

1801 user2, token2 = generate_user() 

1802 

1803 with session_scope() as session: 

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

1805 

1806 with events_session(token1) as api: 

1807 event_id = api.CreateEvent( 

1808 events_pb2.CreateEventReq( 

1809 title="Dummy Title", 

1810 content="Dummy content.", 

1811 location=events_pb2.EventLocation( 

1812 address="Near Null Island", 

1813 lat=0.1, 

1814 lng=0.2, 

1815 ), 

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

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

1818 ) 

1819 ).event_id 

1820 

1821 moderator.approve_event_occurrence(event_id) 

1822 

1823 with events_session(token2) as api: 

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

1825 

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

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

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

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

1830 

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

1832 

1833 with events_session(token1) as api: 

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

1835 

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

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

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

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

1840 

1841 with events_session(token2) as api: 

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

1843 assert res.organizer 

1844 assert res.organizer_count == 2 

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

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

1847 

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

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

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

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

1852 

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

1854 assert not res.organizer 

1855 assert res.organizer_count == 1 

1856 

1857 # Test that event owner can remove co-organizers 

1858 with events_session(token1) as api: 

1859 # Add user2 back as organizer 

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

1861 

1862 # Verify user2 is now an organizer 

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

1864 assert res.organizer_count == 2 

1865 

1866 # Event owner can remove co-organizer 

1867 api.RemoveEventOrganizer( 

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

1869 ) 

1870 

1871 # Verify user2 is no longer an organizer 

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

1873 assert res.organizer_count == 1 

1874 

1875 # Test that non-organizers cannot remove other organizers 

1876 with events_session(token2) as api: 

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

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

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

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

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

1882 

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

1884 with events_session(token1) as api: 

1885 # Add user2 back as organizer 

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

1887 

1888 

1889def test_ListEventAttendees_regression(db): 

1890 # see issue #1617: 

1891 # 

1892 # 1. Create an event 

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

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

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

1896 # 

1897 # **Expected behaviour** 

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

1899 # 

1900 # **Actual/current behaviour** 

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

1902 

1903 user1, token1 = generate_user() 

1904 user2, token2 = generate_user() 

1905 user3, token3 = generate_user() 

1906 user4, token4 = generate_user() 

1907 user5, token5 = generate_user() 

1908 

1909 with session_scope() as session: 

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

1911 

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

1913 end_time = start_time + timedelta(hours=3) 

1914 

1915 with events_session(token1) as api: 

1916 res = api.CreateEvent( 

1917 events_pb2.CreateEventReq( 

1918 title="Dummy Title", 

1919 content="Dummy content.", 

1920 location=events_pb2.EventLocation( 

1921 address="Near Null Island", 

1922 lat=0.1, 

1923 lng=0.2, 

1924 ), 

1925 parent_community_id=c_id, 

1926 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1927 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1928 ) 

1929 ) 

1930 

1931 res = api.TransferEvent( 

1932 events_pb2.TransferEventReq( 

1933 event_id=res.event_id, 

1934 new_owner_community_id=c_id, 

1935 ) 

1936 ) 

1937 

1938 event_id = res.event_id 

1939 

1940 api.SetEventAttendance( 

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

1942 ) 

1943 api.SetEventAttendance( 

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

1945 ) 

1946 

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

1948 assert len(res.attendee_user_ids) == 1 

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

1950 

1951 

1952def test_GetEventCalendarFile(db, moderator: Moderator): 

1953 user1, token1 = generate_user() 

1954 user2, token2 = generate_user() 

1955 

1956 with session_scope() as session: 

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

1958 

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

1960 end_time = start_time + timedelta(hours=3) 

1961 

1962 with events_session(token1) as api: 

1963 created_event: events_pb2.Event = api.CreateEvent( 

1964 events_pb2.CreateEventReq( 

1965 title="Dummy Title", 

1966 content="Dummy content.", 

1967 parent_community_id=c_id, 

1968 location=events_pb2.EventLocation( 

1969 address="Near Null Island", 

1970 lat=0.1, 

1971 lng=0.2, 

1972 ), 

1973 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

1974 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

1975 ) 

1976 ) 

1977 event_id = created_event.event_id 

1978 

1979 moderator.approve_event_occurrence(event_id) 

1980 

1981 with events_session(token1) as api: 

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

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

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

1985 assert "SUMMARY:Dummy Title" in ics_string 

1986 assert "DESCRIPTION:Dummy content." in ics_string 

1987 assert "LOCATION:Near Null Island" in ics_string 

1988 assert "STATUS:CANCELLED" not in ics_string 

1989 pre_cancel_sequence = int(re.search(r"SEQUENCE:(\d+)", ics_string).group(1)) 

1990 

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

1992 

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

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

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

1996 assert "STATUS:CANCELLED" in ics_string 

1997 post_cancel_sequence = int(re.search(r"SEQUENCE:(\d+)", ics_string).group(1)) 

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

1999 assert post_cancel_sequence >= pre_cancel_sequence 

2000 

2001 

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

2003 user1, token1 = generate_user() 

2004 user2, token2 = generate_user() 

2005 user3, token3 = generate_user() 

2006 user4, token4 = generate_user() 

2007 

2008 with session_scope() as session: 

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

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

2011 c_id = c.id 

2012 h_id = h.id 

2013 user4_id = user4.id 

2014 

2015 with events_session(token1) as api: 

2016 event = api.CreateEvent( 

2017 events_pb2.CreateEventReq( 

2018 title="Dummy Title", 

2019 content="Dummy content.", 

2020 location=events_pb2.EventLocation( 

2021 address="Near Null Island", 

2022 lat=0.1, 

2023 lng=0.2, 

2024 ), 

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

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

2027 ) 

2028 ) 

2029 

2030 moderator.approve_event_occurrence(event.event_id) 

2031 

2032 with threads_session(token2) as api: 

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

2034 

2035 moderator.approve_thread_post(reply_id) 

2036 

2037 with events_session(token3) as api: 

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

2039 assert res.thread.num_responses == 1 

2040 

2041 with threads_session(token3) as api: 

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

2043 assert len(ret.replies) == 1 

2044 assert not ret.next_page_token 

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

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

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

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

2049 

2050 nested_reply_id = api.PostReply( 

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

2052 ).thread_id 

2053 

2054 moderator.approve_thread_post(nested_reply_id) 

2055 

2056 process_jobs() 

2057 

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

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

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

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

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

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

2064 

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

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

2067 

2068 assert push_collector.count_for_user(user4_id) == 0 

2069 

2070 

2071def test_can_overlap_other_events_schedule_regression(db): 

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

2073 user, token = generate_user() 

2074 

2075 with session_scope() as session: 

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

2077 

2078 start = now() 

2079 

2080 with events_session(token) as api: 

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

2082 api.CreateEvent( 

2083 events_pb2.CreateEventReq( 

2084 title="Dummy Title", 

2085 content="Dummy content.", 

2086 parent_community_id=c_id, 

2087 location=events_pb2.EventLocation( 

2088 address="Near Null Island", 

2089 lat=0.1, 

2090 lng=0.2, 

2091 ), 

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

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

2094 ) 

2095 ) 

2096 

2097 # this event 

2098 res = api.CreateEvent( 

2099 events_pb2.CreateEventReq( 

2100 title="Dummy Title", 

2101 content="Dummy content.", 

2102 parent_community_id=c_id, 

2103 location=events_pb2.EventLocation( 

2104 address="Near Null Island", 

2105 lat=0.1, 

2106 lng=0.2, 

2107 ), 

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

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

2110 ) 

2111 ) 

2112 

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

2114 api.ScheduleEvent( 

2115 events_pb2.ScheduleEventReq( 

2116 event_id=res.event_id, 

2117 content="New event occurrence", 

2118 location=events_pb2.EventLocation( 

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

2120 lat=0.3, 

2121 lng=0.2, 

2122 ), 

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

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

2125 ) 

2126 ) 

2127 

2128 

2129def test_can_overlap_other_events_update_regression(db): 

2130 user, token = generate_user() 

2131 

2132 with session_scope() as session: 

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

2134 

2135 start = now() 

2136 

2137 with events_session(token) as api: 

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

2139 api.CreateEvent( 

2140 events_pb2.CreateEventReq( 

2141 title="Dummy Title", 

2142 content="Dummy content.", 

2143 parent_community_id=c_id, 

2144 location=events_pb2.EventLocation( 

2145 address="Near Null Island", 

2146 lat=0.1, 

2147 lng=0.2, 

2148 ), 

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

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

2151 ) 

2152 ) 

2153 

2154 res = api.CreateEvent( 

2155 events_pb2.CreateEventReq( 

2156 title="Dummy Title", 

2157 content="Dummy content.", 

2158 parent_community_id=c_id, 

2159 location=events_pb2.EventLocation( 

2160 address="Near Null Island", 

2161 lat=0.1, 

2162 lng=0.2, 

2163 ), 

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

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

2166 ) 

2167 ) 

2168 

2169 event_id = api.ScheduleEvent( 

2170 events_pb2.ScheduleEventReq( 

2171 event_id=res.event_id, 

2172 content="New event occurrence", 

2173 location=events_pb2.EventLocation( 

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

2175 lat=0.3, 

2176 lng=0.2, 

2177 ), 

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

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

2180 ) 

2181 ).event_id 

2182 

2183 # can overlap with this current existing occurrence 

2184 api.UpdateEvent( 

2185 events_pb2.UpdateEventReq( 

2186 event_id=event_id, 

2187 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2189 ), 

2190 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2192 ), 

2193 ) 

2194 ) 

2195 

2196 api.UpdateEvent( 

2197 events_pb2.UpdateEventReq( 

2198 event_id=event_id, 

2199 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2201 ), 

2202 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2204 ), 

2205 ) 

2206 ) 

2207 

2208 

2209def test_list_past_events_regression(db): 

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

2211 user, token = generate_user() 

2212 

2213 with session_scope() as session: 

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

2215 

2216 start = now() 

2217 

2218 with events_session(token) as api: 

2219 api.CreateEvent( 

2220 events_pb2.CreateEventReq( 

2221 title="Dummy Title", 

2222 content="Dummy content.", 

2223 parent_community_id=c_id, 

2224 location=events_pb2.EventLocation( 

2225 address="Near Null Island", 

2226 lat=0.1, 

2227 lng=0.2, 

2228 ), 

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

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

2231 ) 

2232 ) 

2233 

2234 with session_scope() as session: 

2235 session.execute( 

2236 update(EventOccurrence).values( 

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

2238 ) 

2239 ) 

2240 

2241 with events_session(token) as api: 

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

2243 assert len(res.events) == 1 

2244 

2245 

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

2247 user1, token1 = generate_user(complete_profile=True) 

2248 user2, token2 = generate_user() 

2249 user3, token3 = generate_user() 

2250 user4, token4 = generate_user() 

2251 user5, token5 = generate_user(is_superuser=True) 

2252 

2253 with session_scope() as session: 

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

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

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

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

2258 

2259 enforce_community_memberships() 

2260 

2261 with events_session(token1) as api: 

2262 res = api.CreateEvent( 

2263 events_pb2.CreateEventReq( 

2264 title="Dummy Title", 

2265 content="Dummy content.", 

2266 parent_community_id=c_id, 

2267 location=events_pb2.EventLocation( 

2268 address="Near Null Island", 

2269 lat=0.1, 

2270 lng=0.2, 

2271 ), 

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

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

2274 ) 

2275 ) 

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

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

2278 

2279 event_id = res.event_id 

2280 

2281 moderator.approve_event_occurrence(event_id) 

2282 

2283 with events_session(token1) as api: 

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

2285 

2286 email = email_collector.pop_for_mods(last=True) 

2287 

2288 assert user_url in email.plain 

2289 assert event_url in email.plain 

2290 

2291 # can't send another req 

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

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

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

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

2296 

2297 # another user can send one though 

2298 with events_session(token3) as api: 

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

2300 

2301 # but not a non-admin 

2302 with events_session(token2) as api: 

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

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

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

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

2307 

2308 with real_editor_session(token5) as editor: 

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

2310 assert len(res.requests) == 2 

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

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

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

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

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

2316 

2317 editor.DecideEventCommunityInviteRequest( 

2318 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2320 approve=False, 

2321 ) 

2322 ) 

2323 

2324 editor.DecideEventCommunityInviteRequest( 

2325 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2327 approve=True, 

2328 ) 

2329 ) 

2330 

2331 # not after approve 

2332 with events_session(token4) as api: 

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

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

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

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

2337 

2338 

2339def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator): 

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

2341 # community invite notification when it is approved. 

2342 organizer, organizer_token = generate_user() 

2343 attendee, attendee_token = generate_user() 

2344 member, _ = generate_user() 

2345 superuser, superuser_token = generate_user(is_superuser=True) 

2346 

2347 with session_scope() as session: 

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

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

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

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

2352 

2353 enforce_community_memberships() 

2354 

2355 with events_session(organizer_token) as api: 

2356 event_id = api.CreateEvent( 

2357 events_pb2.CreateEventReq( 

2358 title="Dummy Title", 

2359 content="Dummy content.", 

2360 parent_community_id=c_id, 

2361 location=events_pb2.EventLocation( 

2362 address="Near Null Island", 

2363 lat=0.1, 

2364 lng=0.2, 

2365 ), 

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

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

2368 ) 

2369 ).event_id 

2370 

2371 moderator.approve_event_occurrence(event_id) 

2372 

2373 # the attendee RSVPs before the community invite is approved 

2374 with events_session(attendee_token) as api: 

2375 api.SetEventAttendance( 

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

2377 ) 

2378 

2379 with events_session(organizer_token) as api: 

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

2381 

2382 with real_editor_session(superuser_token) as editor: 

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

2384 editor.DecideEventCommunityInviteRequest( 

2385 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2387 approve=True, 

2388 ) 

2389 ) 

2390 

2391 process_jobs() 

2392 

2393 with session_scope() as session: 

2394 

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

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

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

2398 

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

2400 assert invite_notification_count(member.id) == 1 

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

2402 assert invite_notification_count(attendee.id) == 0 

2403 assert invite_notification_count(organizer.id) == 0 

2404 

2405 

2406def test_update_event_should_notify_queues_job(): 

2407 user, token = generate_user() 

2408 start = now() 

2409 

2410 with session_scope() as session: 

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

2412 

2413 # create an event 

2414 with events_session(token) as api: 

2415 create_res = api.CreateEvent( 

2416 events_pb2.CreateEventReq( 

2417 title="Dummy Title", 

2418 content="Dummy content.", 

2419 parent_community_id=c_id, 

2420 location=events_pb2.EventLocation( 

2421 address="Near Null Island", 

2422 lat=1.0, 

2423 lng=2.0, 

2424 ), 

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

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

2427 ) 

2428 ) 

2429 

2430 event_id = create_res.event_id 

2431 

2432 # measure initial background job queue length 

2433 with session_scope() as session: 

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

2435 job_length_before_update = len(jobs) 

2436 

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

2438 api.UpdateEvent( 

2439 events_pb2.UpdateEventReq( 

2440 event_id=event_id, 

2441 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2443 ), 

2444 should_notify=False, 

2445 ) 

2446 ) 

2447 

2448 with session_scope() as session: 

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

2450 assert len(jobs) == job_length_before_update 

2451 

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

2453 api.UpdateEvent( 

2454 events_pb2.UpdateEventReq( 

2455 event_id=event_id, 

2456 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2458 ), 

2459 should_notify=True, 

2460 ) 

2461 ) 

2462 

2463 with session_scope() as session: 

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

2465 assert len(jobs) == job_length_before_update + 1 

2466 

2467 

2468def test_event_photo_key(db): 

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

2470 user, token = generate_user() 

2471 

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

2473 end_time = start_time + timedelta(hours=3) 

2474 

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

2476 with session_scope() as session: 

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

2478 upload = Upload( 

2479 key="test_event_photo_key_123", 

2480 filename="test_event_photo_key_123.jpg", 

2481 creator_user_id=user.id, 

2482 ) 

2483 session.add(upload) 

2484 

2485 with events_session(token) as api: 

2486 # Create event without photo 

2487 res = api.CreateEvent( 

2488 events_pb2.CreateEventReq( 

2489 title="Event Without Photo", 

2490 content="No photo content.", 

2491 photo_key=None, 

2492 location=events_pb2.EventLocation( 

2493 address="Near Null Island", 

2494 lat=0.1, 

2495 lng=0.2, 

2496 ), 

2497 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2498 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2499 ) 

2500 ) 

2501 

2502 assert res.photo_key == "" 

2503 assert res.photo_url == "" 

2504 

2505 # Create event with photo 

2506 res_with_photo = api.CreateEvent( 

2507 events_pb2.CreateEventReq( 

2508 title="Event With Photo", 

2509 content="Has photo content.", 

2510 photo_key="test_event_photo_key_123", 

2511 location=events_pb2.EventLocation( 

2512 address="Near Null Island", 

2513 lat=0.1, 

2514 lng=0.2, 

2515 ), 

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

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

2518 ) 

2519 ) 

2520 

2521 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2522 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2523 

2524 event_id = res_with_photo.event_id 

2525 

2526 # Verify photo_key is returned when getting the event 

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

2528 assert get_res.photo_key == "test_event_photo_key_123" 

2529 assert "test_event_photo_key_123" in get_res.photo_url 

2530 

2531 

2532def test_event_timezone(db): 

2533 user, token = generate_user() 

2534 

2535 with session_scope() as session: 

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

2537 

2538 # Midnight future day, UTC timezone 

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

2540 end_time = start_time + timedelta(days=1) 

2541 

2542 with events_session(token) as api: 

2543 create_res: events_pb2.Event = api.CreateEvent( 

2544 events_pb2.CreateEventReq( 

2545 title="Dummy Title", 

2546 content="Dummy content.", 

2547 photo_key=None, 

2548 parent_community_id=c_id, 

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

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

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

2552 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2553 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2554 ) 

2555 ) 

2556 

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

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

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

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

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

2562 

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

2564 update_res: events_pb2.Event = api.UpdateEvent( 

2565 events_pb2.UpdateEventReq( 

2566 event_id=create_res.event_id, 

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

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

2569 ) 

2570 ) 

2571 

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

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

2574 # but now in the New York timezone. 

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

2576 assert update_res.start_time != create_res.start_time 

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

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

2579 

2580 # Also validate GetEvent 

2581 get_res: events_pb2.Event = api.GetEvent( 

2582 events_pb2.GetEventReq( 

2583 event_id=create_res.event_id, 

2584 ) 

2585 ) 

2586 

2587 assert get_res.timezone == update_res.timezone 

2588 assert get_res.start_time == update_res.start_time 

2589 

2590 

2591def test_event_created_with_shadowed_visibility(db): 

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

2593 user, token = generate_user() 

2594 

2595 with session_scope() as session: 

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

2597 

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

2599 end_time = start_time + timedelta(hours=3) 

2600 

2601 with events_session(token) as api: 

2602 res = api.CreateEvent( 

2603 events_pb2.CreateEventReq( 

2604 title="Test UMS Event", 

2605 content="UMS content.", 

2606 location=events_pb2.EventLocation( 

2607 address="Near Null Island", 

2608 lat=0.1, 

2609 lng=0.2, 

2610 ), 

2611 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2612 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2613 ) 

2614 ) 

2615 event_id = res.event_id 

2616 

2617 with session_scope() as session: 

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

2619 mod_state = session.execute( 

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

2621 ).scalar_one() 

2622 assert mod_state.visibility == ModerationVisibility.shadowed 

2623 

2624 

2625def test_shadowed_event_visible_to_creator_only(db): 

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

2627 user1, token1 = generate_user() 

2628 user2, token2 = generate_user() 

2629 

2630 with session_scope() as session: 

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

2632 

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

2634 end_time = start_time + timedelta(hours=3) 

2635 

2636 with events_session(token1) as api: 

2637 res = api.CreateEvent( 

2638 events_pb2.CreateEventReq( 

2639 title="Shadowed Event", 

2640 content="Content.", 

2641 location=events_pb2.EventLocation( 

2642 address="Near Null Island", 

2643 lat=0.1, 

2644 lng=0.2, 

2645 ), 

2646 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2647 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2648 ) 

2649 ) 

2650 event_id = res.event_id 

2651 

2652 # Creator can see it 

2653 with events_session(token1) as api: 

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

2655 assert res.title == "Shadowed Event" 

2656 

2657 # Other user cannot 

2658 with events_session(token2) as api: 

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

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

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

2662 

2663 

2664def test_event_visible_after_approval(db, moderator: Moderator): 

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

2666 user1, token1 = generate_user() 

2667 user2, token2 = generate_user() 

2668 

2669 with session_scope() as session: 

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

2671 

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

2673 end_time = start_time + timedelta(hours=3) 

2674 

2675 with events_session(token1) as api: 

2676 res = api.CreateEvent( 

2677 events_pb2.CreateEventReq( 

2678 title="Approved Event", 

2679 content="Content.", 

2680 location=events_pb2.EventLocation( 

2681 address="Near Null Island", 

2682 lat=0.1, 

2683 lng=0.2, 

2684 ), 

2685 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2686 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2687 ) 

2688 ) 

2689 event_id = res.event_id 

2690 

2691 # Other user cannot see it yet 

2692 with events_session(token2) as api: 

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

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

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

2696 

2697 # Approve the event 

2698 moderator.approve_event_occurrence(event_id) 

2699 

2700 # Now other user can see it 

2701 with events_session(token2) as api: 

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

2703 assert res.title == "Approved Event" 

2704 

2705 

2706def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2708 user1, token1 = generate_user() 

2709 user2, token2 = generate_user() 

2710 

2711 with session_scope() as session: 

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

2713 

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

2715 end_time = start_time + timedelta(hours=3) 

2716 

2717 with events_session(token1) as api: 

2718 res = api.CreateEvent( 

2719 events_pb2.CreateEventReq( 

2720 title="List Test Event", 

2721 content="Content.", 

2722 location=events_pb2.EventLocation( 

2723 address="Near Null Island", 

2724 lat=0.1, 

2725 lng=0.2, 

2726 ), 

2727 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2728 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2729 ) 

2730 ) 

2731 event_id = res.event_id 

2732 

2733 # Creator can see their own SHADOWED event in lists 

2734 with events_session(token1) as api: 

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

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

2737 assert event_id in event_ids 

2738 

2739 # Other user cannot see the SHADOWED event in lists 

2740 with events_session(token2) as api: 

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

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

2743 assert event_id not in event_ids 

2744 

2745 # After approval, other user can see it 

2746 moderator.approve_event_occurrence(event_id) 

2747 

2748 with events_session(token2) as api: 

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

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

2751 assert event_id in event_ids 

2752 

2753 

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

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

2756 user1, token1 = generate_user() 

2757 user2, token2 = generate_user() 

2758 

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

2760 with session_scope() as session: 

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

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

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

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

2765 

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

2767 end_time = start_time + timedelta(hours=3) 

2768 

2769 with events_session(token1) as api: 

2770 res = api.CreateEvent( 

2771 events_pb2.CreateEventReq( 

2772 title="Deferred Event", 

2773 content="Content.", 

2774 location=events_pb2.EventLocation( 

2775 address="Near Null Island", 

2776 lat=0.1, 

2777 lng=0.2, 

2778 ), 

2779 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2780 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2781 ) 

2782 ) 

2783 event_id = res.event_id 

2784 

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

2786 process_jobs() 

2787 

2788 with session_scope() as session: 

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

2790 # Notification was created with moderation_state_id for deferral 

2791 assert notif.moderation_state_id is not None 

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

2793 delivery_count = session.execute( 

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

2795 ).scalar_one_or_none() 

2796 assert delivery_count is None 

2797 

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

2799 moderator.approve_event_occurrence(event_id) 

2800 

2801 # Verify handle_notification job was queued 

2802 with session_scope() as session: 

2803 pending_jobs = ( 

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

2805 .scalars() 

2806 .all() 

2807 ) 

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

2809 

2810 

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

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

2813 user1, token1 = generate_user() 

2814 user2, token2 = generate_user() 

2815 

2816 with session_scope() as session: 

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

2818 

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

2820 end_time = start_time + timedelta(hours=3) 

2821 

2822 with events_session(token1) as api: 

2823 res = api.CreateEvent( 

2824 events_pb2.CreateEventReq( 

2825 title="Update Test", 

2826 content="Content.", 

2827 location=events_pb2.EventLocation( 

2828 address="Near Null Island", 

2829 lat=0.1, 

2830 lng=0.2, 

2831 ), 

2832 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2833 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2834 ) 

2835 ) 

2836 event_id = res.event_id 

2837 

2838 moderator.approve_event_occurrence(event_id) 

2839 process_jobs() 

2840 # Clear any create notifications 

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

2842 push_collector.pop_for_user(user2.id) 

2843 

2844 # User2 subscribes to the event 

2845 with events_session(token2) as api: 

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

2847 

2848 # User1 updates the event with should_notify=True 

2849 with events_session(token1) as api: 

2850 api.UpdateEvent( 

2851 events_pb2.UpdateEventReq( 

2852 event_id=event_id, 

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

2854 should_notify=True, 

2855 ) 

2856 ) 

2857 

2858 process_jobs() 

2859 

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

2861 with session_scope() as session: 

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

2863 

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

2865 # Find the update notification (most recent one) 

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

2867 assert len(update_notifs) == 1 

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

2869 

2870 

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

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

2873 user1, token1 = generate_user() 

2874 user2, token2 = generate_user() 

2875 

2876 with session_scope() as session: 

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

2878 

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

2880 end_time = start_time + timedelta(hours=3) 

2881 

2882 with events_session(token1) as api: 

2883 res = api.CreateEvent( 

2884 events_pb2.CreateEventReq( 

2885 title="Cancel Test", 

2886 content="Content.", 

2887 location=events_pb2.EventLocation( 

2888 address="Near Null Island", 

2889 lat=0.1, 

2890 lng=0.2, 

2891 ), 

2892 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2893 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2894 ) 

2895 ) 

2896 event_id = res.event_id 

2897 

2898 moderator.approve_event_occurrence(event_id) 

2899 process_jobs() 

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

2901 push_collector.pop_for_user(user2.id) 

2902 

2903 # User2 subscribes 

2904 with events_session(token2) as api: 

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

2906 

2907 # User1 cancels the event 

2908 with events_session(token1) as api: 

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

2910 

2911 process_jobs() 

2912 

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

2914 with session_scope() as session: 

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

2916 

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

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

2919 assert len(cancel_notifs) == 1 

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

2921 

2922 

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

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

2925 user1, token1 = generate_user() 

2926 user2, token2 = generate_user() 

2927 

2928 with session_scope() as session: 

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

2930 

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

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

2933 end_time = start_time + timedelta(hours=1) 

2934 

2935 with events_session(token1) as api: 

2936 res = api.CreateEvent( 

2937 events_pb2.CreateEventReq( 

2938 title="Reminder Test", 

2939 content="Content.", 

2940 location=events_pb2.EventLocation( 

2941 address="Near Null Island", 

2942 lat=0.1, 

2943 lng=0.2, 

2944 ), 

2945 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2946 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2947 ) 

2948 ) 

2949 event_id = res.event_id 

2950 

2951 moderator.approve_event_occurrence(event_id) 

2952 process_jobs() 

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

2954 push_collector.pop_for_user(user2.id) 

2955 

2956 # User2 marks attendance 

2957 with events_session(token2) as api: 

2958 api.SetEventAttendance( 

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

2960 ) 

2961 

2962 # Run the event reminder handler 

2963 send_event_reminders(empty_pb2.Empty()) 

2964 process_jobs() 

2965 

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

2967 with session_scope() as session: 

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

2969 

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

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

2972 assert len(reminder_notifs) == 1 

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

2974 

2975 

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

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

2978 user1, token1 = generate_user() 

2979 user2, token2 = generate_user() 

2980 

2981 with session_scope() as session: 

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

2983 

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

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

2986 end_time = start_time + timedelta(hours=1) 

2987 

2988 with events_session(token1) as api: 

2989 res = api.CreateEvent( 

2990 events_pb2.CreateEventReq( 

2991 title="Cancelled Reminder Test", 

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 moderator.approve_event_occurrence(event_id) 

3005 process_jobs() 

3006 

3007 # User2 marks attendance 

3008 with events_session(token2) as api: 

3009 api.SetEventAttendance( 

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

3011 ) 

3012 

3013 # User1 cancels the event 

3014 with events_session(token1) as api: 

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

3016 

3017 process_jobs() 

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

3019 while push_collector.count_for_user(user2.id): 

3020 push_collector.pop_for_user(user2.id) 

3021 

3022 # Run the event reminder handler 

3023 send_event_reminders(empty_pb2.Empty()) 

3024 process_jobs() 

3025 

3026 # Verify that no reminder notification was sent for user2 

3027 with session_scope() as session: 

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

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

3030 assert len(reminder_notifs) == 0 

3031 

3032 

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

3034def test_event_reminder_not_sent_for_invisible_attendee( 

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

3036): 

3037 user1, token1 = generate_user() 

3038 user2, token2 = generate_user() 

3039 

3040 with session_scope() as session: 

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

3042 

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

3044 end_time = start_time + timedelta(hours=1) 

3045 

3046 with events_session(token1) as api: 

3047 res = api.CreateEvent( 

3048 events_pb2.CreateEventReq( 

3049 title="Invisible Attendee Reminder Test", 

3050 content="Content.", 

3051 location=events_pb2.EventLocation( 

3052 address="Near Null Island", 

3053 lat=0.1, 

3054 lng=0.2, 

3055 ), 

3056 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3057 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3058 ) 

3059 ) 

3060 event_id = res.event_id 

3061 

3062 moderator.approve_event_occurrence(event_id) 

3063 process_jobs() 

3064 

3065 with events_session(token2) as api: 

3066 api.SetEventAttendance( 

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

3068 ) 

3069 

3070 with session_scope() as session: 

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

3072 

3073 send_event_reminders(empty_pb2.Empty()) 

3074 process_jobs() 

3075 

3076 with session_scope() as session: 

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

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

3079 assert len(reminder_notifs) == 0 

3080 

3081 

3082def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3084 user1, token1 = generate_user() 

3085 user2, token2 = generate_user() 

3086 

3087 with session_scope() as session: 

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

3089 

3090 start = now() 

3091 

3092 # User1 creates event A with 3 occurrences 

3093 event_a_ids = [] 

3094 with events_session(token1) as api: 

3095 res = api.CreateEvent( 

3096 events_pb2.CreateEventReq( 

3097 title="Event A", 

3098 content="Content A.", 

3099 parent_community_id=c_id, 

3100 location=events_pb2.EventLocation( 

3101 address="Near Null Island", 

3102 lat=0.1, 

3103 lng=0.2, 

3104 ), 

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

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

3107 ) 

3108 ) 

3109 event_a_ids.append(res.event_id) 

3110 for i in range(2): 

3111 res = api.ScheduleEvent( 

3112 events_pb2.ScheduleEventReq( 

3113 event_id=event_a_ids[-1], 

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

3115 location=events_pb2.EventLocation( 

3116 address="Near Null Island", 

3117 lat=0.1, 

3118 lng=0.2, 

3119 ), 

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

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

3122 ) 

3123 ) 

3124 event_a_ids.append(res.event_id) 

3125 

3126 # User2 creates event B with 2 occurrences 

3127 event_b_ids = [] 

3128 with events_session(token2) as api: 

3129 res = api.CreateEvent( 

3130 events_pb2.CreateEventReq( 

3131 title="Event B", 

3132 content="Content B.", 

3133 parent_community_id=c_id, 

3134 location=events_pb2.EventLocation( 

3135 address="Near Null Island", 

3136 lat=0.1, 

3137 lng=0.2, 

3138 ), 

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

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

3141 ) 

3142 ) 

3143 event_b_ids.append(res.event_id) 

3144 res = api.ScheduleEvent( 

3145 events_pb2.ScheduleEventReq( 

3146 event_id=event_b_ids[-1], 

3147 content="B occurrence 1", 

3148 location=events_pb2.EventLocation( 

3149 address="Near Null Island", 

3150 lat=0.1, 

3151 lng=0.2, 

3152 ), 

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

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

3155 ) 

3156 ) 

3157 event_b_ids.append(res.event_id) 

3158 

3159 moderator.approve_event_occurrence(event_a_ids[0]) 

3160 moderator.approve_event_occurrence(event_b_ids[0]) 

3161 

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

3163 with events_session(token1) as api: 

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

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

3166 assert sorted(returned_ids) == sorted(event_a_ids) 

3167 

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

3169 with events_session(token2) as api: 

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

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

3172 assert sorted(returned_ids) == sorted(event_b_ids) 

3173 

3174 

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

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

3177 user1, token1 = generate_user() 

3178 user2, token2 = generate_user() 

3179 

3180 with session_scope() as session: 

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

3182 

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

3184 end_time = start_time + timedelta(hours=3) 

3185 

3186 with events_session(token1) as api: 

3187 res = api.CreateEvent( 

3188 events_pb2.CreateEventReq( 

3189 title="Comment Test", 

3190 content="Content.", 

3191 parent_community_id=c_id, 

3192 location=events_pb2.EventLocation( 

3193 address="Near Null Island", 

3194 lat=0.1, 

3195 lng=0.2, 

3196 ), 

3197 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3198 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3199 ) 

3200 ) 

3201 event_id = res.event_id 

3202 thread_id = res.thread.thread_id 

3203 

3204 moderator.approve_event_occurrence(event_id) 

3205 process_jobs() 

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

3207 push_collector.pop_for_user(user1.id) 

3208 

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

3210 with events_session(token1) as api: 

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

3212 

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

3214 with threads_session(token2) as api: 

3215 comment_thread_id = api.PostReply( 

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

3217 ).thread_id 

3218 

3219 process_jobs() 

3220 

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

3222 comment_db_id = comment_thread_id // 10 

3223 with session_scope() as session: 

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

3225 

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

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

3228 assert len(comment_notifs) == 1 

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

3230 

3231 

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

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

3234 user1, token1 = generate_user() 

3235 user2, token2 = generate_user() 

3236 user3, token3 = generate_user() 

3237 

3238 with session_scope() as session: 

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

3240 

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

3242 end_time = start_time + timedelta(hours=3) 

3243 

3244 with events_session(token1) as api: 

3245 res = api.CreateEvent( 

3246 events_pb2.CreateEventReq( 

3247 title="Reply Test", 

3248 content="Content.", 

3249 location=events_pb2.EventLocation( 

3250 address="Near Null Island", 

3251 lat=0.1, 

3252 lng=0.2, 

3253 ), 

3254 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3255 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3256 ) 

3257 ) 

3258 event_id = res.event_id 

3259 thread_id = res.thread.thread_id 

3260 

3261 moderator.approve_event_occurrence(event_id) 

3262 process_jobs() 

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

3264 push_collector.pop_for_user(user1.id) 

3265 

3266 # User2 posts a top-level comment 

3267 with threads_session(token2) as api: 

3268 comment_thread_id = api.PostReply( 

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

3270 ).thread_id 

3271 

3272 process_jobs() 

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

3274 push_collector.pop_for_user(user1.id) 

3275 

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

3277 with threads_session(token3) as api: 

3278 nested_reply_thread_id = api.PostReply( 

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

3280 ).thread_id 

3281 

3282 process_jobs() 

3283 

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

3285 nested_reply_db_id = nested_reply_thread_id // 10 

3286 with session_scope() as session: 

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

3288 

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

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

3291 assert len(reply_notifs) == 1 

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