2323from itsdangerous import BadSignature
2424from pydantic import ValidationError
2525
26- from app .core .auth .exceptions import HTTPForbiddenException
26+ from app .core .auth .exceptions import (
27+ HTTPForbiddenException ,
28+ HTTPUnauthorizedException ,
29+ )
2730from app .core .exceptions import HTTPConflictException , HTTPNotFoundException
2831from app .models import CasdoorUser
2932from app .sep .config import sep_settings
6871EXPECTED_NODE_COUNT = 5
6972
7073
71- def _make_request () -> Request :
72- """Build a minimal Request with messages state for testing."""
74+ def _make_request (authorization : str | None = None ) -> Request :
75+ """Build a minimal Request with messages state for testing.
76+
77+ :param authorization: Value for the ``Authorization`` header, if any.
78+ :type authorization: str | None
79+ """
80+ headers = []
81+ if authorization is not None :
82+ headers .append ((b"authorization" , authorization .encode ()))
7383 scope = {
7484 "type" : "http" ,
75- "headers" : [] ,
85+ "headers" : headers ,
7686 "client" : ("127.0.0.1" , "80" ),
7787 "path" : "/" ,
7888 "app" : MagicMock (),
@@ -98,6 +108,134 @@ def test_returns_setting_when_configured(self) -> None:
98108class TestGetCurrentUser :
99109 """Test get_current_user dependency."""
100110
111+ @pytest .mark .asyncio
112+ async def test_valid_bearer_returns_user (self ) -> None :
113+ """Assert a valid Bearer path returns the user from the API dependency."""
114+ request = _make_request (authorization = "Bearer bearer-token" )
115+ active_user = CasdoorUserFactory .build (is_forbidden = False )
116+ mock_oauth2 = AsyncMock (return_value = "bearer-token" )
117+ mock_api_user = AsyncMock (return_value = active_user )
118+ with (
119+ patch ("app.sep.deps.oauth2_scheme" , mock_oauth2 ),
120+ patch ("app.sep.deps.get_current_user_api" , mock_api_user ),
121+ patch (
122+ "app.sep.deps.get_access_token_from_cookie" ,
123+ side_effect = AssertionError (
124+ "cookie must not be read when Bearer succeeds"
125+ ),
126+ ),
127+ ):
128+ result = await get_current_user (request )
129+ assert result is active_user
130+ mock_oauth2 .assert_awaited_once_with (request )
131+ mock_api_user .assert_awaited_once_with ("bearer-token" )
132+
133+ @pytest .mark .asyncio
134+ async def test_valid_cookie_returns_user (self ) -> None :
135+ """Assert cookie-only auth still returns an active user."""
136+ request = _make_request ()
137+ active_user = CasdoorUserFactory .build (is_forbidden = False )
138+ with (
139+ patch (
140+ "app.sep.deps.oauth2_scheme" ,
141+ AsyncMock (
142+ side_effect = AssertionError (
143+ "oauth2_scheme must not be called without a Bearer header"
144+ )
145+ ),
146+ ),
147+ patch (
148+ "app.sep.deps.get_access_token_from_cookie" , return_value = "cookie-token"
149+ ),
150+ patch .object (CasdoorUser , "from_jwt" , return_value = active_user ),
151+ ):
152+ result = await get_current_user (request )
153+ assert result is active_user
154+
155+ @pytest .mark .asyncio
156+ async def test_bearer_and_cookie_present_bearer_wins (self ) -> None :
157+ """Assert Authorization Bearer is preferred over session cookie."""
158+ request = _make_request (authorization = "Bearer bearer-token" )
159+ bearer_user = CasdoorUserFactory .build (username = "bearer-user" )
160+ cookie_user = CasdoorUserFactory .build (username = "cookie-user" )
161+ with (
162+ patch ("app.sep.deps.oauth2_scheme" , AsyncMock (return_value = "bearer-token" )),
163+ patch (
164+ "app.sep.deps.get_current_user_api" , AsyncMock (return_value = bearer_user )
165+ ),
166+ patch (
167+ "app.sep.deps.get_access_token_from_cookie" ,
168+ return_value = "cookie-token" ,
169+ ),
170+ patch .object (
171+ CasdoorUser ,
172+ "from_jwt" ,
173+ return_value = cookie_user ,
174+ ),
175+ ):
176+ result = await get_current_user (request )
177+ assert result .username == "bearer-user"
178+
179+ @pytest .mark .asyncio
180+ async def test_neither_bearer_nor_cookie_raises_redirect (self ) -> None :
181+ """Assert missing Bearer and missing cookie raises LoginRedirectException."""
182+ request = _make_request ()
183+ with (
184+ patch (
185+ "app.sep.deps.get_access_token_from_cookie" ,
186+ side_effect = LoginRedirectException (request ),
187+ ),
188+ pytest .raises (LoginRedirectException ),
189+ ):
190+ await get_current_user (request )
191+
192+ @pytest .mark .asyncio
193+ async def test_invalid_bearer_raises_unauthorized (self ) -> None :
194+ """Assert invalid JWT via Bearer raises HTTPUnauthorizedException."""
195+ request = _make_request (authorization = "Bearer bad-token" )
196+ with (
197+ patch ("app.sep.deps.oauth2_scheme" , AsyncMock (return_value = "bad-token" )),
198+ patch (
199+ "app.sep.deps.get_current_user_api" ,
200+ AsyncMock (side_effect = HTTPUnauthorizedException ),
201+ ),
202+ pytest .raises (HTTPUnauthorizedException ),
203+ ):
204+ await get_current_user (request )
205+
206+ @pytest .mark .asyncio
207+ async def test_inactive_user_via_bearer_raises_forbidden (self ) -> None :
208+ """Assert inactive user resolved via Bearer raises HTTPForbiddenException."""
209+ request = _make_request (authorization = "Bearer token" )
210+ with (
211+ patch ("app.sep.deps.oauth2_scheme" , AsyncMock (return_value = "token" )),
212+ patch (
213+ "app.sep.deps.get_current_user_api" ,
214+ AsyncMock (side_effect = HTTPForbiddenException ("User is not active" )),
215+ ),
216+ pytest .raises (HTTPForbiddenException ),
217+ ):
218+ await get_current_user (request )
219+
220+ @pytest .mark .asyncio
221+ async def test_malformed_bearer_header_does_not_fall_back_to_cookie (self ) -> None :
222+ """Assert a malformed Bearer header surfaces the 401 without cookie fallback."""
223+ request = _make_request (authorization = "Bearer " )
224+ with (
225+ patch (
226+ "app.sep.deps.oauth2_scheme" ,
227+ AsyncMock (side_effect = HTTPException (status_code = 401 )),
228+ ),
229+ patch (
230+ "app.sep.deps.get_access_token_from_cookie" ,
231+ side_effect = AssertionError (
232+ "cookie must not be read for a Bearer-authenticated request"
233+ ),
234+ ),
235+ pytest .raises (HTTPException ),
236+ ):
237+ await get_current_user (request )
238+
101239 @pytest .mark .asyncio
102240 async def test_bad_signature_raises_redirect (self ) -> None :
103241 """Assert BadSignature during JWT decode raises LoginRedirectException."""
0 commit comments