deer-flow/backend/tests/test_authorization_principal.py

147 lines
5.0 KiB
Python

"""Tests for build_principal_from_context — the single Principal builder.
This builder is the only sanctioned way to construct a Principal from runtime
context. Both Layer 1 (tool assembly) and Layer 2 (GuardrailAuthorizationAdapter)
must use it so identity semantics stay consistent.
"""
from __future__ import annotations
import pytest
from deerflow.authz.principal import build_principal_from_context
class TestPrincipalBuilderFields:
"""Verify all 7 Principal fields are explicitly constructed."""
def test_empty_context(self):
p = build_principal_from_context({}, default_role="user")
assert p.user_id is None
assert p.role == "user"
assert p.oauth_provider is None
assert p.oauth_id is None
assert p.channel_user_id is None
assert p.is_internal is False
assert p.attributes == {}
def test_full_field_mapping(self):
context = {
"user_id": "u1",
"user_role": "admin",
"oauth_provider": "github",
"oauth_id": "gh-123",
"channel_user_id": "ou_sender_1",
"is_internal": True,
"authz_attributes": {"department": "eng"},
}
p = build_principal_from_context(context, default_role="user")
assert p.user_id == "u1"
assert p.role == "admin"
assert p.oauth_provider == "github"
assert p.oauth_id == "gh-123"
assert p.channel_user_id == "ou_sender_1"
assert p.is_internal is True
assert p.attributes == {"department": "eng"}
def test_partial_context(self):
context = {"user_id": "u1", "user_role": "user"}
p = build_principal_from_context(context, default_role="admin")
assert p.user_id == "u1"
assert p.role == "user"
assert p.oauth_provider is None
assert p.oauth_id is None
class TestRoleResolution:
"""Role fallback rules."""
def test_none_role_uses_default(self):
p = build_principal_from_context({"user_role": None}, default_role="guest")
assert p.role == "guest"
def test_empty_string_role_uses_default(self):
p = build_principal_from_context({"user_role": ""}, default_role="guest")
assert p.role == "guest"
def test_missing_role_uses_default(self):
p = build_principal_from_context({}, default_role="guest")
assert p.role == "guest"
def test_unknown_role_preserved(self):
"""A non-empty but unknown role must NOT fall back to default_role."""
p = build_principal_from_context({"user_role": "editor"}, default_role="user")
assert p.role == "editor"
class TestIsInternalStrictBool:
"""is_internal must only be True when the value is strictly True."""
@pytest.mark.parametrize(
"value,expected",
[
(True, True),
(False, False),
(1, False),
("true", False),
("1", False),
(None, False),
([], False),
({}, False),
],
)
def test_is_internal_strict_bool(self, value, expected):
p = build_principal_from_context({"is_internal": value}, default_role="user")
assert p.is_internal is expected
class TestAttributes:
"""Attributes copy and validation semantics."""
def test_missing_attributes(self):
p = build_principal_from_context({}, default_role="user")
assert p.attributes == {}
def test_none_attributes(self):
p = build_principal_from_context({"authz_attributes": None}, default_role="user")
assert p.attributes == {}
def test_mapping_attributes_copied(self):
attrs = {"team": "platform"}
p = build_principal_from_context({"authz_attributes": attrs}, default_role="user")
assert p.attributes == {"team": "platform"}
# Mutating input after build must not affect Principal
attrs["team"] = "changed"
assert p.attributes["team"] == "platform"
def test_empty_mapping_attributes(self):
p = build_principal_from_context({"authz_attributes": {}}, default_role="user")
assert p.attributes == {}
@pytest.mark.parametrize(
"invalid",
[
[("key", "value")], # list of tuples (not a Mapping at runtime)
"not a mapping",
42,
[1, 2, 3],
],
)
def test_non_mapping_attributes_raises_type_error(self, invalid):
with pytest.raises(TypeError, match="authz_attributes must be a Mapping"):
build_principal_from_context({"authz_attributes": invalid}, default_role="user")
def test_type_error_includes_actual_type(self):
with pytest.raises(TypeError, match="list"):
build_principal_from_context({"authz_attributes": [1, 2]}, default_role="user")
class TestPureFunction:
"""Builder must not modify its input."""
def test_input_not_modified(self):
context = {"user_id": "u1", "user_role": "admin", "authz_attributes": {"k": "v"}}
original = dict(context)
build_principal_from_context(context, default_role="user")
assert context == original