Skip to content

Commit dc36e08

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

File tree

14 files changed

+2316
-56
lines changed

14 files changed

+2316
-56
lines changed

scim2_tester/checkers/patch_add.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
context,
52+
model,
53+
required=[Required.false],
54+
mutability=[Mutability.read_write, Mutability.write_only],
55+
)
56+
)
57+
58+
if not all_urns:
59+
return [
60+
CheckResult(
61+
status=Status.SKIPPED,
62+
reason=f"No addable attributes found for {model.__name__}",
63+
resource_type=model.__name__,
64+
)
65+
]
66+
67+
base_resource = context.resource_manager.create_and_register(model)
68+
69+
for urn, source_model in all_urns:
70+
patch_value = generate_random_value(context, urn=urn, model=source_model)
71+
72+
patch_op = PatchOp[type(base_resource)](
73+
operations=[
74+
PatchOperation(
75+
op=PatchOperation.Op.add,
76+
path=urn,
77+
value=patch_value,
78+
)
79+
]
80+
)
81+
82+
try:
83+
updated_resource = context.client.modify(
84+
resource_model=type(base_resource),
85+
id=base_resource.id,
86+
patch_op=patch_op,
87+
)
88+
except SCIMClientError as exc:
89+
results.append(
90+
CheckResult(
91+
status=Status.ERROR,
92+
reason=f"Failed to add attribute '{urn}': {exc}",
93+
resource_type=model.__name__,
94+
data={
95+
"urn": urn,
96+
"error": exc,
97+
"patch_value": patch_value,
98+
},
99+
)
100+
)
101+
continue
102+
103+
actual_value = get_value_by_urn(updated_resource, urn)
104+
105+
if get_annotation_by_urn(
106+
Mutability, urn, source_model
107+
) == Mutability.write_only or compare_field(patch_value, actual_value):
108+
results.append(
109+
CheckResult(
110+
status=Status.SUCCESS,
111+
reason=f"Successfully added attribute '{urn}'",
112+
resource_type=model.__name__,
113+
data={
114+
"urn": urn,
115+
"value": patch_value,
116+
},
117+
)
118+
)
119+
else:
120+
results.append(
121+
CheckResult(
122+
status=Status.ERROR,
123+
reason=f"Attribute '{urn}' was not added or has incorrect value",
124+
resource_type=model.__name__,
125+
data={
126+
"urn": urn,
127+
"expected": patch_value,
128+
"actual": actual_value,
129+
},
130+
)
131+
)
132+
133+
return results
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
context,
51+
model,
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 (
106+
get_annotation_by_urn(Mutability, urn, source_model)
107+
== Mutability.write_only
108+
or actual_value is None
109+
):
110+
results.append(
111+
CheckResult(
112+
status=Status.SUCCESS,
113+
reason=f"Successfully removed attribute '{urn}'",
114+
resource_type=model.__name__,
115+
data={
116+
"urn": urn,
117+
"initial_value": initial_value,
118+
},
119+
)
120+
)
121+
else:
122+
results.append(
123+
CheckResult(
124+
status=Status.ERROR,
125+
reason=f"Attribute '{urn}' was not removed",
126+
resource_type=model.__name__,
127+
data={
128+
"urn": urn,
129+
"initial_value": initial_value,
130+
"actual_value": actual_value,
131+
},
132+
)
133+
)
134+
135+
return results

0 commit comments

Comments
 (0)