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
« 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
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
19@pytest.fixture(autouse=True)
20def _(testconfig):
21 pass
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"""
57POINT_GEOJSON = """
58{ "type": "Point", "coordinates": [100.0, 0.0] }
59"""
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()
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"
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)
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"
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)
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"
118def test_CreateCommunity_invalid_geojson(db):
119 """CreateCommunity should reject invalid GeoJSON"""
120 editor_user, editor_token = generate_user(is_editor=True)
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."
136def test_UpdateCommunity_invalid_geojson(db):
137 """UpdateCommunity should reject invalid GeoJSON"""
138 editor_user, editor_token = generate_user(is_editor=True)
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()
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."
165def test_UpdateCommunity_invalid_id(db):
166 """UpdateCommunity should reject invalid community IDs"""
167 editor_user, editor_token = generate_user(is_editor=True)
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 )
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."
192def test_UpdateCommunity(db):
193 """UpdateCommunity should successfully update a community"""
194 editor_user, editor_token = generate_user(is_editor=True)
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()
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()
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"
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"
242def test_UpdateCommunity2(db):
243 editor_user, editor_token = generate_user(is_editor=True)
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"
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()
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()
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"
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
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()
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 )
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
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
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()
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 )
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
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()
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 )
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
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)
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."
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()
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 )
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."
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()
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."
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()
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 )
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 )
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
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
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()
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 )
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 )
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
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()
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."
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()
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 )
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."
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()
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 )
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."
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()
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 )
602 # List volunteers (only current ones by default)
603 res = api.ListVolunteers(editor_pb2.ListVolunteersReq(include_past=False))
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}
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
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()
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 )
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 )
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")
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}
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"
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()
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)))
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)))
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)))
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
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)
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