Skip to content

Commit 5a36098

Browse files
committed
feat: implement patch nominal case checkers
1 parent 3d29aa7 commit 5a36098

File tree

14 files changed

+4178
-67
lines changed

14 files changed

+4178
-67
lines changed

scim2_tester/checkers/patch_add.py

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

0 commit comments

Comments
 (0)