Skip to content

Commit e563a40

Browse files
author
Yoav Nahshon
committed
Add unit tests for access-rights and groups modules
test_access_extended.py: - Role hierarchy ordering and >= comparison - min/max_access_level return Role instances (not int) - min/max_access_level raise ValueError for unmapped operations (CREATE) - Error message lists valid operations - serialize_role_json: JSON mode → int, Python mode → name string - model_dump(mode='json') produces integer roles for wire format - model_dump(mode='python') produces string role names - Round-trip from backend dict (int roles) through model and back - user_by_role property - None user_access/group_access converted to [] test_groups.py: - Group model: basic, subgroups, deeply nested - GroupList: flat flattening, flat returns BaseGroup, by_id, by_name (including subgroup traversal) - User model: basic, with groups - UserList: by_id, by_username, by_name, __getitem__, missing key - INTERNALLY/EXTERNALLY_PUBLIC_GROUP IDs use hyphens - refresh_public_groups with custom config and without arg - DSMS.user_groups caches result; refresh_user_groups() re-fetches - DSMS.users caches result; refresh_users() re-fetches - DSMS.get_user() returns typed User object; raises on 404
1 parent 0e260f4 commit e563a40

2 files changed

Lines changed: 579 additions & 0 deletions

File tree

‎tests/test_access_extended.py‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""Extended tests for access.py — covering gaps identified in post-merge review."""
2+
3+
import pytest
4+
5+
from dsms.knowledge.properties.access import (
6+
GroupAccessProperty,
7+
KItemAccessProperties,
8+
OperationType,
9+
Role,
10+
RoleMapping,
11+
UserAccessProperty,
12+
)
13+
14+
15+
# ---------------------------------------------------------------------------
16+
# Role hierarchy
17+
# ---------------------------------------------------------------------------
18+
19+
20+
def test_role_ordering():
21+
"""Role integer values must be strictly ascending: USER < CONTRIBUTOR < OWNER < ADMIN."""
22+
assert Role.USER < Role.CONTRIBUTOR < Role.OWNER < Role.ADMIN
23+
24+
25+
def test_role_gte_comparison():
26+
""">= on Role values must work correctly for threshold checks."""
27+
assert Role.OWNER >= Role.CONTRIBUTOR
28+
assert Role.ADMIN >= Role.OWNER
29+
assert not (Role.USER >= Role.CONTRIBUTOR)
30+
31+
32+
# ---------------------------------------------------------------------------
33+
# min_access_level / max_access_level return type
34+
# ---------------------------------------------------------------------------
35+
36+
37+
def test_min_access_level_returns_role_instance():
38+
"""min_access_level must return a Role member, not a plain int."""
39+
result = RoleMapping.min_access_level(OperationType.READ)
40+
assert isinstance(result, Role)
41+
assert result is Role.USER
42+
43+
44+
def test_max_access_level_returns_role_instance():
45+
"""max_access_level must return a Role member, not a plain int."""
46+
result = RoleMapping.max_access_level(OperationType.READ)
47+
assert isinstance(result, Role)
48+
assert result is Role.ADMIN
49+
50+
51+
@pytest.mark.parametrize(
52+
"operation, expected_min",
53+
[
54+
(OperationType.READ, Role.USER),
55+
(OperationType.UPDATE, Role.CONTRIBUTOR),
56+
(OperationType.DELETE, Role.OWNER),
57+
(OperationType.MANAGE, Role.OWNER),
58+
],
59+
)
60+
def test_min_access_level_correct_role(operation, expected_min):
61+
assert RoleMapping.min_access_level(operation) is expected_min
62+
63+
64+
@pytest.mark.parametrize(
65+
"operation",
66+
[
67+
OperationType.READ,
68+
OperationType.UPDATE,
69+
OperationType.DELETE,
70+
OperationType.MANAGE,
71+
],
72+
)
73+
def test_max_access_level_is_admin(operation):
74+
"""ADMIN always holds the maximum access level for every mapped operation."""
75+
assert RoleMapping.max_access_level(operation) is Role.ADMIN
76+
77+
78+
# ---------------------------------------------------------------------------
79+
# Unmapped operations raise ValueError
80+
# ---------------------------------------------------------------------------
81+
82+
83+
def test_min_access_level_create_raises():
84+
"""CREATE is not assigned to any role — min_access_level must raise ValueError."""
85+
with pytest.raises(ValueError, match="create"):
86+
RoleMapping.min_access_level(OperationType.CREATE)
87+
88+
89+
def test_max_access_level_create_raises():
90+
"""CREATE is not assigned to any role — max_access_level must raise ValueError."""
91+
with pytest.raises(ValueError, match="create"):
92+
RoleMapping.max_access_level(OperationType.CREATE)
93+
94+
95+
def test_error_message_lists_valid_operations():
96+
"""The ValueError message must tell the caller which operations are valid."""
97+
with pytest.raises(ValueError) as exc_info:
98+
RoleMapping.min_access_level(OperationType.CREATE)
99+
msg = str(exc_info.value)
100+
for op in (OperationType.READ, OperationType.UPDATE, OperationType.DELETE, OperationType.MANAGE):
101+
assert op.value in msg
102+
103+
104+
# ---------------------------------------------------------------------------
105+
# serialize_role_json — correct mode behaviour
106+
# ---------------------------------------------------------------------------
107+
108+
109+
def test_serialize_role_json_mode():
110+
"""Python mode → name string; JSON/wire mode → integer value."""
111+
prop = UserAccessProperty(user_id="u1", role=Role.OWNER)
112+
113+
python_dump = prop.model_dump(mode="python")
114+
assert python_dump["role"] == "OWNER"
115+
assert isinstance(python_dump["role"], str)
116+
117+
json_dump = prop.model_dump(mode="json")
118+
assert json_dump["role"] == Role.OWNER.value
119+
assert isinstance(json_dump["role"], int)
120+
121+
122+
# ---------------------------------------------------------------------------
123+
# KItemAccessProperties wire format
124+
# ---------------------------------------------------------------------------
125+
126+
127+
def test_model_dump_json_produces_integer_roles():
128+
"""model_dump(mode='json') must produce integer role values for the wire format."""
129+
props = KItemAccessProperties(
130+
user_access=[UserAccessProperty(user_id="u1", role=Role.OWNER)],
131+
group_access=[GroupAccessProperty(group_id="g1", role=Role.USER)],
132+
)
133+
payload = props.model_dump(mode="json")
134+
135+
assert payload["user_access"][0]["role"] == Role.OWNER.value
136+
assert isinstance(payload["user_access"][0]["role"], int)
137+
assert payload["group_access"][0]["role"] == Role.USER.value
138+
assert isinstance(payload["group_access"][0]["role"], int)
139+
140+
141+
def test_model_dump_python_produces_string_roles():
142+
"""model_dump(mode='python') must produce string role names for display."""
143+
props = KItemAccessProperties(
144+
user_access=[UserAccessProperty(user_id="u1", role=Role.CONTRIBUTOR)],
145+
group_access=[],
146+
)
147+
payload = props.model_dump(mode="python")
148+
assert payload["user_access"][0]["role"] == "CONTRIBUTOR"
149+
150+
151+
def test_round_trip_from_backend_dict():
152+
"""A payload as returned by the backend (integer roles) must round-trip correctly."""
153+
backend_payload = {
154+
"user_access": [
155+
{"user_id": "alice", "role": Role.OWNER.value},
156+
{"user_id": "bob", "role": Role.USER.value},
157+
],
158+
"group_access": [
159+
{"group_id": "dsms:internally-public", "role": Role.USER.value},
160+
],
161+
}
162+
163+
props = KItemAccessProperties(**backend_payload)
164+
165+
assert props.by_user["alice"].role is Role.OWNER
166+
assert props.by_user["bob"].role is Role.USER
167+
assert props.by_group["dsms:internally-public"].role is Role.USER
168+
169+
# Serialise back and verify identity
170+
re_serialised = props.model_dump(mode="json")
171+
assert re_serialised["user_access"][0] == {"user_id": "alice", "role": Role.OWNER.value}
172+
assert re_serialised["group_access"][0] == {
173+
"group_id": "dsms:internally-public",
174+
"role": Role.USER.value,
175+
}
176+
177+
178+
# ---------------------------------------------------------------------------
179+
# user_by_role property (untested in original suite)
180+
# ---------------------------------------------------------------------------
181+
182+
183+
def test_user_by_role():
184+
"""user_by_role must group user IDs by their Role."""
185+
props = KItemAccessProperties(
186+
user_access=[
187+
UserAccessProperty(user_id="alice", role=Role.OWNER),
188+
UserAccessProperty(user_id="bob", role=Role.USER),
189+
UserAccessProperty(user_id="carol", role=Role.USER),
190+
]
191+
)
192+
by_role = props.user_by_role
193+
assert by_role[Role.OWNER] == ["alice"]
194+
assert set(by_role[Role.USER]) == {"bob", "carol"}
195+
assert Role.CONTRIBUTOR not in by_role
196+
197+
198+
def test_user_by_role_empty():
199+
assert KItemAccessProperties().user_by_role == {}
200+
201+
202+
# ---------------------------------------------------------------------------
203+
# Validator: None inputs become empty lists
204+
# ---------------------------------------------------------------------------
205+
206+
207+
def test_none_user_access_becomes_empty_list():
208+
props = KItemAccessProperties(user_access=None, group_access=None)
209+
assert props.user_access == []
210+
assert props.group_access == []

0 commit comments

Comments
 (0)