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
15 changes: 4 additions & 11 deletions scim2_tester/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from scim2_tester.checkers import random_url
from scim2_tester.checkers import resource_type_tests
from scim2_tester.checkers import resource_types_endpoint
from scim2_tester.checkers import schemas_endpoint
from scim2_tester.checkers import service_provider_config_endpoint
from scim2_tester.checkers.resource_types import _resource_types_endpoint
from scim2_tester.checkers.schemas import _schemas_endpoint
from scim2_tester.utils import CheckConfig
from scim2_tester.utils import CheckContext
from scim2_tester.utils import CheckResult
Expand Down Expand Up @@ -74,25 +74,22 @@ def check_server(
context = CheckContext(client, conf)
results = []

# Get the initial basic objects
result_spc = service_provider_config_endpoint(context)
results.append(result_spc)
if result_spc.status != Status.SKIPPED and not client.service_provider_config:
client.service_provider_config = result_spc.data

results_resource_types = resource_types_endpoint(context)
results_resource_types = _resource_types_endpoint(context)
results.extend(results_resource_types)
if not client.resource_types:
# Find first non-skipped result with data
for rt_result in results_resource_types:
if rt_result.status != Status.SKIPPED and rt_result.data:
client.resource_types = rt_result.data
break

results_schemas = schemas_endpoint(context)
results_schemas = _schemas_endpoint(context)
results.extend(results_schemas)
if not client.resource_models:
# Find first non-skipped result with data
for schema_result in results_schemas:
if schema_result.status != Status.SKIPPED and schema_result.data:
client.resource_models = client.build_resource_models(
Expand All @@ -107,18 +104,14 @@ def check_server(
):
return results

# Miscelleaneous checks
result_random = random_url(context)
results.append(result_random)

# Resource checks
for resource_type in client.resource_types or []:
# Filter by resource type if specified
if conf.resource_types and resource_type.name not in conf.resource_types:
continue

resource_results = resource_type_tests(context, resource_type)
# Add resource type to each result for better tracking
for result in resource_results:
result.resource_type = resource_type.name
results.extend(resource_results)
Expand Down
21 changes: 12 additions & 9 deletions scim2_tester/checkers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,34 @@
from .resource_types import access_invalid_resource_type
from .resource_types import query_all_resource_types
from .resource_types import query_resource_type_by_id
from .resource_types import resource_types_endpoint
from .resource_types import resource_types_endpoint_methods
from .resource_types import resource_types_schema_validation
from .schemas import access_invalid_schema
from .schemas import access_schema_by_id
from .schemas import core_schemas_validation
from .schemas import query_all_schemas
from .schemas import query_schema_by_id
from .schemas import schemas_endpoint
from .schemas import schemas_endpoint_methods
from .service_provider_config import service_provider_config_endpoint
from .service_provider_config import service_provider_config_endpoint_methods

__all__ = [
# Discovery checkers
"service_provider_config_endpoint",
"resource_types_endpoint",
"service_provider_config_endpoint_methods",
"resource_types_endpoint_methods",
"resource_types_schema_validation",
"query_all_resource_types",
"query_resource_type_by_id",
"access_invalid_resource_type",
"schemas_endpoint",
"schemas_endpoint_methods",
"query_all_schemas",
"query_schema_by_id",
"access_schema_by_id",
"access_invalid_schema",
# CRUD checkers
"core_schemas_validation",
"object_creation",
"object_query",
"object_query_without_id",
"object_replacement",
"object_deletion",
"resource_type_tests",
# Miscellaneous checkers
"random_url",
]
61 changes: 61 additions & 0 deletions scim2_tester/checkers/_discovery_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Utility functions for discovery endpoint testing."""

from scim2_client import SCIMClientError

from ..utils import CheckContext
from ..utils import CheckResult
from ..utils import Status


def _test_discovery_endpoint_methods(
context: CheckContext, endpoint: str
) -> list[CheckResult]:
"""Test that unsupported HTTP methods return 405 Method Not Allowed.

Tests that POST, PUT, PATCH, and DELETE methods on the specified discovery
endpoint correctly return HTTP 405 Method Not Allowed status, as only GET is supported.

**Status:**

- :attr:`~scim2_tester.Status.SUCCESS`: All unsupported methods return 405 status
- :attr:`~scim2_tester.Status.ERROR`: One or more methods return unexpected status

.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`

Discovery endpoints only support GET method. Other HTTP methods
should return appropriate error responses.
"""
results = []
methods = ["POST", "PUT", "PATCH", "DELETE"]

for method in methods:
try:
response = context.client.client.request(
method=method,
url=endpoint,
)
if response.status_code == 405:
results.append(
CheckResult(
status=Status.SUCCESS,
reason=f"{method} {endpoint} correctly returned 405 Method Not Allowed",
data=response,
)
)
else:
results.append(
CheckResult(
status=Status.ERROR,
reason=f"{method} {endpoint} returned {response.status_code} instead of 405",
data=response,
)
)
except SCIMClientError as e:
results.append(
CheckResult(
status=Status.ERROR,
reason=f"{method} {endpoint} failed: {str(e)}",
)
)

return results
4 changes: 0 additions & 4 deletions scim2_tester/checkers/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ def resource_type_tests(

results = []

# Each test is now completely independent and handles its own cleanup
# These functions have @checker decorators so we call them with client, conf
# The decorator will create a context and call the function appropriately
# For now, call them directly - may need adjustment based on actual function signatures
results.append(object_creation(context, model))
results.append(object_query(context, model))
results.append(object_query_without_id(context, model))
Expand Down
2 changes: 0 additions & 2 deletions scim2_tester/checkers/resource_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def object_deletion(context: CheckContext, model: type[Resource[Any]]) -> CheckR
"""
test_obj = context.resource_manager.create_and_register(model)

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

Expand All @@ -48,7 +47,6 @@ def object_deletion(context: CheckContext, model: type[Resource[Any]]) -> CheckR
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(
Expand Down
88 changes: 81 additions & 7 deletions scim2_tester/checkers/resource_types.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import uuid

from scim2_client import SCIMClientError
from scim2_models import Error
from scim2_models import ResourceType
from scim2_models import Schema

from ..utils import CheckContext
from ..utils import CheckResult
from ..utils import Status
from ..utils import checker
from ._discovery_utils import _test_discovery_endpoint_methods


def resource_types_endpoint(context: CheckContext) -> list[CheckResult]:
def _resource_types_endpoint(context: CheckContext) -> list[CheckResult]:
"""Orchestrate validation of the ResourceTypes discovery endpoint.

Runs comprehensive tests on the ``/ResourceTypes`` endpoint including listing
Expand All @@ -28,12 +31,6 @@ def resource_types_endpoint(context: CheckContext) -> list[CheckResult]:

"Service providers MUST provide this endpoint."

.. todo::

- Check POST/PUT/PATCH/DELETE on the endpoint
- Check that query parameters are ignored
- Check that a 403 response is returned if a filter is passed
- Check that the `schema` attribute exists and is available.
"""
resource_types_result = query_all_resource_types(context)
results = [resource_types_result]
Expand All @@ -42,11 +39,88 @@ def resource_types_endpoint(context: CheckContext) -> list[CheckResult]:
for resource_type in resource_types_result.data:
results.append(query_resource_type_by_id(context, resource_type))

results.extend(resource_types_schema_validation(context))

results.append(access_invalid_resource_type(context))

return results


@checker("discovery", "resource-types")
def resource_types_endpoint_methods(
context: CheckContext,
) -> list[CheckResult]:
"""Validate that unsupported HTTP methods return 405 Method Not Allowed.

Tests that POST, PUT, PATCH, and DELETE methods on the ``/ResourceTypes``
endpoint correctly return HTTP 405 Method Not Allowed status, as only GET is supported.

**Status:**

- :attr:`~scim2_tester.Status.SUCCESS`: All unsupported methods return 405 status
- :attr:`~scim2_tester.Status.ERROR`: One or more methods return unexpected status

.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`

"An HTTP GET to this endpoint is used to discover the types of resources
available on a SCIM service provider."

Only GET method is specified, other methods should return appropriate errors.
"""
return _test_discovery_endpoint_methods(context, "/ResourceTypes")


@checker("discovery", "resource-types")
def resource_types_schema_validation(
context: CheckContext,
) -> list[CheckResult]:
"""Validate that ResourceType schemas exist and are accessible.

Tests that all :class:`~scim2_models.ResourceType` objects returned by the
``/ResourceTypes`` endpoint reference valid schemas that can be retrieved
from the ``/Schemas`` endpoint.

**Status:**

- :attr:`~scim2_tester.Status.SUCCESS`: All ResourceType schemas are accessible
- :attr:`~scim2_tester.Status.ERROR`: One or more ResourceType schemas are missing or inaccessible

.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`

"Each resource type defines the endpoint, the core schema URI that defines
the resource, and any supported schema extensions."
"""
response = context.client.query(
ResourceType, expected_status_codes=context.conf.expected_status_codes or [200]
)

results = []
for resource_type in response.resources:
schema_id = resource_type.schema_
try:
schema_response = context.client.query(
Schema,
schema_id,
expected_status_codes=context.conf.expected_status_codes or [200],
)
results.append(
CheckResult(
status=Status.SUCCESS,
reason=f"ResourceType '{resource_type.name}' schema '{schema_id}' is accessible",
data=schema_response,
)
)
except SCIMClientError as e:
results.append(
CheckResult(
status=Status.ERROR,
reason=f"ResourceType '{resource_type.name}' schema '{schema_id}' is not accessible: {str(e)}",
)
)

return results


@checker("discovery", "resource-types")
def query_all_resource_types(context: CheckContext) -> CheckResult:
"""Validate retrieval of all available resource types.
Expand Down
Loading
Loading