44
55import pytest
66
7+ from tracecat .auth .types import Role
78from tracecat .authz .controls import (
89 get_missing_scopes ,
910 has_all_scopes ,
2425 PRESET_ROLE_SCOPES ,
2526 VIEWER_SCOPES ,
2627)
27- from tracecat .contexts import ctx_scopes
28+ from tracecat .contexts import ctx_role
2829from tracecat .exceptions import ScopeDeniedError
2930
3031
32+ def _set_role_with_scopes (scopes : frozenset [str ]) -> None :
33+ """Helper to set ctx_role with the given scopes."""
34+ role = Role (type = "user" , service_id = "tracecat-api" , scopes = scopes )
35+ ctx_role .set (role )
36+
37+
3138class TestValidateScopeString :
3239 """Tests for scope string validation."""
3340
@@ -228,7 +235,7 @@ class TestRequireScopeDecorator:
228235 """Tests for the @require_scope decorator."""
229236
230237 def test_require_scope_passes_with_exact_scope (self ):
231- ctx_scopes . set (frozenset ({"workflow:read" }))
238+ _set_role_with_scopes (frozenset ({"workflow:read" }))
232239
233240 @require_scope ("workflow:read" )
234241 def protected_func ():
@@ -237,7 +244,7 @@ def protected_func():
237244 assert protected_func () == "success"
238245
239246 def test_require_scope_passes_with_wildcard (self ):
240- ctx_scopes . set (frozenset ({"workflow:*" }))
247+ _set_role_with_scopes (frozenset ({"workflow:*" }))
241248
242249 @require_scope ("workflow:read" )
243250 def protected_func ():
@@ -246,7 +253,7 @@ def protected_func():
246253 assert protected_func () == "success"
247254
248255 def test_require_scope_passes_with_superuser (self ):
249- ctx_scopes . set (frozenset ({"*" }))
256+ _set_role_with_scopes (frozenset ({"*" }))
250257
251258 @require_scope ("org:delete" )
252259 def protected_func ():
@@ -255,7 +262,7 @@ def protected_func():
255262 assert protected_func () == "success"
256263
257264 def test_require_scope_fails_without_scope (self ):
258- ctx_scopes . set (frozenset ({"case:read" }))
265+ _set_role_with_scopes (frozenset ({"case:read" }))
259266
260267 @require_scope ("workflow:read" )
261268 def protected_func ():
@@ -268,7 +275,7 @@ def protected_func():
268275 assert "workflow:read" in exc_info .value .missing_scopes
269276
270277 def test_require_scope_multiple_all_required (self ):
271- ctx_scopes . set (frozenset ({"workflow:read" , "workflow:execute" }))
278+ _set_role_with_scopes (frozenset ({"workflow:read" , "workflow:execute" }))
272279
273280 @require_scope ("workflow:read" , "workflow:execute" , require_all = True )
274281 def protected_func ():
@@ -277,7 +284,7 @@ def protected_func():
277284 assert protected_func () == "success"
278285
279286 def test_require_scope_multiple_missing_one (self ):
280- ctx_scopes . set (frozenset ({"workflow:read" }))
287+ _set_role_with_scopes (frozenset ({"workflow:read" }))
281288
282289 @require_scope ("workflow:read" , "workflow:execute" , require_all = True )
283290 def protected_func ():
@@ -289,7 +296,7 @@ def protected_func():
289296 assert "workflow:execute" in exc_info .value .missing_scopes
290297
291298 def test_require_scope_any_one_sufficient (self ):
292- ctx_scopes . set (frozenset ({"workflow:read" }))
299+ _set_role_with_scopes (frozenset ({"workflow:read" }))
293300
294301 @require_scope ("workflow:read" , "workflow:execute" , require_all = False )
295302 def protected_func ():
@@ -298,7 +305,7 @@ def protected_func():
298305 assert protected_func () == "success"
299306
300307 def test_require_scope_any_none_present (self ):
301- ctx_scopes . set (frozenset ({"case:read" }))
308+ _set_role_with_scopes (frozenset ({"case:read" }))
302309
303310 @require_scope ("workflow:read" , "workflow:execute" , require_all = False )
304311 def protected_func ():
@@ -307,9 +314,22 @@ def protected_func():
307314 with pytest .raises (ScopeDeniedError ):
308315 protected_func ()
309316
317+ def test_require_scope_fails_without_role (self ):
318+ """Test that require_scope fails when ctx_role is None."""
319+ ctx_role .set (None )
320+
321+ @require_scope ("workflow:read" )
322+ def protected_func ():
323+ return "success"
324+
325+ with pytest .raises (ScopeDeniedError ) as exc_info :
326+ protected_func ()
327+
328+ assert "workflow:read" in exc_info .value .required_scopes
329+
310330 @pytest .mark .anyio
311331 async def test_require_scope_async_function (self ):
312- ctx_scopes . set (frozenset ({"workflow:read" }))
332+ _set_role_with_scopes (frozenset ({"workflow:read" }))
313333
314334 @require_scope ("workflow:read" )
315335 async def async_protected_func ():
@@ -320,7 +340,7 @@ async def async_protected_func():
320340
321341 @pytest .mark .anyio
322342 async def test_require_scope_async_function_denied (self ):
323- ctx_scopes . set (frozenset ({"case:read" }))
343+ _set_role_with_scopes (frozenset ({"case:read" }))
324344
325345 @require_scope ("workflow:read" )
326346 async def async_protected_func ():
0 commit comments