Skip to content

Commit f1065e6

Browse files
committed
feat: implement check filtering
1 parent 94af23d commit f1065e6

16 files changed

+657
-65
lines changed

doc/tutorial.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,73 @@ and pass it to the :meth:`~scim2_tester.check_server` method:
3636
print(result.status.name, result.title)
3737
3838
39+
scim2-tester supports filtering tests using tags and resource types to run only specific subsets of checks.
40+
This is useful for targeted testing or when developing specific features.
41+
42+
Tag-based filtering
43+
~~~~~~~~~~~~~~~~~~~
44+
45+
Tests are organized using hierarchical tags that allow fine-grained control over which checks to execute. Use the :paramref:`~scim2_tester.check_server.include_tags` and :paramref:`~scim2_tester.check_server.exclude_tags` parameters:
46+
47+
.. code-block:: python
48+
49+
from scim2_tester import check_server
50+
51+
# Run only discovery tests
52+
results = check_server(client, include_tags={"discovery"})
53+
54+
# Run all CRUD operations except delete
55+
results = check_server(
56+
client,
57+
include_tags={"crud"},
58+
exclude_tags={"crud:delete"}
59+
)
60+
61+
# Run only resource creation tests
62+
results = check_server(client, include_tags={"crud:create"})
63+
64+
Available tags include:
65+
66+
- **discovery**: Configuration endpoint tests (:class:`~scim2_models.ServiceProviderConfig`, :class:`~scim2_models.ResourceType`, :class:`~scim2_models.Schema`)
67+
- **service-provider-config**: :class:`~scim2_models.ServiceProviderConfig` endpoint tests
68+
- **resource-types**: :class:`~scim2_models.ResourceType` endpoint tests
69+
- **schemas**: :class:`~scim2_models.Schema` endpoint tests
70+
- **crud**: All CRUD operation tests
71+
- **crud:create**: Resource creation tests
72+
- **crud:read**: Resource reading tests
73+
- **crud:update**: Resource update tests
74+
- **crud:delete**: Resource deletion tests
75+
- **misc**: Miscellaneous tests
76+
77+
The tag system is hierarchical, so ``crud`` will match ``crud:create``, ``crud:read``, etc.
78+
79+
Resource type filtering
80+
~~~~~~~~~~~~~~~~~~~~~~~
81+
82+
You can also filter tests by resource type using the :paramref:`~scim2_tester.check_server.resource_types` parameter:
83+
84+
.. code-block:: python
85+
86+
# Test only User resources
87+
results = check_server(client, resource_types=["User"])
88+
89+
# Test both User and Group resources
90+
results = check_server(client, resource_types=["User", "Group"])
91+
92+
Combining filters
93+
~~~~~~~~~~~~~~~~~
94+
95+
Filters can be combined for precise control using both :paramref:`~scim2_tester.check_server.include_tags` and :paramref:`~scim2_tester.check_server.resource_types` parameters:
96+
97+
.. code-block:: python
98+
99+
# Test only User creation and reading
100+
results = check_server(
101+
client,
102+
include_tags={"crud:create", "crud:read"},
103+
resource_types=["User"]
104+
)
105+
39106
Unit test suite integration
40107
===========================
41108

@@ -61,3 +128,29 @@ As :class:`~scim2_client.engines.werkzeug.TestSCIMClient` relies on :doc:`Werkze
61128
testclient = Client(app)
62129
client = TestSCIMClient(app=testclient, scim_prefix="/scim/v2")
63130
check_server(client, raise_exceptions=True)
131+
132+
Parametrized testing
133+
~~~~~~~~~~~~~~~~~~~~
134+
135+
For comprehensive test coverage, you can create parametrized tests that exercise different combinations of tags and resource types using :func:`~scim2_tester.discovery.get_all_available_tags` and :func:`~scim2_tester.discovery.get_standard_resource_types`:
136+
137+
.. code-block:: python
138+
139+
import pytest
140+
from scim2_tester import Status, check_server
141+
from scim2_tester.discovery import get_all_available_tags, get_standard_resource_types
142+
143+
@pytest.mark.parametrize("tag", get_all_available_tags())
144+
@pytest.mark.parametrize("resource_type", [None] + get_standard_resource_types())
145+
def test_individual_filters(scim_client, tag, resource_type):
146+
results = check_server(
147+
scim_client,
148+
raise_exceptions=False,
149+
include_tags={tag},
150+
resource_types=resource_type
151+
)
152+
153+
for result in results:
154+
assert result.status in (Status.SKIPPED, Status.SUCCESS)
155+
156+
This parametrized approach automatically discovers all available tags and resource types, ensuring that your test suite covers all possible combinations as your SCIM implementation evolves. Each test verifies that results have either :attr:`~scim2_tester.Status.SUCCESS` or :attr:`~scim2_tester.Status.SKIPPED` status.

scim2_tester/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
from .checker import check_server
2+
from .discovery import get_all_available_tags
3+
from .discovery import get_standard_resource_types
24
from .utils import CheckConfig
35
from .utils import CheckResult
46
from .utils import SCIMTesterError
57
from .utils import Status
68

7-
__all__ = ["check_server", "Status", "CheckResult", "CheckConfig", "SCIMTesterError"]
9+
__all__ = [
10+
"check_server",
11+
"Status",
12+
"CheckResult",
13+
"CheckConfig",
14+
"SCIMTesterError",
15+
"get_all_available_tags",
16+
"get_standard_resource_types",
17+
]

scim2_tester/checker.py

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from scim2_tester.utils import checker
1515

1616

17-
@checker
17+
@checker("misc")
1818
def check_random_url(conf: CheckConfig) -> CheckResult:
1919
"""Check that a request to a random URL returns a 404 Error object."""
2020
probably_invalid_url = f"/{str(uuid.uuid4())}"
@@ -44,7 +44,13 @@ def check_random_url(conf: CheckConfig) -> CheckResult:
4444
)
4545

4646

47-
def check_server(client: SCIMClient, raise_exceptions=False) -> list[CheckResult]:
47+
def check_server(
48+
client: SCIMClient,
49+
raise_exceptions=False,
50+
include_tags: set[str] | None = None,
51+
exclude_tags: set[str] | None = None,
52+
resource_types: list[str] | None = None,
53+
) -> list[CheckResult]:
4854
"""Perform a series of check to a SCIM server.
4955
5056
It starts by retrieving the standard :class:`~scim2_models.ServiceProviderConfig`,
@@ -56,27 +62,74 @@ def check_server(client: SCIMClient, raise_exceptions=False) -> list[CheckResult
5662
5763
:param client: A SCIM client that will perform the requests.
5864
:param raise_exceptions: Whether exceptions should be raised or stored in a :class:`~scim2_tester.CheckResult` object.
65+
:param include_tags: Execute only checks with at least one of these tags.
66+
:param exclude_tags: Skip checks with any of these tags.
67+
:param resource_types: Filter by resource type names (e.g., ["User", "Group"]).
68+
69+
Available tags:
70+
- **discovery**: Tests for configuration endpoints (ServiceProviderConfig, ResourceTypes, Schemas)
71+
- **service-provider-config**: Tests for ServiceProviderConfig endpoint
72+
- **resource-types**: Tests for ResourceTypes endpoint
73+
- **schemas**: Tests for Schemas endpoint
74+
- **crud**: All CRUD operation tests
75+
- **crud:create**: Resource creation tests
76+
- **crud:read**: Resource reading tests
77+
- **crud:update**: Resource update tests
78+
- **crud:delete**: Resource deletion tests
79+
- **misc**: Miscellaneous tests (e.g., random URL access)
80+
81+
Example usage::
82+
83+
# Run only discovery tests
84+
results = check_server(client, include_tags={"discovery"})
85+
86+
# Run CRUD tests except delete operations
87+
results = check_server(
88+
client, include_tags={"crud"}, exclude_tags={"crud:delete"}
89+
)
90+
91+
# Test only User resources
92+
results = check_server(client, resource_types=["User"])
93+
94+
# Test only User creation and reading
95+
results = check_server(
96+
client, include_tags={"crud:create", "crud:read"}, resource_types=["User"]
97+
)
5998
"""
60-
conf = CheckConfig(client, raise_exceptions)
99+
conf = CheckConfig(
100+
client=client,
101+
raise_exceptions=raise_exceptions,
102+
include_tags=include_tags,
103+
exclude_tags=exclude_tags,
104+
resource_types=resource_types,
105+
)
61106
results = []
62107

63108
# Get the initial basic objects
64109
result_spc = check_service_provider_config_endpoint(conf)
65110
results.append(result_spc)
66-
if not conf.client.service_provider_config:
111+
if result_spc.status != Status.SKIPPED and not conf.client.service_provider_config:
67112
conf.client.service_provider_config = result_spc.data
68113

69114
results_resource_types = check_resource_types_endpoint(conf)
70115
results.extend(results_resource_types)
71116
if not conf.client.resource_types:
72-
conf.client.resource_types = results_resource_types[0].data
117+
# Find first non-skipped result with data
118+
for rt_result in results_resource_types:
119+
if rt_result.status != Status.SKIPPED and rt_result.data:
120+
conf.client.resource_types = rt_result.data
121+
break
73122

74123
results_schemas = check_schemas_endpoint(conf)
75124
results.extend(results_schemas)
76125
if not conf.client.resource_models:
77-
conf.client.resource_models = conf.client.build_resource_models(
78-
conf.client.resource_types or [], results_schemas[0].data or []
79-
)
126+
# Find first non-skipped result with data
127+
for schema_result in results_schemas:
128+
if schema_result.status != Status.SKIPPED and schema_result.data:
129+
conf.client.resource_models = conf.client.build_resource_models(
130+
conf.client.resource_types or [], schema_result.data or []
131+
)
132+
break
80133

81134
if (
82135
not conf.client.service_provider_config
@@ -91,7 +144,15 @@ def check_server(client: SCIMClient, raise_exceptions=False) -> list[CheckResult
91144

92145
# Resource checks
93146
for resource_type in conf.client.resource_types or []:
94-
results.extend(check_resource_type(conf, resource_type))
147+
# Filter by resource type if specified
148+
if conf.resource_types and resource_type.name not in conf.resource_types:
149+
continue
150+
151+
resource_results = check_resource_type(conf, resource_type)
152+
# Add resource type to each result for better tracking
153+
for result in resource_results:
154+
result.resource_type = resource_type.name
155+
results.extend(resource_results)
95156

96157
return results
97158

@@ -100,10 +161,28 @@ def check_server(client: SCIMClient, raise_exceptions=False) -> list[CheckResult
100161
from httpx import Client
101162
from scim2_client.engines.httpx import SyncSCIMClient
102163

103-
parser = argparse.ArgumentParser(description="Process some integers.")
164+
parser = argparse.ArgumentParser(description="SCIM server compliance checker.")
104165
parser.add_argument("host")
105166
parser.add_argument("--token", required=False)
106167
parser.add_argument("--verbose", required=False, action="store_true")
168+
parser.add_argument(
169+
"--include-tags",
170+
nargs="+",
171+
help="Run only checks with these tags",
172+
required=False,
173+
)
174+
parser.add_argument(
175+
"--exclude-tags",
176+
nargs="+",
177+
help="Skip checks with these tags",
178+
required=False,
179+
)
180+
parser.add_argument(
181+
"--resource-types",
182+
nargs="+",
183+
help="Filter by resource type names",
184+
required=False,
185+
)
107186
args = parser.parse_args()
108187

109188
client = Client(
@@ -112,9 +191,24 @@ def check_server(client: SCIMClient, raise_exceptions=False) -> list[CheckResult
112191
)
113192
scim = SyncSCIMClient(client)
114193
scim.discover()
115-
results = check_server(scim)
194+
195+
include_tags: set[str] | None = (
196+
set(args.include_tags) if args.include_tags else None
197+
)
198+
exclude_tags: set[str] | None = (
199+
set(args.exclude_tags) if args.exclude_tags else None
200+
)
201+
202+
results = check_server(
203+
scim,
204+
include_tags=include_tags,
205+
exclude_tags=exclude_tags,
206+
resource_types=args.resource_types,
207+
)
208+
116209
for result in results:
117-
print(result.status.name, result.title)
210+
resource_info = f" [{result.resource_type}]" if result.resource_type else ""
211+
print(f"{result.status.name} {result.title}{resource_info}")
118212
if result.reason:
119213
print(" ", result.reason)
120214
if args.verbose and result.data:

scim2_tester/discovery.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Utility functions for discovering available tags and resources."""
2+
3+
from scim2_tester.utils import get_registered_tags
4+
5+
6+
def get_all_available_tags() -> set[str]:
7+
"""Get all available tags from the global registry.
8+
9+
This function returns tags that have been registered by checker decorators
10+
throughout the codebase. The registration happens automatically when
11+
modules containing @checker decorators are imported.
12+
13+
:returns: Set of all unique tags found in the codebase.
14+
:rtype: set[str]
15+
"""
16+
# Import all scim2_tester modules to ensure decorators are executed
17+
_ensure_modules_imported()
18+
19+
# Get registered tags from the global registry
20+
registered_tags = get_registered_tags()
21+
22+
return registered_tags
23+
24+
25+
def _ensure_modules_imported() -> None:
26+
"""Ensure all scim2_tester modules are imported to register their tags."""
27+
# Import key modules that contain checker decorators
28+
try:
29+
import scim2_tester.checker # noqa: F401
30+
import scim2_tester.resource_delete # noqa: F401
31+
import scim2_tester.resource_get # noqa: F401
32+
import scim2_tester.resource_post # noqa: F401
33+
import scim2_tester.resource_put # noqa: F401
34+
import scim2_tester.resource_types # noqa: F401
35+
import scim2_tester.schemas # noqa: F401
36+
import scim2_tester.service_provider_config # noqa: F401
37+
except ImportError:
38+
pass # In case some modules don't exist or have import issues
39+
40+
41+
def get_standard_resource_types() -> list[str]:
42+
"""Get standard SCIM resource types.
43+
44+
:returns: List of standard resource type names.
45+
:rtype: list[str]
46+
"""
47+
return ["User", "Group"]

scim2_tester/filling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def create_minimal_object(
3636

3737

3838
def model_from_ref_type(
39-
conf: CheckConfig, ref_type: type, different_than: Resource
39+
conf: CheckConfig, ref_type: type, different_than: type[Resource]
4040
) -> type[Resource]:
4141
"""Return "User" from "Union[Literal['User'], Literal['Group']]"."""
4242

0 commit comments

Comments
 (0)