Skip to content

Commit 39ed2f9

Browse files
committed
add better error handling for resolving participants and volunteers + tests
1 parent f892389 commit 39ed2f9

File tree

2 files changed

+162
-13
lines changed

2 files changed

+162
-13
lines changed

backend/app/routes/match.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,32 +203,45 @@ async def request_new_volunteers(
203203
async def _resolve_acting_participant_id(request: Request, user_service: UserService) -> Optional[UUID]:
204204
auth_id = getattr(request.state, "user_id", None)
205205
if not auth_id:
206-
return None
206+
raise HTTPException(status_code=401, detail="Authentication required")
207207

208208
try:
209209
role_name = user_service.get_user_role_by_auth_id(auth_id)
210-
except ValueError:
211-
return None
212-
213-
if role_name != UserRole.PARTICIPANT.value:
210+
except ValueError as exc:
211+
raise HTTPException(status_code=401, detail="User not found") from exc
212+
213+
if role_name == UserRole.PARTICIPANT.value:
214+
try:
215+
participant_id_str = await user_service.get_user_id_by_auth_id(auth_id)
216+
except ValueError as exc:
217+
raise HTTPException(status_code=404, detail="Participant not found") from exc
218+
return UUID(participant_id_str)
219+
220+
if role_name == UserRole.ADMIN.value:
221+
# Admin callers bypass ownership checks
214222
return None
215223

216-
participant_id_str = await user_service.get_user_id_by_auth_id(auth_id)
217-
return UUID(participant_id_str)
224+
raise HTTPException(status_code=403, detail="Insufficient role for participant operation")
218225

219226

220227
async def _resolve_acting_volunteer_id(request: Request, user_service: UserService) -> Optional[UUID]:
221228
auth_id = getattr(request.state, "user_id", None)
222229
if not auth_id:
223-
return None
230+
raise HTTPException(status_code=401, detail="Authentication required")
224231

225232
try:
226233
role_name = user_service.get_user_role_by_auth_id(auth_id)
227-
except ValueError:
228-
return None
234+
except ValueError as exc:
235+
raise HTTPException(status_code=401, detail="User not found") from exc
236+
237+
if role_name == UserRole.VOLUNTEER.value:
238+
try:
239+
volunteer_id_str = await user_service.get_user_id_by_auth_id(auth_id)
240+
except ValueError as exc:
241+
raise HTTPException(status_code=404, detail="Volunteer not found") from exc
242+
return UUID(volunteer_id_str)
229243

230-
if role_name != UserRole.VOLUNTEER.value:
244+
if role_name == UserRole.ADMIN.value:
231245
return None
232246

233-
volunteer_id_str = await user_service.get_user_id_by_auth_id(auth_id)
234-
return UUID(volunteer_id_str)
247+
raise HTTPException(status_code=403, detail="Insufficient role for volunteer operation")
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import pytest
2+
from fastapi import HTTPException
3+
from starlette.requests import Request
4+
5+
from app.routes.match import _resolve_acting_participant_id, _resolve_acting_volunteer_id
6+
from app.schemas.user import UserRole
7+
8+
9+
class DummyUserService:
10+
def __init__(self, role_map, id_map):
11+
self.role_map = role_map
12+
self.id_map = id_map
13+
14+
def get_user_role_by_auth_id(self, auth_id: str) -> str:
15+
if auth_id not in self.role_map:
16+
raise ValueError("User not found")
17+
return self.role_map[auth_id]
18+
19+
async def get_user_id_by_auth_id(self, auth_id: str) -> str:
20+
if auth_id not in self.id_map:
21+
raise ValueError("ID not found")
22+
return self.id_map[auth_id]
23+
24+
25+
@pytest.mark.asyncio
26+
async def test_resolve_participant_success():
27+
request = Request({"type": "http"})
28+
request.state.user_id = "auth_participant"
29+
30+
service = DummyUserService(
31+
role_map={"auth_participant": UserRole.PARTICIPANT.value},
32+
id_map={"auth_participant": "11111111-1111-1111-1111-111111111111"},
33+
)
34+
35+
participant_id = await _resolve_acting_participant_id(request, service)
36+
assert str(participant_id) == "11111111-1111-1111-1111-111111111111"
37+
38+
39+
@pytest.mark.asyncio
40+
async def test_resolve_participant_admin_bypass():
41+
request = Request({"type": "http"})
42+
request.state.user_id = "auth_admin"
43+
44+
service = DummyUserService(
45+
role_map={"auth_admin": UserRole.ADMIN.value},
46+
id_map={},
47+
)
48+
49+
participant_id = await _resolve_acting_participant_id(request, service)
50+
assert participant_id is None
51+
52+
53+
@pytest.mark.asyncio
54+
async def test_resolve_participant_missing_user_raises():
55+
request = Request({"type": "http"})
56+
request.state.user_id = "auth_unknown"
57+
58+
service = DummyUserService(role_map={}, id_map={})
59+
60+
with pytest.raises(HTTPException) as exc:
61+
await _resolve_acting_participant_id(request, service)
62+
63+
assert exc.value.status_code == 401
64+
65+
66+
@pytest.mark.asyncio
67+
async def test_resolve_participant_wrong_role_raises():
68+
request = Request({"type": "http"})
69+
request.state.user_id = "auth_volunteer"
70+
71+
service = DummyUserService(
72+
role_map={"auth_volunteer": UserRole.VOLUNTEER.value},
73+
id_map={},
74+
)
75+
76+
with pytest.raises(HTTPException) as exc:
77+
await _resolve_acting_participant_id(request, service)
78+
79+
assert exc.value.status_code == 403
80+
81+
82+
@pytest.mark.asyncio
83+
async def test_resolve_volunteer_success():
84+
request = Request({"type": "http"})
85+
request.state.user_id = "auth_volunteer"
86+
87+
service = DummyUserService(
88+
role_map={"auth_volunteer": UserRole.VOLUNTEER.value},
89+
id_map={"auth_volunteer": "22222222-2222-2222-2222-222222222222"},
90+
)
91+
92+
volunteer_id = await _resolve_acting_volunteer_id(request, service)
93+
assert str(volunteer_id) == "22222222-2222-2222-2222-222222222222"
94+
95+
96+
@pytest.mark.asyncio
97+
async def test_resolve_volunteer_missing_user_raises():
98+
request = Request({"type": "http"})
99+
request.state.user_id = "auth_missing"
100+
101+
service = DummyUserService(role_map={}, id_map={})
102+
103+
with pytest.raises(HTTPException) as exc:
104+
await _resolve_acting_volunteer_id(request, service)
105+
106+
assert exc.value.status_code == 401
107+
108+
109+
@pytest.mark.asyncio
110+
async def test_resolve_volunteer_wrong_role_raises():
111+
request = Request({"type": "http"})
112+
request.state.user_id = "auth_participant"
113+
114+
service = DummyUserService(
115+
role_map={"auth_participant": UserRole.PARTICIPANT.value},
116+
id_map={},
117+
)
118+
119+
with pytest.raises(HTTPException) as exc:
120+
await _resolve_acting_volunteer_id(request, service)
121+
122+
assert exc.value.status_code == 403
123+
124+
125+
@pytest.mark.asyncio
126+
async def test_resolve_volunteer_admin_bypass():
127+
request = Request({"type": "http"})
128+
request.state.user_id = "auth_admin"
129+
130+
service = DummyUserService(
131+
role_map={"auth_admin": UserRole.ADMIN.value},
132+
id_map={},
133+
)
134+
135+
volunteer_id = await _resolve_acting_volunteer_id(request, service)
136+
assert volunteer_id is None

0 commit comments

Comments
 (0)