Skip to content

Commit db67ac7

Browse files
authored
Merge pull request #33 from python-scim/resource-manager
Implement a resource manager to take care of garbages objects
2 parents 8bc9708 + fcd27d2 commit db67ac7

9 files changed

Lines changed: 270 additions & 132 deletions

File tree

scim2_tester/filling.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import uuid
44
from enum import Enum
55
from inspect import isclass
6+
from typing import TYPE_CHECKING
67
from typing import Annotated
78
from typing import Any
89
from typing import get_args
@@ -13,26 +14,14 @@
1314
from scim2_models import ExternalReference
1415
from scim2_models import Meta
1516
from scim2_models import Reference
16-
from scim2_models import Required
1717
from scim2_models import Resource
1818
from scim2_models import URIReference
1919
from scim2_models.utils import UNION_TYPES
2020

2121
from scim2_tester.utils import CheckConfig
2222

23-
24-
def create_minimal_object(
25-
conf: CheckConfig, model: type[Resource]
26-
) -> tuple[Resource, list[Resource]]:
27-
"""Create an object filling with the minimum required field set."""
28-
field_names = [
29-
field_name
30-
for field_name in model.model_fields
31-
if model.get_field_annotation(field_name, Required) == Required.true
32-
]
33-
obj, garbages = fill_with_random_values(conf, model(), field_names)
34-
obj = conf.client.create(obj)
35-
return obj, garbages
23+
if TYPE_CHECKING:
24+
from scim2_tester.utils import ResourceManager
3625

3726

3827
def model_from_ref_type(
@@ -58,10 +47,19 @@ def model_from_ref_type_(ref_type):
5847

5948

6049
def fill_with_random_values(
61-
conf: CheckConfig, obj: Resource, field_names: list[str] | None = None
62-
) -> Resource:
63-
"""Fill an object with random values generated according the attribute types."""
64-
garbages = []
50+
conf: CheckConfig,
51+
obj: Resource,
52+
resource_manager: "ResourceManager",
53+
field_names: list[str] | None = None,
54+
) -> Resource | None:
55+
"""Fill an object with random values generated according the attribute types.
56+
57+
:param conf: The check configuration containing the SCIM client
58+
:param obj: The Resource object to fill with random values
59+
:param resource_manager: Resource manager for automatic cleanup
60+
:param field_names: Optional list of field names to fill (defaults to all)
61+
:returns: The filled object or None if the object ends up empty
62+
"""
6563
for field_name in field_names or obj.__class__.model_fields.keys():
6664
field = obj.__class__.model_fields[field_name]
6765
if field.default:
@@ -110,9 +108,8 @@ def fill_with_random_values(
110108
model = model_from_ref_type(
111109
conf, ref_type, different_than=obj.__class__
112110
)
113-
ref_obj, sub_garbages = create_minimal_object(conf, model)
111+
ref_obj = resource_manager.create_and_register(model)
114112
value = ref_obj.meta.location
115-
garbages += sub_garbages
116113

117114
else:
118115
value = f"https://{str(uuid.uuid4())}.test"
@@ -121,21 +118,18 @@ def fill_with_random_values(
121118
value = random.choice(list(field_type))
122119

123120
elif isclass(field_type) and issubclass(field_type, ComplexAttribute):
124-
value, sub_garbages = fill_with_random_values(conf, field_type())
125-
garbages += sub_garbages
121+
value = fill_with_random_values(conf, field_type(), resource_manager)
126122

127123
elif isclass(field_type) and issubclass(field_type, Extension):
128-
value, sub_garbages = fill_with_random_values(conf, field_type())
129-
garbages += sub_garbages
124+
value = fill_with_random_values(conf, field_type(), resource_manager)
130125

131126
else:
132127
# Put emails so this will be accepted by EmailStr too
133128
value = str(uuid.uuid4())
134129

135130
if is_multiple:
136131
setattr(obj, field_name, [value])
137-
138132
else:
139133
setattr(obj, field_name, value)
140134

141-
return obj, garbages
135+
return obj

scim2_tester/resource.py

Lines changed: 12 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
from scim2_models import Mutability
21
from scim2_models import ResourceType
32

4-
from scim2_tester.filling import fill_with_random_values
53
from scim2_tester.resource_delete import check_object_deletion
64
from scim2_tester.resource_get import check_object_query
75
from scim2_tester.resource_get import check_object_query_without_id
@@ -17,6 +15,12 @@ def check_resource_type(
1715
conf: CheckConfig,
1816
resource_type: ResourceType,
1917
) -> list[CheckResult]:
18+
"""Orchestrate CRUD tests for a resource type.
19+
20+
:param conf: The check configuration containing the SCIM client
21+
:param resource_type: The ResourceType object to test
22+
:returns: A list of check results for all tested operations
23+
"""
2024
model = model_from_resource_type(conf, resource_type)
2125
if not model:
2226
return [
@@ -28,59 +32,12 @@ def check_resource_type(
2832
]
2933

3034
results = []
31-
garbages = []
32-
created_obj = None
33-
34-
# Always try to create an object - the decorator will decide if it should be skipped
35-
field_names = [
36-
field_name
37-
for field_name in model.model_fields.keys()
38-
if model.get_field_annotation(field_name, Mutability)
39-
in (Mutability.read_write, Mutability.write_only, Mutability.immutable)
40-
]
41-
obj, obj_garbages = fill_with_random_values(conf, model(), field_names)
42-
garbages += obj_garbages
43-
44-
create_result = check_object_creation(conf, obj)
45-
46-
# Only add to results if creation was explicitly requested (not skipped)
47-
if create_result.status != Status.SKIPPED:
48-
results.append(create_result)
49-
50-
# If creation succeeded (either explicitly or as dependency), we have an object
51-
if create_result.status == Status.SUCCESS:
52-
created_obj = create_result.data
53-
54-
# Try read operations - decorator will skip if not needed
55-
read_result = check_object_query(conf, created_obj)
56-
if read_result.status != Status.SKIPPED:
57-
results.append(read_result)
58-
59-
read_without_id_result = check_object_query_without_id(conf, created_obj)
60-
if read_without_id_result.status != Status.SKIPPED:
61-
results.append(read_without_id_result)
62-
63-
# Try update operations - decorator will skip if not needed
64-
field_names = [
65-
field_name
66-
for field_name in model.model_fields.keys()
67-
if model.get_field_annotation(field_name, Mutability)
68-
in (Mutability.read_write, Mutability.write_only)
69-
]
70-
_, obj_garbages = fill_with_random_values(conf, created_obj, field_names)
71-
garbages += obj_garbages
72-
73-
update_result = check_object_replacement(conf, created_obj)
74-
if update_result.status != Status.SKIPPED:
75-
results.append(update_result)
76-
77-
# Try delete operations - decorator will skip if not needed
78-
delete_result = check_object_deletion(conf, created_obj)
79-
if delete_result.status != Status.SKIPPED:
80-
results.append(delete_result)
8135

82-
# Cleanup remaining garbage
83-
for garbage in reversed(garbages):
84-
conf.client.delete(garbage)
36+
# Each test is now completely independent and handles its own cleanup
37+
results.append(check_object_creation(conf, model))
38+
results.append(check_object_query(conf, model))
39+
results.append(check_object_query_without_id(conf, model))
40+
results.append(check_object_replacement(conf, model))
41+
results.append(check_object_deletion(conf, model))
8542

8643
return results

scim2_tester/resource_delete.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,48 @@
22

33
from scim2_tester.utils import CheckConfig
44
from scim2_tester.utils import CheckResult
5+
from scim2_tester.utils import ResourceManager
56
from scim2_tester.utils import Status
67
from scim2_tester.utils import checker
78

89

910
@checker("crud:delete")
10-
def check_object_deletion(conf: CheckConfig, obj: Resource) -> CheckResult:
11-
"""Perform an object deletion."""
11+
def check_object_deletion(
12+
conf: CheckConfig, model: type[Resource], resources: ResourceManager
13+
) -> CheckResult:
14+
"""Test object deletion with automatic cleanup.
15+
16+
Creates a test object specifically for deletion testing, performs the
17+
delete operation, and verifies the object no longer exists.
18+
19+
:param conf: The check configuration containing the SCIM client
20+
:param model: The Resource model class to test
21+
:param resources: Resource manager for automatic cleanup
22+
:returns: The result of the check operation
23+
"""
24+
test_obj = resources.create_and_register(model)
25+
26+
# Remove from resource manager since we're testing deletion explicitly
27+
if test_obj in resources.resources:
28+
resources.resources.remove(test_obj)
29+
1230
conf.client.delete(
13-
obj.__class__, obj.id, expected_status_codes=conf.expected_status_codes or [204]
31+
model, test_obj.id, expected_status_codes=conf.expected_status_codes or [204]
1432
)
33+
34+
try:
35+
conf.client.query(model, test_obj.id)
36+
return CheckResult(
37+
conf,
38+
status=Status.ERROR,
39+
reason=f"{model.__name__} object with id {test_obj.id} still exists after deletion",
40+
)
41+
except Exception:
42+
# Expected - object should not exist after deletion
43+
pass
44+
1545
return CheckResult(
1646
conf,
1747
status=Status.SUCCESS,
18-
reason=f"Successful deletion of a {obj.__class__.__name__} object with id {obj.id}",
48+
reason=f"Successfully deleted {model.__name__} object with id {test_obj.id}",
1949
)

scim2_tester/resource_get.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from scim2_tester.utils import CheckConfig
55
from scim2_tester.utils import CheckResult
6+
from scim2_tester.utils import ResourceManager
67
from scim2_tester.utils import Status
78
from scim2_tester.utils import checker
89

@@ -27,50 +28,65 @@ def model_from_resource_type(
2728

2829

2930
@checker("crud:read")
30-
def check_object_query(conf: CheckConfig, obj: Resource) -> CheckResult:
31-
"""Perform an object query by knowing its id.
31+
def check_object_query(
32+
conf: CheckConfig, model: type[Resource], resources: ResourceManager
33+
) -> CheckResult:
34+
"""Test object query by ID with automatic cleanup.
3235
33-
Todo:
34-
- check if the fields of the result object are the same than the
35-
fields of the request object
36+
Creates a temporary test object, queries it by ID to validate the
37+
read operation.
3638
39+
:param conf: The check configuration containing the SCIM client
40+
:param model: The Resource model class to test
41+
:param resources: Resource manager for automatic cleanup
42+
:returns: The result of the check operation
3743
"""
44+
test_obj = resources.create_and_register(model)
45+
3846
response = conf.client.query(
39-
obj.__class__, obj.id, expected_status_codes=conf.expected_status_codes or [200]
47+
model, test_obj.id, expected_status_codes=conf.expected_status_codes or [200]
4048
)
49+
4150
return CheckResult(
4251
conf,
4352
status=Status.SUCCESS,
44-
reason=f"Successful query of a {obj.__class__.__name__} object with id {response.id}",
53+
reason=f"Successfully queried {model.__name__} object with id {test_obj.id}",
4554
data=response,
4655
)
4756

4857

4958
@checker("crud:read")
50-
def check_object_query_without_id(conf: CheckConfig, obj: Resource) -> CheckResult:
51-
"""Perform the query of all objects of one kind.
59+
def check_object_query_without_id(
60+
conf: CheckConfig, model: type[Resource], resources: ResourceManager
61+
) -> CheckResult:
62+
"""Test object listing without ID with automatic cleanup.
5263
53-
Todo:
54-
- look for the object across several pages
55-
- check if the fields of the result object are the same than the
56-
fields of the request object
64+
Creates a temporary test object, performs a list/search operation to
65+
validate bulk retrieval.
5766
67+
:param conf: The check configuration containing the SCIM client
68+
:param model: The Resource model class to test
69+
:param resources: Resource manager for automatic cleanup
70+
:returns: The result of the check operation
5871
"""
72+
test_obj = resources.create_and_register(model)
73+
5974
response = conf.client.query(
60-
obj.__class__, expected_status_codes=conf.expected_status_codes or [200]
75+
model, expected_status_codes=conf.expected_status_codes or [200]
6176
)
62-
found = any(obj.id == resource.id for resource in response.resources)
77+
78+
found = any(test_obj.id == resource.id for resource in response.resources)
6379
if not found:
6480
return CheckResult(
6581
conf,
6682
status=Status.ERROR,
67-
reason=f"Could not find object {obj.__class__.__name__} with id : {obj.id}",
83+
reason=f"Could not find {model.__name__} object with id {test_obj.id} in list response",
6884
data=response,
6985
)
7086

7187
return CheckResult(
7288
conf,
7389
status=Status.SUCCESS,
74-
reason=f"Successful query of a {obj.__class__.__name__} object with id {obj.id}",
90+
reason=f"Successfully found {model.__name__} object with id {test_obj.id} in list response",
7591
data=response,
7692
)

scim2_tester/resource_post.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,30 @@
22

33
from scim2_tester.utils import CheckConfig
44
from scim2_tester.utils import CheckResult
5+
from scim2_tester.utils import ResourceManager
56
from scim2_tester.utils import Status
67
from scim2_tester.utils import checker
78

89

910
@checker("crud:create")
10-
def check_object_creation(conf: CheckConfig, obj: Resource) -> CheckResult:
11-
"""Perform an object creation.
11+
def check_object_creation(
12+
conf: CheckConfig, model: type[Resource], resources: ResourceManager
13+
) -> CheckResult:
14+
"""Test object creation with automatic cleanup.
1215
13-
Todo:
14-
- check if the fields of the result object are the same than the
15-
fields of the request object
16+
Creates a test object of the specified model type, validates the creation
17+
operation.
1618
19+
:param conf: The check configuration containing the SCIM client
20+
:param model: The Resource model class to test
21+
:param resources: Resource manager for automatic cleanup
22+
:returns: The result of the check operation
1723
"""
18-
response = conf.client.create(
19-
obj, expected_status_codes=conf.expected_status_codes or [201]
20-
)
24+
created_obj = resources.create_and_register(model)
2125

2226
return CheckResult(
2327
conf,
2428
status=Status.SUCCESS,
25-
reason=f"Successful creation of a {obj.__class__.__name__} object with id {response.id}",
26-
data=response,
29+
reason=f"Successfully created {model.__name__} object with id {created_obj.id}",
30+
data=created_obj,
2731
)

0 commit comments

Comments
 (0)