mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-26 16:07:53 +00:00
147 lines
5.0 KiB
Python
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
|