Skip to content

Commit 4e55311

Browse files
committed
feat: implement patch nominal case checkers
1 parent 640e859 commit 4e55311

13 files changed

Lines changed: 1525 additions & 62 deletions

scim2_tester/checkers/patch_add.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""PATCH add operation checkers for SCIM compliance testing."""
2+
3+
from inspect import isclass
4+
from typing import Any
5+
6+
from scim2_client import SCIMClientError
7+
from scim2_models import ComplexAttribute
8+
from scim2_models import Mutability
9+
from scim2_models import PatchOp
10+
from scim2_models import PatchOperation
11+
from scim2_models import Required
12+
from scim2_models import Resource
13+
14+
from ..filling import generate_random_value
15+
from ..utils import CheckContext
16+
from ..utils import CheckResult
17+
from ..utils import Status
18+
from ..utils import checker
19+
from .utils import compare_field
20+
from .utils import get_attribute_type_by_urn
21+
from .utils import get_value_by_urn
22+
from .utils import iter_all_urns
23+
24+
25+
@checker("patch:add")
26+
def check_add_attribute(
27+
context: CheckContext, model: type[Resource[Any]]
28+
) -> list[CheckResult]:
29+
"""Test PATCH add operation on all attributes (simple, complex, and extensions).
30+
31+
Creates a minimal resource, then iterates over ALL possible URNs (base model,
32+
extensions, and sub-attributes) to test PATCH add operations systematically.
33+
Uses a unified approach that handles all attribute types consistently.
34+
35+
**Tested Behavior:**
36+
- Adding new attribute values (simple, complex, and extension attributes)
37+
- Server accepts the PATCH request with correct URN paths for extensions
38+
- Response contains the added attribute with correct values
39+
40+
**Status:**
41+
- :attr:`~scim2_tester.Status.SUCCESS`: Attribute successfully added
42+
- :attr:`~scim2_tester.Status.ERROR`: Failed to add attribute
43+
- :attr:`~scim2_tester.Status.SKIPPED`: No addable attributes found
44+
45+
.. pull-quote:: :rfc:`RFC 7644 Section 3.5.2.1 - Add Operation <7644#section-3.5.2.1>`
46+
47+
"The 'add' operation is used to add a new attribute and/or values to
48+
an existing resource."
49+
"""
50+
results = []
51+
all_urns = list(
52+
iter_all_urns(
53+
context,
54+
model,
55+
context.resource_manager,
56+
required=[Required.false],
57+
mutability=[Mutability.read_write, Mutability.write_only],
58+
)
59+
)
60+
61+
if not all_urns:
62+
return [
63+
CheckResult(
64+
status=Status.SKIPPED,
65+
reason=f"No addable attributes found for {model.__name__}",
66+
resource_type=model.__name__,
67+
)
68+
]
69+
70+
base_resource = context.resource_manager.create_and_register(model)
71+
72+
for urn, source_model in all_urns:
73+
attr_type = get_attribute_type_by_urn(source_model, urn)
74+
if attr_type is None:
75+
continue
76+
77+
field_name = urn.split(".")[-1].split(":")[-1]
78+
patch_value = generate_random_value(context, field_name, field_type=attr_type)
79+
80+
if is_complex := isclass(attr_type) and issubclass(attr_type, ComplexAttribute):
81+
patch_value = patch_value.model_dump(mode="json", exclude_unset=True)
82+
83+
patch_op = PatchOp[type(base_resource)](
84+
operations=[
85+
PatchOperation(
86+
op=PatchOperation.Op.add,
87+
path=urn,
88+
value=patch_value,
89+
)
90+
]
91+
)
92+
93+
try:
94+
updated_resource = context.client.modify(
95+
resource_model=type(base_resource),
96+
id=base_resource.id,
97+
patch_op=patch_op,
98+
)
99+
except SCIMClientError as exc:
100+
results.append(
101+
CheckResult(
102+
status=Status.ERROR,
103+
reason=f"Failed to add attribute '{urn}': {exc}",
104+
resource_type=model.__name__,
105+
data={
106+
"urn": urn,
107+
"error": exc,
108+
"is_complex": is_complex,
109+
"patch_value": patch_value,
110+
},
111+
)
112+
)
113+
continue
114+
115+
actual_value = get_value_by_urn(updated_resource, urn)
116+
success = compare_field(patch_value, actual_value, is_complex)
117+
118+
if success:
119+
results.append(
120+
CheckResult(
121+
status=Status.SUCCESS,
122+
reason=f"Successfully added attribute '{urn}'",
123+
resource_type=model.__name__,
124+
data={
125+
"urn": urn,
126+
"value": patch_value,
127+
"is_complex": is_complex,
128+
},
129+
)
130+
)
131+
else:
132+
results.append(
133+
CheckResult(
134+
status=Status.ERROR,
135+
reason=f"Attribute '{urn}' was not added or has incorrect value",
136+
resource_type=model.__name__,
137+
data={
138+
"urn": urn,
139+
"expected": patch_value,
140+
"actual": actual_value,
141+
"is_complex": is_complex,
142+
},
143+
)
144+
)
145+
146+
return results
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""PATCH remove operation checkers for SCIM compliance testing."""
2+
3+
from typing import Any
4+
5+
from scim2_client import SCIMClientError
6+
from scim2_models import Mutability
7+
from scim2_models import PatchOp
8+
from scim2_models import PatchOperation
9+
from scim2_models import Required
10+
from scim2_models import Resource
11+
12+
from ..utils import CheckContext
13+
from ..utils import CheckResult
14+
from ..utils import Status
15+
from ..utils import checker
16+
from .utils import get_value_by_urn
17+
from .utils import iter_all_urns
18+
19+
20+
@checker("patch:remove")
21+
def check_remove_attribute(
22+
context: CheckContext, model: type[Resource[Any]]
23+
) -> list[CheckResult]:
24+
"""Test PATCH remove operation on all attributes (simple, complex, and extensions).
25+
26+
Creates a resource with initial values, then iterates over ALL possible URNs
27+
(base model, extensions, and sub-attributes) to test PATCH remove operations
28+
systematically. Uses a unified approach that handles all attribute types consistently.
29+
30+
**Tested Behavior:**
31+
- Removing attribute values (simple, complex, and extension attributes)
32+
- Server accepts the PATCH request with correct URN paths for extensions
33+
- Response contains the resource with removed attributes (null/missing)
34+
35+
**Status:**
36+
- :attr:`~scim2_tester.Status.SUCCESS`: Attribute successfully removed
37+
- :attr:`~scim2_tester.Status.ERROR`: Failed to remove attribute or attribute still exists
38+
- :attr:`~scim2_tester.Status.SKIPPED`: No removable attributes found
39+
40+
.. pull-quote:: :rfc:`RFC 7644 Section 3.5.2.2 - Remove Operation <7644#section-3.5.2.2>`
41+
42+
"The 'remove' operation removes the value at the target location specified
43+
by the required attribute 'path'. The operation performs the following
44+
functions, depending on the target location specified by 'path'."
45+
"""
46+
results = []
47+
all_urns = list(
48+
iter_all_urns(
49+
context,
50+
model,
51+
context.resource_manager,
52+
required=[Required.false],
53+
mutability=[Mutability.read_write, Mutability.write_only],
54+
)
55+
)
56+
57+
if not all_urns:
58+
return [
59+
CheckResult(
60+
status=Status.SKIPPED,
61+
reason=f"No removable attributes found for {model.__name__}",
62+
resource_type=model.__name__,
63+
)
64+
]
65+
66+
full_resource = context.resource_manager.create_and_register(model, fill_all=True)
67+
68+
for urn, _source_model in all_urns:
69+
initial_value = get_value_by_urn(full_resource, urn)
70+
if initial_value is None:
71+
continue
72+
73+
remove_op = PatchOp[type(full_resource)](
74+
operations=[
75+
PatchOperation(
76+
op=PatchOperation.Op.remove,
77+
path=urn,
78+
)
79+
]
80+
)
81+
82+
try:
83+
updated_resource = context.client.modify(
84+
resource_model=type(full_resource),
85+
id=full_resource.id,
86+
patch_op=remove_op,
87+
)
88+
except SCIMClientError as exc:
89+
results.append(
90+
CheckResult(
91+
status=Status.ERROR,
92+
reason=f"Failed to remove attribute '{urn}': {exc}",
93+
resource_type=model.__name__,
94+
data={
95+
"urn": urn,
96+
"error": exc,
97+
"initial_value": initial_value,
98+
},
99+
)
100+
)
101+
continue
102+
103+
actual_value = get_value_by_urn(updated_resource, urn)
104+
105+
if actual_value is None:
106+
results.append(
107+
CheckResult(
108+
status=Status.SUCCESS,
109+
reason=f"Successfully removed attribute '{urn}'",
110+
resource_type=model.__name__,
111+
data={
112+
"urn": urn,
113+
"initial_value": initial_value,
114+
},
115+
)
116+
)
117+
else:
118+
results.append(
119+
CheckResult(
120+
status=Status.ERROR,
121+
reason=f"Attribute '{urn}' was not removed",
122+
resource_type=model.__name__,
123+
data={
124+
"urn": urn,
125+
"initial_value": initial_value,
126+
"actual_value": actual_value,
127+
},
128+
)
129+
)
130+
131+
return results

0 commit comments

Comments
 (0)