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

326 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-19 15:47 +0000

1import grpc 

2import pytest 

3from google.protobuf import empty_pb2 

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

5from sqlalchemy import select 

6 

7from couchers.db import session_scope 

8from couchers.materialized_views import refresh_materialized_views_rapid 

9from couchers.models import ( 

10 Cluster, 

11 Node, 

12 Volunteer, 

13) 

14from couchers.proto import editor_pb2 

15from tests.fixtures.db import generate_user 

16from tests.fixtures.sessions import real_editor_session 

17 

18VALID_GEOJSON_MULTIPOLYGON = """ 

19 { 

20 "type": "MultiPolygon", 

21 "coordinates": 

22 [ 

23 [ 

24 [ 

25 [ 

26 -73.98114904754641, 

27 40.7470284264813 

28 ], 

29 [ 

30 -73.98314135177611, 

31 40.73416844413217 

32 ], 

33 [ 

34 -74.00538969848634, 

35 40.734314779027144 

36 ], 

37 [ 

38 -74.00479214294432, 

39 40.75027851544338 

40 ], 

41 [ 

42 -73.98114904754641, 

43 40.7470284264813 

44 ] 

45 ] 

46 ] 

47 ] 

48 } 

49""" 

50 

51POINT_GEOJSON = """ 

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

53""" 

54 

55 

56def test_access_by_normal_user(db): 

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

58 normal_user, normal_token = generate_user() 

59 

60 with real_editor_session(normal_token) as api: 

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

62 api.CreateCommunity( 

63 editor_pb2.CreateCommunityReq( 

64 name="test community", 

65 description="community for testing", 

66 admin_ids=[], 

67 geojson=VALID_GEOJSON_MULTIPOLYGON, 

68 ) 

69 ) 

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

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

72 

73 

74def test_access_by_editor_user(db): 

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

76 editor_user, editor_token = generate_user(is_editor=True) 

77 

78 with session_scope() as session: 

79 with real_editor_session(editor_token) as api: 

80 api.CreateCommunity( 

81 editor_pb2.CreateCommunityReq( 

82 name="test community", 

83 description="community for testing", 

84 admin_ids=[], 

85 geojson=VALID_GEOJSON_MULTIPOLYGON, 

86 ) 

87 ) 

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

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

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

91 

92 

93def test_access_by_superuser(db): 

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

95 editor_user, editor_token = generate_user(is_editor=True) 

96 

97 with session_scope() as session: 

98 with real_editor_session(editor_token) as api: 

99 api.CreateCommunity( 

100 editor_pb2.CreateCommunityReq( 

101 name="test community", 

102 description="community for testing", 

103 admin_ids=[], 

104 geojson=VALID_GEOJSON_MULTIPOLYGON, 

105 ) 

106 ) 

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

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

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

110 

111 

112def test_CreateCommunity_invalid_geojson(db): 

113 """CreateCommunity should reject invalid GeoJSON""" 

114 editor_user, editor_token = generate_user(is_editor=True) 

115 

116 with real_editor_session(editor_token) as api: 

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

118 api.CreateCommunity( 

119 editor_pb2.CreateCommunityReq( 

120 name="test community", 

121 description="community for testing", 

122 admin_ids=[], 

123 geojson=POINT_GEOJSON, 

124 ) 

125 ) 

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

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

128 

129 

130def test_UpdateCommunity_invalid_geojson(db): 

131 """UpdateCommunity should reject invalid GeoJSON""" 

132 editor_user, editor_token = generate_user(is_editor=True) 

133 

134 with session_scope() as session: 

135 with real_editor_session(editor_token) as api: 

136 api.CreateCommunity( 

137 editor_pb2.CreateCommunityReq( 

138 name="test community", 

139 description="community for testing", 

140 admin_ids=[], 

141 geojson=VALID_GEOJSON_MULTIPOLYGON, 

142 ) 

143 ) 

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

145 

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

147 api.UpdateCommunity( 

148 editor_pb2.UpdateCommunityReq( 

149 community_id=community.parent_node_id, 

150 name="test community 2", 

151 description="community for testing 2", 

152 geojson=POINT_GEOJSON, 

153 ) 

154 ) 

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

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

157 

158 

159def test_UpdateCommunity_invalid_id(db): 

160 """UpdateCommunity should reject invalid community IDs""" 

161 editor_user, editor_token = generate_user(is_editor=True) 

162 

163 with real_editor_session(editor_token) as api: 

164 api.CreateCommunity( 

165 editor_pb2.CreateCommunityReq( 

166 name="test community", 

167 description="community for testing", 

168 admin_ids=[], 

169 geojson=VALID_GEOJSON_MULTIPOLYGON, 

170 ) 

171 ) 

172 

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

174 api.UpdateCommunity( 

175 editor_pb2.UpdateCommunityReq( 

176 community_id=1000, 

177 name="test community 1000", 

178 description="community for testing 1000", 

179 geojson=VALID_GEOJSON_MULTIPOLYGON, 

180 ) 

181 ) 

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

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

184 

185 

186def test_UpdateCommunity(db): 

187 """UpdateCommunity should successfully update a community""" 

188 editor_user, editor_token = generate_user(is_editor=True) 

189 

190 with session_scope() as session: 

191 with real_editor_session(editor_token) as api: 

192 api.CreateCommunity( 

193 editor_pb2.CreateCommunityReq( 

194 name="test community", 

195 description="community for testing", 

196 admin_ids=[], 

197 geojson=VALID_GEOJSON_MULTIPOLYGON, 

198 ) 

199 ) 

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

201 

202 api.UpdateCommunity( 

203 editor_pb2.UpdateCommunityReq( 

204 community_id=community.parent_node_id, 

205 name="test community updated", 

206 description="community for testing updated", 

207 geojson=VALID_GEOJSON_MULTIPOLYGON, 

208 ) 

209 ) 

210 session.commit() 

211 

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

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

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

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

216 

217 

218def test_CreateCommunity(db): 

219 with session_scope() as session: 

220 editor_user, editor_token = generate_user(is_editor=True) 

221 normal_user, normal_token = generate_user() 

222 with real_editor_session(editor_token) as api: 

223 api.CreateCommunity( 

224 editor_pb2.CreateCommunityReq( 

225 name="test community", 

226 description="community for testing", 

227 admin_ids=[], 

228 geojson=VALID_GEOJSON_MULTIPOLYGON, 

229 ) 

230 ) 

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

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

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

234 

235 

236def test_UpdateCommunity2(db): 

237 editor_user, editor_token = generate_user(is_editor=True) 

238 

239 with session_scope() as session: 

240 with real_editor_session(editor_token) as api: 

241 api.CreateCommunity( 

242 editor_pb2.CreateCommunityReq( 

243 name="test community", 

244 description="community for testing", 

245 admin_ids=[], 

246 geojson=VALID_GEOJSON_MULTIPOLYGON, 

247 ) 

248 ) 

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

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

251 

252 api.CreateCommunity( 

253 editor_pb2.CreateCommunityReq( 

254 name="test community 2", 

255 description="community for testing 2", 

256 admin_ids=[], 

257 geojson=VALID_GEOJSON_MULTIPOLYGON, 

258 ) 

259 ) 

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

261 

262 api.UpdateCommunity( 

263 editor_pb2.UpdateCommunityReq( 

264 community_id=community.parent_node_id, 

265 name="test community 2", 

266 description="community for testing 2", 

267 geojson=VALID_GEOJSON_MULTIPOLYGON, 

268 parent_node_id=community_2.parent_node_id, 

269 ) 

270 ) 

271 session.commit() 

272 

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

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

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

276 

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

278 assert node_updated.parent_node_id == community_2.parent_node_id 

279 

280 

281def test_MakeUserVolunteer(db): 

282 """MakeUserVolunteer should successfully create a volunteer""" 

283 editor_user, editor_token = generate_user(is_editor=True) 

284 normal_user, normal_token = generate_user() 

285 

286 refresh_materialized_views_rapid(empty_pb2.Empty()) 

287 with session_scope() as session: 

288 with real_editor_session(editor_token) as api: 

289 res = api.MakeUserVolunteer( 

290 editor_pb2.MakeUserVolunteerReq( 

291 user_id=normal_user.id, 

292 role="Test Volunteer", 

293 started_volunteering="2024-01-15", 

294 hide_on_team_page=False, 

295 ) 

296 ) 

297 

298 # Check response 

299 assert res.user_id == normal_user.id 

300 assert res.role == "Test Volunteer" 

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

302 assert res.show_on_team_page is True 

303 assert res.username == normal_user.username 

304 assert res.name == normal_user.name 

305 

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

307 assert volunteer.role == "Test Volunteer" 

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

309 assert volunteer.show_on_team_page is True 

310 

311 

312def test_MakeUserVolunteer_default_values(db): 

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

314 editor_user, editor_token = generate_user(is_editor=True) 

315 normal_user, normal_token = generate_user() 

316 

317 refresh_materialized_views_rapid(empty_pb2.Empty()) 

318 with session_scope() as session: 

319 with real_editor_session(editor_token) as api: 

320 api.MakeUserVolunteer( 

321 editor_pb2.MakeUserVolunteerReq( 

322 user_id=normal_user.id, 

323 role="Test Volunteer", 

324 ) 

325 ) 

326 

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

328 assert volunteer.role == "Test Volunteer" 

329 assert volunteer.started_volunteering # defaults to today 

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

331 

332 

333def test_MakeUserVolunteer_hide_on_team_page(db): 

334 """MakeUserVolunteer should respect hide_on_team_page=True""" 

335 editor_user, editor_token = generate_user(is_editor=True) 

336 normal_user, normal_token = generate_user() 

337 

338 refresh_materialized_views_rapid(empty_pb2.Empty()) 

339 with session_scope() as session: 

340 with real_editor_session(editor_token) as api: 

341 api.MakeUserVolunteer( 

342 editor_pb2.MakeUserVolunteerReq( 

343 user_id=normal_user.id, 

344 role="Test Volunteer", 

345 hide_on_team_page=True, 

346 ) 

347 ) 

348 

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

350 assert volunteer.role == "Test Volunteer" 

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

352 

353 

354def test_MakeUserVolunteer_user_not_found(db): 

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

356 editor_user, editor_token = generate_user(is_editor=True) 

357 

358 with real_editor_session(editor_token) as api: 

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

360 api.MakeUserVolunteer( 

361 editor_pb2.MakeUserVolunteerReq( 

362 user_id=999999, 

363 role="Test Volunteer", 

364 ) 

365 ) 

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

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

368 

369 

370def test_MakeUserVolunteer_already_volunteer(db): 

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

372 editor_user, editor_token = generate_user(is_editor=True) 

373 normal_user, normal_token = generate_user() 

374 

375 refresh_materialized_views_rapid(empty_pb2.Empty()) 

376 with real_editor_session(editor_token) as api: 

377 # Create volunteer first time 

378 api.MakeUserVolunteer( 

379 editor_pb2.MakeUserVolunteerReq( 

380 user_id=normal_user.id, 

381 role="Test Volunteer", 

382 ) 

383 ) 

384 

385 # Try to create again 

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

387 api.MakeUserVolunteer( 

388 editor_pb2.MakeUserVolunteerReq( 

389 user_id=normal_user.id, 

390 role="Test Volunteer 2", 

391 ) 

392 ) 

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

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

395 

396 

397def test_MakeUserVolunteer_invalid_date(db): 

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

399 editor_user, editor_token = generate_user(is_editor=True) 

400 normal_user, normal_token = generate_user() 

401 

402 with real_editor_session(editor_token) as api: 

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

404 api.MakeUserVolunteer( 

405 editor_pb2.MakeUserVolunteerReq( 

406 user_id=normal_user.id, 

407 role="Test Volunteer", 

408 started_volunteering="invalid-date", 

409 ) 

410 ) 

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

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

413 

414 

415def test_UpdateVolunteer(db): 

416 """UpdateVolunteer should successfully update volunteer fields""" 

417 editor_user, editor_token = generate_user(is_editor=True) 

418 normal_user, normal_token = generate_user() 

419 

420 refresh_materialized_views_rapid(empty_pb2.Empty()) 

421 with session_scope() as session: 

422 with real_editor_session(editor_token) as api: 

423 # Create volunteer first 

424 api.MakeUserVolunteer( 

425 editor_pb2.MakeUserVolunteerReq( 

426 user_id=normal_user.id, 

427 role="Test Volunteer", 

428 ) 

429 ) 

430 

431 # Update volunteer 

432 res = api.UpdateVolunteer( 

433 editor_pb2.UpdateVolunteerReq( 

434 user_id=normal_user.id, 

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

436 sort_key=DoubleValue(value=10.5), 

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

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

439 show_on_team_page=BoolValue(value=False), 

440 ) 

441 ) 

442 

443 # Check response 

444 assert res.user_id == normal_user.id 

445 assert res.role == "Updated Volunteer" 

446 assert res.sort_key == 10.5 

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

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

449 assert res.show_on_team_page is False 

450 assert res.username == normal_user.username 

451 

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

453 assert volunteer.role == "Updated Volunteer" 

454 assert volunteer.sort_key == 10.5 

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

456 assert volunteer.stopped_volunteering 

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

458 assert volunteer.show_on_team_page is False 

459 

460 

461def test_UpdateVolunteer_partial_update(db): 

462 """UpdateVolunteer should only update provided fields""" 

463 editor_user, editor_token = generate_user(is_editor=True) 

464 normal_user, normal_token = generate_user() 

465 

466 refresh_materialized_views_rapid(empty_pb2.Empty()) 

467 with session_scope() as session: 

468 with real_editor_session(editor_token) as api: 

469 # Create volunteer first 

470 api.MakeUserVolunteer( 

471 editor_pb2.MakeUserVolunteerReq( 

472 user_id=normal_user.id, 

473 role="Test Volunteer", 

474 started_volunteering="2024-01-01", 

475 ) 

476 ) 

477 

478 # Update only role 

479 api.UpdateVolunteer( 

480 editor_pb2.UpdateVolunteerReq( 

481 user_id=normal_user.id, 

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

483 ) 

484 ) 

485 

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

487 assert volunteer.role == "Updated Role" 

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

489 assert volunteer.show_on_team_page is True # unchanged 

490 

491 

492def test_UpdateVolunteer_not_found(db): 

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

494 editor_user, editor_token = generate_user(is_editor=True) 

495 normal_user, normal_token = generate_user() 

496 

497 with real_editor_session(editor_token) as api: 

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

499 api.UpdateVolunteer( 

500 editor_pb2.UpdateVolunteerReq( 

501 user_id=normal_user.id, 

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

503 ) 

504 ) 

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

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

507 

508 

509def test_UpdateVolunteer_invalid_started_date(db): 

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

511 editor_user, editor_token = generate_user(is_editor=True) 

512 normal_user, normal_token = generate_user() 

513 

514 refresh_materialized_views_rapid(empty_pb2.Empty()) 

515 with real_editor_session(editor_token) as api: 

516 # Create volunteer first 

517 api.MakeUserVolunteer( 

518 editor_pb2.MakeUserVolunteerReq( 

519 user_id=normal_user.id, 

520 role="Test Volunteer", 

521 ) 

522 ) 

523 

524 # Try to update with invalid date 

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

526 api.UpdateVolunteer( 

527 editor_pb2.UpdateVolunteerReq( 

528 user_id=normal_user.id, 

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

530 ) 

531 ) 

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

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

534 

535 

536def test_UpdateVolunteer_invalid_stopped_date(db): 

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

538 editor_user, editor_token = generate_user(is_editor=True) 

539 normal_user, normal_token = generate_user() 

540 

541 refresh_materialized_views_rapid(empty_pb2.Empty()) 

542 with real_editor_session(editor_token) as api: 

543 # Create volunteer first 

544 api.MakeUserVolunteer( 

545 editor_pb2.MakeUserVolunteerReq( 

546 user_id=normal_user.id, 

547 role="Test Volunteer", 

548 ) 

549 ) 

550 

551 # Try to update with invalid date 

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

553 api.UpdateVolunteer( 

554 editor_pb2.UpdateVolunteerReq( 

555 user_id=normal_user.id, 

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

557 ) 

558 ) 

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

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

561 

562 

563def test_UpdateVolunteer_reinstate(db): 

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

565 editor_user, editor_token = generate_user(is_editor=True) 

566 normal_user, normal_token = generate_user() 

567 

568 refresh_materialized_views_rapid(empty_pb2.Empty()) 

569 with real_editor_session(editor_token) as api: 

570 api.MakeUserVolunteer( 

571 editor_pb2.MakeUserVolunteerReq( 

572 user_id=normal_user.id, 

573 role="Test Volunteer", 

574 ) 

575 ) 

576 

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

578 api.UpdateVolunteer( 

579 editor_pb2.UpdateVolunteerReq( 

580 user_id=normal_user.id, 

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

582 ) 

583 ) 

584 with session_scope() as session: 

585 volunteer_before = session.execute( 

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

587 ).scalar_one() 

588 assert volunteer_before.stopped_volunteering is not None 

589 

590 # Reinstate them 

591 res = api.UpdateVolunteer( 

592 editor_pb2.UpdateVolunteerReq( 

593 user_id=normal_user.id, 

594 reinstate_volunteer=True, 

595 ) 

596 ) 

597 assert not res.HasField("stopped_volunteering") 

598 

599 with session_scope() as session: 

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

601 assert volunteer_after.stopped_volunteering is None 

602 

603 

604def test_UpdateVolunteer_reinstate_already_current(db): 

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

606 editor_user, editor_token = generate_user(is_editor=True) 

607 normal_user, normal_token = generate_user() 

608 

609 refresh_materialized_views_rapid(empty_pb2.Empty()) 

610 with real_editor_session(editor_token) as api: 

611 api.MakeUserVolunteer( 

612 editor_pb2.MakeUserVolunteerReq( 

613 user_id=normal_user.id, 

614 role="Test Volunteer", 

615 ) 

616 ) 

617 

618 res = api.UpdateVolunteer( 

619 editor_pb2.UpdateVolunteerReq( 

620 user_id=normal_user.id, 

621 reinstate_volunteer=True, 

622 ) 

623 ) 

624 assert not res.HasField("stopped_volunteering") 

625 

626 with session_scope() as session: 

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

628 assert volunteer.stopped_volunteering is None 

629 

630 

631def test_UpdateVolunteer_reinstate_conflict_with_stopped(db): 

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

633 editor_user, editor_token = generate_user(is_editor=True) 

634 normal_user, normal_token = generate_user() 

635 

636 refresh_materialized_views_rapid(empty_pb2.Empty()) 

637 with real_editor_session(editor_token) as api: 

638 api.MakeUserVolunteer( 

639 editor_pb2.MakeUserVolunteerReq( 

640 user_id=normal_user.id, 

641 role="Test Volunteer", 

642 ) 

643 ) 

644 

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

646 api.UpdateVolunteer( 

647 editor_pb2.UpdateVolunteerReq( 

648 user_id=normal_user.id, 

649 reinstate_volunteer=True, 

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

651 ) 

652 ) 

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

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

655 

656 

657def test_ListVolunteers(db): 

658 """ListVolunteers should return all current volunteers""" 

659 editor_user, editor_token = generate_user(is_editor=True) 

660 user1, _ = generate_user() 

661 user2, _ = generate_user() 

662 user3, _ = generate_user() 

663 

664 refresh_materialized_views_rapid(empty_pb2.Empty()) 

665 with session_scope() as session: 

666 with real_editor_session(editor_token) as api: 

667 # Create three volunteers 

668 api.MakeUserVolunteer( 

669 editor_pb2.MakeUserVolunteerReq( 

670 user_id=user1.id, 

671 role="Volunteer 1", 

672 started_volunteering="2024-01-15", 

673 ) 

674 ) 

675 api.MakeUserVolunteer( 

676 editor_pb2.MakeUserVolunteerReq( 

677 user_id=user2.id, 

678 role="Volunteer 2", 

679 started_volunteering="2023-06-01", 

680 ) 

681 ) 

682 api.MakeUserVolunteer( 

683 editor_pb2.MakeUserVolunteerReq( 

684 user_id=user3.id, 

685 role="Volunteer 3", 

686 started_volunteering="2024-03-20", 

687 ) 

688 ) 

689 

690 # List volunteers (only current ones by default) 

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

692 

693 assert len(res.volunteers) == 3 

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

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

696 

697 # Check that all fields are populated 

698 for volunteer in res.volunteers: 

699 assert volunteer.user_id > 0 

700 assert volunteer.role != "" 

701 assert volunteer.username != "" 

702 assert volunteer.name != "" 

703 assert volunteer.started_volunteering != "" 

704 assert volunteer.show_on_team_page is True 

705 

706 

707def test_ListVolunteers_with_past(db): 

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

709 editor_user, editor_token = generate_user(is_editor=True) 

710 user1, _ = generate_user() 

711 user2, _ = generate_user() 

712 

713 refresh_materialized_views_rapid(empty_pb2.Empty()) 

714 with session_scope() as session: 

715 with real_editor_session(editor_token) as api: 

716 # Create current volunteer 

717 api.MakeUserVolunteer( 

718 editor_pb2.MakeUserVolunteerReq( 

719 user_id=user1.id, 

720 role="Current Volunteer", 

721 ) 

722 ) 

723 

724 # Create past volunteer 

725 api.MakeUserVolunteer( 

726 editor_pb2.MakeUserVolunteerReq( 

727 user_id=user2.id, 

728 role="Past Volunteer", 

729 ) 

730 ) 

731 api.UpdateVolunteer( 

732 editor_pb2.UpdateVolunteerReq( 

733 user_id=user2.id, 

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

735 ) 

736 ) 

737 

738 # List only current volunteers 

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

740 assert len(res.volunteers) == 1 

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

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

743 

744 # List all volunteers (including past) 

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

746 assert len(res_with_past.volunteers) == 2 

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

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

749 

750 # Find the past volunteer and verify stopped_volunteering is set 

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

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

753 

754 

755def test_ListVolunteers_ordering(db): 

756 """ListVolunteers should respect sort_key ordering""" 

757 editor_user, editor_token = generate_user(is_editor=True) 

758 user1, _ = generate_user() 

759 user2, _ = generate_user() 

760 user3, _ = generate_user() 

761 

762 refresh_materialized_views_rapid(empty_pb2.Empty()) 

763 with session_scope() as session: 

764 with real_editor_session(editor_token) as api: 

765 # Create volunteers with different sort keys 

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

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

768 

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

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

771 

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

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

774 

775 # List volunteers - should be ordered by sort_key ascending 

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

777 assert len(res.volunteers) == 3 

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

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

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

781 

782 

783def test_ListVolunteers_empty(db): 

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

785 editor_user, editor_token = generate_user(is_editor=True) 

786 

787 refresh_materialized_views_rapid(empty_pb2.Empty()) 

788 with real_editor_session(editor_token) as api: 

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

790 assert len(res.volunteers) == 0