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

1549 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 09:29 +0000

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from psycopg.types.range import TimestamptzRange 

7from sqlalchemy import select 

8from sqlalchemy.sql.expression import update 

9 

10from couchers.db import session_scope 

11from couchers.jobs.handlers import send_event_reminders 

12from couchers.models import ( 

13 BackgroundJob, 

14 BackgroundJobState, 

15 Comment, 

16 EventOccurrence, 

17 ModerationState, 

18 ModerationVisibility, 

19 Notification, 

20 NotificationDelivery, 

21 NotificationTopicAction, 

22 Reply, 

23 Upload, 

24 User, 

25) 

26from couchers.proto import editor_pb2, events_pb2, threads_pb2 

27from couchers.tasks import enforce_community_memberships 

28from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime 

29from tests.fixtures.db import generate_user 

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

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

32from tests.test_communities import create_community, create_group 

33 

34 

35@pytest.fixture(autouse=True) 

36def _(testconfig): 

37 pass 

38 

39 

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

41 # test cases: 

42 # can create event 

43 # cannot create event with missing details 

44 # can create online event 

45 # can create in person event 

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

47 # can create in different timezones 

48 

49 # event creator 

50 user1, token1 = generate_user() 

51 # community moderator 

52 user2, token2 = generate_user() 

53 # third party 

54 user3, token3 = generate_user() 

55 

56 with session_scope() as session: 

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

58 

59 time_before = now() 

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

61 end_time = start_time + timedelta(hours=3) 

62 

63 with events_session(token1) as api: 

64 # in person event 

65 res = api.CreateEvent( 

66 events_pb2.CreateEventReq( 

67 title="Dummy Title", 

68 content="Dummy content.", 

69 photo_key=None, 

70 offline_information=events_pb2.OfflineEventInformation( 

71 address="Near Null Island", 

72 lat=0.1, 

73 lng=0.2, 

74 ), 

75 start_time=Timestamp_from_datetime(start_time), 

76 end_time=Timestamp_from_datetime(end_time), 

77 timezone="UTC", 

78 ) 

79 ) 

80 

81 assert res.is_next 

82 assert res.title == "Dummy Title" 

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

84 assert res.content == "Dummy content." 

85 assert not res.photo_url 

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

87 assert res.offline_information.lat == 0.1 

88 assert res.offline_information.lng == 0.2 

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

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

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

92 assert res.creator_user_id == user1.id 

93 assert to_aware_datetime(res.start_time) == start_time 

94 assert to_aware_datetime(res.end_time) == end_time 

95 # assert res.timezone == "UTC" 

96 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

97 assert res.organizer 

98 assert res.subscriber 

99 assert res.going_count == 1 

100 assert res.organizer_count == 1 

101 assert res.subscriber_count == 1 

102 assert res.owner_user_id == user1.id 

103 assert not res.owner_community_id 

104 assert not res.owner_group_id 

105 assert res.thread.thread_id 

106 assert res.can_edit 

107 assert not res.can_moderate 

108 

109 event_id = res.event_id 

110 

111 # Approve the event so other users can see it 

112 moderator.approve_event_occurrence(event_id) 

113 

114 with events_session(token2) as api: 

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

116 

117 assert res.is_next 

118 assert res.title == "Dummy Title" 

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

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

121 assert not res.photo_url 

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

123 assert res.offline_information.lat == 0.1 

124 assert res.offline_information.lng == 0.2 

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

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

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

128 assert res.creator_user_id == user1.id 

129 assert to_aware_datetime(res.start_time) == start_time 

130 assert to_aware_datetime(res.end_time) == end_time 

131 # assert res.timezone == "UTC" 

132 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

133 assert not res.organizer 

134 assert not res.subscriber 

135 assert res.going_count == 1 

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.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

164 assert not res.organizer 

165 assert not res.subscriber 

166 assert res.going_count == 1 

167 assert res.organizer_count == 1 

168 assert res.subscriber_count == 1 

169 assert res.owner_user_id == user1.id 

170 assert not res.owner_community_id 

171 assert not res.owner_group_id 

172 assert res.thread.thread_id 

173 assert not res.can_edit 

174 assert not res.can_moderate 

175 

176 with events_session(token1) as api: 

177 # online only event 

178 res = api.CreateEvent( 

179 events_pb2.CreateEventReq( 

180 title="Dummy Title", 

181 content="Dummy content.", 

182 photo_key=None, 

183 online_information=events_pb2.OnlineEventInformation( 

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

185 ), 

186 parent_community_id=c_id, 

187 start_time=Timestamp_from_datetime(start_time), 

188 end_time=Timestamp_from_datetime(end_time), 

189 timezone="UTC", 

190 ) 

191 ) 

192 

193 assert res.is_next 

194 assert res.title == "Dummy Title" 

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

196 assert res.content == "Dummy content." 

197 assert not res.photo_url 

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

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

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

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

202 assert res.creator_user_id == user1.id 

203 assert to_aware_datetime(res.start_time) == start_time 

204 assert to_aware_datetime(res.end_time) == end_time 

205 # assert res.timezone == "UTC" 

206 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

207 assert res.organizer 

208 assert res.subscriber 

209 assert res.going_count == 1 

210 assert res.organizer_count == 1 

211 assert res.subscriber_count == 1 

212 assert res.owner_user_id == user1.id 

213 assert not res.owner_community_id 

214 assert not res.owner_group_id 

215 assert res.thread.thread_id 

216 assert res.can_edit 

217 assert not res.can_moderate 

218 

219 event_id = res.event_id 

220 

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

222 moderator.approve_event_occurrence(event_id) 

223 

224 with events_session(token2) as api: 

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

226 

227 assert res.is_next 

228 assert res.title == "Dummy Title" 

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

230 assert res.content == "Dummy content." 

231 assert not res.photo_url 

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

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

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

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

236 assert res.creator_user_id == user1.id 

237 assert to_aware_datetime(res.start_time) == start_time 

238 assert to_aware_datetime(res.end_time) == end_time 

239 # assert res.timezone == "UTC" 

240 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

241 assert not res.organizer 

242 assert not res.subscriber 

243 assert res.going_count == 1 

244 assert res.organizer_count == 1 

245 assert res.subscriber_count == 1 

246 assert res.owner_user_id == user1.id 

247 assert not res.owner_community_id 

248 assert not res.owner_group_id 

249 assert res.thread.thread_id 

250 assert res.can_edit 

251 assert res.can_moderate 

252 

253 with events_session(token3) as api: 

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

255 

256 assert res.is_next 

257 assert res.title == "Dummy Title" 

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

259 assert res.content == "Dummy content." 

260 assert not res.photo_url 

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

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

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

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

265 assert res.creator_user_id == user1.id 

266 assert to_aware_datetime(res.start_time) == start_time 

267 assert to_aware_datetime(res.end_time) == end_time 

268 # assert res.timezone == "UTC" 

269 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

270 assert not res.organizer 

271 assert not res.subscriber 

272 assert res.going_count == 1 

273 assert res.organizer_count == 1 

274 assert res.subscriber_count == 1 

275 assert res.owner_user_id == user1.id 

276 assert not res.owner_community_id 

277 assert not res.owner_group_id 

278 assert res.thread.thread_id 

279 assert not res.can_edit 

280 assert not res.can_moderate 

281 

282 with events_session(token1) as api: 

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

284 api.CreateEvent( 

285 events_pb2.CreateEventReq( 

286 title="Dummy Title", 

287 content="Dummy content.", 

288 photo_key=None, 

289 online_information=events_pb2.OnlineEventInformation( 

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

291 ), 

292 start_time=Timestamp_from_datetime(start_time), 

293 end_time=Timestamp_from_datetime(end_time), 

294 timezone="UTC", 

295 ) 

296 ) 

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

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

299 

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

301 api.CreateEvent( 

302 events_pb2.CreateEventReq( 

303 # title="Dummy Title", 

304 content="Dummy content.", 

305 photo_key=None, 

306 offline_information=events_pb2.OfflineEventInformation( 

307 address="Near Null Island", 

308 lat=0.1, 

309 lng=0.1, 

310 ), 

311 start_time=Timestamp_from_datetime(start_time), 

312 end_time=Timestamp_from_datetime(end_time), 

313 timezone="UTC", 

314 ) 

315 ) 

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

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

318 

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

320 api.CreateEvent( 

321 events_pb2.CreateEventReq( 

322 title="Dummy Title", 

323 # content="Dummy content.", 

324 photo_key=None, 

325 offline_information=events_pb2.OfflineEventInformation( 

326 address="Near Null Island", 

327 lat=0.1, 

328 lng=0.1, 

329 ), 

330 start_time=Timestamp_from_datetime(start_time), 

331 end_time=Timestamp_from_datetime(end_time), 

332 timezone="UTC", 

333 ) 

334 ) 

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

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

337 

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

339 api.CreateEvent( 

340 events_pb2.CreateEventReq( 

341 title="Dummy Title", 

342 content="Dummy content.", 

343 photo_key="nonexistent", 

344 offline_information=events_pb2.OfflineEventInformation( 

345 address="Near Null Island", 

346 lat=0.1, 

347 lng=0.1, 

348 ), 

349 start_time=Timestamp_from_datetime(start_time), 

350 end_time=Timestamp_from_datetime(end_time), 

351 timezone="UTC", 

352 ) 

353 ) 

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

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

356 

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

358 api.CreateEvent( 

359 events_pb2.CreateEventReq( 

360 title="Dummy Title", 

361 content="Dummy content.", 

362 photo_key=None, 

363 offline_information=events_pb2.OfflineEventInformation( 

364 address="Near Null Island", 

365 ), 

366 start_time=Timestamp_from_datetime(start_time), 

367 end_time=Timestamp_from_datetime(end_time), 

368 timezone="UTC", 

369 ) 

370 ) 

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

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

373 

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

375 api.CreateEvent( 

376 events_pb2.CreateEventReq( 

377 title="Dummy Title", 

378 content="Dummy content.", 

379 photo_key=None, 

380 offline_information=events_pb2.OfflineEventInformation( 

381 lat=0.1, 

382 lng=0.1, 

383 ), 

384 start_time=Timestamp_from_datetime(start_time), 

385 end_time=Timestamp_from_datetime(end_time), 

386 timezone="UTC", 

387 ) 

388 ) 

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

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

391 

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

393 api.CreateEvent( 

394 events_pb2.CreateEventReq( 

395 title="Dummy Title", 

396 content="Dummy content.", 

397 photo_key=None, 

398 online_information=events_pb2.OnlineEventInformation(), 

399 start_time=Timestamp_from_datetime(start_time), 

400 end_time=Timestamp_from_datetime(end_time), 

401 timezone="UTC", 

402 ) 

403 ) 

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

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

406 

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

408 api.CreateEvent( 

409 events_pb2.CreateEventReq( 

410 title="Dummy Title", 

411 content="Dummy content.", 

412 parent_community_id=c_id, 

413 online_information=events_pb2.OnlineEventInformation( 

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

415 ), 

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

417 end_time=Timestamp_from_datetime(end_time), 

418 timezone="UTC", 

419 ) 

420 ) 

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

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

423 

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

425 api.CreateEvent( 

426 events_pb2.CreateEventReq( 

427 title="Dummy Title", 

428 content="Dummy content.", 

429 parent_community_id=c_id, 

430 online_information=events_pb2.OnlineEventInformation( 

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

432 ), 

433 start_time=Timestamp_from_datetime(end_time), 

434 end_time=Timestamp_from_datetime(start_time), 

435 timezone="UTC", 

436 ) 

437 ) 

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

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

440 

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

442 api.CreateEvent( 

443 events_pb2.CreateEventReq( 

444 title="Dummy Title", 

445 content="Dummy content.", 

446 parent_community_id=c_id, 

447 online_information=events_pb2.OnlineEventInformation( 

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

449 ), 

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

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

452 timezone="UTC", 

453 ) 

454 ) 

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

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

457 

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

459 api.CreateEvent( 

460 events_pb2.CreateEventReq( 

461 title="Dummy Title", 

462 content="Dummy content.", 

463 parent_community_id=c_id, 

464 online_information=events_pb2.OnlineEventInformation( 

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

466 ), 

467 start_time=Timestamp_from_datetime(start_time), 

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

469 timezone="UTC", 

470 ) 

471 ) 

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

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

474 

475 

476def test_CreateEvent_incomplete_profile(db): 

477 user1, token1 = generate_user(complete_profile=False) 

478 user2, token2 = generate_user() 

479 

480 with session_scope() as session: 

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

482 

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

484 end_time = start_time + timedelta(hours=3) 

485 

486 with events_session(token1) as api: 

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

488 api.CreateEvent( 

489 events_pb2.CreateEventReq( 

490 title="Dummy Title", 

491 content="Dummy content.", 

492 photo_key=None, 

493 offline_information=events_pb2.OfflineEventInformation( 

494 address="Near Null Island", 

495 lat=0.1, 

496 lng=0.2, 

497 ), 

498 start_time=Timestamp_from_datetime(start_time), 

499 end_time=Timestamp_from_datetime(end_time), 

500 timezone="UTC", 

501 ) 

502 ) 

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

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

505 

506 

507def test_ScheduleEvent(db): 

508 # test cases: 

509 # can schedule a new event occurrence 

510 

511 user, token = generate_user() 

512 

513 with session_scope() as session: 

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

515 

516 time_before = now() 

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

518 end_time = start_time + timedelta(hours=3) 

519 

520 with events_session(token) as api: 

521 res = api.CreateEvent( 

522 events_pb2.CreateEventReq( 

523 title="Dummy Title", 

524 content="Dummy content.", 

525 parent_community_id=c_id, 

526 online_information=events_pb2.OnlineEventInformation( 

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

528 ), 

529 start_time=Timestamp_from_datetime(start_time), 

530 end_time=Timestamp_from_datetime(end_time), 

531 timezone="UTC", 

532 ) 

533 ) 

534 

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

536 new_end_time = new_start_time + timedelta(hours=2) 

537 

538 res = api.ScheduleEvent( 

539 events_pb2.ScheduleEventReq( 

540 event_id=res.event_id, 

541 content="New event occurrence", 

542 offline_information=events_pb2.OfflineEventInformation( 

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

544 lat=0.3, 

545 lng=0.2, 

546 ), 

547 start_time=Timestamp_from_datetime(new_start_time), 

548 end_time=Timestamp_from_datetime(new_end_time), 

549 timezone="UTC", 

550 ) 

551 ) 

552 

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

554 

555 assert not res.is_next 

556 assert res.title == "Dummy Title" 

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

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

559 assert not res.photo_url 

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

561 assert res.offline_information.lat == 0.3 

562 assert res.offline_information.lng == 0.2 

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

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

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

566 assert res.creator_user_id == user.id 

567 assert to_aware_datetime(res.start_time) == new_start_time 

568 assert to_aware_datetime(res.end_time) == new_end_time 

569 # assert res.timezone == "UTC" 

570 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

571 assert res.organizer 

572 assert res.subscriber 

573 assert res.going_count == 1 

574 assert res.organizer_count == 1 

575 assert res.subscriber_count == 1 

576 assert res.owner_user_id == user.id 

577 assert not res.owner_community_id 

578 assert not res.owner_group_id 

579 assert res.thread.thread_id 

580 assert res.can_edit 

581 assert res.can_moderate 

582 

583 

584def test_cannot_overlap_occurrences_schedule(db): 

585 user, token = generate_user() 

586 

587 with session_scope() as session: 

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

589 

590 start = now() 

591 

592 with events_session(token) as api: 

593 res = api.CreateEvent( 

594 events_pb2.CreateEventReq( 

595 title="Dummy Title", 

596 content="Dummy content.", 

597 parent_community_id=c_id, 

598 online_information=events_pb2.OnlineEventInformation( 

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

600 ), 

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

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

603 timezone="UTC", 

604 ) 

605 ) 

606 

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

608 api.ScheduleEvent( 

609 events_pb2.ScheduleEventReq( 

610 event_id=res.event_id, 

611 content="New event occurrence", 

612 offline_information=events_pb2.OfflineEventInformation( 

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

614 lat=0.3, 

615 lng=0.2, 

616 ), 

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

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

619 timezone="UTC", 

620 ) 

621 ) 

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

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

624 

625 

626def test_cannot_overlap_occurrences_update(db): 

627 user, token = generate_user() 

628 

629 with session_scope() as session: 

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

631 

632 start = now() 

633 

634 with events_session(token) as api: 

635 res = api.CreateEvent( 

636 events_pb2.CreateEventReq( 

637 title="Dummy Title", 

638 content="Dummy content.", 

639 parent_community_id=c_id, 

640 online_information=events_pb2.OnlineEventInformation( 

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

642 ), 

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

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

645 timezone="UTC", 

646 ) 

647 ) 

648 

649 event_id = api.ScheduleEvent( 

650 events_pb2.ScheduleEventReq( 

651 event_id=res.event_id, 

652 content="New event occurrence", 

653 offline_information=events_pb2.OfflineEventInformation( 

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

655 lat=0.3, 

656 lng=0.2, 

657 ), 

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

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

660 timezone="UTC", 

661 ) 

662 ).event_id 

663 

664 # can overlap with this current existing occurrence 

665 api.UpdateEvent( 

666 events_pb2.UpdateEventReq( 

667 event_id=event_id, 

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

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

670 ) 

671 ) 

672 

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

674 api.UpdateEvent( 

675 events_pb2.UpdateEventReq( 

676 event_id=event_id, 

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

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

679 ) 

680 ) 

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

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

683 

684 

685def test_UpdateEvent_single(db, moderator: Moderator): 

686 # test cases: 

687 # owner can update 

688 # community owner can update 

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

690 # notifies attendees 

691 

692 # event creator 

693 user1, token1 = generate_user() 

694 # community moderator 

695 user2, token2 = generate_user() 

696 # third parties 

697 user3, token3 = generate_user() 

698 user4, token4 = generate_user() 

699 user5, token5 = generate_user() 

700 user6, token6 = generate_user() 

701 

702 with session_scope() as session: 

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

704 

705 time_before = now() 

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

707 end_time = start_time + timedelta(hours=3) 

708 

709 with events_session(token1) as api: 

710 res = api.CreateEvent( 

711 events_pb2.CreateEventReq( 

712 title="Dummy Title", 

713 content="Dummy content.", 

714 offline_information=events_pb2.OfflineEventInformation( 

715 address="Near Null Island", 

716 lat=0.1, 

717 lng=0.2, 

718 ), 

719 start_time=Timestamp_from_datetime(start_time), 

720 end_time=Timestamp_from_datetime(end_time), 

721 timezone="UTC", 

722 ) 

723 ) 

724 

725 event_id = res.event_id 

726 

727 moderator.approve_event_occurrence(event_id) 

728 

729 with events_session(token4) as api: 

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

731 

732 with events_session(token5) as api: 

733 api.SetEventAttendance( 

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

735 ) 

736 

737 with events_session(token6) as api: 

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

739 

740 time_before_update = now() 

741 

742 with events_session(token1) as api: 

743 res = api.UpdateEvent( 

744 events_pb2.UpdateEventReq( 

745 event_id=event_id, 

746 ) 

747 ) 

748 

749 with events_session(token1) as api: 

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

751 

752 assert res.is_next 

753 assert res.title == "Dummy Title" 

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

755 assert res.content == "Dummy content." 

756 assert not res.photo_url 

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

758 assert res.offline_information.lat == 0.1 

759 assert res.offline_information.lng == 0.2 

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

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

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

763 assert res.creator_user_id == user1.id 

764 assert to_aware_datetime(res.start_time) == start_time 

765 assert to_aware_datetime(res.end_time) == end_time 

766 # assert res.timezone == "UTC" 

767 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

768 assert res.organizer 

769 assert res.subscriber 

770 assert res.going_count == 2 

771 assert res.organizer_count == 1 

772 assert res.subscriber_count == 3 

773 assert res.owner_user_id == user1.id 

774 assert not res.owner_community_id 

775 assert not res.owner_group_id 

776 assert res.thread.thread_id 

777 assert res.can_edit 

778 assert not res.can_moderate 

779 

780 with events_session(token2) as api: 

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

782 

783 assert res.is_next 

784 assert res.title == "Dummy Title" 

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

786 assert res.content == "Dummy content." 

787 assert not res.photo_url 

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

789 assert res.offline_information.lat == 0.1 

790 assert res.offline_information.lng == 0.2 

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

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

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

794 assert res.creator_user_id == user1.id 

795 assert to_aware_datetime(res.start_time) == start_time 

796 assert to_aware_datetime(res.end_time) == end_time 

797 # assert res.timezone == "UTC" 

798 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

799 assert not res.organizer 

800 assert not res.subscriber 

801 assert res.going_count == 2 

802 assert res.organizer_count == 1 

803 assert res.subscriber_count == 3 

804 assert res.owner_user_id == user1.id 

805 assert not res.owner_community_id 

806 assert not res.owner_group_id 

807 assert res.thread.thread_id 

808 assert res.can_edit 

809 assert res.can_moderate 

810 

811 with events_session(token3) as api: 

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

813 

814 assert res.is_next 

815 assert res.title == "Dummy Title" 

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

817 assert res.content == "Dummy content." 

818 assert not res.photo_url 

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

820 assert res.offline_information.lat == 0.1 

821 assert res.offline_information.lng == 0.2 

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

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

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

825 assert res.creator_user_id == user1.id 

826 assert to_aware_datetime(res.start_time) == start_time 

827 assert to_aware_datetime(res.end_time) == end_time 

828 # assert res.timezone == "UTC" 

829 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

830 assert not res.organizer 

831 assert not res.subscriber 

832 assert res.going_count == 2 

833 assert res.organizer_count == 1 

834 assert res.subscriber_count == 3 

835 assert res.owner_user_id == user1.id 

836 assert not res.owner_community_id 

837 assert not res.owner_group_id 

838 assert res.thread.thread_id 

839 assert not res.can_edit 

840 assert not res.can_moderate 

841 

842 with events_session(token1) as api: 

843 res = api.UpdateEvent( 

844 events_pb2.UpdateEventReq( 

845 event_id=event_id, 

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

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

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

849 start_time=Timestamp_from_datetime(start_time), 

850 end_time=Timestamp_from_datetime(end_time), 

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

852 ) 

853 ) 

854 

855 assert res.is_next 

856 assert res.title == "Dummy Title" 

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

858 assert res.content == "Dummy content." 

859 assert not res.photo_url 

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

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

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

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

864 assert res.creator_user_id == user1.id 

865 assert to_aware_datetime(res.start_time) == start_time 

866 assert to_aware_datetime(res.end_time) == end_time 

867 # assert res.timezone == "UTC" 

868 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

869 assert res.organizer 

870 assert res.subscriber 

871 assert res.going_count == 2 

872 assert res.organizer_count == 1 

873 assert res.subscriber_count == 3 

874 assert res.owner_user_id == user1.id 

875 assert not res.owner_community_id 

876 assert not res.owner_group_id 

877 assert res.thread.thread_id 

878 assert res.can_edit 

879 assert not res.can_moderate 

880 

881 event_id = res.event_id 

882 

883 with events_session(token2) as api: 

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

885 

886 assert res.is_next 

887 assert res.title == "Dummy Title" 

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

889 assert res.content == "Dummy content." 

890 assert not res.photo_url 

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

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

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

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

895 assert res.creator_user_id == user1.id 

896 assert to_aware_datetime(res.start_time) == start_time 

897 assert to_aware_datetime(res.end_time) == end_time 

898 # assert res.timezone == "UTC" 

899 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

900 assert not res.organizer 

901 assert not res.subscriber 

902 assert res.going_count == 2 

903 assert res.organizer_count == 1 

904 assert res.subscriber_count == 3 

905 assert res.owner_user_id == user1.id 

906 assert not res.owner_community_id 

907 assert not res.owner_group_id 

908 assert res.thread.thread_id 

909 assert res.can_edit 

910 assert res.can_moderate 

911 

912 with events_session(token3) as api: 

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

914 

915 assert res.is_next 

916 assert res.title == "Dummy Title" 

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

918 assert res.content == "Dummy content." 

919 assert not res.photo_url 

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

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

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

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

924 assert res.creator_user_id == user1.id 

925 assert to_aware_datetime(res.start_time) == start_time 

926 assert to_aware_datetime(res.end_time) == end_time 

927 # assert res.timezone == "UTC" 

928 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

929 assert not res.organizer 

930 assert not res.subscriber 

931 assert res.going_count == 2 

932 assert res.organizer_count == 1 

933 assert res.subscriber_count == 3 

934 assert res.owner_user_id == user1.id 

935 assert not res.owner_community_id 

936 assert not res.owner_group_id 

937 assert res.thread.thread_id 

938 assert not res.can_edit 

939 assert not res.can_moderate 

940 

941 with events_session(token1) as api: 

942 res = api.UpdateEvent( 

943 events_pb2.UpdateEventReq( 

944 event_id=event_id, 

945 offline_information=events_pb2.OfflineEventInformation( 

946 address="Near Null Island", 

947 lat=0.1, 

948 lng=0.2, 

949 ), 

950 ) 

951 ) 

952 

953 with events_session(token3) as api: 

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

955 

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

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

958 assert res.offline_information.lat == 0.1 

959 assert res.offline_information.lng == 0.2 

960 

961 

962def test_UpdateEvent_all(db, moderator: Moderator): 

963 # event creator 

964 user1, token1 = generate_user() 

965 # community moderator 

966 user2, token2 = generate_user() 

967 # third parties 

968 user3, token3 = generate_user() 

969 user4, token4 = generate_user() 

970 user5, token5 = generate_user() 

971 user6, token6 = generate_user() 

972 

973 with session_scope() as session: 

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

975 

976 time_before = now() 

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

978 end_time = start_time + timedelta(hours=1.5) 

979 

980 event_ids = [] 

981 

982 with events_session(token1) as api: 

983 res = api.CreateEvent( 

984 events_pb2.CreateEventReq( 

985 title="Dummy Title", 

986 content="0th occurrence", 

987 offline_information=events_pb2.OfflineEventInformation( 

988 address="Near Null Island", 

989 lat=0.1, 

990 lng=0.2, 

991 ), 

992 start_time=Timestamp_from_datetime(start_time), 

993 end_time=Timestamp_from_datetime(end_time), 

994 timezone="UTC", 

995 ) 

996 ) 

997 

998 event_id = res.event_id 

999 event_ids.append(event_id) 

1000 

1001 moderator.approve_event_occurrence(event_id) 

1002 

1003 with events_session(token4) as api: 

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

1005 

1006 with events_session(token5) as api: 

1007 api.SetEventAttendance( 

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

1009 ) 

1010 

1011 with events_session(token6) as api: 

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

1013 

1014 with events_session(token1) as api: 

1015 for i in range(5): 

1016 res = api.ScheduleEvent( 

1017 events_pb2.ScheduleEventReq( 

1018 event_id=event_ids[-1], 

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

1020 online_information=events_pb2.OnlineEventInformation( 

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

1022 ), 

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

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

1025 timezone="UTC", 

1026 ) 

1027 ) 

1028 

1029 event_ids.append(res.event_id) 

1030 

1031 # Approve all scheduled occurrences 

1032 for eid in event_ids[1:]: 

1033 moderator.approve_event_occurrence(eid) 

1034 

1035 updated_event_id = event_ids[3] 

1036 

1037 time_before_update = now() 

1038 

1039 with events_session(token1) as api: 

1040 res = api.UpdateEvent( 

1041 events_pb2.UpdateEventReq( 

1042 event_id=updated_event_id, 

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

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

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

1046 update_all_future=True, 

1047 ) 

1048 ) 

1049 

1050 time_after_update = now() 

1051 

1052 with events_session(token2) as api: 

1053 for i in range(3): 

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

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

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

1057 

1058 for i in range(3, 6): 

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

1060 assert res.content == "New content." 

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

1062 

1063 

1064def test_GetEvent(db, moderator: Moderator): 

1065 # event creator 

1066 user1, token1 = generate_user() 

1067 # community moderator 

1068 user2, token2 = generate_user() 

1069 # third parties 

1070 user3, token3 = generate_user() 

1071 user4, token4 = generate_user() 

1072 user5, token5 = generate_user() 

1073 user6, token6 = generate_user() 

1074 

1075 with session_scope() as session: 

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

1077 

1078 time_before = now() 

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

1080 end_time = start_time + timedelta(hours=3) 

1081 

1082 with events_session(token1) as api: 

1083 # in person event 

1084 res = api.CreateEvent( 

1085 events_pb2.CreateEventReq( 

1086 title="Dummy Title", 

1087 content="Dummy content.", 

1088 offline_information=events_pb2.OfflineEventInformation( 

1089 address="Near Null Island", 

1090 lat=0.1, 

1091 lng=0.2, 

1092 ), 

1093 start_time=Timestamp_from_datetime(start_time), 

1094 end_time=Timestamp_from_datetime(end_time), 

1095 timezone="UTC", 

1096 ) 

1097 ) 

1098 

1099 event_id = res.event_id 

1100 

1101 moderator.approve_event_occurrence(event_id) 

1102 

1103 with events_session(token4) as api: 

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

1105 

1106 with events_session(token5) as api: 

1107 api.SetEventAttendance( 

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

1109 ) 

1110 

1111 with events_session(token6) as api: 

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

1113 

1114 with events_session(token1) as api: 

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

1116 

1117 assert res.is_next 

1118 assert res.title == "Dummy Title" 

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

1120 assert res.content == "Dummy content." 

1121 assert not res.photo_url 

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

1123 assert res.offline_information.lat == 0.1 

1124 assert res.offline_information.lng == 0.2 

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

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

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

1128 assert res.creator_user_id == user1.id 

1129 assert to_aware_datetime(res.start_time) == start_time 

1130 assert to_aware_datetime(res.end_time) == end_time 

1131 # assert res.timezone == "UTC" 

1132 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

1133 assert res.organizer 

1134 assert res.subscriber 

1135 assert res.going_count == 2 

1136 assert res.organizer_count == 1 

1137 assert res.subscriber_count == 3 

1138 assert res.owner_user_id == user1.id 

1139 assert not res.owner_community_id 

1140 assert not res.owner_group_id 

1141 assert res.thread.thread_id 

1142 assert res.can_edit 

1143 assert not res.can_moderate 

1144 

1145 with events_session(token2) as api: 

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

1147 

1148 assert res.is_next 

1149 assert res.title == "Dummy Title" 

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

1151 assert res.content == "Dummy content." 

1152 assert not res.photo_url 

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

1154 assert res.offline_information.lat == 0.1 

1155 assert res.offline_information.lng == 0.2 

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

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

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

1159 assert res.creator_user_id == user1.id 

1160 assert to_aware_datetime(res.start_time) == start_time 

1161 assert to_aware_datetime(res.end_time) == end_time 

1162 # assert res.timezone == "UTC" 

1163 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1164 assert not res.organizer 

1165 assert not res.subscriber 

1166 assert res.going_count == 2 

1167 assert res.organizer_count == 1 

1168 assert res.subscriber_count == 3 

1169 assert res.owner_user_id == user1.id 

1170 assert not res.owner_community_id 

1171 assert not res.owner_group_id 

1172 assert res.thread.thread_id 

1173 assert res.can_edit 

1174 assert res.can_moderate 

1175 

1176 with events_session(token3) as api: 

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

1178 

1179 assert res.is_next 

1180 assert res.title == "Dummy Title" 

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

1182 assert res.content == "Dummy content." 

1183 assert not res.photo_url 

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

1185 assert res.offline_information.lat == 0.1 

1186 assert res.offline_information.lng == 0.2 

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

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

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

1190 assert res.creator_user_id == user1.id 

1191 assert to_aware_datetime(res.start_time) == start_time 

1192 assert to_aware_datetime(res.end_time) == end_time 

1193 # assert res.timezone == "UTC" 

1194 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1195 assert not res.organizer 

1196 assert not res.subscriber 

1197 assert res.going_count == 2 

1198 assert res.organizer_count == 1 

1199 assert res.subscriber_count == 3 

1200 assert res.owner_user_id == user1.id 

1201 assert not res.owner_community_id 

1202 assert not res.owner_group_id 

1203 assert res.thread.thread_id 

1204 assert not res.can_edit 

1205 assert not res.can_moderate 

1206 

1207 

1208def test_CancelEvent(db, moderator: Moderator): 

1209 # event creator 

1210 user1, token1 = generate_user() 

1211 # community moderator 

1212 user2, token2 = generate_user() 

1213 # third parties 

1214 user3, token3 = generate_user() 

1215 user4, token4 = generate_user() 

1216 user5, token5 = generate_user() 

1217 user6, token6 = generate_user() 

1218 

1219 with session_scope() as session: 

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

1221 

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

1223 end_time = start_time + timedelta(hours=3) 

1224 

1225 with events_session(token1) as api: 

1226 res = api.CreateEvent( 

1227 events_pb2.CreateEventReq( 

1228 title="Dummy Title", 

1229 content="Dummy content.", 

1230 offline_information=events_pb2.OfflineEventInformation( 

1231 address="Near Null Island", 

1232 lat=0.1, 

1233 lng=0.2, 

1234 ), 

1235 start_time=Timestamp_from_datetime(start_time), 

1236 end_time=Timestamp_from_datetime(end_time), 

1237 timezone="UTC", 

1238 ) 

1239 ) 

1240 

1241 event_id = res.event_id 

1242 

1243 moderator.approve_event_occurrence(event_id) 

1244 

1245 with events_session(token4) as api: 

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

1247 

1248 with events_session(token5) as api: 

1249 api.SetEventAttendance( 

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

1251 ) 

1252 

1253 with events_session(token6) as api: 

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

1255 

1256 with events_session(token1) as api: 

1257 res = api.CancelEvent( 

1258 events_pb2.CancelEventReq( 

1259 event_id=event_id, 

1260 ) 

1261 ) 

1262 

1263 with events_session(token1) as api: 

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

1265 assert res.is_cancelled 

1266 

1267 with events_session(token1) as api: 

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

1269 api.UpdateEvent( 

1270 events_pb2.UpdateEventReq( 

1271 event_id=event_id, 

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

1273 ) 

1274 ) 

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

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

1277 

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

1279 api.InviteEventOrganizer( 

1280 events_pb2.InviteEventOrganizerReq( 

1281 event_id=event_id, 

1282 user_id=user3.id, 

1283 ) 

1284 ) 

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

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

1287 

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

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

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

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

1292 

1293 with events_session(token3) as api: 

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

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

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

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

1298 

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

1300 api.SetEventAttendance( 

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

1302 ) 

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

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

1305 

1306 with events_session(token1) as api: 

1307 for include_cancelled in [True, False]: 

1308 res = api.ListEventOccurrences( 

1309 events_pb2.ListEventOccurrencesReq( 

1310 event_id=event_id, 

1311 include_cancelled=include_cancelled, 

1312 ) 

1313 ) 

1314 if include_cancelled: 

1315 assert len(res.events) > 0 

1316 else: 

1317 assert len(res.events) == 0 

1318 

1319 res = api.ListMyEvents( 

1320 events_pb2.ListMyEventsReq( 

1321 include_cancelled=include_cancelled, 

1322 ) 

1323 ) 

1324 if include_cancelled: 

1325 assert len(res.events) > 0 

1326 else: 

1327 assert len(res.events) == 0 

1328 

1329 

1330def test_ListEventAttendees(db, moderator: Moderator): 

1331 # event creator 

1332 user1, token1 = generate_user() 

1333 # others 

1334 user2, token2 = generate_user() 

1335 user3, token3 = generate_user() 

1336 user4, token4 = generate_user() 

1337 user5, token5 = generate_user() 

1338 user6, token6 = generate_user() 

1339 

1340 with session_scope() as session: 

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

1342 

1343 with events_session(token1) as api: 

1344 event_id = api.CreateEvent( 

1345 events_pb2.CreateEventReq( 

1346 title="Dummy Title", 

1347 content="Dummy content.", 

1348 offline_information=events_pb2.OfflineEventInformation( 

1349 address="Near Null Island", 

1350 lat=0.1, 

1351 lng=0.2, 

1352 ), 

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

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

1355 timezone="UTC", 

1356 ) 

1357 ).event_id 

1358 

1359 moderator.approve_event_occurrence(event_id) 

1360 

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

1362 with events_session(token) as api: 

1363 api.SetEventAttendance( 

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

1365 ) 

1366 

1367 with events_session(token6) as api: 

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

1369 

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

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

1372 

1373 res = api.ListEventAttendees( 

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

1375 ) 

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

1377 

1378 res = api.ListEventAttendees( 

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

1380 ) 

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

1382 assert not res.next_page_token 

1383 

1384 

1385def test_ListEventSubscribers(db, moderator: Moderator): 

1386 # event creator 

1387 user1, token1 = generate_user() 

1388 # others 

1389 user2, token2 = generate_user() 

1390 user3, token3 = generate_user() 

1391 user4, token4 = generate_user() 

1392 user5, token5 = generate_user() 

1393 user6, token6 = generate_user() 

1394 

1395 with session_scope() as session: 

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

1397 

1398 with events_session(token1) as api: 

1399 event_id = api.CreateEvent( 

1400 events_pb2.CreateEventReq( 

1401 title="Dummy Title", 

1402 content="Dummy content.", 

1403 offline_information=events_pb2.OfflineEventInformation( 

1404 address="Near Null Island", 

1405 lat=0.1, 

1406 lng=0.2, 

1407 ), 

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

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

1410 timezone="UTC", 

1411 ) 

1412 ).event_id 

1413 

1414 moderator.approve_event_occurrence(event_id) 

1415 

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

1417 with events_session(token) as api: 

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

1419 

1420 with events_session(token6) as api: 

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

1422 

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

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

1425 

1426 res = api.ListEventSubscribers( 

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

1428 ) 

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

1430 

1431 res = api.ListEventSubscribers( 

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

1433 ) 

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

1435 assert not res.next_page_token 

1436 

1437 

1438def test_ListEventOrganizers(db, moderator: Moderator): 

1439 # event creator 

1440 user1, token1 = generate_user() 

1441 # others 

1442 user2, token2 = generate_user() 

1443 user3, token3 = generate_user() 

1444 user4, token4 = generate_user() 

1445 user5, token5 = generate_user() 

1446 user6, token6 = generate_user() 

1447 

1448 with session_scope() as session: 

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

1450 

1451 with events_session(token1) as api: 

1452 event_id = api.CreateEvent( 

1453 events_pb2.CreateEventReq( 

1454 title="Dummy Title", 

1455 content="Dummy content.", 

1456 offline_information=events_pb2.OfflineEventInformation( 

1457 address="Near Null Island", 

1458 lat=0.1, 

1459 lng=0.2, 

1460 ), 

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

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

1463 timezone="UTC", 

1464 ) 

1465 ).event_id 

1466 

1467 moderator.approve_event_occurrence(event_id) 

1468 

1469 with events_session(token1) as api: 

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

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

1472 

1473 with events_session(token6) as api: 

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

1475 

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

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

1478 

1479 res = api.ListEventOrganizers( 

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

1481 ) 

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

1483 

1484 res = api.ListEventOrganizers( 

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

1486 ) 

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

1488 assert not res.next_page_token 

1489 

1490 

1491def test_TransferEvent(db): 

1492 user1, token1 = generate_user() 

1493 user2, token2 = generate_user() 

1494 user3, token3 = generate_user() 

1495 user4, token4 = generate_user() 

1496 

1497 with session_scope() as session: 

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

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

1500 c_id = c.id 

1501 h_id = h.id 

1502 

1503 with events_session(token1) as api: 

1504 event_id = api.CreateEvent( 

1505 events_pb2.CreateEventReq( 

1506 title="Dummy Title", 

1507 content="Dummy content.", 

1508 offline_information=events_pb2.OfflineEventInformation( 

1509 address="Near Null Island", 

1510 lat=0.1, 

1511 lng=0.2, 

1512 ), 

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

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

1515 timezone="UTC", 

1516 ) 

1517 ).event_id 

1518 

1519 api.TransferEvent( 

1520 events_pb2.TransferEventReq( 

1521 event_id=event_id, 

1522 new_owner_community_id=c_id, 

1523 ) 

1524 ) 

1525 

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

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

1528 

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

1530 api.TransferEvent( 

1531 events_pb2.TransferEventReq( 

1532 event_id=event_id, 

1533 new_owner_group_id=h_id, 

1534 ) 

1535 ) 

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

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

1538 

1539 event_id = api.CreateEvent( 

1540 events_pb2.CreateEventReq( 

1541 title="Dummy Title", 

1542 content="Dummy content.", 

1543 offline_information=events_pb2.OfflineEventInformation( 

1544 address="Near Null Island", 

1545 lat=0.1, 

1546 lng=0.2, 

1547 ), 

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

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

1550 timezone="UTC", 

1551 ) 

1552 ).event_id 

1553 

1554 api.TransferEvent( 

1555 events_pb2.TransferEventReq( 

1556 event_id=event_id, 

1557 new_owner_group_id=h_id, 

1558 ) 

1559 ) 

1560 

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

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

1563 

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

1565 api.TransferEvent( 

1566 events_pb2.TransferEventReq( 

1567 event_id=event_id, 

1568 new_owner_community_id=c_id, 

1569 ) 

1570 ) 

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

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

1573 

1574 

1575def test_SetEventSubscription(db, moderator: Moderator): 

1576 user1, token1 = generate_user() 

1577 user2, token2 = generate_user() 

1578 

1579 with session_scope() as session: 

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

1581 

1582 with events_session(token1) as api: 

1583 event_id = api.CreateEvent( 

1584 events_pb2.CreateEventReq( 

1585 title="Dummy Title", 

1586 content="Dummy content.", 

1587 offline_information=events_pb2.OfflineEventInformation( 

1588 address="Near Null Island", 

1589 lat=0.1, 

1590 lng=0.2, 

1591 ), 

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

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

1594 timezone="UTC", 

1595 ) 

1596 ).event_id 

1597 

1598 moderator.approve_event_occurrence(event_id) 

1599 

1600 with events_session(token2) as api: 

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

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

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

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

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

1606 

1607 

1608def test_SetEventAttendance(db, moderator: Moderator): 

1609 user1, token1 = generate_user() 

1610 user2, token2 = generate_user() 

1611 

1612 with session_scope() as session: 

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

1614 

1615 with events_session(token1) as api: 

1616 event_id = api.CreateEvent( 

1617 events_pb2.CreateEventReq( 

1618 title="Dummy Title", 

1619 content="Dummy content.", 

1620 offline_information=events_pb2.OfflineEventInformation( 

1621 address="Near Null Island", 

1622 lat=0.1, 

1623 lng=0.2, 

1624 ), 

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

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

1627 timezone="UTC", 

1628 ) 

1629 ).event_id 

1630 

1631 moderator.approve_event_occurrence(event_id) 

1632 

1633 with events_session(token2) as api: 

1634 assert ( 

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

1636 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1637 ) 

1638 api.SetEventAttendance( 

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

1640 ) 

1641 assert ( 

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

1643 == events_pb2.ATTENDANCE_STATE_GOING 

1644 ) 

1645 api.SetEventAttendance( 

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

1647 ) 

1648 assert ( 

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

1650 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1651 ) 

1652 

1653 

1654def test_InviteEventOrganizer(db, moderator: Moderator): 

1655 user1, token1 = generate_user() 

1656 user2, token2 = generate_user() 

1657 

1658 with session_scope() as session: 

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

1660 

1661 with events_session(token1) as api: 

1662 event_id = api.CreateEvent( 

1663 events_pb2.CreateEventReq( 

1664 title="Dummy Title", 

1665 content="Dummy content.", 

1666 offline_information=events_pb2.OfflineEventInformation( 

1667 address="Near Null Island", 

1668 lat=0.1, 

1669 lng=0.2, 

1670 ), 

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

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

1673 timezone="UTC", 

1674 ) 

1675 ).event_id 

1676 

1677 moderator.approve_event_occurrence(event_id) 

1678 

1679 with events_session(token2) as api: 

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

1681 

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

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

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

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

1686 

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

1688 

1689 with events_session(token1) as api: 

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

1691 

1692 with events_session(token2) as api: 

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

1694 

1695 

1696def test_ListEventOccurrences(db): 

1697 user1, token1 = generate_user() 

1698 user2, token2 = generate_user() 

1699 user3, token3 = generate_user() 

1700 

1701 with session_scope() as session: 

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

1703 

1704 start = now() 

1705 

1706 event_ids = [] 

1707 

1708 with events_session(token1) as api: 

1709 res = api.CreateEvent( 

1710 events_pb2.CreateEventReq( 

1711 title="First occurrence", 

1712 content="Dummy content.", 

1713 parent_community_id=c_id, 

1714 online_information=events_pb2.OnlineEventInformation( 

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

1716 ), 

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

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

1719 timezone="UTC", 

1720 ) 

1721 ) 

1722 

1723 event_ids.append(res.event_id) 

1724 

1725 for i in range(5): 

1726 res = api.ScheduleEvent( 

1727 events_pb2.ScheduleEventReq( 

1728 event_id=event_ids[-1], 

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

1730 online_information=events_pb2.OnlineEventInformation( 

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

1732 ), 

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

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

1735 timezone="UTC", 

1736 ) 

1737 ) 

1738 

1739 event_ids.append(res.event_id) 

1740 

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

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

1743 

1744 res = api.ListEventOccurrences( 

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

1746 ) 

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

1748 

1749 res = api.ListEventOccurrences( 

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

1751 ) 

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

1753 assert not res.next_page_token 

1754 

1755 

1756def test_ListMyEvents(db, moderator: Moderator): 

1757 user1, token1 = generate_user() 

1758 user2, token2 = generate_user() 

1759 user3, token3 = generate_user() 

1760 user4, token4 = generate_user() 

1761 user5, token5 = generate_user() 

1762 

1763 with session_scope() as session: 

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

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

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

1767 c_id = global_community.id 

1768 macroregion_community = create_community( 

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

1770 ) 

1771 region_community = create_community( 

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

1773 ) 

1774 subregion_community = create_community( 

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

1776 ) 

1777 c2_id = subregion_community.id 

1778 

1779 start = now() 

1780 

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

1782 if online: 

1783 return events_pb2.CreateEventReq( 

1784 title="Dummy Online Title", 

1785 content="Dummy content.", 

1786 online_information=events_pb2.OnlineEventInformation( 

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

1788 ), 

1789 parent_community_id=community_id, 

1790 timezone="UTC", 

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

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

1793 ) 

1794 else: 

1795 return events_pb2.CreateEventReq( 

1796 title="Dummy Offline Title", 

1797 content="Dummy content.", 

1798 offline_information=events_pb2.OfflineEventInformation( 

1799 address="Near Null Island", 

1800 lat=0.1, 

1801 lng=0.2, 

1802 ), 

1803 parent_community_id=community_id, 

1804 timezone="UTC", 

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

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

1807 ) 

1808 

1809 with events_session(token1) as api: 

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

1811 

1812 moderator.approve_event_occurrence(e2) 

1813 

1814 with events_session(token2) as api: 

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

1816 

1817 moderator.approve_event_occurrence(e1) 

1818 

1819 with events_session(token1) as api: 

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

1821 

1822 moderator.approve_event_occurrence(e3) 

1823 

1824 with events_session(token2) as api: 

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

1826 

1827 moderator.approve_event_occurrence(e5) 

1828 

1829 with events_session(token3) as api: 

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

1831 

1832 moderator.approve_event_occurrence(e4) 

1833 

1834 with events_session(token4) as api: 

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

1836 

1837 moderator.approve_event_occurrence(e6) 

1838 

1839 with events_session(token1) as api: 

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

1841 

1842 with events_session(token1) as api: 

1843 api.SetEventAttendance( 

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

1845 ) 

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

1847 

1848 with events_session(token2) as api: 

1849 api.SetEventAttendance( 

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

1851 ) 

1852 

1853 with events_session(token3) as api: 

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

1855 

1856 with events_session(token1) as api: 

1857 # test pagination with token first 

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

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

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

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

1862 assert not res.next_page_token 

1863 

1864 res = api.ListMyEvents( 

1865 events_pb2.ListMyEventsReq( 

1866 subscribed=True, 

1867 attending=True, 

1868 organizing=True, 

1869 ) 

1870 ) 

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

1872 

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

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

1875 

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

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

1878 

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

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

1881 

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

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

1884 

1885 with events_session(token1) as api: 

1886 # Test pagination with page_number and verify total_items 

1887 res = api.ListMyEvents( 

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

1889 ) 

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

1891 assert res.total_items == 4 

1892 

1893 res = api.ListMyEvents( 

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

1895 ) 

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

1897 assert res.total_items == 4 

1898 

1899 # Verify no more pages 

1900 res = api.ListMyEvents( 

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

1902 ) 

1903 assert not res.events 

1904 assert res.total_items == 4 

1905 

1906 with events_session(token2) as api: 

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

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

1909 

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

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

1912 

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

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

1915 

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

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

1918 

1919 with events_session(token3) as api: 

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

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

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

1923 

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

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

1926 

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

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

1929 

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

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

1932 

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

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

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

1936 

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

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

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

1940 

1941 # my_communities_exclude_global works independently of my_communities flag 

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

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

1944 

1945 # my_communities_exclude_global filters organizing results too 

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

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

1948 

1949 # my_communities_exclude_global filters subscribed results too 

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

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

1952 

1953 with events_session(token5) as api: 

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

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

1956 

1957 

1958def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1959 user1, token1 = generate_user() 

1960 user2, token2 = generate_user() 

1961 

1962 with session_scope() as session: 

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

1964 c_id = c.id 

1965 

1966 start = now() 

1967 

1968 def make_event(hours): 

1969 return events_pb2.CreateEventReq( 

1970 title="Test Event", 

1971 content="Test content.", 

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

1973 parent_community_id=c_id, 

1974 timezone="UTC", 

1975 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)), 

1976 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)), 

1977 ) 

1978 

1979 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1980 with events_session(token1) as api: 

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

1982 

1983 with events_session(token2) as api: 

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

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

1986 # e_both: user1 will be both organizer and attendee 

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

1988 

1989 moderator.approve_event_occurrence(e_own) 

1990 moderator.approve_event_occurrence(e_attending) 

1991 moderator.approve_event_occurrence(e_community_only) 

1992 moderator.approve_event_occurrence(e_both) 

1993 

1994 # invite user1 as organizer of e_both 

1995 with events_session(token2) as api: 

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

1997 

1998 # user1 RSVPs to e_attending and e_both 

1999 with events_session(token1) as api: 

2000 api.SetEventAttendance( 

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

2002 ) 

2003 api.SetEventAttendance( 

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

2005 ) 

2006 

2007 with events_session(token1) as api: 

2008 # baseline: all four community events visible 

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

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

2011 

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

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

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

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

2016 

2017 # exclude_attending with attending=True: invalid combination 

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

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

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

2021 

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

2023 with events_session(token2) as api: 

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

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

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

2027 

2028 

2029def test_RemoveEventOrganizer(db, moderator: Moderator): 

2030 user1, token1 = generate_user() 

2031 user2, token2 = generate_user() 

2032 

2033 with session_scope() as session: 

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

2035 

2036 with events_session(token1) as api: 

2037 event_id = api.CreateEvent( 

2038 events_pb2.CreateEventReq( 

2039 title="Dummy Title", 

2040 content="Dummy content.", 

2041 offline_information=events_pb2.OfflineEventInformation( 

2042 address="Near Null Island", 

2043 lat=0.1, 

2044 lng=0.2, 

2045 ), 

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

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

2048 timezone="UTC", 

2049 ) 

2050 ).event_id 

2051 

2052 moderator.approve_event_occurrence(event_id) 

2053 

2054 with events_session(token2) as api: 

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

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're not allowed to edit that event." 

2061 

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

2063 

2064 with events_session(token1) as api: 

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

2066 

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

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

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

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

2071 

2072 with events_session(token2) as api: 

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

2074 assert res.organizer 

2075 assert res.organizer_count == 2 

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

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

2078 

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

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

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

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

2083 

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

2085 assert not res.organizer 

2086 assert res.organizer_count == 1 

2087 

2088 # Test that event owner can remove co-organizers 

2089 with events_session(token1) as api: 

2090 # Add user2 back as organizer 

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

2092 

2093 # Verify user2 is now an organizer 

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

2095 assert res.organizer_count == 2 

2096 

2097 # Event owner can remove co-organizer 

2098 api.RemoveEventOrganizer( 

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

2100 ) 

2101 

2102 # Verify user2 is no longer an organizer 

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

2104 assert res.organizer_count == 1 

2105 

2106 # Test that non-organizers cannot remove other organizers 

2107 with events_session(token2) as api: 

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

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

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

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

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

2113 

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

2115 with events_session(token1) as api: 

2116 # Add user2 back as organizer 

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

2118 

2119 

2120def test_ListEventAttendees_regression(db): 

2121 # see issue #1617: 

2122 # 

2123 # 1. Create an event 

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

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

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

2127 # 

2128 # **Expected behaviour** 

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

2130 # 

2131 # **Actual/current behaviour** 

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

2133 

2134 user1, token1 = generate_user() 

2135 user2, token2 = generate_user() 

2136 user3, token3 = generate_user() 

2137 user4, token4 = generate_user() 

2138 user5, token5 = generate_user() 

2139 

2140 with session_scope() as session: 

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

2142 

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

2144 end_time = start_time + timedelta(hours=3) 

2145 

2146 with events_session(token1) as api: 

2147 res = api.CreateEvent( 

2148 events_pb2.CreateEventReq( 

2149 title="Dummy Title", 

2150 content="Dummy content.", 

2151 online_information=events_pb2.OnlineEventInformation( 

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

2153 ), 

2154 parent_community_id=c_id, 

2155 start_time=Timestamp_from_datetime(start_time), 

2156 end_time=Timestamp_from_datetime(end_time), 

2157 timezone="UTC", 

2158 ) 

2159 ) 

2160 

2161 res = api.TransferEvent( 

2162 events_pb2.TransferEventReq( 

2163 event_id=res.event_id, 

2164 new_owner_community_id=c_id, 

2165 ) 

2166 ) 

2167 

2168 event_id = res.event_id 

2169 

2170 api.SetEventAttendance( 

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

2172 ) 

2173 api.SetEventAttendance( 

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

2175 ) 

2176 

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

2178 assert len(res.attendee_user_ids) == 1 

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

2180 

2181 

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

2183 user1, token1 = generate_user() 

2184 user2, token2 = generate_user() 

2185 user3, token3 = generate_user() 

2186 user4, token4 = generate_user() 

2187 

2188 with session_scope() as session: 

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

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

2191 c_id = c.id 

2192 h_id = h.id 

2193 user4_id = user4.id 

2194 

2195 with events_session(token1) as api: 

2196 event = api.CreateEvent( 

2197 events_pb2.CreateEventReq( 

2198 title="Dummy Title", 

2199 content="Dummy content.", 

2200 offline_information=events_pb2.OfflineEventInformation( 

2201 address="Near Null Island", 

2202 lat=0.1, 

2203 lng=0.2, 

2204 ), 

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

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

2207 timezone="UTC", 

2208 ) 

2209 ) 

2210 

2211 moderator.approve_event_occurrence(event.event_id) 

2212 

2213 with threads_session(token2) as api: 

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

2215 

2216 moderator.approve_thread_post(reply_id) 

2217 

2218 with events_session(token3) as api: 

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

2220 assert res.thread.num_responses == 1 

2221 

2222 with threads_session(token3) as api: 

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

2224 assert len(ret.replies) == 1 

2225 assert not ret.next_page_token 

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

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

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

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

2230 

2231 nested_reply_id = api.PostReply( 

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

2233 ).thread_id 

2234 

2235 moderator.approve_thread_post(nested_reply_id) 

2236 

2237 process_jobs() 

2238 

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

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

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

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

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

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

2245 

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

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

2248 

2249 assert push_collector.count_for_user(user4_id) == 0 

2250 

2251 

2252def test_can_overlap_other_events_schedule_regression(db): 

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

2254 user, token = generate_user() 

2255 

2256 with session_scope() as session: 

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

2258 

2259 start = now() 

2260 

2261 with events_session(token) as api: 

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

2263 api.CreateEvent( 

2264 events_pb2.CreateEventReq( 

2265 title="Dummy Title", 

2266 content="Dummy content.", 

2267 parent_community_id=c_id, 

2268 online_information=events_pb2.OnlineEventInformation( 

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

2270 ), 

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

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

2273 timezone="UTC", 

2274 ) 

2275 ) 

2276 

2277 # this event 

2278 res = api.CreateEvent( 

2279 events_pb2.CreateEventReq( 

2280 title="Dummy Title", 

2281 content="Dummy content.", 

2282 parent_community_id=c_id, 

2283 online_information=events_pb2.OnlineEventInformation( 

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

2285 ), 

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

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

2288 timezone="UTC", 

2289 ) 

2290 ) 

2291 

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

2293 api.ScheduleEvent( 

2294 events_pb2.ScheduleEventReq( 

2295 event_id=res.event_id, 

2296 content="New event occurrence", 

2297 offline_information=events_pb2.OfflineEventInformation( 

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

2299 lat=0.3, 

2300 lng=0.2, 

2301 ), 

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

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

2304 timezone="UTC", 

2305 ) 

2306 ) 

2307 

2308 

2309def test_can_overlap_other_events_update_regression(db): 

2310 user, token = generate_user() 

2311 

2312 with session_scope() as session: 

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

2314 

2315 start = now() 

2316 

2317 with events_session(token) as api: 

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

2319 api.CreateEvent( 

2320 events_pb2.CreateEventReq( 

2321 title="Dummy Title", 

2322 content="Dummy content.", 

2323 parent_community_id=c_id, 

2324 online_information=events_pb2.OnlineEventInformation( 

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

2326 ), 

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

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

2329 timezone="UTC", 

2330 ) 

2331 ) 

2332 

2333 res = api.CreateEvent( 

2334 events_pb2.CreateEventReq( 

2335 title="Dummy Title", 

2336 content="Dummy content.", 

2337 parent_community_id=c_id, 

2338 online_information=events_pb2.OnlineEventInformation( 

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

2340 ), 

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

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

2343 timezone="UTC", 

2344 ) 

2345 ) 

2346 

2347 event_id = api.ScheduleEvent( 

2348 events_pb2.ScheduleEventReq( 

2349 event_id=res.event_id, 

2350 content="New event occurrence", 

2351 offline_information=events_pb2.OfflineEventInformation( 

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

2353 lat=0.3, 

2354 lng=0.2, 

2355 ), 

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

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

2358 timezone="UTC", 

2359 ) 

2360 ).event_id 

2361 

2362 # can overlap with this current existing occurrence 

2363 api.UpdateEvent( 

2364 events_pb2.UpdateEventReq( 

2365 event_id=event_id, 

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

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

2368 ) 

2369 ) 

2370 

2371 api.UpdateEvent( 

2372 events_pb2.UpdateEventReq( 

2373 event_id=event_id, 

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

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

2376 ) 

2377 ) 

2378 

2379 

2380def test_list_past_events_regression(db): 

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

2382 user, token = generate_user() 

2383 

2384 with session_scope() as session: 

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

2386 

2387 start = now() 

2388 

2389 with events_session(token) as api: 

2390 api.CreateEvent( 

2391 events_pb2.CreateEventReq( 

2392 title="Dummy Title", 

2393 content="Dummy content.", 

2394 parent_community_id=c_id, 

2395 online_information=events_pb2.OnlineEventInformation( 

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

2397 ), 

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

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

2400 timezone="UTC", 

2401 ) 

2402 ) 

2403 

2404 with session_scope() as session: 

2405 session.execute( 

2406 update(EventOccurrence).values( 

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

2408 ) 

2409 ) 

2410 

2411 with events_session(token) as api: 

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

2413 assert len(res.events) == 1 

2414 

2415 

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

2417 user1, token1 = generate_user(complete_profile=True) 

2418 user2, token2 = generate_user() 

2419 user3, token3 = generate_user() 

2420 user4, token4 = generate_user() 

2421 user5, token5 = generate_user(is_superuser=True) 

2422 

2423 with session_scope() as session: 

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

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

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

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

2428 

2429 enforce_community_memberships() 

2430 

2431 with events_session(token1) as api: 

2432 res = api.CreateEvent( 

2433 events_pb2.CreateEventReq( 

2434 title="Dummy Title", 

2435 content="Dummy content.", 

2436 parent_community_id=c_id, 

2437 online_information=events_pb2.OnlineEventInformation( 

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

2439 ), 

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

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

2442 timezone="UTC", 

2443 ) 

2444 ) 

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

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

2447 

2448 event_id = res.event_id 

2449 

2450 moderator.approve_event_occurrence(event_id) 

2451 

2452 with events_session(token1) as api: 

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

2454 

2455 email = email_collector.pop_for_mods(last=True) 

2456 

2457 assert user_url in email.plain 

2458 assert event_url in email.plain 

2459 

2460 # can't send another req 

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

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

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

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

2465 

2466 # another user can send one though 

2467 with events_session(token3) as api: 

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

2469 

2470 # but not a non-admin 

2471 with events_session(token2) as api: 

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

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

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

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

2476 

2477 with real_editor_session(token5) as editor: 

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

2479 assert len(res.requests) == 2 

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

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

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

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

2484 

2485 editor.DecideEventCommunityInviteRequest( 

2486 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2488 approve=False, 

2489 ) 

2490 ) 

2491 

2492 editor.DecideEventCommunityInviteRequest( 

2493 editor_pb2.DecideEventCommunityInviteRequestReq( 

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

2495 approve=True, 

2496 ) 

2497 ) 

2498 

2499 # not after approve 

2500 with events_session(token4) as api: 

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

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

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

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

2505 

2506 

2507def test_update_event_should_notify_queues_job(): 

2508 user, token = generate_user() 

2509 start = now() 

2510 

2511 with session_scope() as session: 

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

2513 

2514 # create an event 

2515 with events_session(token) as api: 

2516 create_res = api.CreateEvent( 

2517 events_pb2.CreateEventReq( 

2518 title="Dummy Title", 

2519 content="Dummy content.", 

2520 parent_community_id=c_id, 

2521 offline_information=events_pb2.OfflineEventInformation( 

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

2523 lat=1.0, 

2524 lng=2.0, 

2525 ), 

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

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

2528 timezone="UTC", 

2529 ) 

2530 ) 

2531 

2532 event_id = create_res.event_id 

2533 

2534 # measure initial background job queue length 

2535 with session_scope() as session: 

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

2537 job_length_before_update = len(jobs) 

2538 

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

2540 api.UpdateEvent( 

2541 events_pb2.UpdateEventReq( 

2542 event_id=event_id, 

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

2544 should_notify=False, 

2545 ) 

2546 ) 

2547 

2548 with session_scope() as session: 

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

2550 assert len(jobs) == job_length_before_update 

2551 

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

2553 api.UpdateEvent( 

2554 events_pb2.UpdateEventReq( 

2555 event_id=event_id, 

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

2557 should_notify=True, 

2558 ) 

2559 ) 

2560 

2561 with session_scope() as session: 

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

2563 assert len(jobs) == job_length_before_update + 1 

2564 

2565 

2566def test_event_photo_key(db): 

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

2568 user, token = generate_user() 

2569 

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

2571 end_time = start_time + timedelta(hours=3) 

2572 

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

2574 with session_scope() as session: 

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

2576 upload = Upload( 

2577 key="test_event_photo_key_123", 

2578 filename="test_event_photo_key_123.jpg", 

2579 creator_user_id=user.id, 

2580 ) 

2581 session.add(upload) 

2582 

2583 with events_session(token) as api: 

2584 # Create event without photo 

2585 res = api.CreateEvent( 

2586 events_pb2.CreateEventReq( 

2587 title="Event Without Photo", 

2588 content="No photo content.", 

2589 photo_key=None, 

2590 offline_information=events_pb2.OfflineEventInformation( 

2591 address="Near Null Island", 

2592 lat=0.1, 

2593 lng=0.2, 

2594 ), 

2595 start_time=Timestamp_from_datetime(start_time), 

2596 end_time=Timestamp_from_datetime(end_time), 

2597 timezone="UTC", 

2598 ) 

2599 ) 

2600 

2601 assert res.photo_key == "" 

2602 assert res.photo_url == "" 

2603 

2604 # Create event with photo 

2605 res_with_photo = api.CreateEvent( 

2606 events_pb2.CreateEventReq( 

2607 title="Event With Photo", 

2608 content="Has photo content.", 

2609 photo_key="test_event_photo_key_123", 

2610 offline_information=events_pb2.OfflineEventInformation( 

2611 address="Near Null Island", 

2612 lat=0.1, 

2613 lng=0.2, 

2614 ), 

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

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

2617 timezone="UTC", 

2618 ) 

2619 ) 

2620 

2621 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2622 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2623 

2624 event_id = res_with_photo.event_id 

2625 

2626 # Verify photo_key is returned when getting the event 

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

2628 assert get_res.photo_key == "test_event_photo_key_123" 

2629 assert "test_event_photo_key_123" in get_res.photo_url 

2630 

2631 

2632def test_event_created_with_shadowed_visibility(db): 

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

2634 user, token = generate_user() 

2635 

2636 with session_scope() as session: 

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

2638 

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

2640 end_time = start_time + timedelta(hours=3) 

2641 

2642 with events_session(token) as api: 

2643 res = api.CreateEvent( 

2644 events_pb2.CreateEventReq( 

2645 title="Test UMS Event", 

2646 content="UMS content.", 

2647 offline_information=events_pb2.OfflineEventInformation( 

2648 address="Near Null Island", 

2649 lat=0.1, 

2650 lng=0.2, 

2651 ), 

2652 start_time=Timestamp_from_datetime(start_time), 

2653 end_time=Timestamp_from_datetime(end_time), 

2654 timezone="UTC", 

2655 ) 

2656 ) 

2657 event_id = res.event_id 

2658 

2659 with session_scope() as session: 

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

2661 mod_state = session.execute( 

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

2663 ).scalar_one() 

2664 assert mod_state.visibility == ModerationVisibility.shadowed 

2665 

2666 

2667def test_shadowed_event_visible_to_creator_only(db): 

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

2669 user1, token1 = generate_user() 

2670 user2, token2 = generate_user() 

2671 

2672 with session_scope() as session: 

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

2674 

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

2676 end_time = start_time + timedelta(hours=3) 

2677 

2678 with events_session(token1) as api: 

2679 res = api.CreateEvent( 

2680 events_pb2.CreateEventReq( 

2681 title="Shadowed Event", 

2682 content="Content.", 

2683 offline_information=events_pb2.OfflineEventInformation( 

2684 address="Near Null Island", 

2685 lat=0.1, 

2686 lng=0.2, 

2687 ), 

2688 start_time=Timestamp_from_datetime(start_time), 

2689 end_time=Timestamp_from_datetime(end_time), 

2690 timezone="UTC", 

2691 ) 

2692 ) 

2693 event_id = res.event_id 

2694 

2695 # Creator can see it 

2696 with events_session(token1) as api: 

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

2698 assert res.title == "Shadowed Event" 

2699 

2700 # Other user cannot 

2701 with events_session(token2) as api: 

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

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

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

2705 

2706 

2707def test_event_visible_after_approval(db, moderator: Moderator): 

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

2709 user1, token1 = generate_user() 

2710 user2, token2 = generate_user() 

2711 

2712 with session_scope() as session: 

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

2714 

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

2716 end_time = start_time + timedelta(hours=3) 

2717 

2718 with events_session(token1) as api: 

2719 res = api.CreateEvent( 

2720 events_pb2.CreateEventReq( 

2721 title="Approved Event", 

2722 content="Content.", 

2723 offline_information=events_pb2.OfflineEventInformation( 

2724 address="Near Null Island", 

2725 lat=0.1, 

2726 lng=0.2, 

2727 ), 

2728 start_time=Timestamp_from_datetime(start_time), 

2729 end_time=Timestamp_from_datetime(end_time), 

2730 timezone="UTC", 

2731 ) 

2732 ) 

2733 event_id = res.event_id 

2734 

2735 # Other user cannot see it yet 

2736 with events_session(token2) as api: 

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

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

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

2740 

2741 # Approve the event 

2742 moderator.approve_event_occurrence(event_id) 

2743 

2744 # Now other user can see it 

2745 with events_session(token2) as api: 

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

2747 assert res.title == "Approved Event" 

2748 

2749 

2750def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

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

2752 user1, token1 = generate_user() 

2753 user2, token2 = generate_user() 

2754 

2755 with session_scope() as session: 

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

2757 

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

2759 end_time = start_time + timedelta(hours=3) 

2760 

2761 with events_session(token1) as api: 

2762 res = api.CreateEvent( 

2763 events_pb2.CreateEventReq( 

2764 title="List Test Event", 

2765 content="Content.", 

2766 offline_information=events_pb2.OfflineEventInformation( 

2767 address="Near Null Island", 

2768 lat=0.1, 

2769 lng=0.2, 

2770 ), 

2771 start_time=Timestamp_from_datetime(start_time), 

2772 end_time=Timestamp_from_datetime(end_time), 

2773 timezone="UTC", 

2774 ) 

2775 ) 

2776 event_id = res.event_id 

2777 

2778 # Creator can see their own SHADOWED event in lists 

2779 with events_session(token1) as api: 

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

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

2782 assert event_id in event_ids 

2783 

2784 # Other user cannot see the SHADOWED event in lists 

2785 with events_session(token2) as api: 

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

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

2788 assert event_id not in event_ids 

2789 

2790 # After approval, other user can see it 

2791 moderator.approve_event_occurrence(event_id) 

2792 

2793 with events_session(token2) as api: 

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

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

2796 assert event_id in event_ids 

2797 

2798 

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

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

2801 user1, token1 = generate_user() 

2802 user2, token2 = generate_user() 

2803 

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

2805 with session_scope() as session: 

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

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

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

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

2810 

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

2812 end_time = start_time + timedelta(hours=3) 

2813 

2814 with events_session(token1) as api: 

2815 res = api.CreateEvent( 

2816 events_pb2.CreateEventReq( 

2817 title="Deferred Event", 

2818 content="Content.", 

2819 offline_information=events_pb2.OfflineEventInformation( 

2820 address="Near Null Island", 

2821 lat=0.1, 

2822 lng=0.2, 

2823 ), 

2824 start_time=Timestamp_from_datetime(start_time), 

2825 end_time=Timestamp_from_datetime(end_time), 

2826 timezone="UTC", 

2827 ) 

2828 ) 

2829 event_id = res.event_id 

2830 

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

2832 process_jobs() 

2833 

2834 with session_scope() as session: 

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

2836 # Notification was created with moderation_state_id for deferral 

2837 assert notif.moderation_state_id is not None 

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

2839 delivery_count = session.execute( 

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

2841 ).scalar_one_or_none() 

2842 assert delivery_count is None 

2843 

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

2845 moderator.approve_event_occurrence(event_id) 

2846 

2847 # Verify handle_notification job was queued 

2848 with session_scope() as session: 

2849 pending_jobs = ( 

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

2851 .scalars() 

2852 .all() 

2853 ) 

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

2855 

2856 

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

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

2859 user1, token1 = generate_user() 

2860 user2, token2 = generate_user() 

2861 

2862 with session_scope() as session: 

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

2864 

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

2866 end_time = start_time + timedelta(hours=3) 

2867 

2868 with events_session(token1) as api: 

2869 res = api.CreateEvent( 

2870 events_pb2.CreateEventReq( 

2871 title="Update Test", 

2872 content="Content.", 

2873 offline_information=events_pb2.OfflineEventInformation( 

2874 address="Near Null Island", 

2875 lat=0.1, 

2876 lng=0.2, 

2877 ), 

2878 start_time=Timestamp_from_datetime(start_time), 

2879 end_time=Timestamp_from_datetime(end_time), 

2880 timezone="UTC", 

2881 ) 

2882 ) 

2883 event_id = res.event_id 

2884 

2885 moderator.approve_event_occurrence(event_id) 

2886 process_jobs() 

2887 # Clear any create notifications 

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

2889 push_collector.pop_for_user(user2.id) 

2890 

2891 # User2 subscribes to the event 

2892 with events_session(token2) as api: 

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

2894 

2895 # User1 updates the event with should_notify=True 

2896 with events_session(token1) as api: 

2897 api.UpdateEvent( 

2898 events_pb2.UpdateEventReq( 

2899 event_id=event_id, 

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

2901 should_notify=True, 

2902 ) 

2903 ) 

2904 

2905 process_jobs() 

2906 

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

2908 with session_scope() as session: 

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

2910 

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

2912 # Find the update notification (most recent one) 

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

2914 assert len(update_notifs) == 1 

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

2916 

2917 

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

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

2920 user1, token1 = generate_user() 

2921 user2, token2 = generate_user() 

2922 

2923 with session_scope() as session: 

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

2925 

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

2927 end_time = start_time + timedelta(hours=3) 

2928 

2929 with events_session(token1) as api: 

2930 res = api.CreateEvent( 

2931 events_pb2.CreateEventReq( 

2932 title="Cancel Test", 

2933 content="Content.", 

2934 offline_information=events_pb2.OfflineEventInformation( 

2935 address="Near Null Island", 

2936 lat=0.1, 

2937 lng=0.2, 

2938 ), 

2939 start_time=Timestamp_from_datetime(start_time), 

2940 end_time=Timestamp_from_datetime(end_time), 

2941 timezone="UTC", 

2942 ) 

2943 ) 

2944 event_id = res.event_id 

2945 

2946 moderator.approve_event_occurrence(event_id) 

2947 process_jobs() 

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

2949 push_collector.pop_for_user(user2.id) 

2950 

2951 # User2 subscribes 

2952 with events_session(token2) as api: 

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

2954 

2955 # User1 cancels the event 

2956 with events_session(token1) as api: 

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

2958 

2959 process_jobs() 

2960 

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

2962 with session_scope() as session: 

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

2964 

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

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

2967 assert len(cancel_notifs) == 1 

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

2969 

2970 

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

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

2973 user1, token1 = generate_user() 

2974 user2, token2 = generate_user() 

2975 

2976 with session_scope() as session: 

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

2978 

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

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

2981 end_time = start_time + timedelta(hours=1) 

2982 

2983 with events_session(token1) as api: 

2984 res = api.CreateEvent( 

2985 events_pb2.CreateEventReq( 

2986 title="Reminder Test", 

2987 content="Content.", 

2988 offline_information=events_pb2.OfflineEventInformation( 

2989 address="Near Null Island", 

2990 lat=0.1, 

2991 lng=0.2, 

2992 ), 

2993 start_time=Timestamp_from_datetime(start_time), 

2994 end_time=Timestamp_from_datetime(end_time), 

2995 timezone="UTC", 

2996 ) 

2997 ) 

2998 event_id = res.event_id 

2999 

3000 moderator.approve_event_occurrence(event_id) 

3001 process_jobs() 

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

3003 push_collector.pop_for_user(user2.id) 

3004 

3005 # User2 marks attendance 

3006 with events_session(token2) as api: 

3007 api.SetEventAttendance( 

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

3009 ) 

3010 

3011 # Run the event reminder handler 

3012 send_event_reminders(empty_pb2.Empty()) 

3013 process_jobs() 

3014 

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

3016 with session_scope() as session: 

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

3018 

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

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

3021 assert len(reminder_notifs) == 1 

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

3023 

3024 

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

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

3027 user1, token1 = generate_user() 

3028 user2, token2 = generate_user() 

3029 

3030 with session_scope() as session: 

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

3032 

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

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

3035 end_time = start_time + timedelta(hours=1) 

3036 

3037 with events_session(token1) as api: 

3038 res = api.CreateEvent( 

3039 events_pb2.CreateEventReq( 

3040 title="Cancelled Reminder Test", 

3041 content="Content.", 

3042 offline_information=events_pb2.OfflineEventInformation( 

3043 address="Near Null Island", 

3044 lat=0.1, 

3045 lng=0.2, 

3046 ), 

3047 start_time=Timestamp_from_datetime(start_time), 

3048 end_time=Timestamp_from_datetime(end_time), 

3049 timezone="UTC", 

3050 ) 

3051 ) 

3052 event_id = res.event_id 

3053 

3054 moderator.approve_event_occurrence(event_id) 

3055 process_jobs() 

3056 

3057 # User2 marks attendance 

3058 with events_session(token2) as api: 

3059 api.SetEventAttendance( 

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

3061 ) 

3062 

3063 # User1 cancels the event 

3064 with events_session(token1) as api: 

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

3066 

3067 process_jobs() 

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

3069 while push_collector.count_for_user(user2.id): 

3070 push_collector.pop_for_user(user2.id) 

3071 

3072 # Run the event reminder handler 

3073 send_event_reminders(empty_pb2.Empty()) 

3074 process_jobs() 

3075 

3076 # Verify that no reminder notification was sent for user2 

3077 with session_scope() as session: 

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

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

3080 assert len(reminder_notifs) == 0 

3081 

3082 

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

3084def test_event_reminder_not_sent_for_invisible_attendee( 

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

3086): 

3087 user1, token1 = generate_user() 

3088 user2, token2 = generate_user() 

3089 

3090 with session_scope() as session: 

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

3092 

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

3094 end_time = start_time + timedelta(hours=1) 

3095 

3096 with events_session(token1) as api: 

3097 res = api.CreateEvent( 

3098 events_pb2.CreateEventReq( 

3099 title="Invisible Attendee Reminder Test", 

3100 content="Content.", 

3101 offline_information=events_pb2.OfflineEventInformation( 

3102 address="Near Null Island", 

3103 lat=0.1, 

3104 lng=0.2, 

3105 ), 

3106 start_time=Timestamp_from_datetime(start_time), 

3107 end_time=Timestamp_from_datetime(end_time), 

3108 timezone="UTC", 

3109 ) 

3110 ) 

3111 event_id = res.event_id 

3112 

3113 moderator.approve_event_occurrence(event_id) 

3114 process_jobs() 

3115 

3116 with events_session(token2) as api: 

3117 api.SetEventAttendance( 

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

3119 ) 

3120 

3121 with session_scope() as session: 

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

3123 

3124 send_event_reminders(empty_pb2.Empty()) 

3125 process_jobs() 

3126 

3127 with session_scope() as session: 

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

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

3130 assert len(reminder_notifs) == 0 

3131 

3132 

3133def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

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

3135 user1, token1 = generate_user() 

3136 user2, token2 = generate_user() 

3137 

3138 with session_scope() as session: 

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

3140 

3141 start = now() 

3142 

3143 # User1 creates event A with 3 occurrences 

3144 event_a_ids = [] 

3145 with events_session(token1) as api: 

3146 res = api.CreateEvent( 

3147 events_pb2.CreateEventReq( 

3148 title="Event A", 

3149 content="Content A.", 

3150 parent_community_id=c_id, 

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

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

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

3154 timezone="UTC", 

3155 ) 

3156 ) 

3157 event_a_ids.append(res.event_id) 

3158 for i in range(2): 

3159 res = api.ScheduleEvent( 

3160 events_pb2.ScheduleEventReq( 

3161 event_id=event_a_ids[-1], 

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

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

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

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

3166 timezone="UTC", 

3167 ) 

3168 ) 

3169 event_a_ids.append(res.event_id) 

3170 

3171 # User2 creates event B with 2 occurrences 

3172 event_b_ids = [] 

3173 with events_session(token2) as api: 

3174 res = api.CreateEvent( 

3175 events_pb2.CreateEventReq( 

3176 title="Event B", 

3177 content="Content B.", 

3178 parent_community_id=c_id, 

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

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

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

3182 timezone="UTC", 

3183 ) 

3184 ) 

3185 event_b_ids.append(res.event_id) 

3186 res = api.ScheduleEvent( 

3187 events_pb2.ScheduleEventReq( 

3188 event_id=event_b_ids[-1], 

3189 content="B occurrence 1", 

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

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

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

3193 timezone="UTC", 

3194 ) 

3195 ) 

3196 event_b_ids.append(res.event_id) 

3197 

3198 moderator.approve_event_occurrence(event_a_ids[0]) 

3199 moderator.approve_event_occurrence(event_b_ids[0]) 

3200 

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

3202 with events_session(token1) as api: 

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

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

3205 assert sorted(returned_ids) == sorted(event_a_ids) 

3206 

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

3208 with events_session(token2) as api: 

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

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

3211 assert sorted(returned_ids) == sorted(event_b_ids) 

3212 

3213 

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

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

3216 user1, token1 = generate_user() 

3217 user2, token2 = generate_user() 

3218 

3219 with session_scope() as session: 

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

3221 

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

3223 end_time = start_time + timedelta(hours=3) 

3224 

3225 with events_session(token1) as api: 

3226 res = api.CreateEvent( 

3227 events_pb2.CreateEventReq( 

3228 title="Comment Test", 

3229 content="Content.", 

3230 offline_information=events_pb2.OfflineEventInformation( 

3231 address="Near Null Island", 

3232 lat=0.1, 

3233 lng=0.2, 

3234 ), 

3235 start_time=Timestamp_from_datetime(start_time), 

3236 end_time=Timestamp_from_datetime(end_time), 

3237 timezone="UTC", 

3238 ) 

3239 ) 

3240 event_id = res.event_id 

3241 thread_id = res.thread.thread_id 

3242 

3243 moderator.approve_event_occurrence(event_id) 

3244 process_jobs() 

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

3246 push_collector.pop_for_user(user1.id) 

3247 

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

3249 with events_session(token1) as api: 

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

3251 

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

3253 with threads_session(token2) as api: 

3254 comment_thread_id = api.PostReply( 

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

3256 ).thread_id 

3257 

3258 process_jobs() 

3259 

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

3261 comment_db_id = comment_thread_id // 10 

3262 with session_scope() as session: 

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

3264 

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

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

3267 assert len(comment_notifs) == 1 

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

3269 

3270 

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

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

3273 user1, token1 = generate_user() 

3274 user2, token2 = generate_user() 

3275 user3, token3 = generate_user() 

3276 

3277 with session_scope() as session: 

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

3279 

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

3281 end_time = start_time + timedelta(hours=3) 

3282 

3283 with events_session(token1) as api: 

3284 res = api.CreateEvent( 

3285 events_pb2.CreateEventReq( 

3286 title="Reply Test", 

3287 content="Content.", 

3288 offline_information=events_pb2.OfflineEventInformation( 

3289 address="Near Null Island", 

3290 lat=0.1, 

3291 lng=0.2, 

3292 ), 

3293 start_time=Timestamp_from_datetime(start_time), 

3294 end_time=Timestamp_from_datetime(end_time), 

3295 timezone="UTC", 

3296 ) 

3297 ) 

3298 event_id = res.event_id 

3299 thread_id = res.thread.thread_id 

3300 

3301 moderator.approve_event_occurrence(event_id) 

3302 process_jobs() 

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

3304 push_collector.pop_for_user(user1.id) 

3305 

3306 # User2 posts a top-level comment 

3307 with threads_session(token2) as api: 

3308 comment_thread_id = api.PostReply( 

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

3310 ).thread_id 

3311 

3312 process_jobs() 

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

3314 push_collector.pop_for_user(user1.id) 

3315 

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

3317 with threads_session(token3) as api: 

3318 nested_reply_thread_id = api.PostReply( 

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

3320 ).thread_id 

3321 

3322 process_jobs() 

3323 

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

3325 nested_reply_db_id = nested_reply_thread_id // 10 

3326 with session_scope() as session: 

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

3328 

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

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

3331 assert len(reply_notifs) == 1 

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