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

1514 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-19 14:14 +0000

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from psycopg2.extras import DateTimeTZRange 

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 Notification, 

17 NotificationDelivery, 

18 Upload, 

19) 

20from couchers.proto import editor_pb2, events_pb2, threads_pb2 

21from couchers.tasks import enforce_community_memberships 

22from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime 

23from tests.fixtures.db import generate_user 

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

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

26from tests.test_communities import create_community, create_group 

27 

28 

29@pytest.fixture(autouse=True) 

30def _(testconfig): 

31 pass 

32 

33 

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

35 # test cases: 

36 # can create event 

37 # cannot create event with missing details 

38 # can create online event 

39 # can create in person event 

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

41 # can create in different timezones 

42 

43 # event creator 

44 user1, token1 = generate_user() 

45 # community moderator 

46 user2, token2 = generate_user() 

47 # third party 

48 user3, token3 = generate_user() 

49 

50 with session_scope() as session: 

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

52 

53 time_before = now() 

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

55 end_time = start_time + timedelta(hours=3) 

56 

57 with events_session(token1) as api: 

58 # in person event 

59 res = api.CreateEvent( 

60 events_pb2.CreateEventReq( 

61 title="Dummy Title", 

62 content="Dummy content.", 

63 photo_key=None, 

64 offline_information=events_pb2.OfflineEventInformation( 

65 address="Near Null Island", 

66 lat=0.1, 

67 lng=0.2, 

68 ), 

69 start_time=Timestamp_from_datetime(start_time), 

70 end_time=Timestamp_from_datetime(end_time), 

71 timezone="UTC", 

72 ) 

73 ) 

74 

75 assert res.is_next 

76 assert res.title == "Dummy Title" 

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

78 assert res.content == "Dummy content." 

79 assert not res.photo_url 

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

81 assert res.offline_information.lat == 0.1 

82 assert res.offline_information.lng == 0.2 

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

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

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

86 assert res.creator_user_id == user1.id 

87 assert to_aware_datetime(res.start_time) == start_time 

88 assert to_aware_datetime(res.end_time) == end_time 

89 # assert res.timezone == "UTC" 

90 assert res.start_time_display 

91 assert res.end_time_display 

92 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

93 assert res.organizer 

94 assert res.subscriber 

95 assert res.going_count == 1 

96 assert res.maybe_count == 0 

97 assert res.organizer_count == 1 

98 assert res.subscriber_count == 1 

99 assert res.owner_user_id == user1.id 

100 assert not res.owner_community_id 

101 assert not res.owner_group_id 

102 assert res.thread.thread_id 

103 assert res.can_edit 

104 assert not res.can_moderate 

105 

106 event_id = res.event_id 

107 

108 # Approve the event so other users can see it 

109 moderator.approve_event_occurrence(event_id) 

110 

111 with events_session(token2) as api: 

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

113 

114 assert res.is_next 

115 assert res.title == "Dummy Title" 

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

117 assert res.content == "Dummy content." 

118 assert not res.photo_url 

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

120 assert res.offline_information.lat == 0.1 

121 assert res.offline_information.lng == 0.2 

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

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

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

125 assert res.creator_user_id == user1.id 

126 assert to_aware_datetime(res.start_time) == start_time 

127 assert to_aware_datetime(res.end_time) == end_time 

128 # assert res.timezone == "UTC" 

129 assert res.start_time_display 

130 assert res.end_time_display 

131 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

132 assert not res.organizer 

133 assert not res.subscriber 

134 assert res.going_count == 1 

135 assert res.maybe_count == 0 

136 assert res.organizer_count == 1 

137 assert res.subscriber_count == 1 

138 assert res.owner_user_id == user1.id 

139 assert not res.owner_community_id 

140 assert not res.owner_group_id 

141 assert res.thread.thread_id 

142 assert res.can_edit 

143 assert res.can_moderate 

144 

145 with events_session(token3) as api: 

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

147 

148 assert res.is_next 

149 assert res.title == "Dummy Title" 

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

151 assert res.content == "Dummy content." 

152 assert not res.photo_url 

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

154 assert res.offline_information.lat == 0.1 

155 assert res.offline_information.lng == 0.2 

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

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

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

159 assert res.creator_user_id == user1.id 

160 assert to_aware_datetime(res.start_time) == start_time 

161 assert to_aware_datetime(res.end_time) == end_time 

162 # assert res.timezone == "UTC" 

163 assert res.start_time_display 

164 assert res.end_time_display 

165 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

166 assert not res.organizer 

167 assert not res.subscriber 

168 assert res.going_count == 1 

169 assert res.maybe_count == 0 

170 assert res.organizer_count == 1 

171 assert res.subscriber_count == 1 

172 assert res.owner_user_id == user1.id 

173 assert not res.owner_community_id 

174 assert not res.owner_group_id 

175 assert res.thread.thread_id 

176 assert not res.can_edit 

177 assert not res.can_moderate 

178 

179 with events_session(token1) as api: 

180 # online only event 

181 res = api.CreateEvent( 

182 events_pb2.CreateEventReq( 

183 title="Dummy Title", 

184 content="Dummy content.", 

185 photo_key=None, 

186 online_information=events_pb2.OnlineEventInformation( 

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

188 ), 

189 parent_community_id=c_id, 

190 start_time=Timestamp_from_datetime(start_time), 

191 end_time=Timestamp_from_datetime(end_time), 

192 timezone="UTC", 

193 ) 

194 ) 

195 

196 assert res.is_next 

197 assert res.title == "Dummy Title" 

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

199 assert res.content == "Dummy content." 

200 assert not res.photo_url 

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

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

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

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

205 assert res.creator_user_id == user1.id 

206 assert to_aware_datetime(res.start_time) == start_time 

207 assert to_aware_datetime(res.end_time) == end_time 

208 # assert res.timezone == "UTC" 

209 assert res.start_time_display 

210 assert res.end_time_display 

211 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

212 assert res.organizer 

213 assert res.subscriber 

214 assert res.going_count == 1 

215 assert res.maybe_count == 0 

216 assert res.organizer_count == 1 

217 assert res.subscriber_count == 1 

218 assert res.owner_user_id == user1.id 

219 assert not res.owner_community_id 

220 assert not res.owner_group_id 

221 assert res.thread.thread_id 

222 assert res.can_edit 

223 assert not res.can_moderate 

224 

225 event_id = res.event_id 

226 

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

228 moderator.approve_event_occurrence(event_id) 

229 

230 with events_session(token2) as api: 

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

232 

233 assert res.is_next 

234 assert res.title == "Dummy Title" 

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

236 assert res.content == "Dummy content." 

237 assert not res.photo_url 

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

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

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

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

242 assert res.creator_user_id == user1.id 

243 assert to_aware_datetime(res.start_time) == start_time 

244 assert to_aware_datetime(res.end_time) == end_time 

245 # assert res.timezone == "UTC" 

246 assert res.start_time_display 

247 assert res.end_time_display 

248 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

249 assert not res.organizer 

250 assert not res.subscriber 

251 assert res.going_count == 1 

252 assert res.maybe_count == 0 

253 assert res.organizer_count == 1 

254 assert res.subscriber_count == 1 

255 assert res.owner_user_id == user1.id 

256 assert not res.owner_community_id 

257 assert not res.owner_group_id 

258 assert res.thread.thread_id 

259 assert res.can_edit 

260 assert res.can_moderate 

261 

262 with events_session(token3) as api: 

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

264 

265 assert res.is_next 

266 assert res.title == "Dummy Title" 

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

268 assert res.content == "Dummy content." 

269 assert not res.photo_url 

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

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

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

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

274 assert res.creator_user_id == user1.id 

275 assert to_aware_datetime(res.start_time) == start_time 

276 assert to_aware_datetime(res.end_time) == end_time 

277 # assert res.timezone == "UTC" 

278 assert res.start_time_display 

279 assert res.end_time_display 

280 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

281 assert not res.organizer 

282 assert not res.subscriber 

283 assert res.going_count == 1 

284 assert res.maybe_count == 0 

285 assert res.organizer_count == 1 

286 assert res.subscriber_count == 1 

287 assert res.owner_user_id == user1.id 

288 assert not res.owner_community_id 

289 assert not res.owner_group_id 

290 assert res.thread.thread_id 

291 assert not res.can_edit 

292 assert not res.can_moderate 

293 

294 with events_session(token1) as api: 

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

296 api.CreateEvent( 

297 events_pb2.CreateEventReq( 

298 title="Dummy Title", 

299 content="Dummy content.", 

300 photo_key=None, 

301 online_information=events_pb2.OnlineEventInformation( 

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

303 ), 

304 start_time=Timestamp_from_datetime(start_time), 

305 end_time=Timestamp_from_datetime(end_time), 

306 timezone="UTC", 

307 ) 

308 ) 

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

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

311 

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

313 api.CreateEvent( 

314 events_pb2.CreateEventReq( 

315 # title="Dummy Title", 

316 content="Dummy content.", 

317 photo_key=None, 

318 offline_information=events_pb2.OfflineEventInformation( 

319 address="Near Null Island", 

320 lat=0.1, 

321 lng=0.1, 

322 ), 

323 start_time=Timestamp_from_datetime(start_time), 

324 end_time=Timestamp_from_datetime(end_time), 

325 timezone="UTC", 

326 ) 

327 ) 

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

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

330 

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

332 api.CreateEvent( 

333 events_pb2.CreateEventReq( 

334 title="Dummy Title", 

335 # content="Dummy content.", 

336 photo_key=None, 

337 offline_information=events_pb2.OfflineEventInformation( 

338 address="Near Null Island", 

339 lat=0.1, 

340 lng=0.1, 

341 ), 

342 start_time=Timestamp_from_datetime(start_time), 

343 end_time=Timestamp_from_datetime(end_time), 

344 timezone="UTC", 

345 ) 

346 ) 

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

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

349 

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

351 api.CreateEvent( 

352 events_pb2.CreateEventReq( 

353 title="Dummy Title", 

354 content="Dummy content.", 

355 photo_key="nonexistent", 

356 offline_information=events_pb2.OfflineEventInformation( 

357 address="Near Null Island", 

358 lat=0.1, 

359 lng=0.1, 

360 ), 

361 start_time=Timestamp_from_datetime(start_time), 

362 end_time=Timestamp_from_datetime(end_time), 

363 timezone="UTC", 

364 ) 

365 ) 

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

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

368 

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

370 api.CreateEvent( 

371 events_pb2.CreateEventReq( 

372 title="Dummy Title", 

373 content="Dummy content.", 

374 photo_key=None, 

375 offline_information=events_pb2.OfflineEventInformation( 

376 address="Near Null Island", 

377 ), 

378 start_time=Timestamp_from_datetime(start_time), 

379 end_time=Timestamp_from_datetime(end_time), 

380 timezone="UTC", 

381 ) 

382 ) 

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

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

385 

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

387 api.CreateEvent( 

388 events_pb2.CreateEventReq( 

389 title="Dummy Title", 

390 content="Dummy content.", 

391 photo_key=None, 

392 offline_information=events_pb2.OfflineEventInformation( 

393 lat=0.1, 

394 lng=0.1, 

395 ), 

396 start_time=Timestamp_from_datetime(start_time), 

397 end_time=Timestamp_from_datetime(end_time), 

398 timezone="UTC", 

399 ) 

400 ) 

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

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

403 

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

405 api.CreateEvent( 

406 events_pb2.CreateEventReq( 

407 title="Dummy Title", 

408 content="Dummy content.", 

409 photo_key=None, 

410 online_information=events_pb2.OnlineEventInformation(), 

411 start_time=Timestamp_from_datetime(start_time), 

412 end_time=Timestamp_from_datetime(end_time), 

413 timezone="UTC", 

414 ) 

415 ) 

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

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

418 

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

420 api.CreateEvent( 

421 events_pb2.CreateEventReq( 

422 title="Dummy Title", 

423 content="Dummy content.", 

424 parent_community_id=c_id, 

425 online_information=events_pb2.OnlineEventInformation( 

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

427 ), 

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

429 end_time=Timestamp_from_datetime(end_time), 

430 timezone="UTC", 

431 ) 

432 ) 

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

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

435 

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

437 api.CreateEvent( 

438 events_pb2.CreateEventReq( 

439 title="Dummy Title", 

440 content="Dummy content.", 

441 parent_community_id=c_id, 

442 online_information=events_pb2.OnlineEventInformation( 

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

444 ), 

445 start_time=Timestamp_from_datetime(end_time), 

446 end_time=Timestamp_from_datetime(start_time), 

447 timezone="UTC", 

448 ) 

449 ) 

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

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

452 

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

454 api.CreateEvent( 

455 events_pb2.CreateEventReq( 

456 title="Dummy Title", 

457 content="Dummy content.", 

458 parent_community_id=c_id, 

459 online_information=events_pb2.OnlineEventInformation( 

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

461 ), 

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

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

464 timezone="UTC", 

465 ) 

466 ) 

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

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

469 

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

471 api.CreateEvent( 

472 events_pb2.CreateEventReq( 

473 title="Dummy Title", 

474 content="Dummy content.", 

475 parent_community_id=c_id, 

476 online_information=events_pb2.OnlineEventInformation( 

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

478 ), 

479 start_time=Timestamp_from_datetime(start_time), 

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

481 timezone="UTC", 

482 ) 

483 ) 

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

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

486 

487 

488def test_CreateEvent_incomplete_profile(db): 

489 user1, token1 = generate_user(complete_profile=False) 

490 user2, token2 = generate_user() 

491 

492 with session_scope() as session: 

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

494 

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

496 end_time = start_time + timedelta(hours=3) 

497 

498 with events_session(token1) as api: 

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

500 api.CreateEvent( 

501 events_pb2.CreateEventReq( 

502 title="Dummy Title", 

503 content="Dummy content.", 

504 photo_key=None, 

505 offline_information=events_pb2.OfflineEventInformation( 

506 address="Near Null Island", 

507 lat=0.1, 

508 lng=0.2, 

509 ), 

510 start_time=Timestamp_from_datetime(start_time), 

511 end_time=Timestamp_from_datetime(end_time), 

512 timezone="UTC", 

513 ) 

514 ) 

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

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

517 

518 

519def test_ScheduleEvent(db): 

520 # test cases: 

521 # can schedule a new event occurrence 

522 

523 user, token = generate_user() 

524 

525 with session_scope() as session: 

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

527 

528 time_before = now() 

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

530 end_time = start_time + timedelta(hours=3) 

531 

532 with events_session(token) as api: 

533 res = api.CreateEvent( 

534 events_pb2.CreateEventReq( 

535 title="Dummy Title", 

536 content="Dummy content.", 

537 parent_community_id=c_id, 

538 online_information=events_pb2.OnlineEventInformation( 

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

540 ), 

541 start_time=Timestamp_from_datetime(start_time), 

542 end_time=Timestamp_from_datetime(end_time), 

543 timezone="UTC", 

544 ) 

545 ) 

546 

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

548 new_end_time = new_start_time + timedelta(hours=2) 

549 

550 res = api.ScheduleEvent( 

551 events_pb2.ScheduleEventReq( 

552 event_id=res.event_id, 

553 content="New event occurrence", 

554 offline_information=events_pb2.OfflineEventInformation( 

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

556 lat=0.3, 

557 lng=0.2, 

558 ), 

559 start_time=Timestamp_from_datetime(new_start_time), 

560 end_time=Timestamp_from_datetime(new_end_time), 

561 timezone="UTC", 

562 ) 

563 ) 

564 

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

566 

567 assert not res.is_next 

568 assert res.title == "Dummy Title" 

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

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

571 assert not res.photo_url 

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

573 assert res.offline_information.lat == 0.3 

574 assert res.offline_information.lng == 0.2 

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

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

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

578 assert res.creator_user_id == user.id 

579 assert to_aware_datetime(res.start_time) == new_start_time 

580 assert to_aware_datetime(res.end_time) == new_end_time 

581 # assert res.timezone == "UTC" 

582 assert res.start_time_display 

583 assert res.end_time_display 

584 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

585 assert res.organizer 

586 assert res.subscriber 

587 assert res.going_count == 1 

588 assert res.maybe_count == 0 

589 assert res.organizer_count == 1 

590 assert res.subscriber_count == 1 

591 assert res.owner_user_id == user.id 

592 assert not res.owner_community_id 

593 assert not res.owner_group_id 

594 assert res.thread.thread_id 

595 assert res.can_edit 

596 assert res.can_moderate 

597 

598 

599def test_cannot_overlap_occurrences_schedule(db): 

600 user, token = generate_user() 

601 

602 with session_scope() as session: 

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

604 

605 start = now() 

606 

607 with events_session(token) as api: 

608 res = api.CreateEvent( 

609 events_pb2.CreateEventReq( 

610 title="Dummy Title", 

611 content="Dummy content.", 

612 parent_community_id=c_id, 

613 online_information=events_pb2.OnlineEventInformation( 

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

615 ), 

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

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

618 timezone="UTC", 

619 ) 

620 ) 

621 

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

623 api.ScheduleEvent( 

624 events_pb2.ScheduleEventReq( 

625 event_id=res.event_id, 

626 content="New event occurrence", 

627 offline_information=events_pb2.OfflineEventInformation( 

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

629 lat=0.3, 

630 lng=0.2, 

631 ), 

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

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

634 timezone="UTC", 

635 ) 

636 ) 

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

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

639 

640 

641def test_cannot_overlap_occurrences_update(db): 

642 user, token = generate_user() 

643 

644 with session_scope() as session: 

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

646 

647 start = now() 

648 

649 with events_session(token) as api: 

650 res = api.CreateEvent( 

651 events_pb2.CreateEventReq( 

652 title="Dummy Title", 

653 content="Dummy content.", 

654 parent_community_id=c_id, 

655 online_information=events_pb2.OnlineEventInformation( 

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

657 ), 

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

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

660 timezone="UTC", 

661 ) 

662 ) 

663 

664 event_id = api.ScheduleEvent( 

665 events_pb2.ScheduleEventReq( 

666 event_id=res.event_id, 

667 content="New event occurrence", 

668 offline_information=events_pb2.OfflineEventInformation( 

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

670 lat=0.3, 

671 lng=0.2, 

672 ), 

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

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

675 timezone="UTC", 

676 ) 

677 ).event_id 

678 

679 # can overlap with this current existing occurrence 

680 api.UpdateEvent( 

681 events_pb2.UpdateEventReq( 

682 event_id=event_id, 

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

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

685 ) 

686 ) 

687 

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

689 api.UpdateEvent( 

690 events_pb2.UpdateEventReq( 

691 event_id=event_id, 

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

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

694 ) 

695 ) 

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

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

698 

699 

700def test_UpdateEvent_single(db, moderator: Moderator): 

701 # test cases: 

702 # owner can update 

703 # community owner can update 

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

705 # notifies attendees 

706 

707 # event creator 

708 user1, token1 = generate_user() 

709 # community moderator 

710 user2, token2 = generate_user() 

711 # third parties 

712 user3, token3 = generate_user() 

713 user4, token4 = generate_user() 

714 user5, token5 = generate_user() 

715 user6, token6 = generate_user() 

716 

717 with session_scope() as session: 

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

719 

720 time_before = now() 

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

722 end_time = start_time + timedelta(hours=3) 

723 

724 with events_session(token1) as api: 

725 res = api.CreateEvent( 

726 events_pb2.CreateEventReq( 

727 title="Dummy Title", 

728 content="Dummy content.", 

729 offline_information=events_pb2.OfflineEventInformation( 

730 address="Near Null Island", 

731 lat=0.1, 

732 lng=0.2, 

733 ), 

734 start_time=Timestamp_from_datetime(start_time), 

735 end_time=Timestamp_from_datetime(end_time), 

736 timezone="UTC", 

737 ) 

738 ) 

739 

740 event_id = res.event_id 

741 

742 moderator.approve_event_occurrence(event_id) 

743 

744 with events_session(token4) as api: 

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

746 

747 with events_session(token5) as api: 

748 api.SetEventAttendance( 

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

750 ) 

751 

752 with events_session(token6) as api: 

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

754 api.SetEventAttendance( 

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

756 ) 

757 

758 time_before_update = now() 

759 

760 with events_session(token1) as api: 

761 res = api.UpdateEvent( 

762 events_pb2.UpdateEventReq( 

763 event_id=event_id, 

764 ) 

765 ) 

766 

767 with events_session(token1) as api: 

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

769 

770 assert res.is_next 

771 assert res.title == "Dummy Title" 

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

773 assert res.content == "Dummy content." 

774 assert not res.photo_url 

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

776 assert res.offline_information.lat == 0.1 

777 assert res.offline_information.lng == 0.2 

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

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

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

781 assert res.creator_user_id == user1.id 

782 assert to_aware_datetime(res.start_time) == start_time 

783 assert to_aware_datetime(res.end_time) == end_time 

784 # assert res.timezone == "UTC" 

785 assert res.start_time_display 

786 assert res.end_time_display 

787 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

788 assert res.organizer 

789 assert res.subscriber 

790 assert res.going_count == 2 

791 assert res.maybe_count == 1 

792 assert res.organizer_count == 1 

793 assert res.subscriber_count == 3 

794 assert res.owner_user_id == user1.id 

795 assert not res.owner_community_id 

796 assert not res.owner_group_id 

797 assert res.thread.thread_id 

798 assert res.can_edit 

799 assert not res.can_moderate 

800 

801 with events_session(token2) as api: 

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

803 

804 assert res.is_next 

805 assert res.title == "Dummy Title" 

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

807 assert res.content == "Dummy content." 

808 assert not res.photo_url 

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

810 assert res.offline_information.lat == 0.1 

811 assert res.offline_information.lng == 0.2 

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

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

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

815 assert res.creator_user_id == user1.id 

816 assert to_aware_datetime(res.start_time) == start_time 

817 assert to_aware_datetime(res.end_time) == end_time 

818 # assert res.timezone == "UTC" 

819 assert res.start_time_display 

820 assert res.end_time_display 

821 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

822 assert not res.organizer 

823 assert not res.subscriber 

824 assert res.going_count == 2 

825 assert res.maybe_count == 1 

826 assert res.organizer_count == 1 

827 assert res.subscriber_count == 3 

828 assert res.owner_user_id == user1.id 

829 assert not res.owner_community_id 

830 assert not res.owner_group_id 

831 assert res.thread.thread_id 

832 assert res.can_edit 

833 assert res.can_moderate 

834 

835 with events_session(token3) as api: 

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

837 

838 assert res.is_next 

839 assert res.title == "Dummy Title" 

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

841 assert res.content == "Dummy content." 

842 assert not res.photo_url 

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

844 assert res.offline_information.lat == 0.1 

845 assert res.offline_information.lng == 0.2 

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

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

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

849 assert res.creator_user_id == user1.id 

850 assert to_aware_datetime(res.start_time) == start_time 

851 assert to_aware_datetime(res.end_time) == end_time 

852 # assert res.timezone == "UTC" 

853 assert res.start_time_display 

854 assert res.end_time_display 

855 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

856 assert not res.organizer 

857 assert not res.subscriber 

858 assert res.going_count == 2 

859 assert res.maybe_count == 1 

860 assert res.organizer_count == 1 

861 assert res.subscriber_count == 3 

862 assert res.owner_user_id == user1.id 

863 assert not res.owner_community_id 

864 assert not res.owner_group_id 

865 assert res.thread.thread_id 

866 assert not res.can_edit 

867 assert not res.can_moderate 

868 

869 with events_session(token1) as api: 

870 res = api.UpdateEvent( 

871 events_pb2.UpdateEventReq( 

872 event_id=event_id, 

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

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

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

876 start_time=Timestamp_from_datetime(start_time), 

877 end_time=Timestamp_from_datetime(end_time), 

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

879 ) 

880 ) 

881 

882 assert res.is_next 

883 assert res.title == "Dummy Title" 

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

885 assert res.content == "Dummy content." 

886 assert not res.photo_url 

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

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

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

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

891 assert res.creator_user_id == user1.id 

892 assert to_aware_datetime(res.start_time) == start_time 

893 assert to_aware_datetime(res.end_time) == end_time 

894 # assert res.timezone == "UTC" 

895 assert res.start_time_display 

896 assert res.end_time_display 

897 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

898 assert res.organizer 

899 assert res.subscriber 

900 assert res.going_count == 2 

901 assert res.maybe_count == 1 

902 assert res.organizer_count == 1 

903 assert res.subscriber_count == 3 

904 assert res.owner_user_id == user1.id 

905 assert not res.owner_community_id 

906 assert not res.owner_group_id 

907 assert res.thread.thread_id 

908 assert res.can_edit 

909 assert not res.can_moderate 

910 

911 event_id = res.event_id 

912 

913 with events_session(token2) as api: 

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

915 

916 assert res.is_next 

917 assert res.title == "Dummy Title" 

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

919 assert res.content == "Dummy content." 

920 assert not res.photo_url 

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

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

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

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

925 assert res.creator_user_id == user1.id 

926 assert to_aware_datetime(res.start_time) == start_time 

927 assert to_aware_datetime(res.end_time) == end_time 

928 # assert res.timezone == "UTC" 

929 assert res.start_time_display 

930 assert res.end_time_display 

931 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

932 assert not res.organizer 

933 assert not res.subscriber 

934 assert res.going_count == 2 

935 assert res.maybe_count == 1 

936 assert res.organizer_count == 1 

937 assert res.subscriber_count == 3 

938 assert res.owner_user_id == user1.id 

939 assert not res.owner_community_id 

940 assert not res.owner_group_id 

941 assert res.thread.thread_id 

942 assert res.can_edit 

943 assert res.can_moderate 

944 

945 with events_session(token3) as api: 

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

947 

948 assert res.is_next 

949 assert res.title == "Dummy Title" 

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

951 assert res.content == "Dummy content." 

952 assert not res.photo_url 

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

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

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

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

957 assert res.creator_user_id == user1.id 

958 assert to_aware_datetime(res.start_time) == start_time 

959 assert to_aware_datetime(res.end_time) == end_time 

960 # assert res.timezone == "UTC" 

961 assert res.start_time_display 

962 assert res.end_time_display 

963 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

964 assert not res.organizer 

965 assert not res.subscriber 

966 assert res.going_count == 2 

967 assert res.maybe_count == 1 

968 assert res.organizer_count == 1 

969 assert res.subscriber_count == 3 

970 assert res.owner_user_id == user1.id 

971 assert not res.owner_community_id 

972 assert not res.owner_group_id 

973 assert res.thread.thread_id 

974 assert not res.can_edit 

975 assert not res.can_moderate 

976 

977 with events_session(token1) as api: 

978 res = api.UpdateEvent( 

979 events_pb2.UpdateEventReq( 

980 event_id=event_id, 

981 offline_information=events_pb2.OfflineEventInformation( 

982 address="Near Null Island", 

983 lat=0.1, 

984 lng=0.2, 

985 ), 

986 ) 

987 ) 

988 

989 with events_session(token3) as api: 

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

991 

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

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

994 assert res.offline_information.lat == 0.1 

995 assert res.offline_information.lng == 0.2 

996 

997 

998def test_UpdateEvent_all(db, moderator: Moderator): 

999 # event creator 

1000 user1, token1 = generate_user() 

1001 # community moderator 

1002 user2, token2 = generate_user() 

1003 # third parties 

1004 user3, token3 = generate_user() 

1005 user4, token4 = generate_user() 

1006 user5, token5 = generate_user() 

1007 user6, token6 = generate_user() 

1008 

1009 with session_scope() as session: 

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

1011 

1012 time_before = now() 

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

1014 end_time = start_time + timedelta(hours=1.5) 

1015 

1016 event_ids = [] 

1017 

1018 with events_session(token1) as api: 

1019 res = api.CreateEvent( 

1020 events_pb2.CreateEventReq( 

1021 title="Dummy Title", 

1022 content="0th occurrence", 

1023 offline_information=events_pb2.OfflineEventInformation( 

1024 address="Near Null Island", 

1025 lat=0.1, 

1026 lng=0.2, 

1027 ), 

1028 start_time=Timestamp_from_datetime(start_time), 

1029 end_time=Timestamp_from_datetime(end_time), 

1030 timezone="UTC", 

1031 ) 

1032 ) 

1033 

1034 event_id = res.event_id 

1035 event_ids.append(event_id) 

1036 

1037 moderator.approve_event_occurrence(event_id) 

1038 

1039 with events_session(token4) as api: 

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

1041 

1042 with events_session(token5) as api: 

1043 api.SetEventAttendance( 

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

1045 ) 

1046 

1047 with events_session(token6) as api: 

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

1049 api.SetEventAttendance( 

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

1051 ) 

1052 

1053 with events_session(token1) as api: 

1054 for i in range(5): 

1055 res = api.ScheduleEvent( 

1056 events_pb2.ScheduleEventReq( 

1057 event_id=event_ids[-1], 

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

1059 online_information=events_pb2.OnlineEventInformation( 

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

1061 ), 

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

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

1064 timezone="UTC", 

1065 ) 

1066 ) 

1067 

1068 event_ids.append(res.event_id) 

1069 

1070 # Approve all scheduled occurrences 

1071 for eid in event_ids[1:]: 

1072 moderator.approve_event_occurrence(eid) 

1073 

1074 updated_event_id = event_ids[3] 

1075 

1076 time_before_update = now() 

1077 

1078 with events_session(token1) as api: 

1079 res = api.UpdateEvent( 

1080 events_pb2.UpdateEventReq( 

1081 event_id=updated_event_id, 

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

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

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

1085 update_all_future=True, 

1086 ) 

1087 ) 

1088 

1089 time_after_update = now() 

1090 

1091 with events_session(token2) as api: 

1092 for i in range(3): 

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

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

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

1096 

1097 for i in range(3, 6): 

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

1099 assert res.content == "New content." 

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

1101 

1102 

1103def test_GetEvent(db, moderator: Moderator): 

1104 # event creator 

1105 user1, token1 = generate_user() 

1106 # community moderator 

1107 user2, token2 = generate_user() 

1108 # third parties 

1109 user3, token3 = generate_user() 

1110 user4, token4 = generate_user() 

1111 user5, token5 = generate_user() 

1112 user6, token6 = generate_user() 

1113 

1114 with session_scope() as session: 

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

1116 

1117 time_before = now() 

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

1119 end_time = start_time + timedelta(hours=3) 

1120 

1121 with events_session(token1) as api: 

1122 # in person event 

1123 res = api.CreateEvent( 

1124 events_pb2.CreateEventReq( 

1125 title="Dummy Title", 

1126 content="Dummy content.", 

1127 offline_information=events_pb2.OfflineEventInformation( 

1128 address="Near Null Island", 

1129 lat=0.1, 

1130 lng=0.2, 

1131 ), 

1132 start_time=Timestamp_from_datetime(start_time), 

1133 end_time=Timestamp_from_datetime(end_time), 

1134 timezone="UTC", 

1135 ) 

1136 ) 

1137 

1138 event_id = res.event_id 

1139 

1140 moderator.approve_event_occurrence(event_id) 

1141 

1142 with events_session(token4) as api: 

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

1144 

1145 with events_session(token5) as api: 

1146 api.SetEventAttendance( 

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

1148 ) 

1149 

1150 with events_session(token6) as api: 

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

1152 api.SetEventAttendance( 

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

1154 ) 

1155 

1156 with events_session(token1) as api: 

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

1158 

1159 assert res.is_next 

1160 assert res.title == "Dummy Title" 

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

1162 assert res.content == "Dummy content." 

1163 assert not res.photo_url 

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

1165 assert res.offline_information.lat == 0.1 

1166 assert res.offline_information.lng == 0.2 

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

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

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

1170 assert res.creator_user_id == user1.id 

1171 assert to_aware_datetime(res.start_time) == start_time 

1172 assert to_aware_datetime(res.end_time) == end_time 

1173 # assert res.timezone == "UTC" 

1174 assert res.start_time_display 

1175 assert res.end_time_display 

1176 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

1177 assert res.organizer 

1178 assert res.subscriber 

1179 assert res.going_count == 2 

1180 assert res.maybe_count == 1 

1181 assert res.organizer_count == 1 

1182 assert res.subscriber_count == 3 

1183 assert res.owner_user_id == user1.id 

1184 assert not res.owner_community_id 

1185 assert not res.owner_group_id 

1186 assert res.thread.thread_id 

1187 assert res.can_edit 

1188 assert not res.can_moderate 

1189 

1190 with events_session(token2) as api: 

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

1192 

1193 assert res.is_next 

1194 assert res.title == "Dummy Title" 

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

1196 assert res.content == "Dummy content." 

1197 assert not res.photo_url 

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

1199 assert res.offline_information.lat == 0.1 

1200 assert res.offline_information.lng == 0.2 

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

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

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

1204 assert res.creator_user_id == user1.id 

1205 assert to_aware_datetime(res.start_time) == start_time 

1206 assert to_aware_datetime(res.end_time) == end_time 

1207 # assert res.timezone == "UTC" 

1208 assert res.start_time_display 

1209 assert res.end_time_display 

1210 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1211 assert not res.organizer 

1212 assert not res.subscriber 

1213 assert res.going_count == 2 

1214 assert res.maybe_count == 1 

1215 assert res.organizer_count == 1 

1216 assert res.subscriber_count == 3 

1217 assert res.owner_user_id == user1.id 

1218 assert not res.owner_community_id 

1219 assert not res.owner_group_id 

1220 assert res.thread.thread_id 

1221 assert res.can_edit 

1222 assert res.can_moderate 

1223 

1224 with events_session(token3) as api: 

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

1226 

1227 assert res.is_next 

1228 assert res.title == "Dummy Title" 

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

1230 assert res.content == "Dummy content." 

1231 assert not res.photo_url 

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

1233 assert res.offline_information.lat == 0.1 

1234 assert res.offline_information.lng == 0.2 

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

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

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

1238 assert res.creator_user_id == user1.id 

1239 assert to_aware_datetime(res.start_time) == start_time 

1240 assert to_aware_datetime(res.end_time) == end_time 

1241 # assert res.timezone == "UTC" 

1242 assert res.start_time_display 

1243 assert res.end_time_display 

1244 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1245 assert not res.organizer 

1246 assert not res.subscriber 

1247 assert res.going_count == 2 

1248 assert res.maybe_count == 1 

1249 assert res.organizer_count == 1 

1250 assert res.subscriber_count == 3 

1251 assert res.owner_user_id == user1.id 

1252 assert not res.owner_community_id 

1253 assert not res.owner_group_id 

1254 assert res.thread.thread_id 

1255 assert not res.can_edit 

1256 assert not res.can_moderate 

1257 

1258 

1259def test_CancelEvent(db, moderator: Moderator): 

1260 # event creator 

1261 user1, token1 = generate_user() 

1262 # community moderator 

1263 user2, token2 = generate_user() 

1264 # third parties 

1265 user3, token3 = generate_user() 

1266 user4, token4 = generate_user() 

1267 user5, token5 = generate_user() 

1268 user6, token6 = generate_user() 

1269 

1270 with session_scope() as session: 

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

1272 

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

1274 end_time = start_time + timedelta(hours=3) 

1275 

1276 with events_session(token1) as api: 

1277 res = api.CreateEvent( 

1278 events_pb2.CreateEventReq( 

1279 title="Dummy Title", 

1280 content="Dummy content.", 

1281 offline_information=events_pb2.OfflineEventInformation( 

1282 address="Near Null Island", 

1283 lat=0.1, 

1284 lng=0.2, 

1285 ), 

1286 start_time=Timestamp_from_datetime(start_time), 

1287 end_time=Timestamp_from_datetime(end_time), 

1288 timezone="UTC", 

1289 ) 

1290 ) 

1291 

1292 event_id = res.event_id 

1293 

1294 moderator.approve_event_occurrence(event_id) 

1295 

1296 with events_session(token4) as api: 

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

1298 

1299 with events_session(token5) as api: 

1300 api.SetEventAttendance( 

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

1302 ) 

1303 

1304 with events_session(token6) as api: 

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

1306 api.SetEventAttendance( 

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

1308 ) 

1309 

1310 with events_session(token1) as api: 

1311 res = api.CancelEvent( 

1312 events_pb2.CancelEventReq( 

1313 event_id=event_id, 

1314 ) 

1315 ) 

1316 

1317 with events_session(token1) as api: 

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

1319 assert res.is_cancelled 

1320 

1321 with events_session(token1) as api: 

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

1323 api.UpdateEvent( 

1324 events_pb2.UpdateEventReq( 

1325 event_id=event_id, 

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

1327 ) 

1328 ) 

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

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

1331 

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

1333 api.InviteEventOrganizer( 

1334 events_pb2.InviteEventOrganizerReq( 

1335 event_id=event_id, 

1336 user_id=user3.id, 

1337 ) 

1338 ) 

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

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

1341 

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

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

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

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

1346 

1347 with events_session(token3) as api: 

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

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

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

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

1352 

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

1354 api.SetEventAttendance( 

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

1356 ) 

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

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

1359 

1360 with events_session(token1) as api: 

1361 for include_cancelled in [True, False]: 

1362 res = api.ListEventOccurrences( 

1363 events_pb2.ListEventOccurrencesReq( 

1364 event_id=event_id, 

1365 include_cancelled=include_cancelled, 

1366 ) 

1367 ) 

1368 if include_cancelled: 

1369 assert len(res.events) > 0 

1370 else: 

1371 assert len(res.events) == 0 

1372 

1373 res = api.ListMyEvents( 

1374 events_pb2.ListMyEventsReq( 

1375 include_cancelled=include_cancelled, 

1376 ) 

1377 ) 

1378 if include_cancelled: 

1379 assert len(res.events) > 0 

1380 else: 

1381 assert len(res.events) == 0 

1382 

1383 

1384def test_ListEventAttendees(db, moderator: Moderator): 

1385 # event creator 

1386 user1, token1 = generate_user() 

1387 # others 

1388 user2, token2 = generate_user() 

1389 user3, token3 = generate_user() 

1390 user4, token4 = generate_user() 

1391 user5, token5 = generate_user() 

1392 user6, token6 = generate_user() 

1393 

1394 with session_scope() as session: 

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

1396 

1397 with events_session(token1) as api: 

1398 event_id = api.CreateEvent( 

1399 events_pb2.CreateEventReq( 

1400 title="Dummy Title", 

1401 content="Dummy content.", 

1402 offline_information=events_pb2.OfflineEventInformation( 

1403 address="Near Null Island", 

1404 lat=0.1, 

1405 lng=0.2, 

1406 ), 

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

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

1409 timezone="UTC", 

1410 ) 

1411 ).event_id 

1412 

1413 moderator.approve_event_occurrence(event_id) 

1414 

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

1416 with events_session(token) as api: 

1417 api.SetEventAttendance( 

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

1419 ) 

1420 

1421 with events_session(token6) as api: 

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

1423 

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

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

1426 

1427 res = api.ListEventAttendees( 

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

1429 ) 

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

1431 

1432 res = api.ListEventAttendees( 

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

1434 ) 

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

1436 assert not res.next_page_token 

1437 

1438 

1439def test_ListEventSubscribers(db, moderator: Moderator): 

1440 # event creator 

1441 user1, token1 = generate_user() 

1442 # others 

1443 user2, token2 = generate_user() 

1444 user3, token3 = generate_user() 

1445 user4, token4 = generate_user() 

1446 user5, token5 = generate_user() 

1447 user6, token6 = generate_user() 

1448 

1449 with session_scope() as session: 

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

1451 

1452 with events_session(token1) as api: 

1453 event_id = api.CreateEvent( 

1454 events_pb2.CreateEventReq( 

1455 title="Dummy Title", 

1456 content="Dummy content.", 

1457 offline_information=events_pb2.OfflineEventInformation( 

1458 address="Near Null Island", 

1459 lat=0.1, 

1460 lng=0.2, 

1461 ), 

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

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

1464 timezone="UTC", 

1465 ) 

1466 ).event_id 

1467 

1468 moderator.approve_event_occurrence(event_id) 

1469 

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

1471 with events_session(token) as api: 

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

1473 

1474 with events_session(token6) as api: 

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

1476 

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

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

1479 

1480 res = api.ListEventSubscribers( 

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

1482 ) 

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

1484 

1485 res = api.ListEventSubscribers( 

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

1487 ) 

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

1489 assert not res.next_page_token 

1490 

1491 

1492def test_ListEventOrganizers(db, moderator: Moderator): 

1493 # event creator 

1494 user1, token1 = generate_user() 

1495 # others 

1496 user2, token2 = generate_user() 

1497 user3, token3 = generate_user() 

1498 user4, token4 = generate_user() 

1499 user5, token5 = generate_user() 

1500 user6, token6 = generate_user() 

1501 

1502 with session_scope() as session: 

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

1504 

1505 with events_session(token1) as api: 

1506 event_id = api.CreateEvent( 

1507 events_pb2.CreateEventReq( 

1508 title="Dummy Title", 

1509 content="Dummy content.", 

1510 offline_information=events_pb2.OfflineEventInformation( 

1511 address="Near Null Island", 

1512 lat=0.1, 

1513 lng=0.2, 

1514 ), 

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

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

1517 timezone="UTC", 

1518 ) 

1519 ).event_id 

1520 

1521 moderator.approve_event_occurrence(event_id) 

1522 

1523 with events_session(token1) as api: 

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

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

1526 

1527 with events_session(token6) as api: 

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

1529 

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

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

1532 

1533 res = api.ListEventOrganizers( 

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

1535 ) 

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

1537 

1538 res = api.ListEventOrganizers( 

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

1540 ) 

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

1542 assert not res.next_page_token 

1543 

1544 

1545def test_TransferEvent(db): 

1546 user1, token1 = generate_user() 

1547 user2, token2 = generate_user() 

1548 user3, token3 = generate_user() 

1549 user4, token4 = generate_user() 

1550 

1551 with session_scope() as session: 

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

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

1554 c_id = c.id 

1555 h_id = h.id 

1556 

1557 with events_session(token1) as api: 

1558 event_id = api.CreateEvent( 

1559 events_pb2.CreateEventReq( 

1560 title="Dummy Title", 

1561 content="Dummy content.", 

1562 offline_information=events_pb2.OfflineEventInformation( 

1563 address="Near Null Island", 

1564 lat=0.1, 

1565 lng=0.2, 

1566 ), 

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

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

1569 timezone="UTC", 

1570 ) 

1571 ).event_id 

1572 

1573 api.TransferEvent( 

1574 events_pb2.TransferEventReq( 

1575 event_id=event_id, 

1576 new_owner_community_id=c_id, 

1577 ) 

1578 ) 

1579 

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

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

1582 

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

1584 api.TransferEvent( 

1585 events_pb2.TransferEventReq( 

1586 event_id=event_id, 

1587 new_owner_group_id=h_id, 

1588 ) 

1589 ) 

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

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

1592 

1593 event_id = api.CreateEvent( 

1594 events_pb2.CreateEventReq( 

1595 title="Dummy Title", 

1596 content="Dummy content.", 

1597 offline_information=events_pb2.OfflineEventInformation( 

1598 address="Near Null Island", 

1599 lat=0.1, 

1600 lng=0.2, 

1601 ), 

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

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

1604 timezone="UTC", 

1605 ) 

1606 ).event_id 

1607 

1608 api.TransferEvent( 

1609 events_pb2.TransferEventReq( 

1610 event_id=event_id, 

1611 new_owner_group_id=h_id, 

1612 ) 

1613 ) 

1614 

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

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

1617 

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

1619 api.TransferEvent( 

1620 events_pb2.TransferEventReq( 

1621 event_id=event_id, 

1622 new_owner_community_id=c_id, 

1623 ) 

1624 ) 

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

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

1627 

1628 

1629def test_SetEventSubscription(db, moderator: Moderator): 

1630 user1, token1 = generate_user() 

1631 user2, token2 = generate_user() 

1632 

1633 with session_scope() as session: 

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

1635 

1636 with events_session(token1) as api: 

1637 event_id = api.CreateEvent( 

1638 events_pb2.CreateEventReq( 

1639 title="Dummy Title", 

1640 content="Dummy content.", 

1641 offline_information=events_pb2.OfflineEventInformation( 

1642 address="Near Null Island", 

1643 lat=0.1, 

1644 lng=0.2, 

1645 ), 

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

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

1648 timezone="UTC", 

1649 ) 

1650 ).event_id 

1651 

1652 moderator.approve_event_occurrence(event_id) 

1653 

1654 with events_session(token2) as api: 

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

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

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

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

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

1660 

1661 

1662def test_SetEventAttendance(db, moderator: Moderator): 

1663 user1, token1 = generate_user() 

1664 user2, token2 = generate_user() 

1665 

1666 with session_scope() as session: 

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

1668 

1669 with events_session(token1) as api: 

1670 event_id = api.CreateEvent( 

1671 events_pb2.CreateEventReq( 

1672 title="Dummy Title", 

1673 content="Dummy content.", 

1674 offline_information=events_pb2.OfflineEventInformation( 

1675 address="Near Null Island", 

1676 lat=0.1, 

1677 lng=0.2, 

1678 ), 

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

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

1681 timezone="UTC", 

1682 ) 

1683 ).event_id 

1684 

1685 moderator.approve_event_occurrence(event_id) 

1686 

1687 with events_session(token2) as api: 

1688 assert ( 

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

1690 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1691 ) 

1692 api.SetEventAttendance( 

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

1694 ) 

1695 assert ( 

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

1697 == events_pb2.ATTENDANCE_STATE_GOING 

1698 ) 

1699 api.SetEventAttendance( 

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

1701 ) 

1702 assert ( 

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

1704 == events_pb2.ATTENDANCE_STATE_MAYBE 

1705 ) 

1706 api.SetEventAttendance( 

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

1708 ) 

1709 assert ( 

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

1711 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1712 ) 

1713 

1714 

1715def test_InviteEventOrganizer(db, moderator: Moderator): 

1716 user1, token1 = generate_user() 

1717 user2, token2 = generate_user() 

1718 

1719 with session_scope() as session: 

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

1721 

1722 with events_session(token1) as api: 

1723 event_id = api.CreateEvent( 

1724 events_pb2.CreateEventReq( 

1725 title="Dummy Title", 

1726 content="Dummy content.", 

1727 offline_information=events_pb2.OfflineEventInformation( 

1728 address="Near Null Island", 

1729 lat=0.1, 

1730 lng=0.2, 

1731 ), 

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

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

1734 timezone="UTC", 

1735 ) 

1736 ).event_id 

1737 

1738 moderator.approve_event_occurrence(event_id) 

1739 

1740 with events_session(token2) as api: 

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

1742 

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

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

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

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

1747 

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

1749 

1750 with events_session(token1) as api: 

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

1752 

1753 with events_session(token2) as api: 

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

1755 

1756 

1757def test_ListEventOccurrences(db): 

1758 user1, token1 = generate_user() 

1759 user2, token2 = generate_user() 

1760 user3, token3 = generate_user() 

1761 

1762 with session_scope() as session: 

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

1764 

1765 start = now() 

1766 

1767 event_ids = [] 

1768 

1769 with events_session(token1) as api: 

1770 res = api.CreateEvent( 

1771 events_pb2.CreateEventReq( 

1772 title="First occurrence", 

1773 content="Dummy content.", 

1774 parent_community_id=c_id, 

1775 online_information=events_pb2.OnlineEventInformation( 

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

1777 ), 

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

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

1780 timezone="UTC", 

1781 ) 

1782 ) 

1783 

1784 event_ids.append(res.event_id) 

1785 

1786 for i in range(5): 

1787 res = api.ScheduleEvent( 

1788 events_pb2.ScheduleEventReq( 

1789 event_id=event_ids[-1], 

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

1791 online_information=events_pb2.OnlineEventInformation( 

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

1793 ), 

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

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

1796 timezone="UTC", 

1797 ) 

1798 ) 

1799 

1800 event_ids.append(res.event_id) 

1801 

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

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

1804 

1805 res = api.ListEventOccurrences( 

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

1807 ) 

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

1809 

1810 res = api.ListEventOccurrences( 

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

1812 ) 

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

1814 assert not res.next_page_token 

1815 

1816 

1817def test_ListMyEvents(db, moderator: Moderator): 

1818 user1, token1 = generate_user() 

1819 user2, token2 = generate_user() 

1820 user3, token3 = generate_user() 

1821 user4, token4 = generate_user() 

1822 user5, token5 = generate_user() 

1823 

1824 with session_scope() as session: 

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

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

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

1828 c_id = global_community.id 

1829 macroregion_community = create_community( 

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

1831 ) 

1832 region_community = create_community( 

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

1834 ) 

1835 subregion_community = create_community( 

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

1837 ) 

1838 c2_id = subregion_community.id 

1839 

1840 start = now() 

1841 

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

1843 if online: 

1844 return events_pb2.CreateEventReq( 

1845 title="Dummy Online Title", 

1846 content="Dummy content.", 

1847 online_information=events_pb2.OnlineEventInformation( 

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

1849 ), 

1850 parent_community_id=community_id, 

1851 timezone="UTC", 

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

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

1854 ) 

1855 else: 

1856 return events_pb2.CreateEventReq( 

1857 title="Dummy Offline Title", 

1858 content="Dummy content.", 

1859 offline_information=events_pb2.OfflineEventInformation( 

1860 address="Near Null Island", 

1861 lat=0.1, 

1862 lng=0.2, 

1863 ), 

1864 parent_community_id=community_id, 

1865 timezone="UTC", 

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

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

1868 ) 

1869 

1870 with events_session(token1) as api: 

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

1872 

1873 moderator.approve_event_occurrence(e2) 

1874 

1875 with events_session(token2) as api: 

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

1877 

1878 moderator.approve_event_occurrence(e1) 

1879 

1880 with events_session(token1) as api: 

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

1882 

1883 moderator.approve_event_occurrence(e3) 

1884 

1885 with events_session(token2) as api: 

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

1887 

1888 moderator.approve_event_occurrence(e5) 

1889 

1890 with events_session(token3) as api: 

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

1892 

1893 moderator.approve_event_occurrence(e4) 

1894 

1895 with events_session(token4) as api: 

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

1897 

1898 moderator.approve_event_occurrence(e6) 

1899 

1900 with events_session(token1) as api: 

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

1902 

1903 with events_session(token1) as api: 

1904 api.SetEventAttendance( 

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

1906 ) 

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

1908 

1909 with events_session(token2) as api: 

1910 api.SetEventAttendance( 

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

1912 ) 

1913 

1914 with events_session(token3) as api: 

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

1916 

1917 with events_session(token1) as api: 

1918 # test pagination with token first 

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

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

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

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

1923 assert not res.next_page_token 

1924 

1925 res = api.ListMyEvents( 

1926 events_pb2.ListMyEventsReq( 

1927 subscribed=True, 

1928 attending=True, 

1929 organizing=True, 

1930 ) 

1931 ) 

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

1933 

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

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

1936 

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

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

1939 

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

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

1942 

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

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

1945 

1946 with events_session(token1) as api: 

1947 # Test pagination with page_number and verify total_items 

1948 res = api.ListMyEvents( 

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

1950 ) 

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

1952 assert res.total_items == 4 

1953 

1954 res = api.ListMyEvents( 

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

1956 ) 

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

1958 assert res.total_items == 4 

1959 

1960 # Verify no more pages 

1961 res = api.ListMyEvents( 

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

1963 ) 

1964 assert not res.events 

1965 assert res.total_items == 4 

1966 

1967 with events_session(token2) as api: 

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

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

1970 

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

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

1973 

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

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

1976 

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

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

1979 

1980 with events_session(token3) as api: 

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

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

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

1984 

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

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

1987 

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

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

1990 

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

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

1993 

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

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

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

1997 

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

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

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

2001 

2002 # my_communities_exclude_global works independently of my_communities flag 

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

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

2005 

2006 # my_communities_exclude_global filters organizing results too 

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

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

2009 

2010 # my_communities_exclude_global filters subscribed results too 

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

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

2013 

2014 with events_session(token5) as api: 

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

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

2017 

2018 

2019def test_RemoveEventOrganizer(db, moderator: Moderator): 

2020 user1, token1 = generate_user() 

2021 user2, token2 = generate_user() 

2022 

2023 with session_scope() as session: 

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

2025 

2026 with events_session(token1) as api: 

2027 event_id = api.CreateEvent( 

2028 events_pb2.CreateEventReq( 

2029 title="Dummy Title", 

2030 content="Dummy content.", 

2031 offline_information=events_pb2.OfflineEventInformation( 

2032 address="Near Null Island", 

2033 lat=0.1, 

2034 lng=0.2, 

2035 ), 

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

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

2038 timezone="UTC", 

2039 ) 

2040 ).event_id 

2041 

2042 moderator.approve_event_occurrence(event_id) 

2043 

2044 with events_session(token2) as api: 

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

2046 

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

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

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

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

2051 

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

2053 

2054 with events_session(token1) as api: 

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

2056 

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

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

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

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

2061 

2062 with events_session(token2) as api: 

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

2064 assert res.organizer 

2065 assert res.organizer_count == 2 

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

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

2068 

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

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

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

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

2073 

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

2075 assert not res.organizer 

2076 assert res.organizer_count == 1 

2077 

2078 # Test that event owner can remove co-organizers 

2079 with events_session(token1) as api: 

2080 # Add user2 back as organizer 

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

2082 

2083 # Verify user2 is now an organizer 

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

2085 assert res.organizer_count == 2 

2086 

2087 # Event owner can remove co-organizer 

2088 api.RemoveEventOrganizer( 

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

2090 ) 

2091 

2092 # Verify user2 is no longer an organizer 

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

2094 assert res.organizer_count == 1 

2095 

2096 # Test that non-organizers cannot remove other organizers 

2097 with events_session(token2) as api: 

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

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

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

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

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

2103 

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

2105 with events_session(token1) as api: 

2106 # Add user2 back as organizer 

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

2108 

2109 

2110def test_ListEventAttendees_regression(db): 

2111 # see issue #1617: 

2112 # 

2113 # 1. Create an event 

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

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

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

2117 # 

2118 # **Expected behaviour** 

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

2120 # 

2121 # **Actual/current behaviour** 

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

2123 

2124 user1, token1 = generate_user() 

2125 user2, token2 = generate_user() 

2126 user3, token3 = generate_user() 

2127 user4, token4 = generate_user() 

2128 user5, token5 = generate_user() 

2129 

2130 with session_scope() as session: 

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

2132 

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

2134 end_time = start_time + timedelta(hours=3) 

2135 

2136 with events_session(token1) as api: 

2137 res = api.CreateEvent( 

2138 events_pb2.CreateEventReq( 

2139 title="Dummy Title", 

2140 content="Dummy content.", 

2141 online_information=events_pb2.OnlineEventInformation( 

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

2143 ), 

2144 parent_community_id=c_id, 

2145 start_time=Timestamp_from_datetime(start_time), 

2146 end_time=Timestamp_from_datetime(end_time), 

2147 timezone="UTC", 

2148 ) 

2149 ) 

2150 

2151 res = api.TransferEvent( 

2152 events_pb2.TransferEventReq( 

2153 event_id=res.event_id, 

2154 new_owner_community_id=c_id, 

2155 ) 

2156 ) 

2157 

2158 event_id = res.event_id 

2159 

2160 api.SetEventAttendance( 

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

2162 ) 

2163 api.SetEventAttendance( 

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

2165 ) 

2166 

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

2168 assert len(res.attendee_user_ids) == 1 

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

2170 

2171 

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

2173 user1, token1 = generate_user() 

2174 user2, token2 = generate_user() 

2175 user3, token3 = generate_user() 

2176 user4, token4 = generate_user() 

2177 

2178 with session_scope() as session: 

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

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

2181 c_id = c.id 

2182 h_id = h.id 

2183 user4_id = user4.id 

2184 

2185 with events_session(token1) as api: 

2186 event = api.CreateEvent( 

2187 events_pb2.CreateEventReq( 

2188 title="Dummy Title", 

2189 content="Dummy content.", 

2190 offline_information=events_pb2.OfflineEventInformation( 

2191 address="Near Null Island", 

2192 lat=0.1, 

2193 lng=0.2, 

2194 ), 

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

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

2197 timezone="UTC", 

2198 ) 

2199 ) 

2200 

2201 moderator.approve_event_occurrence(event.event_id) 

2202 

2203 with threads_session(token2) as api: 

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

2205 

2206 with events_session(token3) as api: 

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

2208 assert res.thread.num_responses == 1 

2209 

2210 with threads_session(token3) as api: 

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

2212 assert len(ret.replies) == 1 

2213 assert not ret.next_page_token 

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

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

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

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

2218 

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

2220 

2221 process_jobs() 

2222 

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

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

2225 assert push_collector.count_for_user(user4_id) == 0 

2226 

2227 

2228def test_can_overlap_other_events_schedule_regression(db): 

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

2230 user, token = generate_user() 

2231 

2232 with session_scope() as session: 

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

2234 

2235 start = now() 

2236 

2237 with events_session(token) as api: 

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

2239 api.CreateEvent( 

2240 events_pb2.CreateEventReq( 

2241 title="Dummy Title", 

2242 content="Dummy content.", 

2243 parent_community_id=c_id, 

2244 online_information=events_pb2.OnlineEventInformation( 

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

2246 ), 

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

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

2249 timezone="UTC", 

2250 ) 

2251 ) 

2252 

2253 # this event 

2254 res = api.CreateEvent( 

2255 events_pb2.CreateEventReq( 

2256 title="Dummy Title", 

2257 content="Dummy content.", 

2258 parent_community_id=c_id, 

2259 online_information=events_pb2.OnlineEventInformation( 

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

2261 ), 

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

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

2264 timezone="UTC", 

2265 ) 

2266 ) 

2267 

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

2269 api.ScheduleEvent( 

2270 events_pb2.ScheduleEventReq( 

2271 event_id=res.event_id, 

2272 content="New event occurrence", 

2273 offline_information=events_pb2.OfflineEventInformation( 

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

2275 lat=0.3, 

2276 lng=0.2, 

2277 ), 

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

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

2280 timezone="UTC", 

2281 ) 

2282 ) 

2283 

2284 

2285def test_can_overlap_other_events_update_regression(db): 

2286 user, token = generate_user() 

2287 

2288 with session_scope() as session: 

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

2290 

2291 start = now() 

2292 

2293 with events_session(token) as api: 

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

2295 api.CreateEvent( 

2296 events_pb2.CreateEventReq( 

2297 title="Dummy Title", 

2298 content="Dummy content.", 

2299 parent_community_id=c_id, 

2300 online_information=events_pb2.OnlineEventInformation( 

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

2302 ), 

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

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

2305 timezone="UTC", 

2306 ) 

2307 ) 

2308 

2309 res = api.CreateEvent( 

2310 events_pb2.CreateEventReq( 

2311 title="Dummy Title", 

2312 content="Dummy content.", 

2313 parent_community_id=c_id, 

2314 online_information=events_pb2.OnlineEventInformation( 

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

2316 ), 

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

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

2319 timezone="UTC", 

2320 ) 

2321 ) 

2322 

2323 event_id = api.ScheduleEvent( 

2324 events_pb2.ScheduleEventReq( 

2325 event_id=res.event_id, 

2326 content="New event occurrence", 

2327 offline_information=events_pb2.OfflineEventInformation( 

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

2329 lat=0.3, 

2330 lng=0.2, 

2331 ), 

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

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

2334 timezone="UTC", 

2335 ) 

2336 ).event_id 

2337 

2338 # can overlap with this current existing occurrence 

2339 api.UpdateEvent( 

2340 events_pb2.UpdateEventReq( 

2341 event_id=event_id, 

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

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

2344 ) 

2345 ) 

2346 

2347 api.UpdateEvent( 

2348 events_pb2.UpdateEventReq( 

2349 event_id=event_id, 

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

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

2352 ) 

2353 ) 

2354 

2355 

2356def test_list_past_events_regression(db): 

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

2358 user, token = generate_user() 

2359 

2360 with session_scope() as session: 

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

2362 

2363 start = now() 

2364 

2365 with events_session(token) as api: 

2366 api.CreateEvent( 

2367 events_pb2.CreateEventReq( 

2368 title="Dummy Title", 

2369 content="Dummy content.", 

2370 parent_community_id=c_id, 

2371 online_information=events_pb2.OnlineEventInformation( 

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

2373 ), 

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

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

2376 timezone="UTC", 

2377 ) 

2378 ) 

2379 

2380 with session_scope() as session: 

2381 session.execute( 

2382 update(EventOccurrence).values( 

2383 during=DateTimeTZRange(start + timedelta(hours=-5), start + timedelta(hours=-4)) 

2384 ) 

2385 ) 

2386 

2387 with events_session(token) as api: 

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

2389 assert len(res.events) == 1 

2390 

2391 

2392def test_community_invite_requests(db, moderator: Moderator): 

2393 user1, token1 = generate_user(complete_profile=True) 

2394 user2, token2 = generate_user() 

2395 user3, token3 = generate_user() 

2396 user4, token4 = generate_user() 

2397 user5, token5 = generate_user(is_superuser=True) 

2398 

2399 with session_scope() as session: 

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

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

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

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

2404 

2405 enforce_community_memberships() 

2406 

2407 with events_session(token1) as api: 

2408 res = api.CreateEvent( 

2409 events_pb2.CreateEventReq( 

2410 title="Dummy Title", 

2411 content="Dummy content.", 

2412 parent_community_id=c_id, 

2413 online_information=events_pb2.OnlineEventInformation( 

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

2415 ), 

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

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

2418 timezone="UTC", 

2419 ) 

2420 ) 

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

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

2423 

2424 event_id = res.event_id 

2425 

2426 moderator.approve_event_occurrence(event_id) 

2427 

2428 with events_session(token1) as api: 

2429 with mock_notification_email() as mock: 

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

2431 assert mock.call_count == 1 

2432 e = email_fields(mock) 

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

2434 

2435 assert user_url in e.plain 

2436 assert event_url in e.plain 

2437 

2438 # can't send another req 

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

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

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

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

2443 

2444 # another user can send one though 

2445 with events_session(token3) as api: 

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

2447 

2448 # but not a non-admin 

2449 with events_session(token2) as api: 

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

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

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

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

2454 

2455 with real_editor_session(token5) as editor: 

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

2457 assert len(res.requests) == 2 

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

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

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

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

2462 

2463 editor.DecideEventCommunityInviteRequest( 

2464 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2466 approve=False, 

2467 ) 

2468 ) 

2469 

2470 editor.DecideEventCommunityInviteRequest( 

2471 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2473 approve=True, 

2474 ) 

2475 ) 

2476 

2477 # not after approve 

2478 with events_session(token4) as api: 

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

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

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

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

2483 

2484 

2485def test_update_event_should_notify_queues_job(): 

2486 user, token = generate_user() 

2487 start = now() 

2488 

2489 with session_scope() as session: 

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

2491 

2492 # create an event 

2493 with events_session(token) as api: 

2494 create_res = api.CreateEvent( 

2495 events_pb2.CreateEventReq( 

2496 title="Dummy Title", 

2497 content="Dummy content.", 

2498 parent_community_id=c_id, 

2499 offline_information=events_pb2.OfflineEventInformation( 

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

2501 lat=1.0, 

2502 lng=2.0, 

2503 ), 

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

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

2506 timezone="UTC", 

2507 ) 

2508 ) 

2509 

2510 event_id = create_res.event_id 

2511 

2512 # measure initial background job queue length 

2513 with session_scope() as session: 

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

2515 job_length_before_update = len(jobs) 

2516 

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

2518 api.UpdateEvent( 

2519 events_pb2.UpdateEventReq( 

2520 event_id=event_id, 

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

2522 should_notify=False, 

2523 ) 

2524 ) 

2525 

2526 with session_scope() as session: 

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

2528 assert len(jobs) == job_length_before_update 

2529 

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

2531 api.UpdateEvent( 

2532 events_pb2.UpdateEventReq( 

2533 event_id=event_id, 

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

2535 should_notify=True, 

2536 ) 

2537 ) 

2538 

2539 with session_scope() as session: 

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

2541 assert len(jobs) == job_length_before_update + 1 

2542 

2543 

2544def test_event_photo_key(db): 

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

2546 user, token = generate_user() 

2547 

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

2549 end_time = start_time + timedelta(hours=3) 

2550 

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

2552 with session_scope() as session: 

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

2554 upload = Upload( 

2555 key="test_event_photo_key_123", 

2556 filename="test_event_photo_key_123.jpg", 

2557 creator_user_id=user.id, 

2558 ) 

2559 session.add(upload) 

2560 

2561 with events_session(token) as api: 

2562 # Create event without photo 

2563 res = api.CreateEvent( 

2564 events_pb2.CreateEventReq( 

2565 title="Event Without Photo", 

2566 content="No photo content.", 

2567 photo_key=None, 

2568 offline_information=events_pb2.OfflineEventInformation( 

2569 address="Near Null Island", 

2570 lat=0.1, 

2571 lng=0.2, 

2572 ), 

2573 start_time=Timestamp_from_datetime(start_time), 

2574 end_time=Timestamp_from_datetime(end_time), 

2575 timezone="UTC", 

2576 ) 

2577 ) 

2578 

2579 assert res.photo_key == "" 

2580 assert res.photo_url == "" 

2581 

2582 # Create event with photo 

2583 res_with_photo = api.CreateEvent( 

2584 events_pb2.CreateEventReq( 

2585 title="Event With Photo", 

2586 content="Has photo content.", 

2587 photo_key="test_event_photo_key_123", 

2588 offline_information=events_pb2.OfflineEventInformation( 

2589 address="Near Null Island", 

2590 lat=0.1, 

2591 lng=0.2, 

2592 ), 

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

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

2595 timezone="UTC", 

2596 ) 

2597 ) 

2598 

2599 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2600 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2601 

2602 event_id = res_with_photo.event_id 

2603 

2604 # Verify photo_key is returned when getting the event 

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

2606 assert get_res.photo_key == "test_event_photo_key_123" 

2607 assert "test_event_photo_key_123" in get_res.photo_url 

2608 

2609 

2610def test_event_created_with_shadowed_visibility(db): 

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

2612 from couchers.models import ModerationState, ModerationVisibility 

2613 

2614 user, token = generate_user() 

2615 

2616 with session_scope() as session: 

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

2618 

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

2620 end_time = start_time + timedelta(hours=3) 

2621 

2622 with events_session(token) as api: 

2623 res = api.CreateEvent( 

2624 events_pb2.CreateEventReq( 

2625 title="Test UMS Event", 

2626 content="UMS content.", 

2627 offline_information=events_pb2.OfflineEventInformation( 

2628 address="Near Null Island", 

2629 lat=0.1, 

2630 lng=0.2, 

2631 ), 

2632 start_time=Timestamp_from_datetime(start_time), 

2633 end_time=Timestamp_from_datetime(end_time), 

2634 timezone="UTC", 

2635 ) 

2636 ) 

2637 event_id = res.event_id 

2638 

2639 with session_scope() as session: 

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

2641 mod_state = session.execute( 

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

2643 ).scalar_one() 

2644 assert mod_state.visibility == ModerationVisibility.shadowed 

2645 

2646 

2647def test_shadowed_event_visible_to_creator_only(db): 

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

2649 user1, token1 = generate_user() 

2650 user2, token2 = generate_user() 

2651 

2652 with session_scope() as session: 

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

2654 

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

2656 end_time = start_time + timedelta(hours=3) 

2657 

2658 with events_session(token1) as api: 

2659 res = api.CreateEvent( 

2660 events_pb2.CreateEventReq( 

2661 title="Shadowed Event", 

2662 content="Content.", 

2663 offline_information=events_pb2.OfflineEventInformation( 

2664 address="Near Null Island", 

2665 lat=0.1, 

2666 lng=0.2, 

2667 ), 

2668 start_time=Timestamp_from_datetime(start_time), 

2669 end_time=Timestamp_from_datetime(end_time), 

2670 timezone="UTC", 

2671 ) 

2672 ) 

2673 event_id = res.event_id 

2674 

2675 # Creator can see it 

2676 with events_session(token1) as api: 

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

2678 assert res.title == "Shadowed Event" 

2679 

2680 # Other user cannot 

2681 with events_session(token2) as api: 

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

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

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

2685 

2686 

2687def test_event_visible_after_approval(db, moderator: Moderator): 

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

2689 user1, token1 = generate_user() 

2690 user2, token2 = generate_user() 

2691 

2692 with session_scope() as session: 

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

2694 

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

2696 end_time = start_time + timedelta(hours=3) 

2697 

2698 with events_session(token1) as api: 

2699 res = api.CreateEvent( 

2700 events_pb2.CreateEventReq( 

2701 title="Approved Event", 

2702 content="Content.", 

2703 offline_information=events_pb2.OfflineEventInformation( 

2704 address="Near Null Island", 

2705 lat=0.1, 

2706 lng=0.2, 

2707 ), 

2708 start_time=Timestamp_from_datetime(start_time), 

2709 end_time=Timestamp_from_datetime(end_time), 

2710 timezone="UTC", 

2711 ) 

2712 ) 

2713 event_id = res.event_id 

2714 

2715 # Other user cannot see it yet 

2716 with events_session(token2) as api: 

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

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

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

2720 

2721 # Approve the event 

2722 moderator.approve_event_occurrence(event_id) 

2723 

2724 # Now other user can see it 

2725 with events_session(token2) as api: 

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

2727 assert res.title == "Approved Event" 

2728 

2729 

2730def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2732 user1, token1 = generate_user() 

2733 user2, token2 = generate_user() 

2734 

2735 with session_scope() as session: 

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

2737 

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

2739 end_time = start_time + timedelta(hours=3) 

2740 

2741 with events_session(token1) as api: 

2742 res = api.CreateEvent( 

2743 events_pb2.CreateEventReq( 

2744 title="List Test Event", 

2745 content="Content.", 

2746 offline_information=events_pb2.OfflineEventInformation( 

2747 address="Near Null Island", 

2748 lat=0.1, 

2749 lng=0.2, 

2750 ), 

2751 start_time=Timestamp_from_datetime(start_time), 

2752 end_time=Timestamp_from_datetime(end_time), 

2753 timezone="UTC", 

2754 ) 

2755 ) 

2756 event_id = res.event_id 

2757 

2758 # Creator can see their own SHADOWED event in lists 

2759 with events_session(token1) as api: 

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

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

2762 assert event_id in event_ids 

2763 

2764 # Other user cannot see the SHADOWED event in lists 

2765 with events_session(token2) as api: 

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

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

2768 assert event_id not in event_ids 

2769 

2770 # After approval, other user can see it 

2771 moderator.approve_event_occurrence(event_id) 

2772 

2773 with events_session(token2) as api: 

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

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

2776 assert event_id in event_ids 

2777 

2778 

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

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

2781 user1, token1 = generate_user() 

2782 user2, token2 = generate_user() 

2783 

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

2785 with session_scope() as session: 

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

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

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

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

2790 

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

2792 end_time = start_time + timedelta(hours=3) 

2793 

2794 with events_session(token1) as api: 

2795 res = api.CreateEvent( 

2796 events_pb2.CreateEventReq( 

2797 title="Deferred Event", 

2798 content="Content.", 

2799 offline_information=events_pb2.OfflineEventInformation( 

2800 address="Near Null Island", 

2801 lat=0.1, 

2802 lng=0.2, 

2803 ), 

2804 start_time=Timestamp_from_datetime(start_time), 

2805 end_time=Timestamp_from_datetime(end_time), 

2806 timezone="UTC", 

2807 ) 

2808 ) 

2809 event_id = res.event_id 

2810 

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

2812 process_jobs() 

2813 

2814 with session_scope() as session: 

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

2816 # Notification was created with moderation_state_id for deferral 

2817 assert notif.moderation_state_id is not None 

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

2819 delivery_count = session.execute( 

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

2821 ).scalar_one_or_none() 

2822 assert delivery_count is None 

2823 

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

2825 moderator.approve_event_occurrence(event_id) 

2826 

2827 # Verify handle_notification job was queued 

2828 with session_scope() as session: 

2829 pending_jobs = ( 

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

2831 .scalars() 

2832 .all() 

2833 ) 

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

2835 

2836 

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

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

2839 user1, token1 = generate_user() 

2840 user2, token2 = generate_user() 

2841 

2842 with session_scope() as session: 

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

2844 

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

2846 end_time = start_time + timedelta(hours=3) 

2847 

2848 with events_session(token1) as api: 

2849 res = api.CreateEvent( 

2850 events_pb2.CreateEventReq( 

2851 title="Update Test", 

2852 content="Content.", 

2853 offline_information=events_pb2.OfflineEventInformation( 

2854 address="Near Null Island", 

2855 lat=0.1, 

2856 lng=0.2, 

2857 ), 

2858 start_time=Timestamp_from_datetime(start_time), 

2859 end_time=Timestamp_from_datetime(end_time), 

2860 timezone="UTC", 

2861 ) 

2862 ) 

2863 event_id = res.event_id 

2864 

2865 moderator.approve_event_occurrence(event_id) 

2866 process_jobs() 

2867 # Clear any create notifications 

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

2869 push_collector.pop_for_user(user2.id) 

2870 

2871 # User2 subscribes to the event 

2872 with events_session(token2) as api: 

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

2874 

2875 # User1 updates the event with should_notify=True 

2876 with events_session(token1) as api: 

2877 api.UpdateEvent( 

2878 events_pb2.UpdateEventReq( 

2879 event_id=event_id, 

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

2881 should_notify=True, 

2882 ) 

2883 ) 

2884 

2885 process_jobs() 

2886 

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

2888 with session_scope() as session: 

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

2890 

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

2892 # Find the update notification (most recent one) 

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

2894 assert len(update_notifs) == 1 

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

2896 

2897 

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

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

2900 user1, token1 = generate_user() 

2901 user2, token2 = generate_user() 

2902 

2903 with session_scope() as session: 

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

2905 

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

2907 end_time = start_time + timedelta(hours=3) 

2908 

2909 with events_session(token1) as api: 

2910 res = api.CreateEvent( 

2911 events_pb2.CreateEventReq( 

2912 title="Cancel Test", 

2913 content="Content.", 

2914 offline_information=events_pb2.OfflineEventInformation( 

2915 address="Near Null Island", 

2916 lat=0.1, 

2917 lng=0.2, 

2918 ), 

2919 start_time=Timestamp_from_datetime(start_time), 

2920 end_time=Timestamp_from_datetime(end_time), 

2921 timezone="UTC", 

2922 ) 

2923 ) 

2924 event_id = res.event_id 

2925 

2926 moderator.approve_event_occurrence(event_id) 

2927 process_jobs() 

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

2929 push_collector.pop_for_user(user2.id) 

2930 

2931 # User2 subscribes 

2932 with events_session(token2) as api: 

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

2934 

2935 # User1 cancels the event 

2936 with events_session(token1) as api: 

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

2938 

2939 process_jobs() 

2940 

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

2942 with session_scope() as session: 

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

2944 

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

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

2947 assert len(cancel_notifs) == 1 

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

2949 

2950 

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

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

2953 user1, token1 = generate_user() 

2954 user2, token2 = generate_user() 

2955 

2956 with session_scope() as session: 

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

2958 

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

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

2961 end_time = start_time + timedelta(hours=1) 

2962 

2963 with events_session(token1) as api: 

2964 res = api.CreateEvent( 

2965 events_pb2.CreateEventReq( 

2966 title="Reminder Test", 

2967 content="Content.", 

2968 offline_information=events_pb2.OfflineEventInformation( 

2969 address="Near Null Island", 

2970 lat=0.1, 

2971 lng=0.2, 

2972 ), 

2973 start_time=Timestamp_from_datetime(start_time), 

2974 end_time=Timestamp_from_datetime(end_time), 

2975 timezone="UTC", 

2976 ) 

2977 ) 

2978 event_id = res.event_id 

2979 

2980 moderator.approve_event_occurrence(event_id) 

2981 process_jobs() 

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

2983 push_collector.pop_for_user(user2.id) 

2984 

2985 # User2 marks attendance 

2986 with events_session(token2) as api: 

2987 api.SetEventAttendance( 

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

2989 ) 

2990 

2991 # Run the event reminder handler 

2992 send_event_reminders(empty_pb2.Empty()) 

2993 process_jobs() 

2994 

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

2996 with session_scope() as session: 

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

2998 

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

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

3001 assert len(reminder_notifs) == 1 

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

3003 

3004 

3005def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3007 user1, token1 = generate_user() 

3008 user2, token2 = generate_user() 

3009 

3010 with session_scope() as session: 

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

3012 

3013 start = now() 

3014 

3015 # User1 creates event A with 3 occurrences 

3016 event_a_ids = [] 

3017 with events_session(token1) as api: 

3018 res = api.CreateEvent( 

3019 events_pb2.CreateEventReq( 

3020 title="Event A", 

3021 content="Content A.", 

3022 parent_community_id=c_id, 

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

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

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

3026 timezone="UTC", 

3027 ) 

3028 ) 

3029 event_a_ids.append(res.event_id) 

3030 for i in range(2): 

3031 res = api.ScheduleEvent( 

3032 events_pb2.ScheduleEventReq( 

3033 event_id=event_a_ids[-1], 

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

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

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

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

3038 timezone="UTC", 

3039 ) 

3040 ) 

3041 event_a_ids.append(res.event_id) 

3042 

3043 # User2 creates event B with 2 occurrences 

3044 event_b_ids = [] 

3045 with events_session(token2) as api: 

3046 res = api.CreateEvent( 

3047 events_pb2.CreateEventReq( 

3048 title="Event B", 

3049 content="Content B.", 

3050 parent_community_id=c_id, 

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

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

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

3054 timezone="UTC", 

3055 ) 

3056 ) 

3057 event_b_ids.append(res.event_id) 

3058 res = api.ScheduleEvent( 

3059 events_pb2.ScheduleEventReq( 

3060 event_id=event_b_ids[-1], 

3061 content="B occurrence 1", 

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

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

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

3065 timezone="UTC", 

3066 ) 

3067 ) 

3068 event_b_ids.append(res.event_id) 

3069 

3070 moderator.approve_event_occurrence(event_a_ids[0]) 

3071 moderator.approve_event_occurrence(event_b_ids[0]) 

3072 

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

3074 with events_session(token1) as api: 

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

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

3077 assert sorted(returned_ids) == sorted(event_a_ids) 

3078 

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

3080 with events_session(token2) as api: 

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

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

3083 assert sorted(returned_ids) == sorted(event_b_ids) 

3084 

3085 

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

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

3088 user1, token1 = generate_user() 

3089 user2, token2 = generate_user() 

3090 

3091 with session_scope() as session: 

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

3093 

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

3095 end_time = start_time + timedelta(hours=3) 

3096 

3097 with events_session(token1) as api: 

3098 res = api.CreateEvent( 

3099 events_pb2.CreateEventReq( 

3100 title="Comment Test", 

3101 content="Content.", 

3102 offline_information=events_pb2.OfflineEventInformation( 

3103 address="Near Null Island", 

3104 lat=0.1, 

3105 lng=0.2, 

3106 ), 

3107 start_time=Timestamp_from_datetime(start_time), 

3108 end_time=Timestamp_from_datetime(end_time), 

3109 timezone="UTC", 

3110 ) 

3111 ) 

3112 event_id = res.event_id 

3113 thread_id = res.thread.thread_id 

3114 

3115 moderator.approve_event_occurrence(event_id) 

3116 process_jobs() 

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

3118 push_collector.pop_for_user(user1.id) 

3119 

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

3121 with events_session(token1) as api: 

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

3123 

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

3125 with threads_session(token2) as api: 

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

3127 

3128 process_jobs() 

3129 

3130 # The comment notification for user1 should have moderation_state_id set 

3131 with session_scope() as session: 

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

3133 

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

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

3136 assert len(comment_notifs) == 1 

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

3138 

3139 

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

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

3142 user1, token1 = generate_user() 

3143 user2, token2 = generate_user() 

3144 user3, token3 = generate_user() 

3145 

3146 with session_scope() as session: 

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

3148 

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

3150 end_time = start_time + timedelta(hours=3) 

3151 

3152 with events_session(token1) as api: 

3153 res = api.CreateEvent( 

3154 events_pb2.CreateEventReq( 

3155 title="Reply Test", 

3156 content="Content.", 

3157 offline_information=events_pb2.OfflineEventInformation( 

3158 address="Near Null Island", 

3159 lat=0.1, 

3160 lng=0.2, 

3161 ), 

3162 start_time=Timestamp_from_datetime(start_time), 

3163 end_time=Timestamp_from_datetime(end_time), 

3164 timezone="UTC", 

3165 ) 

3166 ) 

3167 event_id = res.event_id 

3168 thread_id = res.thread.thread_id 

3169 

3170 moderator.approve_event_occurrence(event_id) 

3171 process_jobs() 

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

3173 push_collector.pop_for_user(user1.id) 

3174 

3175 # User2 posts a top-level comment 

3176 with threads_session(token2) as api: 

3177 comment_thread_id = api.PostReply( 

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

3179 ).thread_id 

3180 

3181 process_jobs() 

3182 while push_collector.count_for_user(user1.id): 

3183 push_collector.pop_for_user(user1.id) 

3184 

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

3186 with threads_session(token3) as api: 

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

3188 

3189 process_jobs() 

3190 

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

3192 with session_scope() as session: 

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

3194 

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

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

3197 assert len(reply_notifs) == 1 

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