planar 0.10.0__py3-none-any.whl → 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. planar/app.py +18 -6
  2. planar/routers/info.py +79 -36
  3. planar/scaffold_templates/pyproject.toml.j2 +1 -1
  4. planar/testing/fixtures.py +7 -4
  5. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/METADATA +9 -1
  6. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/RECORD +8 -60
  7. planar/ai/test_agent_serialization.py +0 -229
  8. planar/ai/test_agent_tool_step_display.py +0 -78
  9. planar/data/test_dataset.py +0 -358
  10. planar/files/storage/test_azure_blob.py +0 -435
  11. planar/files/storage/test_local_directory.py +0 -162
  12. planar/files/storage/test_s3.py +0 -299
  13. planar/files/test_files.py +0 -282
  14. planar/human/test_human.py +0 -385
  15. planar/logging/test_formatter.py +0 -327
  16. planar/modeling/mixins/test_auditable.py +0 -97
  17. planar/modeling/mixins/test_timestamp.py +0 -134
  18. planar/modeling/mixins/test_uuid_primary_key.py +0 -52
  19. planar/routers/test_agents_router.py +0 -174
  20. planar/routers/test_dataset_router.py +0 -429
  21. planar/routers/test_files_router.py +0 -49
  22. planar/routers/test_object_config_router.py +0 -367
  23. planar/routers/test_routes_security.py +0 -168
  24. planar/routers/test_rule_router.py +0 -470
  25. planar/routers/test_workflow_router.py +0 -564
  26. planar/rules/test_data/account_dormancy_management.json +0 -223
  27. planar/rules/test_data/airline_loyalty_points_calculator.json +0 -262
  28. planar/rules/test_data/applicant_risk_assessment.json +0 -435
  29. planar/rules/test_data/booking_fraud_detection.json +0 -407
  30. planar/rules/test_data/cellular_data_rollover_system.json +0 -258
  31. planar/rules/test_data/clinical_trial_eligibility_screener.json +0 -437
  32. planar/rules/test_data/customer_lifetime_value.json +0 -143
  33. planar/rules/test_data/import_duties_calculator.json +0 -289
  34. planar/rules/test_data/insurance_prior_authorization.json +0 -443
  35. planar/rules/test_data/online_check_in_eligibility_system.json +0 -254
  36. planar/rules/test_data/order_consolidation_system.json +0 -375
  37. planar/rules/test_data/portfolio_risk_monitor.json +0 -471
  38. planar/rules/test_data/supply_chain_risk.json +0 -253
  39. planar/rules/test_data/warehouse_cross_docking.json +0 -237
  40. planar/rules/test_rules.py +0 -1494
  41. planar/security/tests/test_auth_middleware.py +0 -162
  42. planar/security/tests/test_authorization_context.py +0 -78
  43. planar/security/tests/test_cedar_basics.py +0 -41
  44. planar/security/tests/test_cedar_policies.py +0 -158
  45. planar/security/tests/test_jwt_principal_context.py +0 -179
  46. planar/test_app.py +0 -142
  47. planar/test_cli.py +0 -394
  48. planar/test_config.py +0 -515
  49. planar/test_object_config.py +0 -527
  50. planar/test_object_registry.py +0 -14
  51. planar/test_sqlalchemy.py +0 -193
  52. planar/test_utils.py +0 -105
  53. planar/testing/test_memory_storage.py +0 -143
  54. planar/workflows/test_concurrency_detection.py +0 -120
  55. planar/workflows/test_lock_timeout.py +0 -140
  56. planar/workflows/test_serialization.py +0 -1203
  57. planar/workflows/test_suspend_deserialization.py +0 -231
  58. planar/workflows/test_workflow.py +0 -2005
  59. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/WHEEL +0 -0
  60. {planar-0.10.0.dist-info → planar-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -1,162 +0,0 @@
1
- from unittest.mock import Mock, patch
2
-
3
- import pytest
4
- from fastapi import FastAPI, HTTPException
5
- from fastapi.responses import JSONResponse
6
-
7
- from planar.security.auth_middleware import AuthMiddleware
8
-
9
-
10
- @pytest.fixture
11
- def app():
12
- return FastAPI()
13
-
14
-
15
- @pytest.fixture
16
- def auth_middleware(app):
17
- return AuthMiddleware(
18
- app=app,
19
- client_id="test-client-id",
20
- org_id="test-org-id",
21
- additional_exclusion_paths=["/test/exclude"],
22
- service_token="plt_test-service-token",
23
- )
24
-
25
-
26
- class TestAuthMiddleware:
27
- def test_org_id_validation_none(self, auth_middleware):
28
- """Test that org_id validation fails when token has None org_id"""
29
- with (
30
- patch.object(auth_middleware, "get_signing_key_from_jwt"),
31
- patch("jwt.decode") as mock_decode,
32
- ):
33
- mock_decode.return_value = {"org_id": None}
34
-
35
- with pytest.raises(HTTPException) as exc_info:
36
- auth_middleware.validate_jwt_token("fake-token")
37
-
38
- assert exc_info.value.status_code == 401
39
- assert exc_info.value.detail == "Invalid organization"
40
-
41
- def test_org_id_validation_empty_string(self, auth_middleware):
42
- """Test that org_id validation fails when token has empty string org_id"""
43
- with (
44
- patch.object(auth_middleware, "get_signing_key_from_jwt"),
45
- patch("jwt.decode") as mock_decode,
46
- ):
47
- mock_decode.return_value = {"org_id": ""}
48
-
49
- with pytest.raises(HTTPException) as exc_info:
50
- auth_middleware.validate_jwt_token("fake-token")
51
-
52
- assert exc_info.value.status_code == 401
53
- assert exc_info.value.detail == "Invalid organization"
54
-
55
- def test_org_id_validation_mismatch(self, auth_middleware):
56
- """Test that org_id validation fails when token org_id doesn't match"""
57
- with (
58
- patch.object(auth_middleware, "get_signing_key_from_jwt"),
59
- patch("jwt.decode") as mock_decode,
60
- ):
61
- mock_decode.return_value = {"org_id": "different-org-id"}
62
-
63
- with pytest.raises(HTTPException) as exc_info:
64
- auth_middleware.validate_jwt_token("fake-token")
65
-
66
- assert exc_info.value.status_code == 401
67
- assert exc_info.value.detail == "Invalid organization"
68
-
69
- def test_org_id_validation_success(self, auth_middleware):
70
- """Test that org_id validation succeeds when token org_id matches"""
71
- with (
72
- patch.object(auth_middleware, "get_signing_key_from_jwt"),
73
- patch("jwt.decode") as mock_decode,
74
- ):
75
- expected_payload = {"org_id": "test-org-id", "user_id": "test-user"}
76
- mock_decode.return_value = expected_payload
77
-
78
- result = auth_middleware.validate_jwt_token("fake-token")
79
-
80
- assert result == expected_payload
81
-
82
- @pytest.mark.asyncio
83
- async def test_service_token_validation_success(self, auth_middleware):
84
- """Test that service token validation succeeds when token matches"""
85
- mock_request = Mock()
86
- mock_request.url.path = "/planar/v1/something"
87
- mock_request.headers = {"Authorization": "Bearer plt_test-service-token"}
88
- mock_call_next = Mock()
89
- mock_call_next.return_value = JSONResponse(
90
- status_code=200, content={"message": "success"}
91
- )
92
-
93
- async def mock_call_next_func(request):
94
- return mock_call_next(request)
95
-
96
- result = await auth_middleware.dispatch(mock_request, mock_call_next_func)
97
- mock_call_next.assert_called_once_with(mock_request)
98
- assert result is not None
99
- assert result.status_code == 200
100
-
101
- @pytest.mark.asyncio
102
- async def test_service_token_validation_failure(self, auth_middleware):
103
- """Test that service token validation succeeds when token matches"""
104
- mock_request = Mock()
105
- mock_request.url.path = "/planar/v1/something"
106
- mock_request.headers = {"Authorization": "Bearer plt_wrong-token"}
107
- mock_call_next = Mock()
108
- mock_call_next.return_value = JSONResponse(
109
- status_code=200, content={"message": "success"}
110
- )
111
-
112
- async def mock_call_next_func(request):
113
- return mock_call_next(request)
114
-
115
- result = await auth_middleware.dispatch(mock_request, mock_call_next_func)
116
- mock_call_next.assert_not_called()
117
- assert result is not None
118
- assert result.status_code == 401
119
-
120
- @pytest.mark.asyncio
121
- async def test_exclusion_paths_includes_health_and_additional(self, app):
122
- """Test that exclusion paths include health endpoint and additional paths"""
123
- middleware = AuthMiddleware(
124
- app=app,
125
- client_id="test-client-id",
126
- org_id="test-org-id",
127
- additional_exclusion_paths=["/custom/path", "/another/path"],
128
- )
129
-
130
- mock_request = Mock()
131
- mock_call_next = Mock()
132
-
133
- async def mock_call_next_func(request):
134
- return mock_call_next(request)
135
-
136
- expected_paths = ["/planar/v1/health", "/custom/path", "/another/path"]
137
- for path in expected_paths:
138
- mock_request.url.path = path
139
- mock_call_next.reset_mock()
140
-
141
- await middleware.dispatch(mock_request, mock_call_next_func) # type: ignore
142
-
143
- mock_call_next.assert_called_once_with(mock_request)
144
-
145
- # Test that non-excluded paths are not excluded
146
- mock_request.url.path = "/not-excluded"
147
- mock_call_next.reset_mock()
148
- result = await middleware.dispatch(mock_request, mock_call_next_func)
149
- assert result is not None
150
- assert result.status_code == 401
151
- mock_call_next.assert_not_called()
152
-
153
- def test_required_org_id_parameter(self, app):
154
- """Test that org_id parameter is required (not optional)"""
155
- # This test ensures org_id cannot be None based on type hints
156
- # The actual enforcement is at the type level, so we just verify
157
- # the constructor works with a valid org_id
158
- middleware = AuthMiddleware(
159
- app=app, client_id="test-client-id", org_id="required-org-id"
160
- )
161
-
162
- assert middleware.org_id == "required-org-id"
@@ -1,78 +0,0 @@
1
- from planar.security.auth_context import Principal
2
- from planar.security.authorization import (
3
- CedarEntity,
4
- PolicyService,
5
- ResourceType,
6
- WorkflowAction,
7
- get_policy_service,
8
- policy_service_context,
9
- set_policy_service,
10
- )
11
-
12
-
13
- def test_policy_service_context_variable():
14
- """Test that the authorization service context variable works correctly."""
15
- # Initially, no authz service should be set
16
- assert get_policy_service() is None
17
-
18
- # Create a mock policy service
19
- policy_service = PolicyService()
20
-
21
- # Set the policy service in context
22
- set_policy_service(policy_service)
23
- assert get_policy_service() is policy_service
24
-
25
- # Reset the context
26
- set_policy_service(None)
27
- assert get_policy_service() is None
28
-
29
- # Test the context manager
30
- async def test_context_manager():
31
- async with policy_service_context(policy_service):
32
- assert get_policy_service() is policy_service
33
- # After exiting the context, it should be None again
34
- assert get_policy_service() is None
35
-
36
- # Run the async test
37
- import asyncio
38
-
39
- asyncio.run(test_context_manager())
40
-
41
-
42
- def test_policy_service_with_principal():
43
- """Test that the policy service works with principal resources."""
44
- policy_service = PolicyService()
45
-
46
- # Create a mock principal with only required fields
47
- principal = Principal(sub="test-user") # type: ignore
48
-
49
- # Create resources
50
- principal_resource = CedarEntity.from_principal(principal)
51
- workflow_resource = CedarEntity.from_workflow("test_workflow")
52
-
53
- # Test that the service can be used
54
- assert principal_resource.resource_type == ResourceType.PRINCIPAL
55
- assert workflow_resource.resource_type == ResourceType.WORKFLOW
56
- assert principal_resource.resource_attributes["sub"] == "test-user"
57
- assert workflow_resource.resource_attributes["function_name"] == "test_workflow"
58
-
59
- assert (
60
- policy_service.is_allowed(
61
- principal_resource, WorkflowAction.WORKFLOW_RUN, workflow_resource
62
- )
63
- is True
64
- )
65
-
66
- assert (
67
- policy_service.is_allowed(
68
- principal_resource, WorkflowAction.WORKFLOW_RUN, workflow_resource
69
- )
70
- is True
71
- )
72
-
73
- assert (
74
- policy_service.is_allowed(
75
- principal_resource, "Workflow::Fail", workflow_resource
76
- )
77
- is False
78
- )
@@ -1,41 +0,0 @@
1
- from cedarpy import Decision, format_policies, is_authorized
2
-
3
-
4
- def test_cedar_permissions():
5
- # Define entities
6
- entities = [
7
- {
8
- "uid": {"__entity": {"type": "Principal", "id": "alice"}},
9
- "attrs": {},
10
- "parents": [],
11
- },
12
- {
13
- "uid": {"__entity": {"type": "Document", "id": "doc1"}},
14
- "attrs": {},
15
- "parents": [],
16
- },
17
- ]
18
-
19
- # Initialize the Cedar policy service
20
- policy = """
21
- permit (
22
- principal,
23
- action == Action::"Run",
24
- resource
25
- );
26
- """
27
- formatted_policy = format_policies(policy)
28
- print(formatted_policy)
29
-
30
- # Create a simple request
31
- request = {
32
- "principal": 'Principal::"alice"',
33
- "action": 'Action::"Run"',
34
- "resource": 'Document::"doc1"',
35
- }
36
-
37
- # Test the authorization
38
- result = is_authorized(request, policy, entities)
39
- assert result.decision == Decision.Allow, (
40
- "Cedar works and basic permission should be allowed"
41
- )
@@ -1,158 +0,0 @@
1
- import pytest
2
-
3
- from planar.security.auth_context import Principal
4
- from planar.security.authorization import (
5
- AgentAction,
6
- CedarEntity,
7
- PolicyService,
8
- RuleAction,
9
- WorkflowAction,
10
- )
11
-
12
-
13
- @pytest.fixture
14
- def policy_service():
15
- return PolicyService()
16
-
17
-
18
- def test_workflow_permissions(policy_service: PolicyService):
19
- # Create a test principal (user)
20
- user_principal = Principal(
21
- sub="user123" # type: ignore
22
- )
23
-
24
- # Create test resources
25
- workflow_resource = CedarEntity.from_workflow("com.example.workflow.ProcessData")
26
-
27
- # Test Workflow Actions
28
- assert policy_service.is_allowed(
29
- CedarEntity.from_principal(user_principal),
30
- WorkflowAction.WORKFLOW_LIST,
31
- workflow_resource,
32
- ), "User should be able to list workflows"
33
-
34
- assert policy_service.is_allowed(
35
- CedarEntity.from_principal(user_principal),
36
- WorkflowAction.WORKFLOW_VIEW_DETAILS,
37
- workflow_resource,
38
- ), "User should be able to view workflow details"
39
-
40
- assert policy_service.is_allowed(
41
- CedarEntity.from_principal(user_principal),
42
- WorkflowAction.WORKFLOW_RUN,
43
- workflow_resource,
44
- ), "User should be able to run workflow"
45
-
46
- assert policy_service.is_allowed(
47
- CedarEntity.from_principal(user_principal),
48
- WorkflowAction.WORKFLOW_CANCEL,
49
- workflow_resource,
50
- ), "User should be able to cancel workflow"
51
-
52
-
53
- def test_agent_permissions(policy_service: PolicyService):
54
- user_principal = Principal(
55
- sub="user123" # type: ignore
56
- )
57
-
58
- agent_resource = CedarEntity.from_agent("OcrAgent")
59
-
60
- # Test Agent Actions
61
- assert policy_service.is_allowed(
62
- CedarEntity.from_principal(user_principal),
63
- AgentAction.AGENT_LIST,
64
- agent_resource,
65
- ), "User should be able to list agents"
66
-
67
- assert policy_service.is_allowed(
68
- CedarEntity.from_principal(user_principal),
69
- AgentAction.AGENT_VIEW_DETAILS,
70
- agent_resource,
71
- ), "User should be able to view agent details"
72
-
73
- assert policy_service.is_allowed(
74
- CedarEntity.from_principal(user_principal),
75
- AgentAction.AGENT_RUN,
76
- agent_resource,
77
- ), "User should be able to run agent"
78
-
79
- assert policy_service.is_allowed(
80
- CedarEntity.from_principal(user_principal),
81
- AgentAction.AGENT_UPDATE,
82
- agent_resource,
83
- ), "User should be able to configure agent"
84
-
85
-
86
- def test_denied_permission(policy_service: PolicyService):
87
- # Create a test principal (user)
88
- user_principal = Principal(
89
- sub="user123" # type: ignore
90
- )
91
-
92
- # Create test resource
93
- workflow_resource = CedarEntity.from_workflow("com.example.workflow.ProcessData")
94
-
95
- # Test that a non-existent action is denied
96
- assert not policy_service.is_allowed(
97
- CedarEntity.from_principal(user_principal),
98
- "Workflow::Delete", # This action is not defined in our policies
99
- workflow_resource,
100
- ), "User should not be able to delete workflows"
101
-
102
-
103
- def test_deny_edits_to_rules_for_member_role(policy_service: PolicyService):
104
- member_jwt_data = {
105
- "org_name": "CoPlane",
106
- "user_first_name": "Donald",
107
- "user_last_name": "Knuth",
108
- "user_email": "don@coplane.com",
109
- "iss": "https://auth-api.coplane.com",
110
- "sub": "user_02JYMGMYETXMAVB0GKT868T8V7",
111
- "sid": "session_01JZ6NJVC1MSR86VZNR54BF9D4",
112
- "jti": "01JZ8EHD793F6RTY8FC4A3H4E9",
113
- "org_id": "org_01JY4QP57Y7H4EQ7HT3BGN7TNK",
114
- "role": "member",
115
- "permissions": [],
116
- "feature_flags": [],
117
- "exp": 1751556901,
118
- "iat": 1751556601,
119
- }
120
-
121
- user_principal = Principal.from_jwt_payload(member_jwt_data)
122
-
123
- rule_resource = CedarEntity.from_rule("complex_business_rule")
124
-
125
- assert not policy_service.is_allowed(
126
- CedarEntity.from_principal(user_principal),
127
- RuleAction.RULE_UPDATE,
128
- rule_resource,
129
- )
130
-
131
-
132
- def test_allow_edits_to_rules_for_admin_role(policy_service: PolicyService):
133
- member_jwt_data = {
134
- "org_name": "CoPlane",
135
- "user_first_name": "Donald",
136
- "user_last_name": "Knuth",
137
- "user_email": "don@coplane.com",
138
- "iss": "https://auth-api.coplane.com",
139
- "sub": "user_02JYMGMYETXMAVB0GKT868T8V7",
140
- "sid": "session_01JZ6NJVC1MSR86VZNR54BF9D4",
141
- "jti": "01JZ8EHD793F6RTY8FC4A3H4E9",
142
- "org_id": "org_01JY4QP57Y7H4EQ7HT3BGN7TNK",
143
- "role": "admin",
144
- "permissions": [],
145
- "feature_flags": [],
146
- "exp": 1751556901,
147
- "iat": 1751556601,
148
- }
149
-
150
- user_principal = Principal.from_jwt_payload(member_jwt_data)
151
-
152
- rule_resource = CedarEntity.from_rule("complex_business_rule")
153
-
154
- assert policy_service.is_allowed(
155
- CedarEntity.from_principal(user_principal),
156
- RuleAction.RULE_UPDATE,
157
- rule_resource,
158
- )
@@ -1,179 +0,0 @@
1
- import pytest
2
-
3
- from planar.security.auth_context import (
4
- Principal,
5
- clear_principal,
6
- get_current_principal,
7
- has_role,
8
- require_principal,
9
- set_principal,
10
- )
11
-
12
-
13
- def test_principal_from_jwt_payload():
14
- # Test with minimal required fields
15
- payload = {"sub": "user123"}
16
- principal = Principal.from_jwt_payload(payload)
17
- assert principal.sub == "user123"
18
- assert principal.extra_claims == {}
19
-
20
- # Test with all standard fields
21
- payload = {
22
- "sub": "user123",
23
- "iss": "https://auth-api.coplane.com",
24
- "exp": 1234567890,
25
- "iat": 1234567890,
26
- "sid": "session123",
27
- "jti": "jwt123",
28
- "org_id": "org123",
29
- "org_name": "Test Org",
30
- "user_first_name": "John",
31
- "user_last_name": "Doe",
32
- "user_email": "john@example.com",
33
- "role": "admin",
34
- "permissions": ["read", "write"],
35
- }
36
- principal = Principal.from_jwt_payload(payload)
37
- assert principal.sub == "user123"
38
- assert principal.iss == "https://auth-api.coplane.com"
39
- assert principal.exp == 1234567890
40
- assert principal.iat == 1234567890
41
- assert principal.sid == "session123"
42
- assert principal.jti == "jwt123"
43
- assert principal.org_id == "org123"
44
- assert principal.org_name == "Test Org"
45
- assert principal.user_first_name == "John"
46
- assert principal.user_last_name == "Doe"
47
- assert principal.user_email == "john@example.com"
48
- assert principal.role == "admin"
49
- assert principal.permissions == ["read", "write"]
50
- assert principal.extra_claims == {}
51
-
52
- # Test with extra claims
53
- payload = {
54
- "sub": "user123",
55
- "custom_field": "custom_value",
56
- "another_field": 123,
57
- }
58
- principal = Principal.from_jwt_payload(payload)
59
- assert principal.sub == "user123"
60
- assert principal.extra_claims == {
61
- "custom_field": "custom_value",
62
- "another_field": 123,
63
- }
64
-
65
- # Test with missing required field
66
- with pytest.raises(ValueError, match="JWT payload must contain 'sub' field"):
67
- Principal.from_jwt_payload({})
68
-
69
-
70
- def test_get_current_principal():
71
- # Test when no principal is set
72
- assert get_current_principal() is None
73
-
74
- # Test when principal is set
75
- principal = Principal(
76
- sub="user123",
77
- iss=None,
78
- exp=None,
79
- iat=None,
80
- sid=None,
81
- jti=None,
82
- org_id=None,
83
- org_name=None,
84
- user_first_name=None,
85
- user_last_name=None,
86
- user_email=None,
87
- role=None,
88
- permissions=None,
89
- )
90
- token = set_principal(principal)
91
- try:
92
- assert get_current_principal() == principal
93
- finally:
94
- clear_principal(token)
95
-
96
- # Verify principal is cleared
97
- assert get_current_principal() is None
98
-
99
-
100
- def test_has_role():
101
- # Test when no principal is set
102
- assert not has_role("admin")
103
-
104
- # Test when principal has matching role
105
- principal = Principal(
106
- sub="user123",
107
- role="admin",
108
- iss=None,
109
- exp=None,
110
- iat=None,
111
- sid=None,
112
- jti=None,
113
- org_id=None,
114
- org_name=None,
115
- user_first_name=None,
116
- user_last_name=None,
117
- user_email=None,
118
- permissions=None,
119
- )
120
- token = set_principal(principal)
121
- try:
122
- assert has_role("admin")
123
- assert not has_role("user")
124
- finally:
125
- clear_principal(token)
126
-
127
- # Test when principal has no role
128
- principal = Principal(
129
- sub="user123",
130
- iss=None,
131
- exp=None,
132
- iat=None,
133
- sid=None,
134
- jti=None,
135
- org_id=None,
136
- org_name=None,
137
- user_first_name=None,
138
- user_last_name=None,
139
- user_email=None,
140
- role=None,
141
- permissions=None,
142
- )
143
- token = set_principal(principal)
144
- try:
145
- assert not has_role("admin")
146
- finally:
147
- clear_principal(token)
148
-
149
-
150
- def test_require_principal():
151
- # Test when no principal is set
152
- with pytest.raises(RuntimeError, match="No authenticated principal in context"):
153
- require_principal()
154
-
155
- # Test when principal is set
156
- principal = Principal(
157
- sub="user123",
158
- iss=None,
159
- exp=None,
160
- iat=None,
161
- sid=None,
162
- jti=None,
163
- org_id=None,
164
- org_name=None,
165
- user_first_name=None,
166
- user_last_name=None,
167
- user_email=None,
168
- role=None,
169
- permissions=None,
170
- )
171
- token = set_principal(principal)
172
- try:
173
- assert require_principal() == principal
174
- finally:
175
- clear_principal(token)
176
-
177
- # Verify principal is cleared
178
- with pytest.raises(RuntimeError, match="No authenticated principal in context"):
179
- require_principal()