Coverage for app/backend/src/tests/test_editor.py: 100%

463 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-03 11:56 +0000

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2 

6from google.protobuf.wrappers_pb2 import BoolValue, DoubleValue, StringValue 

7from sqlalchemy import select, update 

8 

9from couchers.db import session_scope 

10from couchers.materialized_views import refresh_materialized_views_rapid 

11from couchers.models import ( 

12 Cluster, 

13 Event, 

14 Node, 

15 User, 

16 UserActivity, 

17 Volunteer, 

18) 

19from couchers.proto import editor_pb2, events_pb2 

20from couchers.tasks import enforce_community_memberships 

21from couchers.utils import now 

22from tests.fixtures.api import events as events_api 

23from tests.fixtures.db import generate_user 

24from tests.fixtures.sessions import real_editor_session 

25from tests.test_communities import create_1d_point, create_community 

26from tests.utils import local_date_time 

27 

28VALID_GEOJSON_MULTIPOLYGON = """ 

29 { 

30 "type": "MultiPolygon", 

31 "coordinates": 

32 [ 

33 [ 

34 [ 

35 [ 

36 -73.98114904754641, 

37 40.7470284264813 

38 ], 

39 [ 

40 -73.98314135177611, 

41 40.73416844413217 

42 ], 

43 [ 

44 -74.00538969848634, 

45 40.734314779027144 

46 ], 

47 [ 

48 -74.00479214294432, 

49 40.75027851544338 

50 ], 

51 [ 

52 -73.98114904754641, 

53 40.7470284264813 

54 ] 

55 ] 

56 ] 

57 ] 

58 } 

59""" 

60 

61POINT_GEOJSON = """ 

62{ "type": "Point", "coordinates": [100.0, 0.0] } 

63""" 

64 

65 

66def test_access_by_normal_user(db): 

67 """Normal users should not be able to access editor APIs""" 

68 normal_user, normal_token = generate_user() 

69 

70 with real_editor_session(normal_token) as api: 

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

72 api.CreateCommunity( 

73 editor_pb2.CreateCommunityReq( 

74 name="test community", 

75 description="community for testing", 

76 admin_ids=[], 

77 geojson=VALID_GEOJSON_MULTIPOLYGON, 

78 ) 

79 ) 

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

81 assert e.value.details() == "Permission denied" 

82 

83 

84def test_access_by_editor_user(db): 

85 """Editor users should be able to access editor APIs""" 

86 editor_user, editor_token = generate_user(is_editor=True) 

87 

88 with session_scope() as session: 

89 with real_editor_session(editor_token) as api: 

90 api.CreateCommunity( 

91 editor_pb2.CreateCommunityReq( 

92 name="test community", 

93 description="community for testing", 

94 admin_ids=[], 

95 geojson=VALID_GEOJSON_MULTIPOLYGON, 

96 ) 

97 ) 

98 community = session.execute(select(Cluster).where(Cluster.name == "test community")).scalar_one() 

99 assert community.description == "community for testing" 

100 assert community.slug == "test-community" 

101 

102 

103def test_access_by_superuser(db): 

104 """Superusers (who are also editors) should be able to access editor APIs""" 

105 editor_user, editor_token = generate_user(is_editor=True) 

106 

107 with session_scope() as session: 

108 with real_editor_session(editor_token) as api: 

109 api.CreateCommunity( 

110 editor_pb2.CreateCommunityReq( 

111 name="test community", 

112 description="community for testing", 

113 admin_ids=[], 

114 geojson=VALID_GEOJSON_MULTIPOLYGON, 

115 ) 

116 ) 

117 community = session.execute(select(Cluster).where(Cluster.name == "test community")).scalar_one() 

118 assert community.description == "community for testing" 

119 assert community.slug == "test-community" 

120 

121 

122def test_CreateCommunity_invalid_geojson(db): 

123 """CreateCommunity should reject invalid GeoJSON""" 

124 editor_user, editor_token = generate_user(is_editor=True) 

125 

126 with real_editor_session(editor_token) as api: 

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

128 api.CreateCommunity( 

129 editor_pb2.CreateCommunityReq( 

130 name="test community", 

131 description="community for testing", 

132 admin_ids=[], 

133 geojson=POINT_GEOJSON, 

134 ) 

135 ) 

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

137 assert e.value.details() == "GeoJson was not of type MultiPolygon." 

138 

139 

140def test_UpdateCommunity_invalid_geojson(db): 

141 """UpdateCommunity should reject invalid GeoJSON""" 

142 editor_user, editor_token = generate_user(is_editor=True) 

143 

144 with session_scope() as session: 

145 with real_editor_session(editor_token) as api: 

146 api.CreateCommunity( 

147 editor_pb2.CreateCommunityReq( 

148 name="test community", 

149 description="community for testing", 

150 admin_ids=[], 

151 geojson=VALID_GEOJSON_MULTIPOLYGON, 

152 ) 

153 ) 

154 community = session.execute(select(Cluster).where(Cluster.name == "test community")).scalar_one() 

155 

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

157 api.UpdateCommunity( 

158 editor_pb2.UpdateCommunityReq( 

159 community_id=community.parent_node_id, 

160 name="test community 2", 

161 description="community for testing 2", 

162 geojson=POINT_GEOJSON, 

163 ) 

164 ) 

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

166 assert e.value.details() == "GeoJson was not of type MultiPolygon." 

167 

168 

169def test_UpdateCommunity_invalid_id(db): 

170 """UpdateCommunity should reject invalid community IDs""" 

171 editor_user, editor_token = generate_user(is_editor=True) 

172 

173 with real_editor_session(editor_token) as api: 

174 api.CreateCommunity( 

175 editor_pb2.CreateCommunityReq( 

176 name="test community", 

177 description="community for testing", 

178 admin_ids=[], 

179 geojson=VALID_GEOJSON_MULTIPOLYGON, 

180 ) 

181 ) 

182 

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

184 api.UpdateCommunity( 

185 editor_pb2.UpdateCommunityReq( 

186 community_id=1000, 

187 name="test community 1000", 

188 description="community for testing 1000", 

189 geojson=VALID_GEOJSON_MULTIPOLYGON, 

190 ) 

191 ) 

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

193 assert e.value.details() == "Community not found." 

194 

195 

196def test_UpdateCommunity(db): 

197 """UpdateCommunity should successfully update a community""" 

198 editor_user, editor_token = generate_user(is_editor=True) 

199 

200 with session_scope() as session: 

201 with real_editor_session(editor_token) as api: 

202 api.CreateCommunity( 

203 editor_pb2.CreateCommunityReq( 

204 name="test community", 

205 description="community for testing", 

206 admin_ids=[], 

207 geojson=VALID_GEOJSON_MULTIPOLYGON, 

208 ) 

209 ) 

210 community = session.execute(select(Cluster).where(Cluster.name == "test community")).scalar_one() 

211 

212 api.UpdateCommunity( 

213 editor_pb2.UpdateCommunityReq( 

214 community_id=community.parent_node_id, 

215 name="test community updated", 

216 description="community for testing updated", 

217 geojson=VALID_GEOJSON_MULTIPOLYGON, 

218 ) 

219 ) 

220 session.commit() 

221 

222 community_updated = session.execute(select(Cluster).where(Cluster.id == community.id)).scalar_one() 

223 assert community_updated.name == "test community updated" 

224 assert community_updated.description == "community for testing updated" 

225 assert community_updated.slug == "test-community-updated" 

226 

227 

228def test_CreateCommunity(db): 

229 with session_scope() as session: 

230 editor_user, editor_token = generate_user(is_editor=True) 

231 normal_user, normal_token = generate_user() 

232 with real_editor_session(editor_token) as api: 

233 api.CreateCommunity( 

234 editor_pb2.CreateCommunityReq( 

235 name="test community", 

236 description="community for testing", 

237 admin_ids=[], 

238 geojson=VALID_GEOJSON_MULTIPOLYGON, 

239 ) 

240 ) 

241 community = session.execute(select(Cluster).where(Cluster.name == "test community")).scalar_one() 

242 assert community.description == "community for testing" 

243 assert community.slug == "test-community" 

244 

245 

246def test_UpdateCommunity2(db): 

247 editor_user, editor_token = generate_user(is_editor=True) 

248 

249 with session_scope() as session: 

250 with real_editor_session(editor_token) as api: 

251 api.CreateCommunity( 

252 editor_pb2.CreateCommunityReq( 

253 name="test community", 

254 description="community for testing", 

255 admin_ids=[], 

256 geojson=VALID_GEOJSON_MULTIPOLYGON, 

257 ) 

258 ) 

259 community = session.execute(select(Cluster).where(Cluster.name == "test community")).scalar_one() 

260 assert community.description == "community for testing" 

261 

262 api.CreateCommunity( 

263 editor_pb2.CreateCommunityReq( 

264 name="test community 2", 

265 description="community for testing 2", 

266 admin_ids=[], 

267 geojson=VALID_GEOJSON_MULTIPOLYGON, 

268 ) 

269 ) 

270 community_2 = session.execute(select(Cluster).where(Cluster.name == "test community 2")).scalar_one() 

271 

272 api.UpdateCommunity( 

273 editor_pb2.UpdateCommunityReq( 

274 community_id=community.parent_node_id, 

275 name="test community 2", 

276 description="community for testing 2", 

277 geojson=VALID_GEOJSON_MULTIPOLYGON, 

278 parent_node_id=community_2.parent_node_id, 

279 ) 

280 ) 

281 session.commit() 

282 

283 community_updated = session.execute(select(Cluster).where(Cluster.id == community.id)).scalar_one() 

284 assert community_updated.description == "community for testing 2" 

285 assert community_updated.slug == "test-community-2" 

286 

287 node_updated = session.execute(select(Node).where(Node.id == community_updated.parent_node_id)).scalar_one() 

288 assert node_updated.parent_node_id == community_2.parent_node_id 

289 

290 

291def test_MakeUserVolunteer(db): 

292 """MakeUserVolunteer should successfully create a volunteer""" 

293 editor_user, editor_token = generate_user(is_editor=True) 

294 normal_user, normal_token = generate_user() 

295 

296 refresh_materialized_views_rapid(empty_pb2.Empty()) 

297 with session_scope() as session: 

298 with real_editor_session(editor_token) as api: 

299 res = api.MakeUserVolunteer( 

300 editor_pb2.MakeUserVolunteerReq( 

301 user_id=normal_user.id, 

302 role="Test Volunteer", 

303 started_volunteering="2024-01-15", 

304 hide_on_team_page=False, 

305 ) 

306 ) 

307 

308 # Check response 

309 assert res.user_id == normal_user.id 

310 assert res.role == "Test Volunteer" 

311 assert res.started_volunteering == "2024-01-15" 

312 assert res.show_on_team_page is True 

313 assert res.username == normal_user.username 

314 assert res.name == normal_user.name 

315 

316 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

317 assert volunteer.role == "Test Volunteer" 

318 assert volunteer.started_volunteering.isoformat() == "2024-01-15" 

319 assert volunteer.show_on_team_page is True 

320 

321 

322def test_MakeUserVolunteer_default_values(db): 

323 """MakeUserVolunteer should use default values when not provided""" 

324 editor_user, editor_token = generate_user(is_editor=True) 

325 normal_user, normal_token = generate_user() 

326 

327 refresh_materialized_views_rapid(empty_pb2.Empty()) 

328 with session_scope() as session: 

329 with real_editor_session(editor_token) as api: 

330 api.MakeUserVolunteer( 

331 editor_pb2.MakeUserVolunteerReq( 

332 user_id=normal_user.id, 

333 role="Test Volunteer", 

334 ) 

335 ) 

336 

337 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

338 assert volunteer.role == "Test Volunteer" 

339 assert volunteer.started_volunteering # defaults to today 

340 assert volunteer.show_on_team_page is True # hide_on_team_page defaults to False 

341 

342 

343def test_MakeUserVolunteer_hide_on_team_page(db): 

344 """MakeUserVolunteer should respect hide_on_team_page=True""" 

345 editor_user, editor_token = generate_user(is_editor=True) 

346 normal_user, normal_token = generate_user() 

347 

348 refresh_materialized_views_rapid(empty_pb2.Empty()) 

349 with session_scope() as session: 

350 with real_editor_session(editor_token) as api: 

351 api.MakeUserVolunteer( 

352 editor_pb2.MakeUserVolunteerReq( 

353 user_id=normal_user.id, 

354 role="Test Volunteer", 

355 hide_on_team_page=True, 

356 ) 

357 ) 

358 

359 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

360 assert volunteer.role == "Test Volunteer" 

361 assert volunteer.show_on_team_page is False # hide_on_team_page=True means don't show 

362 

363 

364def test_MakeUserVolunteer_user_not_found(db): 

365 """MakeUserVolunteer should fail if user doesn't exist""" 

366 editor_user, editor_token = generate_user(is_editor=True) 

367 

368 with real_editor_session(editor_token) as api: 

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

370 api.MakeUserVolunteer( 

371 editor_pb2.MakeUserVolunteerReq( 

372 user_id=999999, 

373 role="Test Volunteer", 

374 ) 

375 ) 

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

377 assert e.value.details() == "Couldn't find that user." 

378 

379 

380def test_MakeUserVolunteer_already_volunteer(db): 

381 """MakeUserVolunteer should fail if user is already a volunteer""" 

382 editor_user, editor_token = generate_user(is_editor=True) 

383 normal_user, normal_token = generate_user() 

384 

385 refresh_materialized_views_rapid(empty_pb2.Empty()) 

386 with real_editor_session(editor_token) as api: 

387 # Create volunteer first time 

388 api.MakeUserVolunteer( 

389 editor_pb2.MakeUserVolunteerReq( 

390 user_id=normal_user.id, 

391 role="Test Volunteer", 

392 ) 

393 ) 

394 

395 # Try to create again 

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

397 api.MakeUserVolunteer( 

398 editor_pb2.MakeUserVolunteerReq( 

399 user_id=normal_user.id, 

400 role="Test Volunteer 2", 

401 ) 

402 ) 

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

404 assert e.value.details() == "This user is already a volunteer." 

405 

406 

407def test_MakeUserVolunteer_invalid_date(db): 

408 """MakeUserVolunteer should fail with invalid date format""" 

409 editor_user, editor_token = generate_user(is_editor=True) 

410 normal_user, normal_token = generate_user() 

411 

412 with real_editor_session(editor_token) as api: 

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

414 api.MakeUserVolunteer( 

415 editor_pb2.MakeUserVolunteerReq( 

416 user_id=normal_user.id, 

417 role="Test Volunteer", 

418 started_volunteering="invalid-date", 

419 ) 

420 ) 

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

422 assert e.value.details() == "Invalid start date for volunteering." 

423 

424 

425def test_UpdateVolunteer(db): 

426 """UpdateVolunteer should successfully update volunteer fields""" 

427 editor_user, editor_token = generate_user(is_editor=True) 

428 normal_user, normal_token = generate_user() 

429 

430 refresh_materialized_views_rapid(empty_pb2.Empty()) 

431 with session_scope() as session: 

432 with real_editor_session(editor_token) as api: 

433 # Create volunteer first 

434 api.MakeUserVolunteer( 

435 editor_pb2.MakeUserVolunteerReq( 

436 user_id=normal_user.id, 

437 role="Test Volunteer", 

438 ) 

439 ) 

440 

441 # Update volunteer 

442 res = api.UpdateVolunteer( 

443 editor_pb2.UpdateVolunteerReq( 

444 user_id=normal_user.id, 

445 role=StringValue(value="Updated Volunteer"), 

446 sort_key=DoubleValue(value=10.5), 

447 started_volunteering=StringValue(value="2023-06-01"), 

448 stopped_volunteering=StringValue(value="2024-12-31"), 

449 show_on_team_page=BoolValue(value=False), 

450 ) 

451 ) 

452 

453 # Check response 

454 assert res.user_id == normal_user.id 

455 assert res.role == "Updated Volunteer" 

456 assert res.sort_key == 10.5 

457 assert res.started_volunteering == "2023-06-01" 

458 assert res.stopped_volunteering == "2024-12-31" 

459 assert res.show_on_team_page is False 

460 assert res.username == normal_user.username 

461 

462 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

463 assert volunteer.role == "Updated Volunteer" 

464 assert volunteer.sort_key == 10.5 

465 assert volunteer.started_volunteering.isoformat() == "2023-06-01" 

466 assert volunteer.stopped_volunteering 

467 assert volunteer.stopped_volunteering.isoformat() == "2024-12-31" 

468 assert volunteer.show_on_team_page is False 

469 

470 

471def test_UpdateVolunteer_partial_update(db): 

472 """UpdateVolunteer should only update provided fields""" 

473 editor_user, editor_token = generate_user(is_editor=True) 

474 normal_user, normal_token = generate_user() 

475 

476 refresh_materialized_views_rapid(empty_pb2.Empty()) 

477 with session_scope() as session: 

478 with real_editor_session(editor_token) as api: 

479 # Create volunteer first 

480 api.MakeUserVolunteer( 

481 editor_pb2.MakeUserVolunteerReq( 

482 user_id=normal_user.id, 

483 role="Test Volunteer", 

484 started_volunteering="2024-01-01", 

485 ) 

486 ) 

487 

488 # Update only role 

489 api.UpdateVolunteer( 

490 editor_pb2.UpdateVolunteerReq( 

491 user_id=normal_user.id, 

492 role=StringValue(value="Updated Role"), 

493 ) 

494 ) 

495 

496 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

497 assert volunteer.role == "Updated Role" 

498 assert volunteer.started_volunteering.isoformat() == "2024-01-01" # unchanged 

499 assert volunteer.show_on_team_page is True # unchanged 

500 

501 

502def test_UpdateVolunteer_not_found(db): 

503 """UpdateVolunteer should fail if volunteer doesn't exist""" 

504 editor_user, editor_token = generate_user(is_editor=True) 

505 normal_user, normal_token = generate_user() 

506 

507 with real_editor_session(editor_token) as api: 

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

509 api.UpdateVolunteer( 

510 editor_pb2.UpdateVolunteerReq( 

511 user_id=normal_user.id, 

512 role=StringValue(value="Updated Volunteer"), 

513 ) 

514 ) 

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

516 assert e.value.details() == "Volunteer not found." 

517 

518 

519def test_UpdateVolunteer_invalid_started_date(db): 

520 """UpdateVolunteer should fail with invalid started_volunteering date""" 

521 editor_user, editor_token = generate_user(is_editor=True) 

522 normal_user, normal_token = generate_user() 

523 

524 refresh_materialized_views_rapid(empty_pb2.Empty()) 

525 with real_editor_session(editor_token) as api: 

526 # Create volunteer first 

527 api.MakeUserVolunteer( 

528 editor_pb2.MakeUserVolunteerReq( 

529 user_id=normal_user.id, 

530 role="Test Volunteer", 

531 ) 

532 ) 

533 

534 # Try to update with invalid date 

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

536 api.UpdateVolunteer( 

537 editor_pb2.UpdateVolunteerReq( 

538 user_id=normal_user.id, 

539 started_volunteering=StringValue(value="invalid-date"), 

540 ) 

541 ) 

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

543 assert e.value.details() == "Invalid start date for volunteering." 

544 

545 

546def test_UpdateVolunteer_invalid_stopped_date(db): 

547 """UpdateVolunteer should fail with invalid stopped_volunteering date""" 

548 editor_user, editor_token = generate_user(is_editor=True) 

549 normal_user, normal_token = generate_user() 

550 

551 refresh_materialized_views_rapid(empty_pb2.Empty()) 

552 with real_editor_session(editor_token) as api: 

553 # Create volunteer first 

554 api.MakeUserVolunteer( 

555 editor_pb2.MakeUserVolunteerReq( 

556 user_id=normal_user.id, 

557 role="Test Volunteer", 

558 ) 

559 ) 

560 

561 # Try to update with invalid date 

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

563 api.UpdateVolunteer( 

564 editor_pb2.UpdateVolunteerReq( 

565 user_id=normal_user.id, 

566 stopped_volunteering=StringValue(value="not-a-date"), 

567 ) 

568 ) 

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

570 assert e.value.details() == "Invalid end date for volunteering." 

571 

572 

573def test_UpdateVolunteer_reinstate(db): 

574 """UpdateVolunteer should clear stopped_volunteering when reinstate_volunteer=True""" 

575 editor_user, editor_token = generate_user(is_editor=True) 

576 normal_user, normal_token = generate_user() 

577 

578 refresh_materialized_views_rapid(empty_pb2.Empty()) 

579 with real_editor_session(editor_token) as api: 

580 api.MakeUserVolunteer( 

581 editor_pb2.MakeUserVolunteerReq( 

582 user_id=normal_user.id, 

583 role="Test Volunteer", 

584 ) 

585 ) 

586 

587 # Set a stopped date first (make them a former volunteer) 

588 api.UpdateVolunteer( 

589 editor_pb2.UpdateVolunteerReq( 

590 user_id=normal_user.id, 

591 stopped_volunteering=StringValue(value="2024-12-31"), 

592 ) 

593 ) 

594 with session_scope() as session: 

595 volunteer_before = session.execute( 

596 select(Volunteer).where(Volunteer.user_id == normal_user.id) 

597 ).scalar_one() 

598 assert volunteer_before.stopped_volunteering is not None 

599 

600 # Reinstate them 

601 res = api.UpdateVolunteer( 

602 editor_pb2.UpdateVolunteerReq( 

603 user_id=normal_user.id, 

604 reinstate_volunteer=True, 

605 ) 

606 ) 

607 assert not res.HasField("stopped_volunteering") 

608 

609 with session_scope() as session: 

610 volunteer_after = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

611 assert volunteer_after.stopped_volunteering is None 

612 

613 

614def test_UpdateVolunteer_reinstate_already_current(db): 

615 """UpdateVolunteer with reinstate_volunteer=True on a current volunteer is a no-op""" 

616 editor_user, editor_token = generate_user(is_editor=True) 

617 normal_user, normal_token = generate_user() 

618 

619 refresh_materialized_views_rapid(empty_pb2.Empty()) 

620 with real_editor_session(editor_token) as api: 

621 api.MakeUserVolunteer( 

622 editor_pb2.MakeUserVolunteerReq( 

623 user_id=normal_user.id, 

624 role="Test Volunteer", 

625 ) 

626 ) 

627 

628 res = api.UpdateVolunteer( 

629 editor_pb2.UpdateVolunteerReq( 

630 user_id=normal_user.id, 

631 reinstate_volunteer=True, 

632 ) 

633 ) 

634 assert not res.HasField("stopped_volunteering") 

635 

636 with session_scope() as session: 

637 volunteer = session.execute(select(Volunteer).where(Volunteer.user_id == normal_user.id)).scalar_one() 

638 assert volunteer.stopped_volunteering is None 

639 

640 

641def test_UpdateVolunteer_reinstate_conflict_with_stopped(db): 

642 """UpdateVolunteer should fail if reinstate_volunteer=True and stopped_volunteering is also set""" 

643 editor_user, editor_token = generate_user(is_editor=True) 

644 normal_user, normal_token = generate_user() 

645 

646 refresh_materialized_views_rapid(empty_pb2.Empty()) 

647 with real_editor_session(editor_token) as api: 

648 api.MakeUserVolunteer( 

649 editor_pb2.MakeUserVolunteerReq( 

650 user_id=normal_user.id, 

651 role="Test Volunteer", 

652 ) 

653 ) 

654 

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

656 api.UpdateVolunteer( 

657 editor_pb2.UpdateVolunteerReq( 

658 user_id=normal_user.id, 

659 reinstate_volunteer=True, 

660 stopped_volunteering=StringValue(value="2024-12-31"), 

661 ) 

662 ) 

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

664 assert e.value.details() == "Cannot reinstate a volunteer and set a stopped date at the same time." 

665 

666 

667def test_ListVolunteers(db): 

668 """ListVolunteers should return all current volunteers""" 

669 editor_user, editor_token = generate_user(is_editor=True) 

670 user1, _ = generate_user() 

671 user2, _ = generate_user() 

672 user3, _ = generate_user() 

673 

674 refresh_materialized_views_rapid(empty_pb2.Empty()) 

675 with session_scope() as session: 

676 with real_editor_session(editor_token) as api: 

677 # Create three volunteers 

678 api.MakeUserVolunteer( 

679 editor_pb2.MakeUserVolunteerReq( 

680 user_id=user1.id, 

681 role="Volunteer 1", 

682 started_volunteering="2024-01-15", 

683 ) 

684 ) 

685 api.MakeUserVolunteer( 

686 editor_pb2.MakeUserVolunteerReq( 

687 user_id=user2.id, 

688 role="Volunteer 2", 

689 started_volunteering="2023-06-01", 

690 ) 

691 ) 

692 api.MakeUserVolunteer( 

693 editor_pb2.MakeUserVolunteerReq( 

694 user_id=user3.id, 

695 role="Volunteer 3", 

696 started_volunteering="2024-03-20", 

697 ) 

698 ) 

699 

700 # List volunteers (only current ones by default) 

701 res = api.ListVolunteers(editor_pb2.ListVolunteersReq(include_past=False)) 

702 

703 assert len(res.volunteers) == 3 

704 user_ids = {v.user_id for v in res.volunteers} 

705 assert user_ids == {user1.id, user2.id, user3.id} 

706 

707 # Check that all fields are populated 

708 for volunteer in res.volunteers: 

709 assert volunteer.user_id > 0 

710 assert volunteer.role != "" 

711 assert volunteer.username != "" 

712 assert volunteer.name != "" 

713 assert volunteer.started_volunteering != "" 

714 assert volunteer.show_on_team_page is True 

715 

716 

717def test_ListVolunteers_with_past(db): 

718 """ListVolunteers should include past volunteers when requested""" 

719 editor_user, editor_token = generate_user(is_editor=True) 

720 user1, _ = generate_user() 

721 user2, _ = generate_user() 

722 

723 refresh_materialized_views_rapid(empty_pb2.Empty()) 

724 with session_scope() as session: 

725 with real_editor_session(editor_token) as api: 

726 # Create current volunteer 

727 api.MakeUserVolunteer( 

728 editor_pb2.MakeUserVolunteerReq( 

729 user_id=user1.id, 

730 role="Current Volunteer", 

731 ) 

732 ) 

733 

734 # Create past volunteer 

735 api.MakeUserVolunteer( 

736 editor_pb2.MakeUserVolunteerReq( 

737 user_id=user2.id, 

738 role="Past Volunteer", 

739 ) 

740 ) 

741 api.UpdateVolunteer( 

742 editor_pb2.UpdateVolunteerReq( 

743 user_id=user2.id, 

744 stopped_volunteering=StringValue(value="2024-06-30"), 

745 ) 

746 ) 

747 

748 # List only current volunteers 

749 res = api.ListVolunteers(editor_pb2.ListVolunteersReq(include_past=False)) 

750 assert len(res.volunteers) == 1 

751 assert res.volunteers[0].user_id == user1.id 

752 assert not res.volunteers[0].HasField("stopped_volunteering") 

753 

754 # List all volunteers (including past) 

755 res_with_past = api.ListVolunteers(editor_pb2.ListVolunteersReq(include_past=True)) 

756 assert len(res_with_past.volunteers) == 2 

757 user_ids = {v.user_id for v in res_with_past.volunteers} 

758 assert user_ids == {user1.id, user2.id} 

759 

760 # Find the past volunteer and verify stopped_volunteering is set 

761 past_volunteer = next(v for v in res_with_past.volunteers if v.user_id == user2.id) 

762 assert past_volunteer.stopped_volunteering == "2024-06-30" 

763 

764 

765def test_ListVolunteers_ordering(db): 

766 """ListVolunteers should respect sort_key ordering""" 

767 editor_user, editor_token = generate_user(is_editor=True) 

768 user1, _ = generate_user() 

769 user2, _ = generate_user() 

770 user3, _ = generate_user() 

771 

772 refresh_materialized_views_rapid(empty_pb2.Empty()) 

773 with session_scope() as session: 

774 with real_editor_session(editor_token) as api: 

775 # Create volunteers with different sort keys 

776 api.MakeUserVolunteer(editor_pb2.MakeUserVolunteerReq(user_id=user1.id, role="Volunteer 1")) 

777 api.UpdateVolunteer(editor_pb2.UpdateVolunteerReq(user_id=user1.id, sort_key=DoubleValue(value=30.0))) 

778 

779 api.MakeUserVolunteer(editor_pb2.MakeUserVolunteerReq(user_id=user2.id, role="Volunteer 2")) 

780 api.UpdateVolunteer(editor_pb2.UpdateVolunteerReq(user_id=user2.id, sort_key=DoubleValue(value=10.0))) 

781 

782 api.MakeUserVolunteer(editor_pb2.MakeUserVolunteerReq(user_id=user3.id, role="Volunteer 3")) 

783 api.UpdateVolunteer(editor_pb2.UpdateVolunteerReq(user_id=user3.id, sort_key=DoubleValue(value=20.0))) 

784 

785 # List volunteers - should be ordered by sort_key ascending 

786 res = api.ListVolunteers(editor_pb2.ListVolunteersReq(include_past=False)) 

787 assert len(res.volunteers) == 3 

788 assert res.volunteers[0].user_id == user2.id # sort_key 10.0 

789 assert res.volunteers[1].user_id == user3.id # sort_key 20.0 

790 assert res.volunteers[2].user_id == user1.id # sort_key 30.0 

791 

792 

793def test_ListVolunteers_empty(db): 

794 """ListVolunteers should return empty list when no volunteers exist""" 

795 editor_user, editor_token = generate_user(is_editor=True) 

796 

797 refresh_materialized_views_rapid(empty_pb2.Empty()) 

798 with real_editor_session(editor_token) as api: 

799 res = api.ListVolunteers(editor_pb2.ListVolunteersReq(include_past=False)) 

800 assert len(res.volunteers) == 0 

801 

802 

803def test_LookupUsersForDebug(db): 

804 editor_user, editor_token = generate_user(is_editor=True) 

805 user1, _ = generate_user() 

806 user2, _ = generate_user() 

807 user3, _ = generate_user() 

808 user4, _ = generate_user() 

809 user5, _ = generate_user() 

810 

811 shared_sofa = "abcdefghijklMNOPQR" 

812 other_sofa = "zyxwvutsrqpoNMLKJI" 

813 wildcard_sofa = "abcdefghijkXMNOPQR" 

814 

815 with session_scope() as session: 

816 session.execute(update(User).where(User.id == user2.id).values(banned_at=now())) 

817 session.execute(update(User).where(User.id == user3.id).values(deleted_at=now())) 

818 session.execute(update(User).where(User.id == user4.id).values(shadowed_at=now())) 

819 

820 session.add(UserActivity(user_id=user1.id, period=now(), sofa=shared_sofa, api_calls=5)) 

821 session.add(UserActivity(user_id=user2.id, period=now(), sofa=shared_sofa, api_calls=3)) 

822 session.add(UserActivity(user_id=user2.id, period=now(), sofa=other_sofa, api_calls=100)) 

823 session.add(UserActivity(user_id=user3.id, period=now(), sofa=shared_sofa, api_calls=1)) 

824 session.add(UserActivity(user_id=user4.id, period=now(), sofa=shared_sofa, api_calls=1)) 

825 session.add(UserActivity(user_id=user5.id, period=now(), sofa=wildcard_sofa, api_calls=1)) 

826 session.add(UserActivity(user_id=user5.id, period=now(), sofa=None, api_calls=1)) 

827 

828 with real_editor_session(editor_token) as api: 

829 res = api.LookupUsersForDebug(editor_pb2.LookupUsersForDebugReq(sofa_prefix="abcdefghijkl")) 

830 

831 assert [u.user_id for u in res.users] == [user1.id, user2.id, user3.id, user4.id] 

832 assert [(u.banned, u.deleted, u.shadowed) for u in res.users] == [ 

833 (False, False, False), 

834 (True, False, False), 

835 (False, True, False), 

836 (False, False, True), 

837 ] 

838 

839 with real_editor_session(editor_token) as api: 

840 res = api.LookupUsersForDebug(editor_pb2.LookupUsersForDebugReq(sofa_prefix="abcdefghijk_")) 

841 assert list(res.users) == [] 

842 

843 with real_editor_session(editor_token) as api: 

844 for bad_prefix in ["", "abcdefghijk", "abcdefghijklm", shared_sofa]: 

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

846 api.LookupUsersForDebug(editor_pb2.LookupUsersForDebugReq(sofa_prefix=bad_prefix)) 

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

848 assert e.value.details() == "The sofa prefix must be exactly 12 characters." 

849 

850 

851@pytest.fixture 

852def event_in_wrong_community(db): 

853 """An event physically in "City" but parented to the far too large "Country" community.""" 

854 editor_user, editor_token = generate_user(is_editor=True) 

855 creator, creator_token = generate_user(complete_profile=True, geom=create_1d_point(3), geom_radius=0.1) 

856 member1, _ = generate_user(geom=create_1d_point(3), geom_radius=0.1) 

857 member2, _ = generate_user(geom=create_1d_point(4), geom_radius=0.1) 

858 

859 with session_scope() as session: 

860 w = create_community(session, 0, 100, "Global", [editor_user], [], None) 

861 mr = create_community(session, 0, 100, "Macroregion", [editor_user], [], w) 

862 country = create_community(session, 0, 50, "Country", [editor_user], [], mr) 

863 city = create_community(session, 0, 10, "City", [creator], [member1, member2], country) 

864 country_id = country.id 

865 city_id = city.id 

866 

867 enforce_community_memberships() 

868 

869 event_id = events_api.create_event( 

870 token=creator_token, 

871 parent_community_id=country_id, 

872 location=events_pb2.EventLocation(address="Somewhere", lat=3, lng=1), 

873 ).event_id 

874 

875 yield editor_token, creator_token, event_id, country_id, city_id 

876 

877 

878def test_GetEventCommunityInfo(event_in_wrong_community): 

879 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

880 

881 with real_editor_session(editor_token) as api: 

882 res = api.GetEventCommunityInfo(editor_pb2.GetEventCommunityInfoReq(event_id=event_id)) 

883 

884 assert res.event_id == event_id 

885 assert res.title == "Dummy Title" 

886 assert res.address == "Somewhere" 

887 assert res.occurrence_count == 1 

888 assert not res.is_in_smallest_local_community 

889 

890 assert res.current.community.community_id == country_id 

891 assert res.current.community.name == "Country" 

892 assert res.current.too_large_for_notifications 

893 assert res.current.approx_users_to_notify == 0 

894 

895 assert res.smallest_local.community.community_id == city_id 

896 assert res.smallest_local.community.name == "City" 

897 assert not res.smallest_local.too_large_for_notifications 

898 assert not res.smallest_local.notifies_by_proximity 

899 # the two other members of the city; the creator is an organizer so doesn't need an invite 

900 assert res.smallest_local.approx_users_to_notify == 2 

901 

902 

903def test_GetEventCommunityInfo_already_in_smallest_community(event_in_wrong_community): 

904 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

905 

906 correctly_placed_event_id = events_api.create_event( 

907 token=creator_token, 

908 parent_community_id=city_id, 

909 location=events_pb2.EventLocation(address="Somewhere", lat=3, lng=1), 

910 ).event_id 

911 

912 with real_editor_session(editor_token) as api: 

913 res = api.GetEventCommunityInfo(editor_pb2.GetEventCommunityInfoReq(event_id=correctly_placed_event_id)) 

914 

915 assert res.is_in_smallest_local_community 

916 assert res.current.community.community_id == city_id 

917 assert res.smallest_local.community.community_id == city_id 

918 

919 

920def test_GetEventCommunityInfo_event_not_found(db): 

921 _, editor_token = generate_user(is_editor=True) 

922 

923 with real_editor_session(editor_token) as api: 

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

925 api.GetEventCommunityInfo(editor_pb2.GetEventCommunityInfoReq(event_id=42)) 

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

927 assert e.value.details() == "Event not found." 

928 

929 

930def test_GetEventCommunityInfo_access_by_normal_user(event_in_wrong_community): 

931 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

932 

933 with real_editor_session(creator_token) as api: 

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

935 api.GetEventCommunityInfo(editor_pb2.GetEventCommunityInfoReq(event_id=event_id)) 

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

937 

938 

939def test_MoveEventToCommunity_smallest_local_community(event_in_wrong_community): 

940 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

941 

942 with real_editor_session(editor_token) as api: 

943 res = api.MoveEventToCommunity( 

944 editor_pb2.MoveEventToCommunityReq(event_id=event_id, smallest_local_community=True) 

945 ) 

946 

947 assert res.current.community.community_id == city_id 

948 assert res.is_in_smallest_local_community 

949 assert not res.current.too_large_for_notifications 

950 assert res.current.approx_users_to_notify == 2 

951 

952 with session_scope() as session: 

953 assert session.execute(select(Event.parent_node_id)).scalar_one() == city_id 

954 

955 

956def test_MoveEventToCommunity_explicit_community(event_in_wrong_community): 

957 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

958 

959 with real_editor_session(editor_token) as api: 

960 res = api.MoveEventToCommunity(editor_pb2.MoveEventToCommunityReq(event_id=event_id, community_id=city_id)) 

961 assert res.current.community.community_id == city_id 

962 

963 # and back again, editors aren't restricted to the smallest community 

964 res = api.MoveEventToCommunity(editor_pb2.MoveEventToCommunityReq(event_id=event_id, community_id=country_id)) 

965 assert res.current.community.community_id == country_id 

966 assert not res.is_in_smallest_local_community 

967 

968 with session_scope() as session: 

969 assert session.execute(select(Event.parent_node_id)).scalar_one() == country_id 

970 

971 

972def test_MoveEventToCommunity_moves_all_occurrences(event_in_wrong_community): 

973 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

974 

975 # the fixture's occurrence is tomorrow at noon, so schedule this one after it 

976 other_occurrence_id = events_api.schedule_event( 

977 token=creator_token, 

978 event_id=event_id, 

979 location=events_pb2.EventLocation(address="Somewhere", lat=3, lng=1), 

980 start_date_time=local_date_time(now() + timedelta(days=2)), 

981 end_date_time=local_date_time(now() + timedelta(days=2, hours=1)), 

982 ).event_id 

983 

984 with real_editor_session(editor_token) as api: 

985 res = api.MoveEventToCommunity( 

986 editor_pb2.MoveEventToCommunityReq(event_id=event_id, smallest_local_community=True) 

987 ) 

988 assert res.occurrence_count == 2 

989 

990 res = api.GetEventCommunityInfo(editor_pb2.GetEventCommunityInfoReq(event_id=other_occurrence_id)) 

991 assert res.current.community.community_id == city_id 

992 

993 

994def test_MoveEventToCommunity_no_target(event_in_wrong_community): 

995 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

996 

997 with real_editor_session(editor_token) as api: 

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

999 api.MoveEventToCommunity(editor_pb2.MoveEventToCommunityReq(event_id=event_id)) 

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

1001 assert ( 

1002 e.value.details() 

1003 == "Specify either a community to move the event to, or recompute it from the event's location." 

1004 ) 

1005 

1006 

1007def test_MoveEventToCommunity_community_not_found(event_in_wrong_community): 

1008 editor_token, creator_token, event_id, country_id, city_id = event_in_wrong_community 

1009 

1010 with real_editor_session(editor_token) as api: 

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

1012 api.MoveEventToCommunity(editor_pb2.MoveEventToCommunityReq(event_id=event_id, community_id=42)) 

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

1014 assert e.value.details() == "Community not found."