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

1538 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 09:44 +0000

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from psycopg.types.range import TimestamptzRange 

7from sqlalchemy import select 

8from sqlalchemy.sql.expression import update 

9 

10from couchers.db import session_scope 

11from couchers.jobs.handlers import send_event_reminders 

12from couchers.models import ( 

13 BackgroundJob, 

14 BackgroundJobState, 

15 EventOccurrence, 

16 ModerationState, 

17 ModerationVisibility, 

18 Notification, 

19 NotificationDelivery, 

20 NotificationTopicAction, 

21 Upload, 

22) 

23from couchers.proto import editor_pb2, events_pb2, threads_pb2 

24from couchers.tasks import enforce_community_memberships 

25from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime 

26from tests.fixtures.db import generate_user 

27from tests.fixtures.misc import Moderator, PushCollector, email_fields, mock_notification_email, process_jobs 

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

29from tests.test_communities import create_community, create_group 

30 

31 

32@pytest.fixture(autouse=True) 

33def _(testconfig): 

34 pass 

35 

36 

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

38 # test cases: 

39 # can create event 

40 # cannot create event with missing details 

41 # can create online event 

42 # can create in person event 

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

44 # can create in different timezones 

45 

46 # event creator 

47 user1, token1 = generate_user() 

48 # community moderator 

49 user2, token2 = generate_user() 

50 # third party 

51 user3, token3 = generate_user() 

52 

53 with session_scope() as session: 

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

55 

56 time_before = now() 

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

58 end_time = start_time + timedelta(hours=3) 

59 

60 with events_session(token1) as api: 

61 # in person event 

62 res = api.CreateEvent( 

63 events_pb2.CreateEventReq( 

64 title="Dummy Title", 

65 content="Dummy content.", 

66 photo_key=None, 

67 offline_information=events_pb2.OfflineEventInformation( 

68 address="Near Null Island", 

69 lat=0.1, 

70 lng=0.2, 

71 ), 

72 start_time=Timestamp_from_datetime(start_time), 

73 end_time=Timestamp_from_datetime(end_time), 

74 timezone="UTC", 

75 ) 

76 ) 

77 

78 assert res.is_next 

79 assert res.title == "Dummy Title" 

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

81 assert res.content == "Dummy content." 

82 assert not res.photo_url 

83 assert res.WhichOneof("mode") == "offline_information" 

84 assert res.offline_information.lat == 0.1 

85 assert res.offline_information.lng == 0.2 

86 assert res.offline_information.address == "Near Null Island" 

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

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

89 assert res.creator_user_id == user1.id 

90 assert to_aware_datetime(res.start_time) == start_time 

91 assert to_aware_datetime(res.end_time) == end_time 

92 # assert res.timezone == "UTC" 

93 assert res.start_time_display 

94 assert res.end_time_display 

95 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

96 assert res.organizer 

97 assert res.subscriber 

98 assert res.going_count == 1 

99 assert res.maybe_count == 0 

100 assert res.organizer_count == 1 

101 assert res.subscriber_count == 1 

102 assert res.owner_user_id == user1.id 

103 assert not res.owner_community_id 

104 assert not res.owner_group_id 

105 assert res.thread.thread_id 

106 assert res.can_edit 

107 assert not res.can_moderate 

108 

109 event_id = res.event_id 

110 

111 # Approve the event so other users can see it 

112 moderator.approve_event_occurrence(event_id) 

113 

114 with events_session(token2) as api: 

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

116 

117 assert res.is_next 

118 assert res.title == "Dummy Title" 

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

120 assert res.content == "Dummy content." 

121 assert not res.photo_url 

122 assert res.WhichOneof("mode") == "offline_information" 

123 assert res.offline_information.lat == 0.1 

124 assert res.offline_information.lng == 0.2 

125 assert res.offline_information.address == "Near Null Island" 

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

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

128 assert res.creator_user_id == user1.id 

129 assert to_aware_datetime(res.start_time) == start_time 

130 assert to_aware_datetime(res.end_time) == end_time 

131 # assert res.timezone == "UTC" 

132 assert res.start_time_display 

133 assert res.end_time_display 

134 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

135 assert not res.organizer 

136 assert not res.subscriber 

137 assert res.going_count == 1 

138 assert res.maybe_count == 0 

139 assert res.organizer_count == 1 

140 assert res.subscriber_count == 1 

141 assert res.owner_user_id == user1.id 

142 assert not res.owner_community_id 

143 assert not res.owner_group_id 

144 assert res.thread.thread_id 

145 assert res.can_edit 

146 assert res.can_moderate 

147 

148 with events_session(token3) as api: 

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

150 

151 assert res.is_next 

152 assert res.title == "Dummy Title" 

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

154 assert res.content == "Dummy content." 

155 assert not res.photo_url 

156 assert res.WhichOneof("mode") == "offline_information" 

157 assert res.offline_information.lat == 0.1 

158 assert res.offline_information.lng == 0.2 

159 assert res.offline_information.address == "Near Null Island" 

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

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

162 assert res.creator_user_id == user1.id 

163 assert to_aware_datetime(res.start_time) == start_time 

164 assert to_aware_datetime(res.end_time) == end_time 

165 # assert res.timezone == "UTC" 

166 assert res.start_time_display 

167 assert res.end_time_display 

168 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

169 assert not res.organizer 

170 assert not res.subscriber 

171 assert res.going_count == 1 

172 assert res.maybe_count == 0 

173 assert res.organizer_count == 1 

174 assert res.subscriber_count == 1 

175 assert res.owner_user_id == user1.id 

176 assert not res.owner_community_id 

177 assert not res.owner_group_id 

178 assert res.thread.thread_id 

179 assert not res.can_edit 

180 assert not res.can_moderate 

181 

182 with events_session(token1) as api: 

183 # online only event 

184 res = api.CreateEvent( 

185 events_pb2.CreateEventReq( 

186 title="Dummy Title", 

187 content="Dummy content.", 

188 photo_key=None, 

189 online_information=events_pb2.OnlineEventInformation( 

190 link="https://couchers.org/meet/", 

191 ), 

192 parent_community_id=c_id, 

193 start_time=Timestamp_from_datetime(start_time), 

194 end_time=Timestamp_from_datetime(end_time), 

195 timezone="UTC", 

196 ) 

197 ) 

198 

199 assert res.is_next 

200 assert res.title == "Dummy Title" 

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

202 assert res.content == "Dummy content." 

203 assert not res.photo_url 

204 assert res.WhichOneof("mode") == "online_information" 

205 assert res.online_information.link == "https://couchers.org/meet/" 

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

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

208 assert res.creator_user_id == user1.id 

209 assert to_aware_datetime(res.start_time) == start_time 

210 assert to_aware_datetime(res.end_time) == end_time 

211 # assert res.timezone == "UTC" 

212 assert res.start_time_display 

213 assert res.end_time_display 

214 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

215 assert res.organizer 

216 assert res.subscriber 

217 assert res.going_count == 1 

218 assert res.maybe_count == 0 

219 assert res.organizer_count == 1 

220 assert res.subscriber_count == 1 

221 assert res.owner_user_id == user1.id 

222 assert not res.owner_community_id 

223 assert not res.owner_group_id 

224 assert res.thread.thread_id 

225 assert res.can_edit 

226 assert not res.can_moderate 

227 

228 event_id = res.event_id 

229 

230 # Approve the online event so other users can see it 

231 moderator.approve_event_occurrence(event_id) 

232 

233 with events_session(token2) as api: 

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

235 

236 assert res.is_next 

237 assert res.title == "Dummy Title" 

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

239 assert res.content == "Dummy content." 

240 assert not res.photo_url 

241 assert res.WhichOneof("mode") == "online_information" 

242 assert res.online_information.link == "https://couchers.org/meet/" 

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

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

245 assert res.creator_user_id == user1.id 

246 assert to_aware_datetime(res.start_time) == start_time 

247 assert to_aware_datetime(res.end_time) == end_time 

248 # assert res.timezone == "UTC" 

249 assert res.start_time_display 

250 assert res.end_time_display 

251 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

252 assert not res.organizer 

253 assert not res.subscriber 

254 assert res.going_count == 1 

255 assert res.maybe_count == 0 

256 assert res.organizer_count == 1 

257 assert res.subscriber_count == 1 

258 assert res.owner_user_id == user1.id 

259 assert not res.owner_community_id 

260 assert not res.owner_group_id 

261 assert res.thread.thread_id 

262 assert res.can_edit 

263 assert res.can_moderate 

264 

265 with events_session(token3) as api: 

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

267 

268 assert res.is_next 

269 assert res.title == "Dummy Title" 

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

271 assert res.content == "Dummy content." 

272 assert not res.photo_url 

273 assert res.WhichOneof("mode") == "online_information" 

274 assert res.online_information.link == "https://couchers.org/meet/" 

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

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

277 assert res.creator_user_id == user1.id 

278 assert to_aware_datetime(res.start_time) == start_time 

279 assert to_aware_datetime(res.end_time) == end_time 

280 # assert res.timezone == "UTC" 

281 assert res.start_time_display 

282 assert res.end_time_display 

283 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

284 assert not res.organizer 

285 assert not res.subscriber 

286 assert res.going_count == 1 

287 assert res.maybe_count == 0 

288 assert res.organizer_count == 1 

289 assert res.subscriber_count == 1 

290 assert res.owner_user_id == user1.id 

291 assert not res.owner_community_id 

292 assert not res.owner_group_id 

293 assert res.thread.thread_id 

294 assert not res.can_edit 

295 assert not res.can_moderate 

296 

297 with events_session(token1) as api: 

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

299 api.CreateEvent( 

300 events_pb2.CreateEventReq( 

301 title="Dummy Title", 

302 content="Dummy content.", 

303 photo_key=None, 

304 online_information=events_pb2.OnlineEventInformation( 

305 link="https://couchers.org/meet/", 

306 ), 

307 start_time=Timestamp_from_datetime(start_time), 

308 end_time=Timestamp_from_datetime(end_time), 

309 timezone="UTC", 

310 ) 

311 ) 

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

313 assert e.value.details() == "The online event is missing a parent community." 

314 

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

316 api.CreateEvent( 

317 events_pb2.CreateEventReq( 

318 # title="Dummy Title", 

319 content="Dummy content.", 

320 photo_key=None, 

321 offline_information=events_pb2.OfflineEventInformation( 

322 address="Near Null Island", 

323 lat=0.1, 

324 lng=0.1, 

325 ), 

326 start_time=Timestamp_from_datetime(start_time), 

327 end_time=Timestamp_from_datetime(end_time), 

328 timezone="UTC", 

329 ) 

330 ) 

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

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

333 

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

335 api.CreateEvent( 

336 events_pb2.CreateEventReq( 

337 title="Dummy Title", 

338 # content="Dummy content.", 

339 photo_key=None, 

340 offline_information=events_pb2.OfflineEventInformation( 

341 address="Near Null Island", 

342 lat=0.1, 

343 lng=0.1, 

344 ), 

345 start_time=Timestamp_from_datetime(start_time), 

346 end_time=Timestamp_from_datetime(end_time), 

347 timezone="UTC", 

348 ) 

349 ) 

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

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

352 

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

354 api.CreateEvent( 

355 events_pb2.CreateEventReq( 

356 title="Dummy Title", 

357 content="Dummy content.", 

358 photo_key="nonexistent", 

359 offline_information=events_pb2.OfflineEventInformation( 

360 address="Near Null Island", 

361 lat=0.1, 

362 lng=0.1, 

363 ), 

364 start_time=Timestamp_from_datetime(start_time), 

365 end_time=Timestamp_from_datetime(end_time), 

366 timezone="UTC", 

367 ) 

368 ) 

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

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

371 

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

373 api.CreateEvent( 

374 events_pb2.CreateEventReq( 

375 title="Dummy Title", 

376 content="Dummy content.", 

377 photo_key=None, 

378 offline_information=events_pb2.OfflineEventInformation( 

379 address="Near Null Island", 

380 ), 

381 start_time=Timestamp_from_datetime(start_time), 

382 end_time=Timestamp_from_datetime(end_time), 

383 timezone="UTC", 

384 ) 

385 ) 

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

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

388 

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

390 api.CreateEvent( 

391 events_pb2.CreateEventReq( 

392 title="Dummy Title", 

393 content="Dummy content.", 

394 photo_key=None, 

395 offline_information=events_pb2.OfflineEventInformation( 

396 lat=0.1, 

397 lng=0.1, 

398 ), 

399 start_time=Timestamp_from_datetime(start_time), 

400 end_time=Timestamp_from_datetime(end_time), 

401 timezone="UTC", 

402 ) 

403 ) 

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

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

406 

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

408 api.CreateEvent( 

409 events_pb2.CreateEventReq( 

410 title="Dummy Title", 

411 content="Dummy content.", 

412 photo_key=None, 

413 online_information=events_pb2.OnlineEventInformation(), 

414 start_time=Timestamp_from_datetime(start_time), 

415 end_time=Timestamp_from_datetime(end_time), 

416 timezone="UTC", 

417 ) 

418 ) 

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

420 assert e.value.details() == "An online-only event requires a link." 

421 

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

423 api.CreateEvent( 

424 events_pb2.CreateEventReq( 

425 title="Dummy Title", 

426 content="Dummy content.", 

427 parent_community_id=c_id, 

428 online_information=events_pb2.OnlineEventInformation( 

429 link="https://couchers.org/meet/", 

430 ), 

431 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)), 

432 end_time=Timestamp_from_datetime(end_time), 

433 timezone="UTC", 

434 ) 

435 ) 

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

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

438 

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

440 api.CreateEvent( 

441 events_pb2.CreateEventReq( 

442 title="Dummy Title", 

443 content="Dummy content.", 

444 parent_community_id=c_id, 

445 online_information=events_pb2.OnlineEventInformation( 

446 link="https://couchers.org/meet/", 

447 ), 

448 start_time=Timestamp_from_datetime(end_time), 

449 end_time=Timestamp_from_datetime(start_time), 

450 timezone="UTC", 

451 ) 

452 ) 

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

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

455 

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

457 api.CreateEvent( 

458 events_pb2.CreateEventReq( 

459 title="Dummy Title", 

460 content="Dummy content.", 

461 parent_community_id=c_id, 

462 online_information=events_pb2.OnlineEventInformation( 

463 link="https://couchers.org/meet/", 

464 ), 

465 start_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)), 

466 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)), 

467 timezone="UTC", 

468 ) 

469 ) 

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

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

472 

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

474 api.CreateEvent( 

475 events_pb2.CreateEventReq( 

476 title="Dummy Title", 

477 content="Dummy content.", 

478 parent_community_id=c_id, 

479 online_information=events_pb2.OnlineEventInformation( 

480 link="https://couchers.org/meet/", 

481 ), 

482 start_time=Timestamp_from_datetime(start_time), 

483 end_time=Timestamp_from_datetime(now() + timedelta(days=100)), 

484 timezone="UTC", 

485 ) 

486 ) 

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

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

489 

490 

491def test_CreateEvent_incomplete_profile(db): 

492 user1, token1 = generate_user(complete_profile=False) 

493 user2, token2 = generate_user() 

494 

495 with session_scope() as session: 

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

497 

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

499 end_time = start_time + timedelta(hours=3) 

500 

501 with events_session(token1) as api: 

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

503 api.CreateEvent( 

504 events_pb2.CreateEventReq( 

505 title="Dummy Title", 

506 content="Dummy content.", 

507 photo_key=None, 

508 offline_information=events_pb2.OfflineEventInformation( 

509 address="Near Null Island", 

510 lat=0.1, 

511 lng=0.2, 

512 ), 

513 start_time=Timestamp_from_datetime(start_time), 

514 end_time=Timestamp_from_datetime(end_time), 

515 timezone="UTC", 

516 ) 

517 ) 

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

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

520 

521 

522def test_ScheduleEvent(db): 

523 # test cases: 

524 # can schedule a new event occurrence 

525 

526 user, token = generate_user() 

527 

528 with session_scope() as session: 

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

530 

531 time_before = now() 

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

533 end_time = start_time + timedelta(hours=3) 

534 

535 with events_session(token) as api: 

536 res = api.CreateEvent( 

537 events_pb2.CreateEventReq( 

538 title="Dummy Title", 

539 content="Dummy content.", 

540 parent_community_id=c_id, 

541 online_information=events_pb2.OnlineEventInformation( 

542 link="https://couchers.org/meet/", 

543 ), 

544 start_time=Timestamp_from_datetime(start_time), 

545 end_time=Timestamp_from_datetime(end_time), 

546 timezone="UTC", 

547 ) 

548 ) 

549 

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

551 new_end_time = new_start_time + timedelta(hours=2) 

552 

553 res = api.ScheduleEvent( 

554 events_pb2.ScheduleEventReq( 

555 event_id=res.event_id, 

556 content="New event occurrence", 

557 offline_information=events_pb2.OfflineEventInformation( 

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

559 lat=0.3, 

560 lng=0.2, 

561 ), 

562 start_time=Timestamp_from_datetime(new_start_time), 

563 end_time=Timestamp_from_datetime(new_end_time), 

564 timezone="UTC", 

565 ) 

566 ) 

567 

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

569 

570 assert not res.is_next 

571 assert res.title == "Dummy Title" 

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

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

574 assert not res.photo_url 

575 assert res.WhichOneof("mode") == "offline_information" 

576 assert res.offline_information.lat == 0.3 

577 assert res.offline_information.lng == 0.2 

578 assert res.offline_information.address == "A bit further but still near Null Island" 

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

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

581 assert res.creator_user_id == user.id 

582 assert to_aware_datetime(res.start_time) == new_start_time 

583 assert to_aware_datetime(res.end_time) == new_end_time 

584 # assert res.timezone == "UTC" 

585 assert res.start_time_display 

586 assert res.end_time_display 

587 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

588 assert res.organizer 

589 assert res.subscriber 

590 assert res.going_count == 1 

591 assert res.maybe_count == 0 

592 assert res.organizer_count == 1 

593 assert res.subscriber_count == 1 

594 assert res.owner_user_id == user.id 

595 assert not res.owner_community_id 

596 assert not res.owner_group_id 

597 assert res.thread.thread_id 

598 assert res.can_edit 

599 assert res.can_moderate 

600 

601 

602def test_cannot_overlap_occurrences_schedule(db): 

603 user, token = generate_user() 

604 

605 with session_scope() as session: 

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

607 

608 start = now() 

609 

610 with events_session(token) as api: 

611 res = api.CreateEvent( 

612 events_pb2.CreateEventReq( 

613 title="Dummy Title", 

614 content="Dummy content.", 

615 parent_community_id=c_id, 

616 online_information=events_pb2.OnlineEventInformation( 

617 link="https://couchers.org/meet/", 

618 ), 

619 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

620 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

621 timezone="UTC", 

622 ) 

623 ) 

624 

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

626 api.ScheduleEvent( 

627 events_pb2.ScheduleEventReq( 

628 event_id=res.event_id, 

629 content="New event occurrence", 

630 offline_information=events_pb2.OfflineEventInformation( 

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

632 lat=0.3, 

633 lng=0.2, 

634 ), 

635 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

636 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

637 timezone="UTC", 

638 ) 

639 ) 

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

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

642 

643 

644def test_cannot_overlap_occurrences_update(db): 

645 user, token = generate_user() 

646 

647 with session_scope() as session: 

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

649 

650 start = now() 

651 

652 with events_session(token) as api: 

653 res = api.CreateEvent( 

654 events_pb2.CreateEventReq( 

655 title="Dummy Title", 

656 content="Dummy content.", 

657 parent_community_id=c_id, 

658 online_information=events_pb2.OnlineEventInformation( 

659 link="https://couchers.org/meet/", 

660 ), 

661 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

662 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

663 timezone="UTC", 

664 ) 

665 ) 

666 

667 event_id = api.ScheduleEvent( 

668 events_pb2.ScheduleEventReq( 

669 event_id=res.event_id, 

670 content="New event occurrence", 

671 offline_information=events_pb2.OfflineEventInformation( 

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

673 lat=0.3, 

674 lng=0.2, 

675 ), 

676 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

677 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

678 timezone="UTC", 

679 ) 

680 ).event_id 

681 

682 # can overlap with this current existing occurrence 

683 api.UpdateEvent( 

684 events_pb2.UpdateEventReq( 

685 event_id=event_id, 

686 start_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

687 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

688 ) 

689 ) 

690 

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

692 api.UpdateEvent( 

693 events_pb2.UpdateEventReq( 

694 event_id=event_id, 

695 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

696 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

697 ) 

698 ) 

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

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

701 

702 

703def test_UpdateEvent_single(db, moderator: Moderator): 

704 # test cases: 

705 # owner can update 

706 # community owner can update 

707 # can't mess up online/in person dichotomy 

708 # notifies attendees 

709 

710 # event creator 

711 user1, token1 = generate_user() 

712 # community moderator 

713 user2, token2 = generate_user() 

714 # third parties 

715 user3, token3 = generate_user() 

716 user4, token4 = generate_user() 

717 user5, token5 = generate_user() 

718 user6, token6 = generate_user() 

719 

720 with session_scope() as session: 

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

722 

723 time_before = now() 

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

725 end_time = start_time + timedelta(hours=3) 

726 

727 with events_session(token1) as api: 

728 res = api.CreateEvent( 

729 events_pb2.CreateEventReq( 

730 title="Dummy Title", 

731 content="Dummy content.", 

732 offline_information=events_pb2.OfflineEventInformation( 

733 address="Near Null Island", 

734 lat=0.1, 

735 lng=0.2, 

736 ), 

737 start_time=Timestamp_from_datetime(start_time), 

738 end_time=Timestamp_from_datetime(end_time), 

739 timezone="UTC", 

740 ) 

741 ) 

742 

743 event_id = res.event_id 

744 

745 moderator.approve_event_occurrence(event_id) 

746 

747 with events_session(token4) as api: 

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

749 

750 with events_session(token5) as api: 

751 api.SetEventAttendance( 

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

753 ) 

754 

755 with events_session(token6) as api: 

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

757 api.SetEventAttendance( 

758 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE) 

759 ) 

760 

761 time_before_update = now() 

762 

763 with events_session(token1) as api: 

764 res = api.UpdateEvent( 

765 events_pb2.UpdateEventReq( 

766 event_id=event_id, 

767 ) 

768 ) 

769 

770 with events_session(token1) as api: 

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

772 

773 assert res.is_next 

774 assert res.title == "Dummy Title" 

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

776 assert res.content == "Dummy content." 

777 assert not res.photo_url 

778 assert res.WhichOneof("mode") == "offline_information" 

779 assert res.offline_information.lat == 0.1 

780 assert res.offline_information.lng == 0.2 

781 assert res.offline_information.address == "Near Null Island" 

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

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

784 assert res.creator_user_id == user1.id 

785 assert to_aware_datetime(res.start_time) == start_time 

786 assert to_aware_datetime(res.end_time) == end_time 

787 # assert res.timezone == "UTC" 

788 assert res.start_time_display 

789 assert res.end_time_display 

790 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

791 assert res.organizer 

792 assert res.subscriber 

793 assert res.going_count == 2 

794 assert res.maybe_count == 1 

795 assert res.organizer_count == 1 

796 assert res.subscriber_count == 3 

797 assert res.owner_user_id == user1.id 

798 assert not res.owner_community_id 

799 assert not res.owner_group_id 

800 assert res.thread.thread_id 

801 assert res.can_edit 

802 assert not res.can_moderate 

803 

804 with events_session(token2) as api: 

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

806 

807 assert res.is_next 

808 assert res.title == "Dummy Title" 

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

810 assert res.content == "Dummy content." 

811 assert not res.photo_url 

812 assert res.WhichOneof("mode") == "offline_information" 

813 assert res.offline_information.lat == 0.1 

814 assert res.offline_information.lng == 0.2 

815 assert res.offline_information.address == "Near Null Island" 

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

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

818 assert res.creator_user_id == user1.id 

819 assert to_aware_datetime(res.start_time) == start_time 

820 assert to_aware_datetime(res.end_time) == end_time 

821 # assert res.timezone == "UTC" 

822 assert res.start_time_display 

823 assert res.end_time_display 

824 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

825 assert not res.organizer 

826 assert not res.subscriber 

827 assert res.going_count == 2 

828 assert res.maybe_count == 1 

829 assert res.organizer_count == 1 

830 assert res.subscriber_count == 3 

831 assert res.owner_user_id == user1.id 

832 assert not res.owner_community_id 

833 assert not res.owner_group_id 

834 assert res.thread.thread_id 

835 assert res.can_edit 

836 assert res.can_moderate 

837 

838 with events_session(token3) as api: 

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

840 

841 assert res.is_next 

842 assert res.title == "Dummy Title" 

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

844 assert res.content == "Dummy content." 

845 assert not res.photo_url 

846 assert res.WhichOneof("mode") == "offline_information" 

847 assert res.offline_information.lat == 0.1 

848 assert res.offline_information.lng == 0.2 

849 assert res.offline_information.address == "Near Null Island" 

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

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

852 assert res.creator_user_id == user1.id 

853 assert to_aware_datetime(res.start_time) == start_time 

854 assert to_aware_datetime(res.end_time) == end_time 

855 # assert res.timezone == "UTC" 

856 assert res.start_time_display 

857 assert res.end_time_display 

858 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

859 assert not res.organizer 

860 assert not res.subscriber 

861 assert res.going_count == 2 

862 assert res.maybe_count == 1 

863 assert res.organizer_count == 1 

864 assert res.subscriber_count == 3 

865 assert res.owner_user_id == user1.id 

866 assert not res.owner_community_id 

867 assert not res.owner_group_id 

868 assert res.thread.thread_id 

869 assert not res.can_edit 

870 assert not res.can_moderate 

871 

872 with events_session(token1) as api: 

873 res = api.UpdateEvent( 

874 events_pb2.UpdateEventReq( 

875 event_id=event_id, 

876 title=wrappers_pb2.StringValue(value="Dummy Title"), 

877 content=wrappers_pb2.StringValue(value="Dummy content."), 

878 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"), 

879 start_time=Timestamp_from_datetime(start_time), 

880 end_time=Timestamp_from_datetime(end_time), 

881 timezone=wrappers_pb2.StringValue(value="UTC"), 

882 ) 

883 ) 

884 

885 assert res.is_next 

886 assert res.title == "Dummy Title" 

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

888 assert res.content == "Dummy content." 

889 assert not res.photo_url 

890 assert res.WhichOneof("mode") == "online_information" 

891 assert res.online_information.link == "https://couchers.org/meet/" 

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

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

894 assert res.creator_user_id == user1.id 

895 assert to_aware_datetime(res.start_time) == start_time 

896 assert to_aware_datetime(res.end_time) == end_time 

897 # assert res.timezone == "UTC" 

898 assert res.start_time_display 

899 assert res.end_time_display 

900 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

901 assert res.organizer 

902 assert res.subscriber 

903 assert res.going_count == 2 

904 assert res.maybe_count == 1 

905 assert res.organizer_count == 1 

906 assert res.subscriber_count == 3 

907 assert res.owner_user_id == user1.id 

908 assert not res.owner_community_id 

909 assert not res.owner_group_id 

910 assert res.thread.thread_id 

911 assert res.can_edit 

912 assert not res.can_moderate 

913 

914 event_id = res.event_id 

915 

916 with events_session(token2) as api: 

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

918 

919 assert res.is_next 

920 assert res.title == "Dummy Title" 

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

922 assert res.content == "Dummy content." 

923 assert not res.photo_url 

924 assert res.WhichOneof("mode") == "online_information" 

925 assert res.online_information.link == "https://couchers.org/meet/" 

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

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

928 assert res.creator_user_id == user1.id 

929 assert to_aware_datetime(res.start_time) == start_time 

930 assert to_aware_datetime(res.end_time) == end_time 

931 # assert res.timezone == "UTC" 

932 assert res.start_time_display 

933 assert res.end_time_display 

934 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

935 assert not res.organizer 

936 assert not res.subscriber 

937 assert res.going_count == 2 

938 assert res.maybe_count == 1 

939 assert res.organizer_count == 1 

940 assert res.subscriber_count == 3 

941 assert res.owner_user_id == user1.id 

942 assert not res.owner_community_id 

943 assert not res.owner_group_id 

944 assert res.thread.thread_id 

945 assert res.can_edit 

946 assert res.can_moderate 

947 

948 with events_session(token3) as api: 

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

950 

951 assert res.is_next 

952 assert res.title == "Dummy Title" 

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

954 assert res.content == "Dummy content." 

955 assert not res.photo_url 

956 assert res.WhichOneof("mode") == "online_information" 

957 assert res.online_information.link == "https://couchers.org/meet/" 

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

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

960 assert res.creator_user_id == user1.id 

961 assert to_aware_datetime(res.start_time) == start_time 

962 assert to_aware_datetime(res.end_time) == end_time 

963 # assert res.timezone == "UTC" 

964 assert res.start_time_display 

965 assert res.end_time_display 

966 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

967 assert not res.organizer 

968 assert not res.subscriber 

969 assert res.going_count == 2 

970 assert res.maybe_count == 1 

971 assert res.organizer_count == 1 

972 assert res.subscriber_count == 3 

973 assert res.owner_user_id == user1.id 

974 assert not res.owner_community_id 

975 assert not res.owner_group_id 

976 assert res.thread.thread_id 

977 assert not res.can_edit 

978 assert not res.can_moderate 

979 

980 with events_session(token1) as api: 

981 res = api.UpdateEvent( 

982 events_pb2.UpdateEventReq( 

983 event_id=event_id, 

984 offline_information=events_pb2.OfflineEventInformation( 

985 address="Near Null Island", 

986 lat=0.1, 

987 lng=0.2, 

988 ), 

989 ) 

990 ) 

991 

992 with events_session(token3) as api: 

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

994 

995 assert res.WhichOneof("mode") == "offline_information" 

996 assert res.offline_information.address == "Near Null Island" 

997 assert res.offline_information.lat == 0.1 

998 assert res.offline_information.lng == 0.2 

999 

1000 

1001def test_UpdateEvent_all(db, moderator: Moderator): 

1002 # event creator 

1003 user1, token1 = generate_user() 

1004 # community moderator 

1005 user2, token2 = generate_user() 

1006 # third parties 

1007 user3, token3 = generate_user() 

1008 user4, token4 = generate_user() 

1009 user5, token5 = generate_user() 

1010 user6, token6 = generate_user() 

1011 

1012 with session_scope() as session: 

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

1014 

1015 time_before = now() 

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

1017 end_time = start_time + timedelta(hours=1.5) 

1018 

1019 event_ids = [] 

1020 

1021 with events_session(token1) as api: 

1022 res = api.CreateEvent( 

1023 events_pb2.CreateEventReq( 

1024 title="Dummy Title", 

1025 content="0th occurrence", 

1026 offline_information=events_pb2.OfflineEventInformation( 

1027 address="Near Null Island", 

1028 lat=0.1, 

1029 lng=0.2, 

1030 ), 

1031 start_time=Timestamp_from_datetime(start_time), 

1032 end_time=Timestamp_from_datetime(end_time), 

1033 timezone="UTC", 

1034 ) 

1035 ) 

1036 

1037 event_id = res.event_id 

1038 event_ids.append(event_id) 

1039 

1040 moderator.approve_event_occurrence(event_id) 

1041 

1042 with events_session(token4) as api: 

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

1044 

1045 with events_session(token5) as api: 

1046 api.SetEventAttendance( 

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

1048 ) 

1049 

1050 with events_session(token6) as api: 

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

1052 api.SetEventAttendance( 

1053 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE) 

1054 ) 

1055 

1056 with events_session(token1) as api: 

1057 for i in range(5): 

1058 res = api.ScheduleEvent( 

1059 events_pb2.ScheduleEventReq( 

1060 event_id=event_ids[-1], 

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

1062 online_information=events_pb2.OnlineEventInformation( 

1063 link="https://couchers.org/meet/", 

1064 ), 

1065 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)), 

1066 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)), 

1067 timezone="UTC", 

1068 ) 

1069 ) 

1070 

1071 event_ids.append(res.event_id) 

1072 

1073 # Approve all scheduled occurrences 

1074 for eid in event_ids[1:]: 

1075 moderator.approve_event_occurrence(eid) 

1076 

1077 updated_event_id = event_ids[3] 

1078 

1079 time_before_update = now() 

1080 

1081 with events_session(token1) as api: 

1082 res = api.UpdateEvent( 

1083 events_pb2.UpdateEventReq( 

1084 event_id=updated_event_id, 

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

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

1087 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"), 

1088 update_all_future=True, 

1089 ) 

1090 ) 

1091 

1092 time_after_update = now() 

1093 

1094 with events_session(token2) as api: 

1095 for i in range(3): 

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

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

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

1099 

1100 for i in range(3, 6): 

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

1102 assert res.content == "New content." 

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

1104 

1105 

1106def test_GetEvent(db, moderator: Moderator): 

1107 # event creator 

1108 user1, token1 = generate_user() 

1109 # community moderator 

1110 user2, token2 = generate_user() 

1111 # third parties 

1112 user3, token3 = generate_user() 

1113 user4, token4 = generate_user() 

1114 user5, token5 = generate_user() 

1115 user6, token6 = generate_user() 

1116 

1117 with session_scope() as session: 

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

1119 

1120 time_before = now() 

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

1122 end_time = start_time + timedelta(hours=3) 

1123 

1124 with events_session(token1) as api: 

1125 # in person event 

1126 res = api.CreateEvent( 

1127 events_pb2.CreateEventReq( 

1128 title="Dummy Title", 

1129 content="Dummy content.", 

1130 offline_information=events_pb2.OfflineEventInformation( 

1131 address="Near Null Island", 

1132 lat=0.1, 

1133 lng=0.2, 

1134 ), 

1135 start_time=Timestamp_from_datetime(start_time), 

1136 end_time=Timestamp_from_datetime(end_time), 

1137 timezone="UTC", 

1138 ) 

1139 ) 

1140 

1141 event_id = res.event_id 

1142 

1143 moderator.approve_event_occurrence(event_id) 

1144 

1145 with events_session(token4) as api: 

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

1147 

1148 with events_session(token5) 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 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1155 api.SetEventAttendance( 

1156 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE) 

1157 ) 

1158 

1159 with events_session(token1) as api: 

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

1161 

1162 assert res.is_next 

1163 assert res.title == "Dummy Title" 

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

1165 assert res.content == "Dummy content." 

1166 assert not res.photo_url 

1167 assert res.WhichOneof("mode") == "offline_information" 

1168 assert res.offline_information.lat == 0.1 

1169 assert res.offline_information.lng == 0.2 

1170 assert res.offline_information.address == "Near Null Island" 

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

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

1173 assert res.creator_user_id == user1.id 

1174 assert to_aware_datetime(res.start_time) == start_time 

1175 assert to_aware_datetime(res.end_time) == end_time 

1176 # assert res.timezone == "UTC" 

1177 assert res.start_time_display 

1178 assert res.end_time_display 

1179 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

1180 assert res.organizer 

1181 assert res.subscriber 

1182 assert res.going_count == 2 

1183 assert res.maybe_count == 1 

1184 assert res.organizer_count == 1 

1185 assert res.subscriber_count == 3 

1186 assert res.owner_user_id == user1.id 

1187 assert not res.owner_community_id 

1188 assert not res.owner_group_id 

1189 assert res.thread.thread_id 

1190 assert res.can_edit 

1191 assert not res.can_moderate 

1192 

1193 with events_session(token2) as api: 

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

1195 

1196 assert res.is_next 

1197 assert res.title == "Dummy Title" 

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

1199 assert res.content == "Dummy content." 

1200 assert not res.photo_url 

1201 assert res.WhichOneof("mode") == "offline_information" 

1202 assert res.offline_information.lat == 0.1 

1203 assert res.offline_information.lng == 0.2 

1204 assert res.offline_information.address == "Near Null Island" 

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

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

1207 assert res.creator_user_id == user1.id 

1208 assert to_aware_datetime(res.start_time) == start_time 

1209 assert to_aware_datetime(res.end_time) == end_time 

1210 # assert res.timezone == "UTC" 

1211 assert res.start_time_display 

1212 assert res.end_time_display 

1213 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1214 assert not res.organizer 

1215 assert not res.subscriber 

1216 assert res.going_count == 2 

1217 assert res.maybe_count == 1 

1218 assert res.organizer_count == 1 

1219 assert res.subscriber_count == 3 

1220 assert res.owner_user_id == user1.id 

1221 assert not res.owner_community_id 

1222 assert not res.owner_group_id 

1223 assert res.thread.thread_id 

1224 assert res.can_edit 

1225 assert res.can_moderate 

1226 

1227 with events_session(token3) as api: 

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

1229 

1230 assert res.is_next 

1231 assert res.title == "Dummy Title" 

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

1233 assert res.content == "Dummy content." 

1234 assert not res.photo_url 

1235 assert res.WhichOneof("mode") == "offline_information" 

1236 assert res.offline_information.lat == 0.1 

1237 assert res.offline_information.lng == 0.2 

1238 assert res.offline_information.address == "Near Null Island" 

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

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

1241 assert res.creator_user_id == user1.id 

1242 assert to_aware_datetime(res.start_time) == start_time 

1243 assert to_aware_datetime(res.end_time) == end_time 

1244 # assert res.timezone == "UTC" 

1245 assert res.start_time_display 

1246 assert res.end_time_display 

1247 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1248 assert not res.organizer 

1249 assert not res.subscriber 

1250 assert res.going_count == 2 

1251 assert res.maybe_count == 1 

1252 assert res.organizer_count == 1 

1253 assert res.subscriber_count == 3 

1254 assert res.owner_user_id == user1.id 

1255 assert not res.owner_community_id 

1256 assert not res.owner_group_id 

1257 assert res.thread.thread_id 

1258 assert not res.can_edit 

1259 assert not res.can_moderate 

1260 

1261 

1262def test_CancelEvent(db, moderator: Moderator): 

1263 # event creator 

1264 user1, token1 = generate_user() 

1265 # community moderator 

1266 user2, token2 = generate_user() 

1267 # third parties 

1268 user3, token3 = generate_user() 

1269 user4, token4 = generate_user() 

1270 user5, token5 = generate_user() 

1271 user6, token6 = generate_user() 

1272 

1273 with session_scope() as session: 

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

1275 

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

1277 end_time = start_time + timedelta(hours=3) 

1278 

1279 with events_session(token1) as api: 

1280 res = api.CreateEvent( 

1281 events_pb2.CreateEventReq( 

1282 title="Dummy Title", 

1283 content="Dummy content.", 

1284 offline_information=events_pb2.OfflineEventInformation( 

1285 address="Near Null Island", 

1286 lat=0.1, 

1287 lng=0.2, 

1288 ), 

1289 start_time=Timestamp_from_datetime(start_time), 

1290 end_time=Timestamp_from_datetime(end_time), 

1291 timezone="UTC", 

1292 ) 

1293 ) 

1294 

1295 event_id = res.event_id 

1296 

1297 moderator.approve_event_occurrence(event_id) 

1298 

1299 with events_session(token4) as api: 

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

1301 

1302 with events_session(token5) as api: 

1303 api.SetEventAttendance( 

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

1305 ) 

1306 

1307 with events_session(token6) as api: 

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

1309 api.SetEventAttendance( 

1310 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE) 

1311 ) 

1312 

1313 with events_session(token1) as api: 

1314 res = api.CancelEvent( 

1315 events_pb2.CancelEventReq( 

1316 event_id=event_id, 

1317 ) 

1318 ) 

1319 

1320 with events_session(token1) as api: 

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

1322 assert res.is_cancelled 

1323 

1324 with events_session(token1) as api: 

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

1326 api.UpdateEvent( 

1327 events_pb2.UpdateEventReq( 

1328 event_id=event_id, 

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

1330 ) 

1331 ) 

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

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

1334 

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

1336 api.InviteEventOrganizer( 

1337 events_pb2.InviteEventOrganizerReq( 

1338 event_id=event_id, 

1339 user_id=user3.id, 

1340 ) 

1341 ) 

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

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

1344 

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

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

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

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

1349 

1350 with events_session(token3) as api: 

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

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

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

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

1355 

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

1357 api.SetEventAttendance( 

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

1359 ) 

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

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

1362 

1363 with events_session(token1) as api: 

1364 for include_cancelled in [True, False]: 

1365 res = api.ListEventOccurrences( 

1366 events_pb2.ListEventOccurrencesReq( 

1367 event_id=event_id, 

1368 include_cancelled=include_cancelled, 

1369 ) 

1370 ) 

1371 if include_cancelled: 

1372 assert len(res.events) > 0 

1373 else: 

1374 assert len(res.events) == 0 

1375 

1376 res = api.ListMyEvents( 

1377 events_pb2.ListMyEventsReq( 

1378 include_cancelled=include_cancelled, 

1379 ) 

1380 ) 

1381 if include_cancelled: 

1382 assert len(res.events) > 0 

1383 else: 

1384 assert len(res.events) == 0 

1385 

1386 

1387def test_ListEventAttendees(db, moderator: Moderator): 

1388 # event creator 

1389 user1, token1 = generate_user() 

1390 # others 

1391 user2, token2 = generate_user() 

1392 user3, token3 = generate_user() 

1393 user4, token4 = generate_user() 

1394 user5, token5 = generate_user() 

1395 user6, token6 = generate_user() 

1396 

1397 with session_scope() as session: 

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

1399 

1400 with events_session(token1) as api: 

1401 event_id = api.CreateEvent( 

1402 events_pb2.CreateEventReq( 

1403 title="Dummy Title", 

1404 content="Dummy content.", 

1405 offline_information=events_pb2.OfflineEventInformation( 

1406 address="Near Null Island", 

1407 lat=0.1, 

1408 lng=0.2, 

1409 ), 

1410 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1411 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1412 timezone="UTC", 

1413 ) 

1414 ).event_id 

1415 

1416 moderator.approve_event_occurrence(event_id) 

1417 

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

1419 with events_session(token) as api: 

1420 api.SetEventAttendance( 

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

1422 ) 

1423 

1424 with events_session(token6) as api: 

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

1426 

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

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

1429 

1430 res = api.ListEventAttendees( 

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

1432 ) 

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

1434 

1435 res = api.ListEventAttendees( 

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

1437 ) 

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

1439 assert not res.next_page_token 

1440 

1441 

1442def test_ListEventSubscribers(db, moderator: Moderator): 

1443 # event creator 

1444 user1, token1 = generate_user() 

1445 # others 

1446 user2, token2 = generate_user() 

1447 user3, token3 = generate_user() 

1448 user4, token4 = generate_user() 

1449 user5, token5 = generate_user() 

1450 user6, token6 = generate_user() 

1451 

1452 with session_scope() as session: 

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

1454 

1455 with events_session(token1) as api: 

1456 event_id = api.CreateEvent( 

1457 events_pb2.CreateEventReq( 

1458 title="Dummy Title", 

1459 content="Dummy content.", 

1460 offline_information=events_pb2.OfflineEventInformation( 

1461 address="Near Null Island", 

1462 lat=0.1, 

1463 lng=0.2, 

1464 ), 

1465 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1466 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1467 timezone="UTC", 

1468 ) 

1469 ).event_id 

1470 

1471 moderator.approve_event_occurrence(event_id) 

1472 

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

1474 with events_session(token) as api: 

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

1476 

1477 with events_session(token6) as api: 

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

1479 

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

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

1482 

1483 res = api.ListEventSubscribers( 

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

1485 ) 

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

1487 

1488 res = api.ListEventSubscribers( 

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

1490 ) 

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

1492 assert not res.next_page_token 

1493 

1494 

1495def test_ListEventOrganizers(db, moderator: Moderator): 

1496 # event creator 

1497 user1, token1 = generate_user() 

1498 # others 

1499 user2, token2 = generate_user() 

1500 user3, token3 = generate_user() 

1501 user4, token4 = generate_user() 

1502 user5, token5 = generate_user() 

1503 user6, token6 = generate_user() 

1504 

1505 with session_scope() as session: 

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

1507 

1508 with events_session(token1) as api: 

1509 event_id = api.CreateEvent( 

1510 events_pb2.CreateEventReq( 

1511 title="Dummy Title", 

1512 content="Dummy content.", 

1513 offline_information=events_pb2.OfflineEventInformation( 

1514 address="Near Null Island", 

1515 lat=0.1, 

1516 lng=0.2, 

1517 ), 

1518 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1519 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1520 timezone="UTC", 

1521 ) 

1522 ).event_id 

1523 

1524 moderator.approve_event_occurrence(event_id) 

1525 

1526 with events_session(token1) as api: 

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

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

1529 

1530 with events_session(token6) as api: 

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

1532 

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

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

1535 

1536 res = api.ListEventOrganizers( 

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

1538 ) 

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

1540 

1541 res = api.ListEventOrganizers( 

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

1543 ) 

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

1545 assert not res.next_page_token 

1546 

1547 

1548def test_TransferEvent(db): 

1549 user1, token1 = generate_user() 

1550 user2, token2 = generate_user() 

1551 user3, token3 = generate_user() 

1552 user4, token4 = generate_user() 

1553 

1554 with session_scope() as session: 

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

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

1557 c_id = c.id 

1558 h_id = h.id 

1559 

1560 with events_session(token1) as api: 

1561 event_id = api.CreateEvent( 

1562 events_pb2.CreateEventReq( 

1563 title="Dummy Title", 

1564 content="Dummy content.", 

1565 offline_information=events_pb2.OfflineEventInformation( 

1566 address="Near Null Island", 

1567 lat=0.1, 

1568 lng=0.2, 

1569 ), 

1570 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1571 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1572 timezone="UTC", 

1573 ) 

1574 ).event_id 

1575 

1576 api.TransferEvent( 

1577 events_pb2.TransferEventReq( 

1578 event_id=event_id, 

1579 new_owner_community_id=c_id, 

1580 ) 

1581 ) 

1582 

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

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

1585 

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

1587 api.TransferEvent( 

1588 events_pb2.TransferEventReq( 

1589 event_id=event_id, 

1590 new_owner_group_id=h_id, 

1591 ) 

1592 ) 

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

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

1595 

1596 event_id = api.CreateEvent( 

1597 events_pb2.CreateEventReq( 

1598 title="Dummy Title", 

1599 content="Dummy content.", 

1600 offline_information=events_pb2.OfflineEventInformation( 

1601 address="Near Null Island", 

1602 lat=0.1, 

1603 lng=0.2, 

1604 ), 

1605 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1606 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1607 timezone="UTC", 

1608 ) 

1609 ).event_id 

1610 

1611 api.TransferEvent( 

1612 events_pb2.TransferEventReq( 

1613 event_id=event_id, 

1614 new_owner_group_id=h_id, 

1615 ) 

1616 ) 

1617 

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

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

1620 

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

1622 api.TransferEvent( 

1623 events_pb2.TransferEventReq( 

1624 event_id=event_id, 

1625 new_owner_community_id=c_id, 

1626 ) 

1627 ) 

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

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

1630 

1631 

1632def test_SetEventSubscription(db, moderator: Moderator): 

1633 user1, token1 = generate_user() 

1634 user2, token2 = generate_user() 

1635 

1636 with session_scope() as session: 

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

1638 

1639 with events_session(token1) as api: 

1640 event_id = api.CreateEvent( 

1641 events_pb2.CreateEventReq( 

1642 title="Dummy Title", 

1643 content="Dummy content.", 

1644 offline_information=events_pb2.OfflineEventInformation( 

1645 address="Near Null Island", 

1646 lat=0.1, 

1647 lng=0.2, 

1648 ), 

1649 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1650 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1651 timezone="UTC", 

1652 ) 

1653 ).event_id 

1654 

1655 moderator.approve_event_occurrence(event_id) 

1656 

1657 with events_session(token2) as api: 

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

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

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

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

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

1663 

1664 

1665def test_SetEventAttendance(db, moderator: Moderator): 

1666 user1, token1 = generate_user() 

1667 user2, token2 = generate_user() 

1668 

1669 with session_scope() as session: 

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

1671 

1672 with events_session(token1) as api: 

1673 event_id = api.CreateEvent( 

1674 events_pb2.CreateEventReq( 

1675 title="Dummy Title", 

1676 content="Dummy content.", 

1677 offline_information=events_pb2.OfflineEventInformation( 

1678 address="Near Null Island", 

1679 lat=0.1, 

1680 lng=0.2, 

1681 ), 

1682 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1683 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1684 timezone="UTC", 

1685 ) 

1686 ).event_id 

1687 

1688 moderator.approve_event_occurrence(event_id) 

1689 

1690 with events_session(token2) as api: 

1691 assert ( 

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

1693 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1694 ) 

1695 api.SetEventAttendance( 

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

1697 ) 

1698 assert ( 

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

1700 == events_pb2.ATTENDANCE_STATE_GOING 

1701 ) 

1702 api.SetEventAttendance( 

1703 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE) 

1704 ) 

1705 assert ( 

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

1707 == events_pb2.ATTENDANCE_STATE_MAYBE 

1708 ) 

1709 api.SetEventAttendance( 

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

1711 ) 

1712 assert ( 

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

1714 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1715 ) 

1716 

1717 

1718def test_InviteEventOrganizer(db, moderator: Moderator): 

1719 user1, token1 = generate_user() 

1720 user2, token2 = generate_user() 

1721 

1722 with session_scope() as session: 

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

1724 

1725 with events_session(token1) as api: 

1726 event_id = api.CreateEvent( 

1727 events_pb2.CreateEventReq( 

1728 title="Dummy Title", 

1729 content="Dummy content.", 

1730 offline_information=events_pb2.OfflineEventInformation( 

1731 address="Near Null Island", 

1732 lat=0.1, 

1733 lng=0.2, 

1734 ), 

1735 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1736 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1737 timezone="UTC", 

1738 ) 

1739 ).event_id 

1740 

1741 moderator.approve_event_occurrence(event_id) 

1742 

1743 with events_session(token2) as api: 

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

1745 

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

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

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

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

1750 

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

1752 

1753 with events_session(token1) as api: 

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

1755 

1756 with events_session(token2) as api: 

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

1758 

1759 

1760def test_ListEventOccurrences(db): 

1761 user1, token1 = generate_user() 

1762 user2, token2 = generate_user() 

1763 user3, token3 = generate_user() 

1764 

1765 with session_scope() as session: 

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

1767 

1768 start = now() 

1769 

1770 event_ids = [] 

1771 

1772 with events_session(token1) as api: 

1773 res = api.CreateEvent( 

1774 events_pb2.CreateEventReq( 

1775 title="First occurrence", 

1776 content="Dummy content.", 

1777 parent_community_id=c_id, 

1778 online_information=events_pb2.OnlineEventInformation( 

1779 link="https://couchers.org/meet/", 

1780 ), 

1781 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

1782 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)), 

1783 timezone="UTC", 

1784 ) 

1785 ) 

1786 

1787 event_ids.append(res.event_id) 

1788 

1789 for i in range(5): 

1790 res = api.ScheduleEvent( 

1791 events_pb2.ScheduleEventReq( 

1792 event_id=event_ids[-1], 

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

1794 online_information=events_pb2.OnlineEventInformation( 

1795 link="https://couchers.org/meet/", 

1796 ), 

1797 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)), 

1798 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)), 

1799 timezone="UTC", 

1800 ) 

1801 ) 

1802 

1803 event_ids.append(res.event_id) 

1804 

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

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

1807 

1808 res = api.ListEventOccurrences( 

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

1810 ) 

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

1812 

1813 res = api.ListEventOccurrences( 

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

1815 ) 

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

1817 assert not res.next_page_token 

1818 

1819 

1820def test_ListMyEvents(db, moderator: Moderator): 

1821 user1, token1 = generate_user() 

1822 user2, token2 = generate_user() 

1823 user3, token3 = generate_user() 

1824 user4, token4 = generate_user() 

1825 user5, token5 = generate_user() 

1826 

1827 with session_scope() as session: 

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

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

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

1831 c_id = global_community.id 

1832 macroregion_community = create_community( 

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

1834 ) 

1835 region_community = create_community( 

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

1837 ) 

1838 subregion_community = create_community( 

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

1840 ) 

1841 c2_id = subregion_community.id 

1842 

1843 start = now() 

1844 

1845 def new_event(hours_from_now: int, community_id: int, online: bool = True) -> events_pb2.CreateEventReq: 

1846 if online: 

1847 return events_pb2.CreateEventReq( 

1848 title="Dummy Online Title", 

1849 content="Dummy content.", 

1850 online_information=events_pb2.OnlineEventInformation( 

1851 link="https://couchers.org/meet/", 

1852 ), 

1853 parent_community_id=community_id, 

1854 timezone="UTC", 

1855 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)), 

1856 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)), 

1857 ) 

1858 else: 

1859 return events_pb2.CreateEventReq( 

1860 title="Dummy Offline Title", 

1861 content="Dummy content.", 

1862 offline_information=events_pb2.OfflineEventInformation( 

1863 address="Near Null Island", 

1864 lat=0.1, 

1865 lng=0.2, 

1866 ), 

1867 parent_community_id=community_id, 

1868 timezone="UTC", 

1869 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)), 

1870 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)), 

1871 ) 

1872 

1873 with events_session(token1) as api: 

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

1875 

1876 moderator.approve_event_occurrence(e2) 

1877 

1878 with events_session(token2) as api: 

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

1880 

1881 moderator.approve_event_occurrence(e1) 

1882 

1883 with events_session(token1) as api: 

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

1885 

1886 moderator.approve_event_occurrence(e3) 

1887 

1888 with events_session(token2) as api: 

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

1890 

1891 moderator.approve_event_occurrence(e5) 

1892 

1893 with events_session(token3) as api: 

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

1895 

1896 moderator.approve_event_occurrence(e4) 

1897 

1898 with events_session(token4) as api: 

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

1900 

1901 moderator.approve_event_occurrence(e6) 

1902 

1903 with events_session(token1) as api: 

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

1905 

1906 with events_session(token1) as api: 

1907 api.SetEventAttendance( 

1908 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE) 

1909 ) 

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

1911 

1912 with events_session(token2) as api: 

1913 api.SetEventAttendance( 

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

1915 ) 

1916 

1917 with events_session(token3) as api: 

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

1919 

1920 with events_session(token1) as api: 

1921 # test pagination with token first 

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

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

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

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

1926 assert not res.next_page_token 

1927 

1928 res = api.ListMyEvents( 

1929 events_pb2.ListMyEventsReq( 

1930 subscribed=True, 

1931 attending=True, 

1932 organizing=True, 

1933 ) 

1934 ) 

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

1936 

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

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

1939 

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

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

1942 

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

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

1945 

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

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

1948 

1949 with events_session(token1) as api: 

1950 # Test pagination with page_number and verify total_items 

1951 res = api.ListMyEvents( 

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

1953 ) 

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

1955 assert res.total_items == 4 

1956 

1957 res = api.ListMyEvents( 

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

1959 ) 

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

1961 assert res.total_items == 4 

1962 

1963 # Verify no more pages 

1964 res = api.ListMyEvents( 

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

1966 ) 

1967 assert not res.events 

1968 assert res.total_items == 4 

1969 

1970 with events_session(token2) as api: 

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

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

1973 

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

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

1976 

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

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

1979 

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

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

1982 

1983 with events_session(token3) as api: 

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

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

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

1987 

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

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

1990 

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

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

1993 

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

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

1996 

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

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

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

2000 

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

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

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

2004 

2005 # my_communities_exclude_global works independently of my_communities flag 

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

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

2008 

2009 # my_communities_exclude_global filters organizing results too 

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

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

2012 

2013 # my_communities_exclude_global filters subscribed results too 

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

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

2016 

2017 with events_session(token5) as api: 

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

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

2020 

2021 

2022def test_RemoveEventOrganizer(db, moderator: Moderator): 

2023 user1, token1 = generate_user() 

2024 user2, token2 = generate_user() 

2025 

2026 with session_scope() as session: 

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

2028 

2029 with events_session(token1) as api: 

2030 event_id = api.CreateEvent( 

2031 events_pb2.CreateEventReq( 

2032 title="Dummy Title", 

2033 content="Dummy content.", 

2034 offline_information=events_pb2.OfflineEventInformation( 

2035 address="Near Null Island", 

2036 lat=0.1, 

2037 lng=0.2, 

2038 ), 

2039 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

2040 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

2041 timezone="UTC", 

2042 ) 

2043 ).event_id 

2044 

2045 moderator.approve_event_occurrence(event_id) 

2046 

2047 with events_session(token2) as api: 

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

2049 

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

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

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

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

2054 

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

2056 

2057 with events_session(token1) as api: 

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

2059 

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

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

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

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

2064 

2065 with events_session(token2) as api: 

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

2067 assert res.organizer 

2068 assert res.organizer_count == 2 

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

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

2071 

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

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

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

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

2076 

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

2078 assert not res.organizer 

2079 assert res.organizer_count == 1 

2080 

2081 # Test that event owner can remove co-organizers 

2082 with events_session(token1) as api: 

2083 # Add user2 back as organizer 

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

2085 

2086 # Verify user2 is now an organizer 

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

2088 assert res.organizer_count == 2 

2089 

2090 # Event owner can remove co-organizer 

2091 api.RemoveEventOrganizer( 

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

2093 ) 

2094 

2095 # Verify user2 is no longer an organizer 

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

2097 assert res.organizer_count == 1 

2098 

2099 # Test that non-organizers cannot remove other organizers 

2100 with events_session(token2) as api: 

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

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

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

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

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

2106 

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

2108 with events_session(token1) as api: 

2109 # Add user2 back as organizer 

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

2111 

2112 

2113def test_ListEventAttendees_regression(db): 

2114 # see issue #1617: 

2115 # 

2116 # 1. Create an event 

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

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

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

2120 # 

2121 # **Expected behaviour** 

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

2123 # 

2124 # **Actual/current behaviour** 

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

2126 

2127 user1, token1 = generate_user() 

2128 user2, token2 = generate_user() 

2129 user3, token3 = generate_user() 

2130 user4, token4 = generate_user() 

2131 user5, token5 = generate_user() 

2132 

2133 with session_scope() as session: 

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

2135 

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

2137 end_time = start_time + timedelta(hours=3) 

2138 

2139 with events_session(token1) as api: 

2140 res = api.CreateEvent( 

2141 events_pb2.CreateEventReq( 

2142 title="Dummy Title", 

2143 content="Dummy content.", 

2144 online_information=events_pb2.OnlineEventInformation( 

2145 link="https://couchers.org", 

2146 ), 

2147 parent_community_id=c_id, 

2148 start_time=Timestamp_from_datetime(start_time), 

2149 end_time=Timestamp_from_datetime(end_time), 

2150 timezone="UTC", 

2151 ) 

2152 ) 

2153 

2154 res = api.TransferEvent( 

2155 events_pb2.TransferEventReq( 

2156 event_id=res.event_id, 

2157 new_owner_community_id=c_id, 

2158 ) 

2159 ) 

2160 

2161 event_id = res.event_id 

2162 

2163 api.SetEventAttendance( 

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

2165 ) 

2166 api.SetEventAttendance( 

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

2168 ) 

2169 

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

2171 assert len(res.attendee_user_ids) == 1 

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

2173 

2174 

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

2176 user1, token1 = generate_user() 

2177 user2, token2 = generate_user() 

2178 user3, token3 = generate_user() 

2179 user4, token4 = generate_user() 

2180 

2181 with session_scope() as session: 

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

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

2184 c_id = c.id 

2185 h_id = h.id 

2186 user4_id = user4.id 

2187 

2188 with events_session(token1) as api: 

2189 event = api.CreateEvent( 

2190 events_pb2.CreateEventReq( 

2191 title="Dummy Title", 

2192 content="Dummy content.", 

2193 offline_information=events_pb2.OfflineEventInformation( 

2194 address="Near Null Island", 

2195 lat=0.1, 

2196 lng=0.2, 

2197 ), 

2198 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

2199 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

2200 timezone="UTC", 

2201 ) 

2202 ) 

2203 

2204 moderator.approve_event_occurrence(event.event_id) 

2205 

2206 with threads_session(token2) as api: 

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

2208 

2209 with events_session(token3) as api: 

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

2211 assert res.thread.num_responses == 1 

2212 

2213 with threads_session(token3) as api: 

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

2215 assert len(ret.replies) == 1 

2216 assert not ret.next_page_token 

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

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

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

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

2221 

2222 api.PostReply(threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")) 

2223 

2224 process_jobs() 

2225 

2226 assert push_collector.pop_for_user(user1.id, last=True).content.title == f"{user2.name} • Dummy Title" 

2227 assert push_collector.pop_for_user(user2.id, last=True).content.title == f"{user3.name} • Dummy Title" 

2228 assert push_collector.count_for_user(user4_id) == 0 

2229 

2230 

2231def test_can_overlap_other_events_schedule_regression(db): 

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

2233 user, token = generate_user() 

2234 

2235 with session_scope() as session: 

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

2237 

2238 start = now() 

2239 

2240 with events_session(token) as api: 

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

2242 api.CreateEvent( 

2243 events_pb2.CreateEventReq( 

2244 title="Dummy Title", 

2245 content="Dummy content.", 

2246 parent_community_id=c_id, 

2247 online_information=events_pb2.OnlineEventInformation( 

2248 link="https://couchers.org/meet/", 

2249 ), 

2250 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2251 end_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

2252 timezone="UTC", 

2253 ) 

2254 ) 

2255 

2256 # this event 

2257 res = api.CreateEvent( 

2258 events_pb2.CreateEventReq( 

2259 title="Dummy Title", 

2260 content="Dummy content.", 

2261 parent_community_id=c_id, 

2262 online_information=events_pb2.OnlineEventInformation( 

2263 link="https://couchers.org/meet/", 

2264 ), 

2265 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2266 end_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

2267 timezone="UTC", 

2268 ) 

2269 ) 

2270 

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

2272 api.ScheduleEvent( 

2273 events_pb2.ScheduleEventReq( 

2274 event_id=res.event_id, 

2275 content="New event occurrence", 

2276 offline_information=events_pb2.OfflineEventInformation( 

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

2278 lat=0.3, 

2279 lng=0.2, 

2280 ), 

2281 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2282 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2283 timezone="UTC", 

2284 ) 

2285 ) 

2286 

2287 

2288def test_can_overlap_other_events_update_regression(db): 

2289 user, token = generate_user() 

2290 

2291 with session_scope() as session: 

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

2293 

2294 start = now() 

2295 

2296 with events_session(token) as api: 

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

2298 api.CreateEvent( 

2299 events_pb2.CreateEventReq( 

2300 title="Dummy Title", 

2301 content="Dummy content.", 

2302 parent_community_id=c_id, 

2303 online_information=events_pb2.OnlineEventInformation( 

2304 link="https://couchers.org/meet/", 

2305 ), 

2306 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2307 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2308 timezone="UTC", 

2309 ) 

2310 ) 

2311 

2312 res = api.CreateEvent( 

2313 events_pb2.CreateEventReq( 

2314 title="Dummy Title", 

2315 content="Dummy content.", 

2316 parent_community_id=c_id, 

2317 online_information=events_pb2.OnlineEventInformation( 

2318 link="https://couchers.org/meet/", 

2319 ), 

2320 start_time=Timestamp_from_datetime(start + timedelta(hours=7)), 

2321 end_time=Timestamp_from_datetime(start + timedelta(hours=8)), 

2322 timezone="UTC", 

2323 ) 

2324 ) 

2325 

2326 event_id = api.ScheduleEvent( 

2327 events_pb2.ScheduleEventReq( 

2328 event_id=res.event_id, 

2329 content="New event occurrence", 

2330 offline_information=events_pb2.OfflineEventInformation( 

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

2332 lat=0.3, 

2333 lng=0.2, 

2334 ), 

2335 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2336 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2337 timezone="UTC", 

2338 ) 

2339 ).event_id 

2340 

2341 # can overlap with this current existing occurrence 

2342 api.UpdateEvent( 

2343 events_pb2.UpdateEventReq( 

2344 event_id=event_id, 

2345 start_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

2346 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2347 ) 

2348 ) 

2349 

2350 api.UpdateEvent( 

2351 events_pb2.UpdateEventReq( 

2352 event_id=event_id, 

2353 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

2354 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2355 ) 

2356 ) 

2357 

2358 

2359def test_list_past_events_regression(db): 

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

2361 user, token = generate_user() 

2362 

2363 with session_scope() as session: 

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

2365 

2366 start = now() 

2367 

2368 with events_session(token) as api: 

2369 api.CreateEvent( 

2370 events_pb2.CreateEventReq( 

2371 title="Dummy Title", 

2372 content="Dummy content.", 

2373 parent_community_id=c_id, 

2374 online_information=events_pb2.OnlineEventInformation( 

2375 link="https://couchers.org/meet/", 

2376 ), 

2377 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2378 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2379 timezone="UTC", 

2380 ) 

2381 ) 

2382 

2383 with session_scope() as session: 

2384 session.execute( 

2385 update(EventOccurrence).values( 

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

2387 ) 

2388 ) 

2389 

2390 with events_session(token) as api: 

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

2392 assert len(res.events) == 1 

2393 

2394 

2395def test_community_invite_requests(db, moderator: Moderator): 

2396 user1, token1 = generate_user(complete_profile=True) 

2397 user2, token2 = generate_user() 

2398 user3, token3 = generate_user() 

2399 user4, token4 = generate_user() 

2400 user5, token5 = generate_user(is_superuser=True) 

2401 

2402 with session_scope() as session: 

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

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

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

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

2407 

2408 enforce_community_memberships() 

2409 

2410 with events_session(token1) as api: 

2411 res = api.CreateEvent( 

2412 events_pb2.CreateEventReq( 

2413 title="Dummy Title", 

2414 content="Dummy content.", 

2415 parent_community_id=c_id, 

2416 online_information=events_pb2.OnlineEventInformation( 

2417 link="https://couchers.org/meet/", 

2418 ), 

2419 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)), 

2420 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)), 

2421 timezone="UTC", 

2422 ) 

2423 ) 

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

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

2426 

2427 event_id = res.event_id 

2428 

2429 moderator.approve_event_occurrence(event_id) 

2430 

2431 with events_session(token1) as api: 

2432 with mock_notification_email() as mock: 

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

2434 assert mock.call_count == 1 

2435 e = email_fields(mock) 

2436 assert e.recipient == "mods@couchers.org.invalid" 

2437 

2438 assert user_url in e.plain 

2439 assert event_url in e.plain 

2440 

2441 # can't send another req 

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

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

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

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

2446 

2447 # another user can send one though 

2448 with events_session(token3) as api: 

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

2450 

2451 # but not a non-admin 

2452 with events_session(token2) as api: 

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

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

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

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

2457 

2458 with real_editor_session(token5) as editor: 

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

2460 assert len(res.requests) == 2 

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

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

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

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

2465 

2466 editor.DecideEventCommunityInviteRequest( 

2467 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2469 approve=False, 

2470 ) 

2471 ) 

2472 

2473 editor.DecideEventCommunityInviteRequest( 

2474 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2476 approve=True, 

2477 ) 

2478 ) 

2479 

2480 # not after approve 

2481 with events_session(token4) as api: 

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

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

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

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

2486 

2487 

2488def test_update_event_should_notify_queues_job(): 

2489 user, token = generate_user() 

2490 start = now() 

2491 

2492 with session_scope() as session: 

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

2494 

2495 # create an event 

2496 with events_session(token) as api: 

2497 create_res = api.CreateEvent( 

2498 events_pb2.CreateEventReq( 

2499 title="Dummy Title", 

2500 content="Dummy content.", 

2501 parent_community_id=c_id, 

2502 offline_information=events_pb2.OfflineEventInformation( 

2503 address="https://couchers.org/meet/", 

2504 lat=1.0, 

2505 lng=2.0, 

2506 ), 

2507 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2508 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2509 timezone="UTC", 

2510 ) 

2511 ) 

2512 

2513 event_id = create_res.event_id 

2514 

2515 # measure initial background job queue length 

2516 with session_scope() as session: 

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

2518 job_length_before_update = len(jobs) 

2519 

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

2521 api.UpdateEvent( 

2522 events_pb2.UpdateEventReq( 

2523 event_id=event_id, 

2524 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2525 should_notify=False, 

2526 ) 

2527 ) 

2528 

2529 with session_scope() as session: 

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

2531 assert len(jobs) == job_length_before_update 

2532 

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

2534 api.UpdateEvent( 

2535 events_pb2.UpdateEventReq( 

2536 event_id=event_id, 

2537 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2538 should_notify=True, 

2539 ) 

2540 ) 

2541 

2542 with session_scope() as session: 

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

2544 assert len(jobs) == job_length_before_update + 1 

2545 

2546 

2547def test_event_photo_key(db): 

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

2549 user, token = generate_user() 

2550 

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

2552 end_time = start_time + timedelta(hours=3) 

2553 

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

2555 with session_scope() as session: 

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

2557 upload = Upload( 

2558 key="test_event_photo_key_123", 

2559 filename="test_event_photo_key_123.jpg", 

2560 creator_user_id=user.id, 

2561 ) 

2562 session.add(upload) 

2563 

2564 with events_session(token) as api: 

2565 # Create event without photo 

2566 res = api.CreateEvent( 

2567 events_pb2.CreateEventReq( 

2568 title="Event Without Photo", 

2569 content="No photo content.", 

2570 photo_key=None, 

2571 offline_information=events_pb2.OfflineEventInformation( 

2572 address="Near Null Island", 

2573 lat=0.1, 

2574 lng=0.2, 

2575 ), 

2576 start_time=Timestamp_from_datetime(start_time), 

2577 end_time=Timestamp_from_datetime(end_time), 

2578 timezone="UTC", 

2579 ) 

2580 ) 

2581 

2582 assert res.photo_key == "" 

2583 assert res.photo_url == "" 

2584 

2585 # Create event with photo 

2586 res_with_photo = api.CreateEvent( 

2587 events_pb2.CreateEventReq( 

2588 title="Event With Photo", 

2589 content="Has photo content.", 

2590 photo_key="test_event_photo_key_123", 

2591 offline_information=events_pb2.OfflineEventInformation( 

2592 address="Near Null Island", 

2593 lat=0.1, 

2594 lng=0.2, 

2595 ), 

2596 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)), 

2597 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)), 

2598 timezone="UTC", 

2599 ) 

2600 ) 

2601 

2602 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2603 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2604 

2605 event_id = res_with_photo.event_id 

2606 

2607 # Verify photo_key is returned when getting the event 

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

2609 assert get_res.photo_key == "test_event_photo_key_123" 

2610 assert "test_event_photo_key_123" in get_res.photo_url 

2611 

2612 

2613def test_event_created_with_shadowed_visibility(db): 

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

2615 user, token = generate_user() 

2616 

2617 with session_scope() as session: 

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

2619 

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

2621 end_time = start_time + timedelta(hours=3) 

2622 

2623 with events_session(token) as api: 

2624 res = api.CreateEvent( 

2625 events_pb2.CreateEventReq( 

2626 title="Test UMS Event", 

2627 content="UMS content.", 

2628 offline_information=events_pb2.OfflineEventInformation( 

2629 address="Near Null Island", 

2630 lat=0.1, 

2631 lng=0.2, 

2632 ), 

2633 start_time=Timestamp_from_datetime(start_time), 

2634 end_time=Timestamp_from_datetime(end_time), 

2635 timezone="UTC", 

2636 ) 

2637 ) 

2638 event_id = res.event_id 

2639 

2640 with session_scope() as session: 

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

2642 mod_state = session.execute( 

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

2644 ).scalar_one() 

2645 assert mod_state.visibility == ModerationVisibility.shadowed 

2646 

2647 

2648def test_shadowed_event_visible_to_creator_only(db): 

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

2650 user1, token1 = generate_user() 

2651 user2, token2 = generate_user() 

2652 

2653 with session_scope() as session: 

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

2655 

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

2657 end_time = start_time + timedelta(hours=3) 

2658 

2659 with events_session(token1) as api: 

2660 res = api.CreateEvent( 

2661 events_pb2.CreateEventReq( 

2662 title="Shadowed Event", 

2663 content="Content.", 

2664 offline_information=events_pb2.OfflineEventInformation( 

2665 address="Near Null Island", 

2666 lat=0.1, 

2667 lng=0.2, 

2668 ), 

2669 start_time=Timestamp_from_datetime(start_time), 

2670 end_time=Timestamp_from_datetime(end_time), 

2671 timezone="UTC", 

2672 ) 

2673 ) 

2674 event_id = res.event_id 

2675 

2676 # Creator can see it 

2677 with events_session(token1) as api: 

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

2679 assert res.title == "Shadowed Event" 

2680 

2681 # Other user cannot 

2682 with events_session(token2) as api: 

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

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

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

2686 

2687 

2688def test_event_visible_after_approval(db, moderator: Moderator): 

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

2690 user1, token1 = generate_user() 

2691 user2, token2 = generate_user() 

2692 

2693 with session_scope() as session: 

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

2695 

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

2697 end_time = start_time + timedelta(hours=3) 

2698 

2699 with events_session(token1) as api: 

2700 res = api.CreateEvent( 

2701 events_pb2.CreateEventReq( 

2702 title="Approved Event", 

2703 content="Content.", 

2704 offline_information=events_pb2.OfflineEventInformation( 

2705 address="Near Null Island", 

2706 lat=0.1, 

2707 lng=0.2, 

2708 ), 

2709 start_time=Timestamp_from_datetime(start_time), 

2710 end_time=Timestamp_from_datetime(end_time), 

2711 timezone="UTC", 

2712 ) 

2713 ) 

2714 event_id = res.event_id 

2715 

2716 # Other user cannot see it yet 

2717 with events_session(token2) as api: 

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

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

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

2721 

2722 # Approve the event 

2723 moderator.approve_event_occurrence(event_id) 

2724 

2725 # Now other user can see it 

2726 with events_session(token2) as api: 

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

2728 assert res.title == "Approved Event" 

2729 

2730 

2731def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2733 user1, token1 = generate_user() 

2734 user2, token2 = generate_user() 

2735 

2736 with session_scope() as session: 

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

2738 

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

2740 end_time = start_time + timedelta(hours=3) 

2741 

2742 with events_session(token1) as api: 

2743 res = api.CreateEvent( 

2744 events_pb2.CreateEventReq( 

2745 title="List Test Event", 

2746 content="Content.", 

2747 offline_information=events_pb2.OfflineEventInformation( 

2748 address="Near Null Island", 

2749 lat=0.1, 

2750 lng=0.2, 

2751 ), 

2752 start_time=Timestamp_from_datetime(start_time), 

2753 end_time=Timestamp_from_datetime(end_time), 

2754 timezone="UTC", 

2755 ) 

2756 ) 

2757 event_id = res.event_id 

2758 

2759 # Creator can see their own SHADOWED event in lists 

2760 with events_session(token1) as api: 

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

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

2763 assert event_id in event_ids 

2764 

2765 # Other user cannot see the SHADOWED event in lists 

2766 with events_session(token2) as api: 

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

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

2769 assert event_id not in event_ids 

2770 

2771 # After approval, other user can see it 

2772 moderator.approve_event_occurrence(event_id) 

2773 

2774 with events_session(token2) as api: 

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

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

2777 assert event_id in event_ids 

2778 

2779 

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

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

2782 user1, token1 = generate_user() 

2783 user2, token2 = generate_user() 

2784 

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

2786 with session_scope() as session: 

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

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

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

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

2791 

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

2793 end_time = start_time + timedelta(hours=3) 

2794 

2795 with events_session(token1) as api: 

2796 res = api.CreateEvent( 

2797 events_pb2.CreateEventReq( 

2798 title="Deferred Event", 

2799 content="Content.", 

2800 offline_information=events_pb2.OfflineEventInformation( 

2801 address="Near Null Island", 

2802 lat=0.1, 

2803 lng=0.2, 

2804 ), 

2805 start_time=Timestamp_from_datetime(start_time), 

2806 end_time=Timestamp_from_datetime(end_time), 

2807 timezone="UTC", 

2808 ) 

2809 ) 

2810 event_id = res.event_id 

2811 

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

2813 process_jobs() 

2814 

2815 with session_scope() as session: 

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

2817 # Notification was created with moderation_state_id for deferral 

2818 assert notif.moderation_state_id is not None 

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

2820 delivery_count = session.execute( 

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

2822 ).scalar_one_or_none() 

2823 assert delivery_count is None 

2824 

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

2826 moderator.approve_event_occurrence(event_id) 

2827 

2828 # Verify handle_notification job was queued 

2829 with session_scope() as session: 

2830 pending_jobs = ( 

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

2832 .scalars() 

2833 .all() 

2834 ) 

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

2836 

2837 

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

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

2840 user1, token1 = generate_user() 

2841 user2, token2 = generate_user() 

2842 

2843 with session_scope() as session: 

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

2845 

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

2847 end_time = start_time + timedelta(hours=3) 

2848 

2849 with events_session(token1) as api: 

2850 res = api.CreateEvent( 

2851 events_pb2.CreateEventReq( 

2852 title="Update Test", 

2853 content="Content.", 

2854 offline_information=events_pb2.OfflineEventInformation( 

2855 address="Near Null Island", 

2856 lat=0.1, 

2857 lng=0.2, 

2858 ), 

2859 start_time=Timestamp_from_datetime(start_time), 

2860 end_time=Timestamp_from_datetime(end_time), 

2861 timezone="UTC", 

2862 ) 

2863 ) 

2864 event_id = res.event_id 

2865 

2866 moderator.approve_event_occurrence(event_id) 

2867 process_jobs() 

2868 # Clear any create notifications 

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

2870 push_collector.pop_for_user(user2.id) 

2871 

2872 # User2 subscribes to the event 

2873 with events_session(token2) as api: 

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

2875 

2876 # User1 updates the event with should_notify=True 

2877 with events_session(token1) as api: 

2878 api.UpdateEvent( 

2879 events_pb2.UpdateEventReq( 

2880 event_id=event_id, 

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

2882 should_notify=True, 

2883 ) 

2884 ) 

2885 

2886 process_jobs() 

2887 

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

2889 with session_scope() as session: 

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

2891 

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

2893 # Find the update notification (most recent one) 

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

2895 assert len(update_notifs) == 1 

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

2897 

2898 

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

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

2901 user1, token1 = generate_user() 

2902 user2, token2 = generate_user() 

2903 

2904 with session_scope() as session: 

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

2906 

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

2908 end_time = start_time + timedelta(hours=3) 

2909 

2910 with events_session(token1) as api: 

2911 res = api.CreateEvent( 

2912 events_pb2.CreateEventReq( 

2913 title="Cancel Test", 

2914 content="Content.", 

2915 offline_information=events_pb2.OfflineEventInformation( 

2916 address="Near Null Island", 

2917 lat=0.1, 

2918 lng=0.2, 

2919 ), 

2920 start_time=Timestamp_from_datetime(start_time), 

2921 end_time=Timestamp_from_datetime(end_time), 

2922 timezone="UTC", 

2923 ) 

2924 ) 

2925 event_id = res.event_id 

2926 

2927 moderator.approve_event_occurrence(event_id) 

2928 process_jobs() 

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

2930 push_collector.pop_for_user(user2.id) 

2931 

2932 # User2 subscribes 

2933 with events_session(token2) as api: 

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

2935 

2936 # User1 cancels the event 

2937 with events_session(token1) as api: 

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

2939 

2940 process_jobs() 

2941 

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

2943 with session_scope() as session: 

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

2945 

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

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

2948 assert len(cancel_notifs) == 1 

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

2950 

2951 

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

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

2954 user1, token1 = generate_user() 

2955 user2, token2 = generate_user() 

2956 

2957 with session_scope() as session: 

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

2959 

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

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

2962 end_time = start_time + timedelta(hours=1) 

2963 

2964 with events_session(token1) as api: 

2965 res = api.CreateEvent( 

2966 events_pb2.CreateEventReq( 

2967 title="Reminder Test", 

2968 content="Content.", 

2969 offline_information=events_pb2.OfflineEventInformation( 

2970 address="Near Null Island", 

2971 lat=0.1, 

2972 lng=0.2, 

2973 ), 

2974 start_time=Timestamp_from_datetime(start_time), 

2975 end_time=Timestamp_from_datetime(end_time), 

2976 timezone="UTC", 

2977 ) 

2978 ) 

2979 event_id = res.event_id 

2980 

2981 moderator.approve_event_occurrence(event_id) 

2982 process_jobs() 

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

2984 push_collector.pop_for_user(user2.id) 

2985 

2986 # User2 marks attendance 

2987 with events_session(token2) as api: 

2988 api.SetEventAttendance( 

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

2990 ) 

2991 

2992 # Run the event reminder handler 

2993 send_event_reminders(empty_pb2.Empty()) 

2994 process_jobs() 

2995 

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

2997 with session_scope() as session: 

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

2999 

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

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

3002 assert len(reminder_notifs) == 1 

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

3004 

3005 

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

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

3008 user1, token1 = generate_user() 

3009 user2, token2 = generate_user() 

3010 

3011 with session_scope() as session: 

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

3013 

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

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

3016 end_time = start_time + timedelta(hours=1) 

3017 

3018 with events_session(token1) as api: 

3019 res = api.CreateEvent( 

3020 events_pb2.CreateEventReq( 

3021 title="Cancelled Reminder Test", 

3022 content="Content.", 

3023 offline_information=events_pb2.OfflineEventInformation( 

3024 address="Near Null Island", 

3025 lat=0.1, 

3026 lng=0.2, 

3027 ), 

3028 start_time=Timestamp_from_datetime(start_time), 

3029 end_time=Timestamp_from_datetime(end_time), 

3030 timezone="UTC", 

3031 ) 

3032 ) 

3033 event_id = res.event_id 

3034 

3035 moderator.approve_event_occurrence(event_id) 

3036 process_jobs() 

3037 

3038 # User2 marks attendance 

3039 with events_session(token2) as api: 

3040 api.SetEventAttendance( 

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

3042 ) 

3043 

3044 # User1 cancels the event 

3045 with events_session(token1) as api: 

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

3047 

3048 process_jobs() 

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

3050 while push_collector.count_for_user(user2.id): 

3051 push_collector.pop_for_user(user2.id) 

3052 

3053 # Run the event reminder handler 

3054 send_event_reminders(empty_pb2.Empty()) 

3055 process_jobs() 

3056 

3057 # Verify that no reminder notification was sent for user2 

3058 with session_scope() as session: 

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

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

3061 assert len(reminder_notifs) == 0 

3062 

3063 

3064def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3066 user1, token1 = generate_user() 

3067 user2, token2 = generate_user() 

3068 

3069 with session_scope() as session: 

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

3071 

3072 start = now() 

3073 

3074 # User1 creates event A with 3 occurrences 

3075 event_a_ids = [] 

3076 with events_session(token1) as api: 

3077 res = api.CreateEvent( 

3078 events_pb2.CreateEventReq( 

3079 title="Event A", 

3080 content="Content A.", 

3081 parent_community_id=c_id, 

3082 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"), 

3083 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

3084 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)), 

3085 timezone="UTC", 

3086 ) 

3087 ) 

3088 event_a_ids.append(res.event_id) 

3089 for i in range(2): 

3090 res = api.ScheduleEvent( 

3091 events_pb2.ScheduleEventReq( 

3092 event_id=event_a_ids[-1], 

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

3094 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"), 

3095 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)), 

3096 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)), 

3097 timezone="UTC", 

3098 ) 

3099 ) 

3100 event_a_ids.append(res.event_id) 

3101 

3102 # User2 creates event B with 2 occurrences 

3103 event_b_ids = [] 

3104 with events_session(token2) as api: 

3105 res = api.CreateEvent( 

3106 events_pb2.CreateEventReq( 

3107 title="Event B", 

3108 content="Content B.", 

3109 parent_community_id=c_id, 

3110 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"), 

3111 start_time=Timestamp_from_datetime(start + timedelta(hours=10)), 

3112 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)), 

3113 timezone="UTC", 

3114 ) 

3115 ) 

3116 event_b_ids.append(res.event_id) 

3117 res = api.ScheduleEvent( 

3118 events_pb2.ScheduleEventReq( 

3119 event_id=event_b_ids[-1], 

3120 content="B occurrence 1", 

3121 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"), 

3122 start_time=Timestamp_from_datetime(start + timedelta(hours=11)), 

3123 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)), 

3124 timezone="UTC", 

3125 ) 

3126 ) 

3127 event_b_ids.append(res.event_id) 

3128 

3129 moderator.approve_event_occurrence(event_a_ids[0]) 

3130 moderator.approve_event_occurrence(event_b_ids[0]) 

3131 

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

3133 with events_session(token1) as api: 

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

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

3136 assert sorted(returned_ids) == sorted(event_a_ids) 

3137 

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

3139 with events_session(token2) as api: 

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

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

3142 assert sorted(returned_ids) == sorted(event_b_ids) 

3143 

3144 

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

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

3147 user1, token1 = generate_user() 

3148 user2, token2 = generate_user() 

3149 

3150 with session_scope() as session: 

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

3152 

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

3154 end_time = start_time + timedelta(hours=3) 

3155 

3156 with events_session(token1) as api: 

3157 res = api.CreateEvent( 

3158 events_pb2.CreateEventReq( 

3159 title="Comment Test", 

3160 content="Content.", 

3161 offline_information=events_pb2.OfflineEventInformation( 

3162 address="Near Null Island", 

3163 lat=0.1, 

3164 lng=0.2, 

3165 ), 

3166 start_time=Timestamp_from_datetime(start_time), 

3167 end_time=Timestamp_from_datetime(end_time), 

3168 timezone="UTC", 

3169 ) 

3170 ) 

3171 event_id = res.event_id 

3172 thread_id = res.thread.thread_id 

3173 

3174 moderator.approve_event_occurrence(event_id) 

3175 process_jobs() 

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

3177 push_collector.pop_for_user(user1.id) 

3178 

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

3180 with events_session(token1) as api: 

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

3182 

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

3184 with threads_session(token2) as api: 

3185 api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")) 

3186 

3187 process_jobs() 

3188 

3189 # The comment notification for user1 should have moderation_state_id set 

3190 with session_scope() as session: 

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

3192 

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

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

3195 assert len(comment_notifs) == 1 

3196 assert comment_notifs[0].moderation_state_id == occurrence.moderation_state_id 

3197 

3198 

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

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

3201 user1, token1 = generate_user() 

3202 user2, token2 = generate_user() 

3203 user3, token3 = generate_user() 

3204 

3205 with session_scope() as session: 

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

3207 

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

3209 end_time = start_time + timedelta(hours=3) 

3210 

3211 with events_session(token1) as api: 

3212 res = api.CreateEvent( 

3213 events_pb2.CreateEventReq( 

3214 title="Reply Test", 

3215 content="Content.", 

3216 offline_information=events_pb2.OfflineEventInformation( 

3217 address="Near Null Island", 

3218 lat=0.1, 

3219 lng=0.2, 

3220 ), 

3221 start_time=Timestamp_from_datetime(start_time), 

3222 end_time=Timestamp_from_datetime(end_time), 

3223 timezone="UTC", 

3224 ) 

3225 ) 

3226 event_id = res.event_id 

3227 thread_id = res.thread.thread_id 

3228 

3229 moderator.approve_event_occurrence(event_id) 

3230 process_jobs() 

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

3232 push_collector.pop_for_user(user1.id) 

3233 

3234 # User2 posts a top-level comment 

3235 with threads_session(token2) as api: 

3236 comment_thread_id = api.PostReply( 

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

3238 ).thread_id 

3239 

3240 process_jobs() 

3241 while push_collector.count_for_user(user1.id): 

3242 push_collector.pop_for_user(user1.id) 

3243 

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

3245 with threads_session(token3) as api: 

3246 api.PostReply(threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")) 

3247 

3248 process_jobs() 

3249 

3250 # The nested reply notification for user2 should have moderation_state_id set 

3251 with session_scope() as session: 

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

3253 

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

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

3256 assert len(reply_notifs) == 1 

3257 assert reply_notifs[0].moderation_state_id == occurrence.moderation_state_id