Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 20 additions & 26 deletions scim2_tester/filling.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import uuid
from enum import Enum
from inspect import isclass
from typing import TYPE_CHECKING
from typing import Annotated
from typing import Any
from typing import get_args
Expand All @@ -13,26 +14,14 @@
from scim2_models import ExternalReference
from scim2_models import Meta
from scim2_models import Reference
from scim2_models import Required
from scim2_models import Resource
from scim2_models import URIReference
from scim2_models.utils import UNION_TYPES

from scim2_tester.utils import CheckConfig


def create_minimal_object(
conf: CheckConfig, model: type[Resource]
) -> tuple[Resource, list[Resource]]:
"""Create an object filling with the minimum required field set."""
field_names = [
field_name
for field_name in model.model_fields
if model.get_field_annotation(field_name, Required) == Required.true
]
obj, garbages = fill_with_random_values(conf, model(), field_names)
obj = conf.client.create(obj)
return obj, garbages
if TYPE_CHECKING:
from scim2_tester.utils import ResourceManager


def model_from_ref_type(
Expand All @@ -58,10 +47,19 @@ def model_from_ref_type_(ref_type):


def fill_with_random_values(
conf: CheckConfig, obj: Resource, field_names: list[str] | None = None
) -> Resource:
"""Fill an object with random values generated according the attribute types."""
garbages = []
conf: CheckConfig,
obj: Resource,
resource_manager: "ResourceManager",
field_names: list[str] | None = None,
) -> Resource | None:
"""Fill an object with random values generated according the attribute types.

:param conf: The check configuration containing the SCIM client
:param obj: The Resource object to fill with random values
:param resource_manager: Resource manager for automatic cleanup
:param field_names: Optional list of field names to fill (defaults to all)
:returns: The filled object or None if the object ends up empty
"""
for field_name in field_names or obj.__class__.model_fields.keys():
field = obj.__class__.model_fields[field_name]
if field.default:
Expand Down Expand Up @@ -110,9 +108,8 @@ def fill_with_random_values(
model = model_from_ref_type(
conf, ref_type, different_than=obj.__class__
)
ref_obj, sub_garbages = create_minimal_object(conf, model)
ref_obj = resource_manager.create_and_register(model)
value = ref_obj.meta.location
garbages += sub_garbages

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

elif isclass(field_type) and issubclass(field_type, ComplexAttribute):
value, sub_garbages = fill_with_random_values(conf, field_type())
garbages += sub_garbages
value = fill_with_random_values(conf, field_type(), resource_manager)

elif isclass(field_type) and issubclass(field_type, Extension):
value, sub_garbages = fill_with_random_values(conf, field_type())
garbages += sub_garbages
value = fill_with_random_values(conf, field_type(), resource_manager)

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

if is_multiple:
setattr(obj, field_name, [value])

else:
setattr(obj, field_name, value)

return obj, garbages
return obj
67 changes: 12 additions & 55 deletions scim2_tester/resource.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from scim2_models import Mutability
from scim2_models import ResourceType

from scim2_tester.filling import fill_with_random_values
from scim2_tester.resource_delete import check_object_deletion
from scim2_tester.resource_get import check_object_query
from scim2_tester.resource_get import check_object_query_without_id
Expand All @@ -17,6 +15,12 @@ def check_resource_type(
conf: CheckConfig,
resource_type: ResourceType,
) -> list[CheckResult]:
"""Orchestrate CRUD tests for a resource type.

:param conf: The check configuration containing the SCIM client
:param resource_type: The ResourceType object to test
:returns: A list of check results for all tested operations
"""
model = model_from_resource_type(conf, resource_type)
if not model:
return [
Expand All @@ -28,59 +32,12 @@ def check_resource_type(
]

results = []
garbages = []
created_obj = None

# Always try to create an object - the decorator will decide if it should be skipped
field_names = [
field_name
for field_name in model.model_fields.keys()
if model.get_field_annotation(field_name, Mutability)
in (Mutability.read_write, Mutability.write_only, Mutability.immutable)
]
obj, obj_garbages = fill_with_random_values(conf, model(), field_names)
garbages += obj_garbages

create_result = check_object_creation(conf, obj)

# Only add to results if creation was explicitly requested (not skipped)
if create_result.status != Status.SKIPPED:
results.append(create_result)

# If creation succeeded (either explicitly or as dependency), we have an object
if create_result.status == Status.SUCCESS:
created_obj = create_result.data

# Try read operations - decorator will skip if not needed
read_result = check_object_query(conf, created_obj)
if read_result.status != Status.SKIPPED:
results.append(read_result)

read_without_id_result = check_object_query_without_id(conf, created_obj)
if read_without_id_result.status != Status.SKIPPED:
results.append(read_without_id_result)

# Try update operations - decorator will skip if not needed
field_names = [
field_name
for field_name in model.model_fields.keys()
if model.get_field_annotation(field_name, Mutability)
in (Mutability.read_write, Mutability.write_only)
]
_, obj_garbages = fill_with_random_values(conf, created_obj, field_names)
garbages += obj_garbages

update_result = check_object_replacement(conf, created_obj)
if update_result.status != Status.SKIPPED:
results.append(update_result)

# Try delete operations - decorator will skip if not needed
delete_result = check_object_deletion(conf, created_obj)
if delete_result.status != Status.SKIPPED:
results.append(delete_result)

# Cleanup remaining garbage
for garbage in reversed(garbages):
conf.client.delete(garbage)
# Each test is now completely independent and handles its own cleanup
results.append(check_object_creation(conf, model))
results.append(check_object_query(conf, model))
results.append(check_object_query_without_id(conf, model))
results.append(check_object_replacement(conf, model))
results.append(check_object_deletion(conf, model))

return results
38 changes: 34 additions & 4 deletions scim2_tester/resource_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,48 @@

from scim2_tester.utils import CheckConfig
from scim2_tester.utils import CheckResult
from scim2_tester.utils import ResourceManager
from scim2_tester.utils import Status
from scim2_tester.utils import checker


@checker("crud:delete")
def check_object_deletion(conf: CheckConfig, obj: Resource) -> CheckResult:
"""Perform an object deletion."""
def check_object_deletion(
conf: CheckConfig, model: type[Resource], resources: ResourceManager
) -> CheckResult:
"""Test object deletion with automatic cleanup.

Creates a test object specifically for deletion testing, performs the
delete operation, and verifies the object no longer exists.

:param conf: The check configuration containing the SCIM client
:param model: The Resource model class to test
:param resources: Resource manager for automatic cleanup
:returns: The result of the check operation
"""
test_obj = resources.create_and_register(model)

# Remove from resource manager since we're testing deletion explicitly
if test_obj in resources.resources:
resources.resources.remove(test_obj)

conf.client.delete(
obj.__class__, obj.id, expected_status_codes=conf.expected_status_codes or [204]
model, test_obj.id, expected_status_codes=conf.expected_status_codes or [204]
)

try:
conf.client.query(model, test_obj.id)
return CheckResult(
conf,
status=Status.ERROR,
reason=f"{model.__name__} object with id {test_obj.id} still exists after deletion",
)
except Exception:
# Expected - object should not exist after deletion
pass

return CheckResult(
conf,
status=Status.SUCCESS,
reason=f"Successful deletion of a {obj.__class__.__name__} object with id {obj.id}",
reason=f"Successfully deleted {model.__name__} object with id {test_obj.id}",
)
50 changes: 33 additions & 17 deletions scim2_tester/resource_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from scim2_tester.utils import CheckConfig
from scim2_tester.utils import CheckResult
from scim2_tester.utils import ResourceManager
from scim2_tester.utils import Status
from scim2_tester.utils import checker

Expand All @@ -27,50 +28,65 @@ def model_from_resource_type(


@checker("crud:read")
def check_object_query(conf: CheckConfig, obj: Resource) -> CheckResult:
"""Perform an object query by knowing its id.
def check_object_query(
conf: CheckConfig, model: type[Resource], resources: ResourceManager
) -> CheckResult:
"""Test object query by ID with automatic cleanup.

Todo:
- check if the fields of the result object are the same than the
fields of the request object
Creates a temporary test object, queries it by ID to validate the
read operation.

:param conf: The check configuration containing the SCIM client
:param model: The Resource model class to test
:param resources: Resource manager for automatic cleanup
:returns: The result of the check operation
"""
test_obj = resources.create_and_register(model)

response = conf.client.query(
obj.__class__, obj.id, expected_status_codes=conf.expected_status_codes or [200]
model, test_obj.id, expected_status_codes=conf.expected_status_codes or [200]
)

return CheckResult(
conf,
status=Status.SUCCESS,
reason=f"Successful query of a {obj.__class__.__name__} object with id {response.id}",
reason=f"Successfully queried {model.__name__} object with id {test_obj.id}",
data=response,
)


@checker("crud:read")
def check_object_query_without_id(conf: CheckConfig, obj: Resource) -> CheckResult:
"""Perform the query of all objects of one kind.
def check_object_query_without_id(
conf: CheckConfig, model: type[Resource], resources: ResourceManager
) -> CheckResult:
"""Test object listing without ID with automatic cleanup.

Todo:
- look for the object across several pages
- check if the fields of the result object are the same than the
fields of the request object
Creates a temporary test object, performs a list/search operation to
validate bulk retrieval.

:param conf: The check configuration containing the SCIM client
:param model: The Resource model class to test
:param resources: Resource manager for automatic cleanup
:returns: The result of the check operation
"""
test_obj = resources.create_and_register(model)

response = conf.client.query(
obj.__class__, expected_status_codes=conf.expected_status_codes or [200]
model, expected_status_codes=conf.expected_status_codes or [200]
)
found = any(obj.id == resource.id for resource in response.resources)

found = any(test_obj.id == resource.id for resource in response.resources)
if not found:
return CheckResult(
conf,
status=Status.ERROR,
reason=f"Could not find object {obj.__class__.__name__} with id : {obj.id}",
reason=f"Could not find {model.__name__} object with id {test_obj.id} in list response",
data=response,
)

return CheckResult(
conf,
status=Status.SUCCESS,
reason=f"Successful query of a {obj.__class__.__name__} object with id {obj.id}",
reason=f"Successfully found {model.__name__} object with id {test_obj.id} in list response",
data=response,
)
24 changes: 14 additions & 10 deletions scim2_tester/resource_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@

from scim2_tester.utils import CheckConfig
from scim2_tester.utils import CheckResult
from scim2_tester.utils import ResourceManager
from scim2_tester.utils import Status
from scim2_tester.utils import checker


@checker("crud:create")
def check_object_creation(conf: CheckConfig, obj: Resource) -> CheckResult:
"""Perform an object creation.
def check_object_creation(
conf: CheckConfig, model: type[Resource], resources: ResourceManager
) -> CheckResult:
"""Test object creation with automatic cleanup.

Todo:
- check if the fields of the result object are the same than the
fields of the request object
Creates a test object of the specified model type, validates the creation
operation.

:param conf: The check configuration containing the SCIM client
:param model: The Resource model class to test
:param resources: Resource manager for automatic cleanup
:returns: The result of the check operation
"""
response = conf.client.create(
obj, expected_status_codes=conf.expected_status_codes or [201]
)
created_obj = resources.create_and_register(model)

return CheckResult(
conf,
status=Status.SUCCESS,
reason=f"Successful creation of a {obj.__class__.__name__} object with id {response.id}",
data=response,
reason=f"Successfully created {model.__name__} object with id {created_obj.id}",
data=created_obj,
)
Loading
Loading