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

1445 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-25 18:54 +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 ics_string = file_res.data.decode("utf-8") 

1984 assert "SUMMARY:Dummy Title" in ics_string 

1985 assert "DESCRIPTION:Dummy content." in ics_string 

1986 assert "LOCATION:Near Null Island" in ics_string 

1987 assert "STATUS:CANCELLED" not in ics_string 

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

1989 

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

1991 

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

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

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

1995 assert "STATUS:CANCELLED" in ics_string 

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

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

1998 assert post_cancel_sequence >= pre_cancel_sequence 

1999 

2000 

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

2002 user1, token1 = generate_user() 

2003 user2, token2 = generate_user() 

2004 user3, token3 = generate_user() 

2005 user4, token4 = generate_user() 

2006 

2007 with session_scope() as session: 

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

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

2010 c_id = c.id 

2011 h_id = h.id 

2012 user4_id = user4.id 

2013 

2014 with events_session(token1) as api: 

2015 event = api.CreateEvent( 

2016 events_pb2.CreateEventReq( 

2017 title="Dummy Title", 

2018 content="Dummy content.", 

2019 location=events_pb2.EventLocation( 

2020 address="Near Null Island", 

2021 lat=0.1, 

2022 lng=0.2, 

2023 ), 

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

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

2026 ) 

2027 ) 

2028 

2029 moderator.approve_event_occurrence(event.event_id) 

2030 

2031 with threads_session(token2) as api: 

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

2033 

2034 moderator.approve_thread_post(reply_id) 

2035 

2036 with events_session(token3) as api: 

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

2038 assert res.thread.num_responses == 1 

2039 

2040 with threads_session(token3) as api: 

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

2042 assert len(ret.replies) == 1 

2043 assert not ret.next_page_token 

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

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

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

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

2048 

2049 nested_reply_id = api.PostReply( 

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

2051 ).thread_id 

2052 

2053 moderator.approve_thread_post(nested_reply_id) 

2054 

2055 process_jobs() 

2056 

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

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

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

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

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

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

2063 

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

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

2066 

2067 assert push_collector.count_for_user(user4_id) == 0 

2068 

2069 

2070def test_can_overlap_other_events_schedule_regression(db): 

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

2072 user, token = generate_user() 

2073 

2074 with session_scope() as session: 

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

2076 

2077 start = now() 

2078 

2079 with events_session(token) as api: 

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

2081 api.CreateEvent( 

2082 events_pb2.CreateEventReq( 

2083 title="Dummy Title", 

2084 content="Dummy content.", 

2085 parent_community_id=c_id, 

2086 location=events_pb2.EventLocation( 

2087 address="Near Null Island", 

2088 lat=0.1, 

2089 lng=0.2, 

2090 ), 

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

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

2093 ) 

2094 ) 

2095 

2096 # this event 

2097 res = api.CreateEvent( 

2098 events_pb2.CreateEventReq( 

2099 title="Dummy Title", 

2100 content="Dummy content.", 

2101 parent_community_id=c_id, 

2102 location=events_pb2.EventLocation( 

2103 address="Near Null Island", 

2104 lat=0.1, 

2105 lng=0.2, 

2106 ), 

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

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

2109 ) 

2110 ) 

2111 

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

2113 api.ScheduleEvent( 

2114 events_pb2.ScheduleEventReq( 

2115 event_id=res.event_id, 

2116 content="New event occurrence", 

2117 location=events_pb2.EventLocation( 

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

2119 lat=0.3, 

2120 lng=0.2, 

2121 ), 

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

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

2124 ) 

2125 ) 

2126 

2127 

2128def test_can_overlap_other_events_update_regression(db): 

2129 user, token = generate_user() 

2130 

2131 with session_scope() as session: 

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

2133 

2134 start = now() 

2135 

2136 with events_session(token) as api: 

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

2138 api.CreateEvent( 

2139 events_pb2.CreateEventReq( 

2140 title="Dummy Title", 

2141 content="Dummy content.", 

2142 parent_community_id=c_id, 

2143 location=events_pb2.EventLocation( 

2144 address="Near Null Island", 

2145 lat=0.1, 

2146 lng=0.2, 

2147 ), 

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

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

2150 ) 

2151 ) 

2152 

2153 res = api.CreateEvent( 

2154 events_pb2.CreateEventReq( 

2155 title="Dummy Title", 

2156 content="Dummy content.", 

2157 parent_community_id=c_id, 

2158 location=events_pb2.EventLocation( 

2159 address="Near Null Island", 

2160 lat=0.1, 

2161 lng=0.2, 

2162 ), 

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

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

2165 ) 

2166 ) 

2167 

2168 event_id = api.ScheduleEvent( 

2169 events_pb2.ScheduleEventReq( 

2170 event_id=res.event_id, 

2171 content="New event occurrence", 

2172 location=events_pb2.EventLocation( 

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

2174 lat=0.3, 

2175 lng=0.2, 

2176 ), 

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

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

2179 ) 

2180 ).event_id 

2181 

2182 # can overlap with this current existing occurrence 

2183 api.UpdateEvent( 

2184 events_pb2.UpdateEventReq( 

2185 event_id=event_id, 

2186 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2188 ), 

2189 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2191 ), 

2192 ) 

2193 ) 

2194 

2195 api.UpdateEvent( 

2196 events_pb2.UpdateEventReq( 

2197 event_id=event_id, 

2198 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2200 ), 

2201 end_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2203 ), 

2204 ) 

2205 ) 

2206 

2207 

2208def test_list_past_events_regression(db): 

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

2210 user, token = generate_user() 

2211 

2212 with session_scope() as session: 

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

2214 

2215 start = now() 

2216 

2217 with events_session(token) as api: 

2218 api.CreateEvent( 

2219 events_pb2.CreateEventReq( 

2220 title="Dummy Title", 

2221 content="Dummy content.", 

2222 parent_community_id=c_id, 

2223 location=events_pb2.EventLocation( 

2224 address="Near Null Island", 

2225 lat=0.1, 

2226 lng=0.2, 

2227 ), 

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

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

2230 ) 

2231 ) 

2232 

2233 with session_scope() as session: 

2234 session.execute( 

2235 update(EventOccurrence).values( 

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

2237 ) 

2238 ) 

2239 

2240 with events_session(token) as api: 

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

2242 assert len(res.events) == 1 

2243 

2244 

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

2246 user1, token1 = generate_user(complete_profile=True) 

2247 user2, token2 = generate_user() 

2248 user3, token3 = generate_user() 

2249 user4, token4 = generate_user() 

2250 user5, token5 = generate_user(is_superuser=True) 

2251 

2252 with session_scope() as session: 

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

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

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

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

2257 

2258 enforce_community_memberships() 

2259 

2260 with events_session(token1) as api: 

2261 res = api.CreateEvent( 

2262 events_pb2.CreateEventReq( 

2263 title="Dummy Title", 

2264 content="Dummy content.", 

2265 parent_community_id=c_id, 

2266 location=events_pb2.EventLocation( 

2267 address="Near Null Island", 

2268 lat=0.1, 

2269 lng=0.2, 

2270 ), 

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

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

2273 ) 

2274 ) 

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

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

2277 

2278 event_id = res.event_id 

2279 

2280 moderator.approve_event_occurrence(event_id) 

2281 

2282 with events_session(token1) as api: 

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

2284 

2285 email = email_collector.pop_for_mods(last=True) 

2286 

2287 assert user_url in email.plain 

2288 assert event_url in email.plain 

2289 

2290 # can't send another req 

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

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

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

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

2295 

2296 # another user can send one though 

2297 with events_session(token3) as api: 

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

2299 

2300 # but not a non-admin 

2301 with events_session(token2) as api: 

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

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

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

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

2306 

2307 with real_editor_session(token5) as editor: 

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

2309 assert len(res.requests) == 2 

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

2311 assert res.requests[0].approx_users_to_notify == 3 

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

2313 assert res.requests[1].approx_users_to_notify == 3 

2314 

2315 editor.DecideEventCommunityInviteRequest( 

2316 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2318 approve=False, 

2319 ) 

2320 ) 

2321 

2322 editor.DecideEventCommunityInviteRequest( 

2323 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2325 approve=True, 

2326 ) 

2327 ) 

2328 

2329 # not after approve 

2330 with events_session(token4) as api: 

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

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

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

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

2335 

2336 

2337def test_update_event_should_notify_queues_job(): 

2338 user, token = generate_user() 

2339 start = now() 

2340 

2341 with session_scope() as session: 

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

2343 

2344 # create an event 

2345 with events_session(token) as api: 

2346 create_res = api.CreateEvent( 

2347 events_pb2.CreateEventReq( 

2348 title="Dummy Title", 

2349 content="Dummy content.", 

2350 parent_community_id=c_id, 

2351 location=events_pb2.EventLocation( 

2352 address="Near Null Island", 

2353 lat=1.0, 

2354 lng=2.0, 

2355 ), 

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

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

2358 ) 

2359 ) 

2360 

2361 event_id = create_res.event_id 

2362 

2363 # measure initial background job queue length 

2364 with session_scope() as session: 

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

2366 job_length_before_update = len(jobs) 

2367 

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

2369 api.UpdateEvent( 

2370 events_pb2.UpdateEventReq( 

2371 event_id=event_id, 

2372 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2374 ), 

2375 should_notify=False, 

2376 ) 

2377 ) 

2378 

2379 with session_scope() as session: 

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

2381 assert len(jobs) == job_length_before_update 

2382 

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

2384 api.UpdateEvent( 

2385 events_pb2.UpdateEventReq( 

2386 event_id=event_id, 

2387 start_datetime_iso8601_local=wrappers_pb2.StringValue( 

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

2389 ), 

2390 should_notify=True, 

2391 ) 

2392 ) 

2393 

2394 with session_scope() as session: 

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

2396 assert len(jobs) == job_length_before_update + 1 

2397 

2398 

2399def test_event_photo_key(db): 

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

2401 user, token = generate_user() 

2402 

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

2404 end_time = start_time + timedelta(hours=3) 

2405 

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

2407 with session_scope() as session: 

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

2409 upload = Upload( 

2410 key="test_event_photo_key_123", 

2411 filename="test_event_photo_key_123.jpg", 

2412 creator_user_id=user.id, 

2413 ) 

2414 session.add(upload) 

2415 

2416 with events_session(token) as api: 

2417 # Create event without photo 

2418 res = api.CreateEvent( 

2419 events_pb2.CreateEventReq( 

2420 title="Event Without Photo", 

2421 content="No photo content.", 

2422 photo_key=None, 

2423 location=events_pb2.EventLocation( 

2424 address="Near Null Island", 

2425 lat=0.1, 

2426 lng=0.2, 

2427 ), 

2428 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2429 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2430 ) 

2431 ) 

2432 

2433 assert res.photo_key == "" 

2434 assert res.photo_url == "" 

2435 

2436 # Create event with photo 

2437 res_with_photo = api.CreateEvent( 

2438 events_pb2.CreateEventReq( 

2439 title="Event With Photo", 

2440 content="Has photo content.", 

2441 photo_key="test_event_photo_key_123", 

2442 location=events_pb2.EventLocation( 

2443 address="Near Null Island", 

2444 lat=0.1, 

2445 lng=0.2, 

2446 ), 

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

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

2449 ) 

2450 ) 

2451 

2452 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2453 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2454 

2455 event_id = res_with_photo.event_id 

2456 

2457 # Verify photo_key is returned when getting the event 

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

2459 assert get_res.photo_key == "test_event_photo_key_123" 

2460 assert "test_event_photo_key_123" in get_res.photo_url 

2461 

2462 

2463def test_event_timezone(db): 

2464 user, token = generate_user() 

2465 

2466 with session_scope() as session: 

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

2468 

2469 # Midnight future day, UTC timezone 

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

2471 end_time = start_time + timedelta(days=1) 

2472 

2473 with events_session(token) as api: 

2474 create_res: events_pb2.Event = api.CreateEvent( 

2475 events_pb2.CreateEventReq( 

2476 title="Dummy Title", 

2477 content="Dummy content.", 

2478 photo_key=None, 

2479 parent_community_id=c_id, 

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

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

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

2483 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2484 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2485 ) 

2486 ) 

2487 

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

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

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

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

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

2493 

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

2495 update_res: events_pb2.Event = api.UpdateEvent( 

2496 events_pb2.UpdateEventReq( 

2497 event_id=create_res.event_id, 

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

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

2500 ) 

2501 ) 

2502 

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

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

2505 # but now in the New York timezone. 

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

2507 assert update_res.start_time != create_res.start_time 

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

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

2510 

2511 # Also validate GetEvent 

2512 get_res: events_pb2.Event = api.GetEvent( 

2513 events_pb2.GetEventReq( 

2514 event_id=create_res.event_id, 

2515 ) 

2516 ) 

2517 

2518 assert get_res.timezone == update_res.timezone 

2519 assert get_res.start_time == update_res.start_time 

2520 

2521 

2522def test_event_created_with_shadowed_visibility(db): 

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

2524 user, token = generate_user() 

2525 

2526 with session_scope() as session: 

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

2528 

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

2530 end_time = start_time + timedelta(hours=3) 

2531 

2532 with events_session(token) as api: 

2533 res = api.CreateEvent( 

2534 events_pb2.CreateEventReq( 

2535 title="Test UMS Event", 

2536 content="UMS content.", 

2537 location=events_pb2.EventLocation( 

2538 address="Near Null Island", 

2539 lat=0.1, 

2540 lng=0.2, 

2541 ), 

2542 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2543 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2544 ) 

2545 ) 

2546 event_id = res.event_id 

2547 

2548 with session_scope() as session: 

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

2550 mod_state = session.execute( 

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

2552 ).scalar_one() 

2553 assert mod_state.visibility == ModerationVisibility.shadowed 

2554 

2555 

2556def test_shadowed_event_visible_to_creator_only(db): 

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

2558 user1, token1 = generate_user() 

2559 user2, token2 = generate_user() 

2560 

2561 with session_scope() as session: 

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

2563 

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

2565 end_time = start_time + timedelta(hours=3) 

2566 

2567 with events_session(token1) as api: 

2568 res = api.CreateEvent( 

2569 events_pb2.CreateEventReq( 

2570 title="Shadowed Event", 

2571 content="Content.", 

2572 location=events_pb2.EventLocation( 

2573 address="Near Null Island", 

2574 lat=0.1, 

2575 lng=0.2, 

2576 ), 

2577 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2578 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2579 ) 

2580 ) 

2581 event_id = res.event_id 

2582 

2583 # Creator can see it 

2584 with events_session(token1) as api: 

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

2586 assert res.title == "Shadowed Event" 

2587 

2588 # Other user cannot 

2589 with events_session(token2) as api: 

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

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

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

2593 

2594 

2595def test_event_visible_after_approval(db, moderator: Moderator): 

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

2597 user1, token1 = generate_user() 

2598 user2, token2 = generate_user() 

2599 

2600 with session_scope() as session: 

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

2602 

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

2604 end_time = start_time + timedelta(hours=3) 

2605 

2606 with events_session(token1) as api: 

2607 res = api.CreateEvent( 

2608 events_pb2.CreateEventReq( 

2609 title="Approved Event", 

2610 content="Content.", 

2611 location=events_pb2.EventLocation( 

2612 address="Near Null Island", 

2613 lat=0.1, 

2614 lng=0.2, 

2615 ), 

2616 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2617 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2618 ) 

2619 ) 

2620 event_id = res.event_id 

2621 

2622 # Other user cannot see it yet 

2623 with events_session(token2) as api: 

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

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

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

2627 

2628 # Approve the event 

2629 moderator.approve_event_occurrence(event_id) 

2630 

2631 # Now other user can see it 

2632 with events_session(token2) as api: 

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

2634 assert res.title == "Approved Event" 

2635 

2636 

2637def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2639 user1, token1 = generate_user() 

2640 user2, token2 = generate_user() 

2641 

2642 with session_scope() as session: 

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

2644 

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

2646 end_time = start_time + timedelta(hours=3) 

2647 

2648 with events_session(token1) as api: 

2649 res = api.CreateEvent( 

2650 events_pb2.CreateEventReq( 

2651 title="List Test Event", 

2652 content="Content.", 

2653 location=events_pb2.EventLocation( 

2654 address="Near Null Island", 

2655 lat=0.1, 

2656 lng=0.2, 

2657 ), 

2658 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2659 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2660 ) 

2661 ) 

2662 event_id = res.event_id 

2663 

2664 # Creator can see their own SHADOWED event in lists 

2665 with events_session(token1) as api: 

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

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

2668 assert event_id in event_ids 

2669 

2670 # Other user cannot see the SHADOWED event in lists 

2671 with events_session(token2) as api: 

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

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

2674 assert event_id not in event_ids 

2675 

2676 # After approval, other user can see it 

2677 moderator.approve_event_occurrence(event_id) 

2678 

2679 with events_session(token2) as api: 

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

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

2682 assert event_id in event_ids 

2683 

2684 

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

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

2687 user1, token1 = generate_user() 

2688 user2, token2 = generate_user() 

2689 

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

2691 with session_scope() as session: 

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

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

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

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

2696 

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

2698 end_time = start_time + timedelta(hours=3) 

2699 

2700 with events_session(token1) as api: 

2701 res = api.CreateEvent( 

2702 events_pb2.CreateEventReq( 

2703 title="Deferred Event", 

2704 content="Content.", 

2705 location=events_pb2.EventLocation( 

2706 address="Near Null Island", 

2707 lat=0.1, 

2708 lng=0.2, 

2709 ), 

2710 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2711 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2712 ) 

2713 ) 

2714 event_id = res.event_id 

2715 

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

2717 process_jobs() 

2718 

2719 with session_scope() as session: 

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

2721 # Notification was created with moderation_state_id for deferral 

2722 assert notif.moderation_state_id is not None 

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

2724 delivery_count = session.execute( 

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

2726 ).scalar_one_or_none() 

2727 assert delivery_count is None 

2728 

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

2730 moderator.approve_event_occurrence(event_id) 

2731 

2732 # Verify handle_notification job was queued 

2733 with session_scope() as session: 

2734 pending_jobs = ( 

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

2736 .scalars() 

2737 .all() 

2738 ) 

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

2740 

2741 

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

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

2744 user1, token1 = generate_user() 

2745 user2, token2 = generate_user() 

2746 

2747 with session_scope() as session: 

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

2749 

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

2751 end_time = start_time + timedelta(hours=3) 

2752 

2753 with events_session(token1) as api: 

2754 res = api.CreateEvent( 

2755 events_pb2.CreateEventReq( 

2756 title="Update Test", 

2757 content="Content.", 

2758 location=events_pb2.EventLocation( 

2759 address="Near Null Island", 

2760 lat=0.1, 

2761 lng=0.2, 

2762 ), 

2763 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2764 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2765 ) 

2766 ) 

2767 event_id = res.event_id 

2768 

2769 moderator.approve_event_occurrence(event_id) 

2770 process_jobs() 

2771 # Clear any create notifications 

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

2773 push_collector.pop_for_user(user2.id) 

2774 

2775 # User2 subscribes to the event 

2776 with events_session(token2) as api: 

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

2778 

2779 # User1 updates the event with should_notify=True 

2780 with events_session(token1) as api: 

2781 api.UpdateEvent( 

2782 events_pb2.UpdateEventReq( 

2783 event_id=event_id, 

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

2785 should_notify=True, 

2786 ) 

2787 ) 

2788 

2789 process_jobs() 

2790 

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

2792 with session_scope() as session: 

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

2794 

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

2796 # Find the update notification (most recent one) 

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

2798 assert len(update_notifs) == 1 

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

2800 

2801 

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

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

2804 user1, token1 = generate_user() 

2805 user2, token2 = generate_user() 

2806 

2807 with session_scope() as session: 

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

2809 

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

2811 end_time = start_time + timedelta(hours=3) 

2812 

2813 with events_session(token1) as api: 

2814 res = api.CreateEvent( 

2815 events_pb2.CreateEventReq( 

2816 title="Cancel Test", 

2817 content="Content.", 

2818 location=events_pb2.EventLocation( 

2819 address="Near Null Island", 

2820 lat=0.1, 

2821 lng=0.2, 

2822 ), 

2823 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2824 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2825 ) 

2826 ) 

2827 event_id = res.event_id 

2828 

2829 moderator.approve_event_occurrence(event_id) 

2830 process_jobs() 

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

2832 push_collector.pop_for_user(user2.id) 

2833 

2834 # User2 subscribes 

2835 with events_session(token2) as api: 

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

2837 

2838 # User1 cancels the event 

2839 with events_session(token1) as api: 

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

2841 

2842 process_jobs() 

2843 

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

2845 with session_scope() as session: 

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

2847 

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

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

2850 assert len(cancel_notifs) == 1 

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

2852 

2853 

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

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

2856 user1, token1 = generate_user() 

2857 user2, token2 = generate_user() 

2858 

2859 with session_scope() as session: 

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

2861 

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

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

2864 end_time = start_time + timedelta(hours=1) 

2865 

2866 with events_session(token1) as api: 

2867 res = api.CreateEvent( 

2868 events_pb2.CreateEventReq( 

2869 title="Reminder Test", 

2870 content="Content.", 

2871 location=events_pb2.EventLocation( 

2872 address="Near Null Island", 

2873 lat=0.1, 

2874 lng=0.2, 

2875 ), 

2876 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2877 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2878 ) 

2879 ) 

2880 event_id = res.event_id 

2881 

2882 moderator.approve_event_occurrence(event_id) 

2883 process_jobs() 

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

2885 push_collector.pop_for_user(user2.id) 

2886 

2887 # User2 marks attendance 

2888 with events_session(token2) as api: 

2889 api.SetEventAttendance( 

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

2891 ) 

2892 

2893 # Run the event reminder handler 

2894 send_event_reminders(empty_pb2.Empty()) 

2895 process_jobs() 

2896 

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

2898 with session_scope() as session: 

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

2900 

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

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

2903 assert len(reminder_notifs) == 1 

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

2905 

2906 

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

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

2909 user1, token1 = generate_user() 

2910 user2, token2 = generate_user() 

2911 

2912 with session_scope() as session: 

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

2914 

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

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

2917 end_time = start_time + timedelta(hours=1) 

2918 

2919 with events_session(token1) as api: 

2920 res = api.CreateEvent( 

2921 events_pb2.CreateEventReq( 

2922 title="Cancelled Reminder Test", 

2923 content="Content.", 

2924 location=events_pb2.EventLocation( 

2925 address="Near Null Island", 

2926 lat=0.1, 

2927 lng=0.2, 

2928 ), 

2929 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2930 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2931 ) 

2932 ) 

2933 event_id = res.event_id 

2934 

2935 moderator.approve_event_occurrence(event_id) 

2936 process_jobs() 

2937 

2938 # User2 marks attendance 

2939 with events_session(token2) as api: 

2940 api.SetEventAttendance( 

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

2942 ) 

2943 

2944 # User1 cancels the event 

2945 with events_session(token1) as api: 

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

2947 

2948 process_jobs() 

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

2950 while push_collector.count_for_user(user2.id): 

2951 push_collector.pop_for_user(user2.id) 

2952 

2953 # Run the event reminder handler 

2954 send_event_reminders(empty_pb2.Empty()) 

2955 process_jobs() 

2956 

2957 # Verify that no reminder notification was sent for user2 

2958 with session_scope() as session: 

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

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

2961 assert len(reminder_notifs) == 0 

2962 

2963 

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

2965def test_event_reminder_not_sent_for_invisible_attendee( 

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

2967): 

2968 user1, token1 = generate_user() 

2969 user2, token2 = generate_user() 

2970 

2971 with session_scope() as session: 

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

2973 

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

2975 end_time = start_time + timedelta(hours=1) 

2976 

2977 with events_session(token1) as api: 

2978 res = api.CreateEvent( 

2979 events_pb2.CreateEventReq( 

2980 title="Invisible Attendee Reminder Test", 

2981 content="Content.", 

2982 location=events_pb2.EventLocation( 

2983 address="Near Null Island", 

2984 lat=0.1, 

2985 lng=0.2, 

2986 ), 

2987 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

2988 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

2989 ) 

2990 ) 

2991 event_id = res.event_id 

2992 

2993 moderator.approve_event_occurrence(event_id) 

2994 process_jobs() 

2995 

2996 with events_session(token2) as api: 

2997 api.SetEventAttendance( 

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

2999 ) 

3000 

3001 with session_scope() as session: 

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

3003 

3004 send_event_reminders(empty_pb2.Empty()) 

3005 process_jobs() 

3006 

3007 with session_scope() as session: 

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

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

3010 assert len(reminder_notifs) == 0 

3011 

3012 

3013def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3015 user1, token1 = generate_user() 

3016 user2, token2 = generate_user() 

3017 

3018 with session_scope() as session: 

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

3020 

3021 start = now() 

3022 

3023 # User1 creates event A with 3 occurrences 

3024 event_a_ids = [] 

3025 with events_session(token1) as api: 

3026 res = api.CreateEvent( 

3027 events_pb2.CreateEventReq( 

3028 title="Event A", 

3029 content="Content A.", 

3030 parent_community_id=c_id, 

3031 location=events_pb2.EventLocation( 

3032 address="Near Null Island", 

3033 lat=0.1, 

3034 lng=0.2, 

3035 ), 

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

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

3038 ) 

3039 ) 

3040 event_a_ids.append(res.event_id) 

3041 for i in range(2): 

3042 res = api.ScheduleEvent( 

3043 events_pb2.ScheduleEventReq( 

3044 event_id=event_a_ids[-1], 

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

3046 location=events_pb2.EventLocation( 

3047 address="Near Null Island", 

3048 lat=0.1, 

3049 lng=0.2, 

3050 ), 

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

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

3053 ) 

3054 ) 

3055 event_a_ids.append(res.event_id) 

3056 

3057 # User2 creates event B with 2 occurrences 

3058 event_b_ids = [] 

3059 with events_session(token2) as api: 

3060 res = api.CreateEvent( 

3061 events_pb2.CreateEventReq( 

3062 title="Event B", 

3063 content="Content B.", 

3064 parent_community_id=c_id, 

3065 location=events_pb2.EventLocation( 

3066 address="Near Null Island", 

3067 lat=0.1, 

3068 lng=0.2, 

3069 ), 

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

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

3072 ) 

3073 ) 

3074 event_b_ids.append(res.event_id) 

3075 res = api.ScheduleEvent( 

3076 events_pb2.ScheduleEventReq( 

3077 event_id=event_b_ids[-1], 

3078 content="B occurrence 1", 

3079 location=events_pb2.EventLocation( 

3080 address="Near Null Island", 

3081 lat=0.1, 

3082 lng=0.2, 

3083 ), 

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

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

3086 ) 

3087 ) 

3088 event_b_ids.append(res.event_id) 

3089 

3090 moderator.approve_event_occurrence(event_a_ids[0]) 

3091 moderator.approve_event_occurrence(event_b_ids[0]) 

3092 

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

3094 with events_session(token1) as api: 

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

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

3097 assert sorted(returned_ids) == sorted(event_a_ids) 

3098 

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

3100 with events_session(token2) as api: 

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

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

3103 assert sorted(returned_ids) == sorted(event_b_ids) 

3104 

3105 

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

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

3108 user1, token1 = generate_user() 

3109 user2, token2 = generate_user() 

3110 

3111 with session_scope() as session: 

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

3113 

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

3115 end_time = start_time + timedelta(hours=3) 

3116 

3117 with events_session(token1) as api: 

3118 res = api.CreateEvent( 

3119 events_pb2.CreateEventReq( 

3120 title="Comment Test", 

3121 content="Content.", 

3122 parent_community_id=c_id, 

3123 location=events_pb2.EventLocation( 

3124 address="Near Null Island", 

3125 lat=0.1, 

3126 lng=0.2, 

3127 ), 

3128 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3129 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3130 ) 

3131 ) 

3132 event_id = res.event_id 

3133 thread_id = res.thread.thread_id 

3134 

3135 moderator.approve_event_occurrence(event_id) 

3136 process_jobs() 

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

3138 push_collector.pop_for_user(user1.id) 

3139 

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

3141 with events_session(token1) as api: 

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

3143 

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

3145 with threads_session(token2) as api: 

3146 comment_thread_id = api.PostReply( 

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

3148 ).thread_id 

3149 

3150 process_jobs() 

3151 

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

3153 comment_db_id = comment_thread_id // 10 

3154 with session_scope() as session: 

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

3156 

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

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

3159 assert len(comment_notifs) == 1 

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

3161 

3162 

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

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

3165 user1, token1 = generate_user() 

3166 user2, token2 = generate_user() 

3167 user3, token3 = generate_user() 

3168 

3169 with session_scope() as session: 

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

3171 

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

3173 end_time = start_time + timedelta(hours=3) 

3174 

3175 with events_session(token1) as api: 

3176 res = api.CreateEvent( 

3177 events_pb2.CreateEventReq( 

3178 title="Reply Test", 

3179 content="Content.", 

3180 location=events_pb2.EventLocation( 

3181 address="Near Null Island", 

3182 lat=0.1, 

3183 lng=0.2, 

3184 ), 

3185 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time), 

3186 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time), 

3187 ) 

3188 ) 

3189 event_id = res.event_id 

3190 thread_id = res.thread.thread_id 

3191 

3192 moderator.approve_event_occurrence(event_id) 

3193 process_jobs() 

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

3195 push_collector.pop_for_user(user1.id) 

3196 

3197 # User2 posts a top-level comment 

3198 with threads_session(token2) as api: 

3199 comment_thread_id = api.PostReply( 

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

3201 ).thread_id 

3202 

3203 process_jobs() 

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

3205 push_collector.pop_for_user(user1.id) 

3206 

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

3208 with threads_session(token3) as api: 

3209 nested_reply_thread_id = api.PostReply( 

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

3211 ).thread_id 

3212 

3213 process_jobs() 

3214 

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

3216 nested_reply_db_id = nested_reply_thread_id // 10 

3217 with session_scope() as session: 

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

3219 

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

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

3222 assert len(reply_notifs) == 1 

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