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

293 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-03 06:18 +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 

18 

19@pytest.fixture(autouse=True) 

20def _(testconfig): 

21 pass 

22 

23 

24VALID_GEOJSON_MULTIPOLYGON = """ 

25 { 

26 "type": "MultiPolygon", 

27 "coordinates": 

28 [ 

29 [ 

30 [ 

31 [ 

32 -73.98114904754641, 

33 40.7470284264813 

34 ], 

35 [ 

36 -73.98314135177611, 

37 40.73416844413217 

38 ], 

39 [ 

40 -74.00538969848634, 

41 40.734314779027144 

42 ], 

43 [ 

44 -74.00479214294432, 

45 40.75027851544338 

46 ], 

47 [ 

48 -73.98114904754641, 

49 40.7470284264813 

50 ] 

51 ] 

52 ] 

53 ] 

54 } 

55""" 

56 

57POINT_GEOJSON = """ 

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

59""" 

60 

61 

62def test_access_by_normal_user(db): 

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

64 normal_user, normal_token = generate_user() 

65 

66 with real_editor_session(normal_token) as api: 

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

68 api.CreateCommunity( 

69 editor_pb2.CreateCommunityReq( 

70 name="test community", 

71 description="community for testing", 

72 admin_ids=[], 

73 geojson=VALID_GEOJSON_MULTIPOLYGON, 

74 ) 

75 ) 

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

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

78 

79 

80def test_access_by_editor_user(db): 

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

82 editor_user, editor_token = generate_user(is_editor=True) 

83 

84 with session_scope() as session: 

85 with real_editor_session(editor_token) as api: 

86 api.CreateCommunity( 

87 editor_pb2.CreateCommunityReq( 

88 name="test community", 

89 description="community for testing", 

90 admin_ids=[], 

91 geojson=VALID_GEOJSON_MULTIPOLYGON, 

92 ) 

93 ) 

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

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

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

97 

98 

99def test_access_by_superuser(db): 

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

101 editor_user, editor_token = generate_user(is_editor=True) 

102 

103 with session_scope() as session: 

104 with real_editor_session(editor_token) as api: 

105 api.CreateCommunity( 

106 editor_pb2.CreateCommunityReq( 

107 name="test community", 

108 description="community for testing", 

109 admin_ids=[], 

110 geojson=VALID_GEOJSON_MULTIPOLYGON, 

111 ) 

112 ) 

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

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

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

116 

117 

118def test_CreateCommunity_invalid_geojson(db): 

119 """CreateCommunity should reject invalid GeoJSON""" 

120 editor_user, editor_token = generate_user(is_editor=True) 

121 

122 with real_editor_session(editor_token) as api: 

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

124 api.CreateCommunity( 

125 editor_pb2.CreateCommunityReq( 

126 name="test community", 

127 description="community for testing", 

128 admin_ids=[], 

129 geojson=POINT_GEOJSON, 

130 ) 

131 ) 

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

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

134 

135 

136def test_UpdateCommunity_invalid_geojson(db): 

137 """UpdateCommunity should reject invalid GeoJSON""" 

138 editor_user, editor_token = generate_user(is_editor=True) 

139 

140 with session_scope() as session: 

141 with real_editor_session(editor_token) as api: 

142 api.CreateCommunity( 

143 editor_pb2.CreateCommunityReq( 

144 name="test community", 

145 description="community for testing", 

146 admin_ids=[], 

147 geojson=VALID_GEOJSON_MULTIPOLYGON, 

148 ) 

149 ) 

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

151 

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

153 api.UpdateCommunity( 

154 editor_pb2.UpdateCommunityReq( 

155 community_id=community.parent_node_id, 

156 name="test community 2", 

157 description="community for testing 2", 

158 geojson=POINT_GEOJSON, 

159 ) 

160 ) 

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

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

163 

164 

165def test_UpdateCommunity_invalid_id(db): 

166 """UpdateCommunity should reject invalid community IDs""" 

167 editor_user, editor_token = generate_user(is_editor=True) 

168 

169 with real_editor_session(editor_token) as api: 

170 api.CreateCommunity( 

171 editor_pb2.CreateCommunityReq( 

172 name="test community", 

173 description="community for testing", 

174 admin_ids=[], 

175 geojson=VALID_GEOJSON_MULTIPOLYGON, 

176 ) 

177 ) 

178 

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

180 api.UpdateCommunity( 

181 editor_pb2.UpdateCommunityReq( 

182 community_id=1000, 

183 name="test community 1000", 

184 description="community for testing 1000", 

185 geojson=VALID_GEOJSON_MULTIPOLYGON, 

186 ) 

187 ) 

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

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

190 

191 

192def test_UpdateCommunity(db): 

193 """UpdateCommunity should successfully update a community""" 

194 editor_user, editor_token = generate_user(is_editor=True) 

195 

196 with session_scope() as session: 

197 with real_editor_session(editor_token) as api: 

198 api.CreateCommunity( 

199 editor_pb2.CreateCommunityReq( 

200 name="test community", 

201 description="community for testing", 

202 admin_ids=[], 

203 geojson=VALID_GEOJSON_MULTIPOLYGON, 

204 ) 

205 ) 

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

207 

208 api.UpdateCommunity( 

209 editor_pb2.UpdateCommunityReq( 

210 community_id=community.parent_node_id, 

211 name="test community updated", 

212 description="community for testing updated", 

213 geojson=VALID_GEOJSON_MULTIPOLYGON, 

214 ) 

215 ) 

216 session.commit() 

217 

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

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

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

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

222 

223 

224def test_CreateCommunity(db): 

225 with session_scope() as session: 

226 editor_user, editor_token = generate_user(is_editor=True) 

227 normal_user, normal_token = generate_user() 

228 with real_editor_session(editor_token) as api: 

229 api.CreateCommunity( 

230 editor_pb2.CreateCommunityReq( 

231 name="test community", 

232 description="community for testing", 

233 admin_ids=[], 

234 geojson=VALID_GEOJSON_MULTIPOLYGON, 

235 ) 

236 ) 

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

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

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

240 

241 

242def test_UpdateCommunity2(db): 

243 editor_user, editor_token = generate_user(is_editor=True) 

244 

245 with session_scope() as session: 

246 with real_editor_session(editor_token) as api: 

247 api.CreateCommunity( 

248 editor_pb2.CreateCommunityReq( 

249 name="test community", 

250 description="community for testing", 

251 admin_ids=[], 

252 geojson=VALID_GEOJSON_MULTIPOLYGON, 

253 ) 

254 ) 

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

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

257 

258 api.CreateCommunity( 

259 editor_pb2.CreateCommunityReq( 

260 name="test community 2", 

261 description="community for testing 2", 

262 admin_ids=[], 

263 geojson=VALID_GEOJSON_MULTIPOLYGON, 

264 ) 

265 ) 

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

267 

268 api.UpdateCommunity( 

269 editor_pb2.UpdateCommunityReq( 

270 community_id=community.parent_node_id, 

271 name="test community 2", 

272 description="community for testing 2", 

273 geojson=VALID_GEOJSON_MULTIPOLYGON, 

274 parent_node_id=community_2.parent_node_id, 

275 ) 

276 ) 

277 session.commit() 

278 

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

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

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

282 

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

284 assert node_updated.parent_node_id == community_2.parent_node_id 

285 

286 

287def test_MakeUserVolunteer(db): 

288 """MakeUserVolunteer should successfully create a volunteer""" 

289 editor_user, editor_token = generate_user(is_editor=True) 

290 normal_user, normal_token = generate_user() 

291 

292 refresh_materialized_views_rapid(empty_pb2.Empty()) 

293 with session_scope() as session: 

294 with real_editor_session(editor_token) as api: 

295 res = api.MakeUserVolunteer( 

296 editor_pb2.MakeUserVolunteerReq( 

297 user_id=normal_user.id, 

298 role="Test Volunteer", 

299 started_volunteering="2024-01-15", 

300 hide_on_team_page=False, 

301 ) 

302 ) 

303 

304 # Check response 

305 assert res.user_id == normal_user.id 

306 assert res.role == "Test Volunteer" 

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

308 assert res.show_on_team_page is True 

309 assert res.username == normal_user.username 

310 assert res.name == normal_user.name 

311 

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

313 assert volunteer.role == "Test Volunteer" 

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

315 assert volunteer.show_on_team_page is True 

316 

317 

318def test_MakeUserVolunteer_default_values(db): 

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

320 editor_user, editor_token = generate_user(is_editor=True) 

321 normal_user, normal_token = generate_user() 

322 

323 refresh_materialized_views_rapid(empty_pb2.Empty()) 

324 with session_scope() as session: 

325 with real_editor_session(editor_token) as api: 

326 api.MakeUserVolunteer( 

327 editor_pb2.MakeUserVolunteerReq( 

328 user_id=normal_user.id, 

329 role="Test Volunteer", 

330 ) 

331 ) 

332 

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

334 assert volunteer.role == "Test Volunteer" 

335 assert volunteer.started_volunteering # defaults to today 

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

337 

338 

339def test_MakeUserVolunteer_hide_on_team_page(db): 

340 """MakeUserVolunteer should respect hide_on_team_page=True""" 

341 editor_user, editor_token = generate_user(is_editor=True) 

342 normal_user, normal_token = generate_user() 

343 

344 refresh_materialized_views_rapid(empty_pb2.Empty()) 

345 with session_scope() as session: 

346 with real_editor_session(editor_token) as api: 

347 api.MakeUserVolunteer( 

348 editor_pb2.MakeUserVolunteerReq( 

349 user_id=normal_user.id, 

350 role="Test Volunteer", 

351 hide_on_team_page=True, 

352 ) 

353 ) 

354 

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

356 assert volunteer.role == "Test Volunteer" 

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

358 

359 

360def test_MakeUserVolunteer_user_not_found(db): 

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

362 editor_user, editor_token = generate_user(is_editor=True) 

363 

364 with real_editor_session(editor_token) as api: 

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

366 api.MakeUserVolunteer( 

367 editor_pb2.MakeUserVolunteerReq( 

368 user_id=999999, 

369 role="Test Volunteer", 

370 ) 

371 ) 

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

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

374 

375 

376def test_MakeUserVolunteer_already_volunteer(db): 

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

378 editor_user, editor_token = generate_user(is_editor=True) 

379 normal_user, normal_token = generate_user() 

380 

381 refresh_materialized_views_rapid(empty_pb2.Empty()) 

382 with real_editor_session(editor_token) as api: 

383 # Create volunteer first time 

384 api.MakeUserVolunteer( 

385 editor_pb2.MakeUserVolunteerReq( 

386 user_id=normal_user.id, 

387 role="Test Volunteer", 

388 ) 

389 ) 

390 

391 # Try to create again 

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

393 api.MakeUserVolunteer( 

394 editor_pb2.MakeUserVolunteerReq( 

395 user_id=normal_user.id, 

396 role="Test Volunteer 2", 

397 ) 

398 ) 

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

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

401 

402 

403def test_MakeUserVolunteer_invalid_date(db): 

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

405 editor_user, editor_token = generate_user(is_editor=True) 

406 normal_user, normal_token = generate_user() 

407 

408 with real_editor_session(editor_token) as api: 

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

410 api.MakeUserVolunteer( 

411 editor_pb2.MakeUserVolunteerReq( 

412 user_id=normal_user.id, 

413 role="Test Volunteer", 

414 started_volunteering="invalid-date", 

415 ) 

416 ) 

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

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

419 

420 

421def test_UpdateVolunteer(db): 

422 """UpdateVolunteer should successfully update volunteer fields""" 

423 editor_user, editor_token = generate_user(is_editor=True) 

424 normal_user, normal_token = generate_user() 

425 

426 refresh_materialized_views_rapid(empty_pb2.Empty()) 

427 with session_scope() as session: 

428 with real_editor_session(editor_token) as api: 

429 # Create volunteer first 

430 api.MakeUserVolunteer( 

431 editor_pb2.MakeUserVolunteerReq( 

432 user_id=normal_user.id, 

433 role="Test Volunteer", 

434 ) 

435 ) 

436 

437 # Update volunteer 

438 res = api.UpdateVolunteer( 

439 editor_pb2.UpdateVolunteerReq( 

440 user_id=normal_user.id, 

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

442 sort_key=DoubleValue(value=10.5), 

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

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

445 show_on_team_page=BoolValue(value=False), 

446 ) 

447 ) 

448 

449 # Check response 

450 assert res.user_id == normal_user.id 

451 assert res.role == "Updated Volunteer" 

452 assert res.sort_key == 10.5 

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

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

455 assert res.show_on_team_page is False 

456 assert res.username == normal_user.username 

457 

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

459 assert volunteer.role == "Updated Volunteer" 

460 assert volunteer.sort_key == 10.5 

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

462 assert volunteer.stopped_volunteering 

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

464 assert volunteer.show_on_team_page is False 

465 

466 

467def test_UpdateVolunteer_partial_update(db): 

468 """UpdateVolunteer should only update provided fields""" 

469 editor_user, editor_token = generate_user(is_editor=True) 

470 normal_user, normal_token = generate_user() 

471 

472 refresh_materialized_views_rapid(empty_pb2.Empty()) 

473 with session_scope() as session: 

474 with real_editor_session(editor_token) as api: 

475 # Create volunteer first 

476 api.MakeUserVolunteer( 

477 editor_pb2.MakeUserVolunteerReq( 

478 user_id=normal_user.id, 

479 role="Test Volunteer", 

480 started_volunteering="2024-01-01", 

481 ) 

482 ) 

483 

484 # Update only role 

485 api.UpdateVolunteer( 

486 editor_pb2.UpdateVolunteerReq( 

487 user_id=normal_user.id, 

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

489 ) 

490 ) 

491 

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

493 assert volunteer.role == "Updated Role" 

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

495 assert volunteer.show_on_team_page is True # unchanged 

496 

497 

498def test_UpdateVolunteer_not_found(db): 

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

500 editor_user, editor_token = generate_user(is_editor=True) 

501 normal_user, normal_token = generate_user() 

502 

503 with real_editor_session(editor_token) as api: 

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

505 api.UpdateVolunteer( 

506 editor_pb2.UpdateVolunteerReq( 

507 user_id=normal_user.id, 

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

509 ) 

510 ) 

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

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

513 

514 

515def test_UpdateVolunteer_invalid_started_date(db): 

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

517 editor_user, editor_token = generate_user(is_editor=True) 

518 normal_user, normal_token = generate_user() 

519 

520 refresh_materialized_views_rapid(empty_pb2.Empty()) 

521 with real_editor_session(editor_token) as api: 

522 # Create volunteer first 

523 api.MakeUserVolunteer( 

524 editor_pb2.MakeUserVolunteerReq( 

525 user_id=normal_user.id, 

526 role="Test Volunteer", 

527 ) 

528 ) 

529 

530 # Try to update with invalid date 

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

532 api.UpdateVolunteer( 

533 editor_pb2.UpdateVolunteerReq( 

534 user_id=normal_user.id, 

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

536 ) 

537 ) 

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

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

540 

541 

542def test_UpdateVolunteer_invalid_stopped_date(db): 

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

544 editor_user, editor_token = generate_user(is_editor=True) 

545 normal_user, normal_token = generate_user() 

546 

547 refresh_materialized_views_rapid(empty_pb2.Empty()) 

548 with real_editor_session(editor_token) as api: 

549 # Create volunteer first 

550 api.MakeUserVolunteer( 

551 editor_pb2.MakeUserVolunteerReq( 

552 user_id=normal_user.id, 

553 role="Test Volunteer", 

554 ) 

555 ) 

556 

557 # Try to update with invalid date 

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

559 api.UpdateVolunteer( 

560 editor_pb2.UpdateVolunteerReq( 

561 user_id=normal_user.id, 

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

563 ) 

564 ) 

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

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

567 

568 

569def test_ListVolunteers(db): 

570 """ListVolunteers should return all current volunteers""" 

571 editor_user, editor_token = generate_user(is_editor=True) 

572 user1, _ = generate_user() 

573 user2, _ = generate_user() 

574 user3, _ = generate_user() 

575 

576 refresh_materialized_views_rapid(empty_pb2.Empty()) 

577 with session_scope() as session: 

578 with real_editor_session(editor_token) as api: 

579 # Create three volunteers 

580 api.MakeUserVolunteer( 

581 editor_pb2.MakeUserVolunteerReq( 

582 user_id=user1.id, 

583 role="Volunteer 1", 

584 started_volunteering="2024-01-15", 

585 ) 

586 ) 

587 api.MakeUserVolunteer( 

588 editor_pb2.MakeUserVolunteerReq( 

589 user_id=user2.id, 

590 role="Volunteer 2", 

591 started_volunteering="2023-06-01", 

592 ) 

593 ) 

594 api.MakeUserVolunteer( 

595 editor_pb2.MakeUserVolunteerReq( 

596 user_id=user3.id, 

597 role="Volunteer 3", 

598 started_volunteering="2024-03-20", 

599 ) 

600 ) 

601 

602 # List volunteers (only current ones by default) 

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

604 

605 assert len(res.volunteers) == 3 

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

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

608 

609 # Check that all fields are populated 

610 for volunteer in res.volunteers: 

611 assert volunteer.user_id > 0 

612 assert volunteer.role != "" 

613 assert volunteer.username != "" 

614 assert volunteer.name != "" 

615 assert volunteer.started_volunteering != "" 

616 assert volunteer.show_on_team_page is True 

617 

618 

619def test_ListVolunteers_with_past(db): 

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

621 editor_user, editor_token = generate_user(is_editor=True) 

622 user1, _ = generate_user() 

623 user2, _ = generate_user() 

624 

625 refresh_materialized_views_rapid(empty_pb2.Empty()) 

626 with session_scope() as session: 

627 with real_editor_session(editor_token) as api: 

628 # Create current volunteer 

629 api.MakeUserVolunteer( 

630 editor_pb2.MakeUserVolunteerReq( 

631 user_id=user1.id, 

632 role="Current Volunteer", 

633 ) 

634 ) 

635 

636 # Create past volunteer 

637 api.MakeUserVolunteer( 

638 editor_pb2.MakeUserVolunteerReq( 

639 user_id=user2.id, 

640 role="Past Volunteer", 

641 ) 

642 ) 

643 api.UpdateVolunteer( 

644 editor_pb2.UpdateVolunteerReq( 

645 user_id=user2.id, 

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

647 ) 

648 ) 

649 

650 # List only current volunteers 

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

652 assert len(res.volunteers) == 1 

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

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

655 

656 # List all volunteers (including past) 

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

658 assert len(res_with_past.volunteers) == 2 

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

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

661 

662 # Find the past volunteer and verify stopped_volunteering is set 

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

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

665 

666 

667def test_ListVolunteers_ordering(db): 

668 """ListVolunteers should respect sort_key ordering""" 

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 volunteers with different sort keys 

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

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

680 

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

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

683 

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

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

686 

687 # List volunteers - should be ordered by sort_key ascending 

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

689 assert len(res.volunteers) == 3 

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

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

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

693 

694 

695def test_ListVolunteers_empty(db): 

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

697 editor_user, editor_token = generate_user(is_editor=True) 

698 

699 refresh_materialized_views_rapid(empty_pb2.Empty()) 

700 with real_editor_session(editor_token) as api: 

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

702 assert len(res.volunteers) == 0