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

290 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-12-06 23:17 +0000

1import grpc 

2import pytest 

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

4 

5from couchers.db import session_scope 

6from couchers.materialized_views import refresh_materialized_views_rapid 

7from couchers.models import ( 

8 Cluster, 

9 Node, 

10 Volunteer, 

11) 

12from couchers.proto import editor_pb2 

13from couchers.sql import couchers_select as select 

14from tests.test_fixtures import db, generate_user, real_editor_session, testconfig # noqa 

15 

16 

17@pytest.fixture(autouse=True) 

18def _(testconfig): 

19 pass 

20 

21 

22VALID_GEOJSON_MULTIPOLYGON = """ 

23 { 

24 "type": "MultiPolygon", 

25 "coordinates": 

26 [ 

27 [ 

28 [ 

29 [ 

30 -73.98114904754641, 

31 40.7470284264813 

32 ], 

33 [ 

34 -73.98314135177611, 

35 40.73416844413217 

36 ], 

37 [ 

38 -74.00538969848634, 

39 40.734314779027144 

40 ], 

41 [ 

42 -74.00479214294432, 

43 40.75027851544338 

44 ], 

45 [ 

46 -73.98114904754641, 

47 40.7470284264813 

48 ] 

49 ] 

50 ] 

51 ] 

52 } 

53""" 

54 

55POINT_GEOJSON = """ 

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

57""" 

58 

59 

60def test_access_by_normal_user(db): 

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

62 normal_user, normal_token = generate_user() 

63 

64 with real_editor_session(normal_token) as api: 

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

66 api.CreateCommunity( 

67 editor_pb2.CreateCommunityReq( 

68 name="test community", 

69 description="community for testing", 

70 admin_ids=[], 

71 geojson=VALID_GEOJSON_MULTIPOLYGON, 

72 ) 

73 ) 

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

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

76 

77 

78def test_access_by_editor_user(db): 

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

80 editor_user, editor_token = generate_user(is_editor=True) 

81 

82 with session_scope() as session: 

83 with real_editor_session(editor_token) as api: 

84 api.CreateCommunity( 

85 editor_pb2.CreateCommunityReq( 

86 name="test community", 

87 description="community for testing", 

88 admin_ids=[], 

89 geojson=VALID_GEOJSON_MULTIPOLYGON, 

90 ) 

91 ) 

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

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

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

95 

96 

97def test_access_by_superuser(db): 

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

99 editor_user, editor_token = generate_user(is_editor=True) 

100 

101 with session_scope() as session: 

102 with real_editor_session(editor_token) as api: 

103 api.CreateCommunity( 

104 editor_pb2.CreateCommunityReq( 

105 name="test community", 

106 description="community for testing", 

107 admin_ids=[], 

108 geojson=VALID_GEOJSON_MULTIPOLYGON, 

109 ) 

110 ) 

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

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

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

114 

115 

116def test_CreateCommunity_invalid_geojson(db): 

117 """CreateCommunity should reject invalid GeoJSON""" 

118 editor_user, editor_token = generate_user(is_editor=True) 

119 

120 with real_editor_session(editor_token) as api: 

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

122 api.CreateCommunity( 

123 editor_pb2.CreateCommunityReq( 

124 name="test community", 

125 description="community for testing", 

126 admin_ids=[], 

127 geojson=POINT_GEOJSON, 

128 ) 

129 ) 

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

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

132 

133 

134def test_UpdateCommunity_invalid_geojson(db): 

135 """UpdateCommunity should reject invalid GeoJSON""" 

136 editor_user, editor_token = generate_user(is_editor=True) 

137 

138 with session_scope() as session: 

139 with real_editor_session(editor_token) as api: 

140 api.CreateCommunity( 

141 editor_pb2.CreateCommunityReq( 

142 name="test community", 

143 description="community for testing", 

144 admin_ids=[], 

145 geojson=VALID_GEOJSON_MULTIPOLYGON, 

146 ) 

147 ) 

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

149 

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

151 api.UpdateCommunity( 

152 editor_pb2.UpdateCommunityReq( 

153 community_id=community.parent_node_id, 

154 name="test community 2", 

155 description="community for testing 2", 

156 geojson=POINT_GEOJSON, 

157 ) 

158 ) 

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

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

161 

162 

163def test_UpdateCommunity_invalid_id(db): 

164 """UpdateCommunity should reject invalid community IDs""" 

165 editor_user, editor_token = generate_user(is_editor=True) 

166 

167 with real_editor_session(editor_token) as api: 

168 api.CreateCommunity( 

169 editor_pb2.CreateCommunityReq( 

170 name="test community", 

171 description="community for testing", 

172 admin_ids=[], 

173 geojson=VALID_GEOJSON_MULTIPOLYGON, 

174 ) 

175 ) 

176 

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

178 api.UpdateCommunity( 

179 editor_pb2.UpdateCommunityReq( 

180 community_id=1000, 

181 name="test community 1000", 

182 description="community for testing 1000", 

183 geojson=VALID_GEOJSON_MULTIPOLYGON, 

184 ) 

185 ) 

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

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

188 

189 

190def test_UpdateCommunity(db): 

191 """UpdateCommunity should successfully update a community""" 

192 editor_user, editor_token = generate_user(is_editor=True) 

193 

194 with session_scope() as session: 

195 with real_editor_session(editor_token) as api: 

196 api.CreateCommunity( 

197 editor_pb2.CreateCommunityReq( 

198 name="test community", 

199 description="community for testing", 

200 admin_ids=[], 

201 geojson=VALID_GEOJSON_MULTIPOLYGON, 

202 ) 

203 ) 

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

205 

206 api.UpdateCommunity( 

207 editor_pb2.UpdateCommunityReq( 

208 community_id=community.parent_node_id, 

209 name="test community updated", 

210 description="community for testing updated", 

211 geojson=VALID_GEOJSON_MULTIPOLYGON, 

212 ) 

213 ) 

214 session.commit() 

215 

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

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

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

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

220 

221 

222def test_CreateCommunity(db): 

223 with session_scope() as session: 

224 editor_user, editor_token = generate_user(is_editor=True) 

225 normal_user, normal_token = generate_user() 

226 with real_editor_session(editor_token) as api: 

227 api.CreateCommunity( 

228 editor_pb2.CreateCommunityReq( 

229 name="test community", 

230 description="community for testing", 

231 admin_ids=[], 

232 geojson=VALID_GEOJSON_MULTIPOLYGON, 

233 ) 

234 ) 

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

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

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

238 

239 

240def test_UpdateCommunity2(db): 

241 editor_user, editor_token = generate_user(is_editor=True) 

242 

243 with session_scope() as session: 

244 with real_editor_session(editor_token) as api: 

245 api.CreateCommunity( 

246 editor_pb2.CreateCommunityReq( 

247 name="test community", 

248 description="community for testing", 

249 admin_ids=[], 

250 geojson=VALID_GEOJSON_MULTIPOLYGON, 

251 ) 

252 ) 

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

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

255 

256 api.CreateCommunity( 

257 editor_pb2.CreateCommunityReq( 

258 name="test community 2", 

259 description="community for testing 2", 

260 admin_ids=[], 

261 geojson=VALID_GEOJSON_MULTIPOLYGON, 

262 ) 

263 ) 

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

265 

266 api.UpdateCommunity( 

267 editor_pb2.UpdateCommunityReq( 

268 community_id=community.parent_node_id, 

269 name="test community 2", 

270 description="community for testing 2", 

271 geojson=VALID_GEOJSON_MULTIPOLYGON, 

272 parent_node_id=community_2.parent_node_id, 

273 ) 

274 ) 

275 session.commit() 

276 

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

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

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

280 

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

282 assert node_updated.parent_node_id == community_2.parent_node_id 

283 

284 

285def test_MakeUserVolunteer(db): 

286 """MakeUserVolunteer should successfully create a volunteer""" 

287 editor_user, editor_token = generate_user(is_editor=True) 

288 normal_user, normal_token = generate_user() 

289 

290 refresh_materialized_views_rapid(None) 

291 with session_scope() as session: 

292 with real_editor_session(editor_token) as api: 

293 res = api.MakeUserVolunteer( 

294 editor_pb2.MakeUserVolunteerReq( 

295 user_id=normal_user.id, 

296 role="Test Volunteer", 

297 started_volunteering="2024-01-15", 

298 hide_on_team_page=False, 

299 ) 

300 ) 

301 

302 # Check response 

303 assert res.user_id == normal_user.id 

304 assert res.role == "Test Volunteer" 

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

306 assert res.show_on_team_page is True 

307 assert res.username == normal_user.username 

308 assert res.name == normal_user.name 

309 

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

311 assert volunteer.role == "Test Volunteer" 

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

313 assert volunteer.show_on_team_page is True 

314 

315 

316def test_MakeUserVolunteer_default_values(db): 

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

318 editor_user, editor_token = generate_user(is_editor=True) 

319 normal_user, normal_token = generate_user() 

320 

321 refresh_materialized_views_rapid(None) 

322 with session_scope() as session: 

323 with real_editor_session(editor_token) as api: 

324 api.MakeUserVolunteer( 

325 editor_pb2.MakeUserVolunteerReq( 

326 user_id=normal_user.id, 

327 role="Test Volunteer", 

328 ) 

329 ) 

330 

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

332 assert volunteer.role == "Test Volunteer" 

333 assert volunteer.started_volunteering is not None # defaults to today 

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

335 

336 

337def test_MakeUserVolunteer_hide_on_team_page(db): 

338 """MakeUserVolunteer should respect hide_on_team_page=True""" 

339 editor_user, editor_token = generate_user(is_editor=True) 

340 normal_user, normal_token = generate_user() 

341 

342 refresh_materialized_views_rapid(None) 

343 with session_scope() as session: 

344 with real_editor_session(editor_token) as api: 

345 api.MakeUserVolunteer( 

346 editor_pb2.MakeUserVolunteerReq( 

347 user_id=normal_user.id, 

348 role="Test Volunteer", 

349 hide_on_team_page=True, 

350 ) 

351 ) 

352 

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

354 assert volunteer.role == "Test Volunteer" 

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

356 

357 

358def test_MakeUserVolunteer_user_not_found(db): 

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

360 editor_user, editor_token = generate_user(is_editor=True) 

361 

362 with real_editor_session(editor_token) as api: 

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

364 api.MakeUserVolunteer( 

365 editor_pb2.MakeUserVolunteerReq( 

366 user_id=999999, 

367 role="Test Volunteer", 

368 ) 

369 ) 

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

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

372 

373 

374def test_MakeUserVolunteer_already_volunteer(db): 

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

376 editor_user, editor_token = generate_user(is_editor=True) 

377 normal_user, normal_token = generate_user() 

378 

379 refresh_materialized_views_rapid(None) 

380 with real_editor_session(editor_token) as api: 

381 # Create volunteer first time 

382 api.MakeUserVolunteer( 

383 editor_pb2.MakeUserVolunteerReq( 

384 user_id=normal_user.id, 

385 role="Test Volunteer", 

386 ) 

387 ) 

388 

389 # Try to create again 

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

391 api.MakeUserVolunteer( 

392 editor_pb2.MakeUserVolunteerReq( 

393 user_id=normal_user.id, 

394 role="Test Volunteer 2", 

395 ) 

396 ) 

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

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

399 

400 

401def test_MakeUserVolunteer_invalid_date(db): 

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

403 editor_user, editor_token = generate_user(is_editor=True) 

404 normal_user, normal_token = generate_user() 

405 

406 with real_editor_session(editor_token) as api: 

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

408 api.MakeUserVolunteer( 

409 editor_pb2.MakeUserVolunteerReq( 

410 user_id=normal_user.id, 

411 role="Test Volunteer", 

412 started_volunteering="invalid-date", 

413 ) 

414 ) 

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

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

417 

418 

419def test_UpdateVolunteer(db): 

420 """UpdateVolunteer should successfully update volunteer fields""" 

421 editor_user, editor_token = generate_user(is_editor=True) 

422 normal_user, normal_token = generate_user() 

423 

424 refresh_materialized_views_rapid(None) 

425 with session_scope() as session: 

426 with real_editor_session(editor_token) as api: 

427 # Create volunteer first 

428 api.MakeUserVolunteer( 

429 editor_pb2.MakeUserVolunteerReq( 

430 user_id=normal_user.id, 

431 role="Test Volunteer", 

432 ) 

433 ) 

434 

435 # Update volunteer 

436 res = api.UpdateVolunteer( 

437 editor_pb2.UpdateVolunteerReq( 

438 user_id=normal_user.id, 

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

440 sort_key=DoubleValue(value=10.5), 

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

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

443 show_on_team_page=BoolValue(value=False), 

444 ) 

445 ) 

446 

447 # Check response 

448 assert res.user_id == normal_user.id 

449 assert res.role == "Updated Volunteer" 

450 assert res.sort_key == 10.5 

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

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

453 assert res.show_on_team_page is False 

454 assert res.username == normal_user.username 

455 

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

457 assert volunteer.role == "Updated Volunteer" 

458 assert volunteer.sort_key == 10.5 

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

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

461 assert volunteer.show_on_team_page is False 

462 

463 

464def test_UpdateVolunteer_partial_update(db): 

465 """UpdateVolunteer should only update provided fields""" 

466 editor_user, editor_token = generate_user(is_editor=True) 

467 normal_user, normal_token = generate_user() 

468 

469 refresh_materialized_views_rapid(None) 

470 with session_scope() as session: 

471 with real_editor_session(editor_token) as api: 

472 # Create volunteer first 

473 api.MakeUserVolunteer( 

474 editor_pb2.MakeUserVolunteerReq( 

475 user_id=normal_user.id, 

476 role="Test Volunteer", 

477 started_volunteering="2024-01-01", 

478 ) 

479 ) 

480 

481 # Update only role 

482 api.UpdateVolunteer( 

483 editor_pb2.UpdateVolunteerReq( 

484 user_id=normal_user.id, 

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

486 ) 

487 ) 

488 

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

490 assert volunteer.role == "Updated Role" 

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

492 assert volunteer.show_on_team_page is True # unchanged 

493 

494 

495def test_UpdateVolunteer_not_found(db): 

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

497 editor_user, editor_token = generate_user(is_editor=True) 

498 normal_user, normal_token = generate_user() 

499 

500 with real_editor_session(editor_token) as api: 

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

502 api.UpdateVolunteer( 

503 editor_pb2.UpdateVolunteerReq( 

504 user_id=normal_user.id, 

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

506 ) 

507 ) 

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

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

510 

511 

512def test_UpdateVolunteer_invalid_started_date(db): 

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

514 editor_user, editor_token = generate_user(is_editor=True) 

515 normal_user, normal_token = generate_user() 

516 

517 refresh_materialized_views_rapid(None) 

518 with real_editor_session(editor_token) as api: 

519 # Create volunteer first 

520 api.MakeUserVolunteer( 

521 editor_pb2.MakeUserVolunteerReq( 

522 user_id=normal_user.id, 

523 role="Test Volunteer", 

524 ) 

525 ) 

526 

527 # Try to update with invalid date 

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

529 api.UpdateVolunteer( 

530 editor_pb2.UpdateVolunteerReq( 

531 user_id=normal_user.id, 

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

533 ) 

534 ) 

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

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

537 

538 

539def test_UpdateVolunteer_invalid_stopped_date(db): 

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

541 editor_user, editor_token = generate_user(is_editor=True) 

542 normal_user, normal_token = generate_user() 

543 

544 refresh_materialized_views_rapid(None) 

545 with real_editor_session(editor_token) as api: 

546 # Create volunteer first 

547 api.MakeUserVolunteer( 

548 editor_pb2.MakeUserVolunteerReq( 

549 user_id=normal_user.id, 

550 role="Test Volunteer", 

551 ) 

552 ) 

553 

554 # Try to update with invalid date 

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

556 api.UpdateVolunteer( 

557 editor_pb2.UpdateVolunteerReq( 

558 user_id=normal_user.id, 

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

560 ) 

561 ) 

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

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

564 

565 

566def test_ListVolunteers(db): 

567 """ListVolunteers should return all current volunteers""" 

568 editor_user, editor_token = generate_user(is_editor=True) 

569 user1, _ = generate_user() 

570 user2, _ = generate_user() 

571 user3, _ = generate_user() 

572 

573 refresh_materialized_views_rapid(None) 

574 with session_scope() as session: 

575 with real_editor_session(editor_token) as api: 

576 # Create three volunteers 

577 api.MakeUserVolunteer( 

578 editor_pb2.MakeUserVolunteerReq( 

579 user_id=user1.id, 

580 role="Volunteer 1", 

581 started_volunteering="2024-01-15", 

582 ) 

583 ) 

584 api.MakeUserVolunteer( 

585 editor_pb2.MakeUserVolunteerReq( 

586 user_id=user2.id, 

587 role="Volunteer 2", 

588 started_volunteering="2023-06-01", 

589 ) 

590 ) 

591 api.MakeUserVolunteer( 

592 editor_pb2.MakeUserVolunteerReq( 

593 user_id=user3.id, 

594 role="Volunteer 3", 

595 started_volunteering="2024-03-20", 

596 ) 

597 ) 

598 

599 # List volunteers (only current ones by default) 

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

601 

602 assert len(res.volunteers) == 3 

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

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

605 

606 # Check that all fields are populated 

607 for volunteer in res.volunteers: 

608 assert volunteer.user_id > 0 

609 assert volunteer.role != "" 

610 assert volunteer.username != "" 

611 assert volunteer.name != "" 

612 assert volunteer.started_volunteering != "" 

613 assert volunteer.show_on_team_page is True 

614 

615 

616def test_ListVolunteers_with_past(db): 

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

618 editor_user, editor_token = generate_user(is_editor=True) 

619 user1, _ = generate_user() 

620 user2, _ = generate_user() 

621 

622 refresh_materialized_views_rapid(None) 

623 with session_scope() as session: 

624 with real_editor_session(editor_token) as api: 

625 # Create current volunteer 

626 api.MakeUserVolunteer( 

627 editor_pb2.MakeUserVolunteerReq( 

628 user_id=user1.id, 

629 role="Current Volunteer", 

630 ) 

631 ) 

632 

633 # Create past volunteer 

634 api.MakeUserVolunteer( 

635 editor_pb2.MakeUserVolunteerReq( 

636 user_id=user2.id, 

637 role="Past Volunteer", 

638 ) 

639 ) 

640 api.UpdateVolunteer( 

641 editor_pb2.UpdateVolunteerReq( 

642 user_id=user2.id, 

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

644 ) 

645 ) 

646 

647 # List only current volunteers 

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

649 assert len(res.volunteers) == 1 

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

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

652 

653 # List all volunteers (including past) 

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

655 assert len(res_with_past.volunteers) == 2 

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

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

658 

659 # Find the past volunteer and verify stopped_volunteering is set 

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

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

662 

663 

664def test_ListVolunteers_ordering(db): 

665 """ListVolunteers should respect sort_key ordering""" 

666 editor_user, editor_token = generate_user(is_editor=True) 

667 user1, _ = generate_user() 

668 user2, _ = generate_user() 

669 user3, _ = generate_user() 

670 

671 refresh_materialized_views_rapid(None) 

672 with session_scope() as session: 

673 with real_editor_session(editor_token) as api: 

674 # Create volunteers with different sort keys 

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

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

677 

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

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

680 

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

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

683 

684 # List volunteers - should be ordered by sort_key ascending 

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

686 assert len(res.volunteers) == 3 

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

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

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

690 

691 

692def test_ListVolunteers_empty(db): 

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

694 editor_user, editor_token = generate_user(is_editor=True) 

695 

696 refresh_materialized_views_rapid(None) 

697 with real_editor_session(editor_token) as api: 

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

699 assert len(res.volunteers) == 0