From 7596afd6ceb7177e081efb070dcde541ec938bd8 Mon Sep 17 00:00:00 2001 From: Jan Burgmeier Date: Wed, 11 Jun 2025 12:02:28 +0200 Subject: [PATCH 1/5] Use Union instead of tuple for creating a Resource with multiple extension Using a tuple results in a pydantic error: File "/usr/local/lib/python3.11/site-packages/pydantic/_internal/_generics.py", line 373, in map_generic_model_arguments raise TypeError(f'Too many arguments for {cls}; actual {len(args)}, expected {expected_len}') TypeError: Too many arguments for ; actual 3, expected 1 --- scim2_client/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scim2_client/client.py b/scim2_client/client.py index 41464ed..c0d8fb8 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -552,13 +552,13 @@ def build_resource_models( for schema, resource_type in resource_types_by_schema.items(): schema_obj = schema_objs_by_schema[schema] model = Resource.from_schema(schema_obj) - extensions = [] + extensions = () for ext_schema in resource_type.schema_extensions or []: schema_obj = schema_objs_by_schema[ext_schema.schema_] extension = Extension.from_schema(schema_obj) - extensions.append(extension) + extensions = extensions + (extension,) if extensions: - model = model[tuple(extensions)] + model = model[Union[extensions]] resource_models.append(model) return tuple(resource_models) From 69e4f024c76c02e3d4ba8636a4af3a1705b54f3b Mon Sep 17 00:00:00 2001 From: Jan Burgmeier Date: Mon, 30 Jun 2025 14:33:06 +0200 Subject: [PATCH 2/5] Add unit test for discovering resource types with multiple extensions --- scim2_client/client.py | 3 +- tests/test_discovery.py | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 tests/test_discovery.py diff --git a/scim2_client/client.py b/scim2_client/client.py index c0d8fb8..c9333f0 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -2,6 +2,7 @@ import sys from collections.abc import Collection from dataclasses import dataclass +from typing import Any from typing import Optional from typing import Union @@ -552,7 +553,7 @@ def build_resource_models( for schema, resource_type in resource_types_by_schema.items(): schema_obj = schema_objs_by_schema[schema] model = Resource.from_schema(schema_obj) - extensions = () + extensions: tuple[Any, ...] = () for ext_schema in resource_type.schema_extensions or []: schema_obj = schema_objs_by_schema[ext_schema.schema_] extension = Extension.from_schema(schema_obj) diff --git a/tests/test_discovery.py b/tests/test_discovery.py new file mode 100644 index 0000000..8d6221a --- /dev/null +++ b/tests/test_discovery.py @@ -0,0 +1,90 @@ +import threading +import wsgiref.simple_server +from typing import Annotated +from typing import Union + +import portpicker +import pytest +from httpx import Client +from scim2_models import EnterpriseUser +from scim2_models import Extension +from scim2_models import Group +from scim2_models import Meta +from scim2_models import Required +from scim2_models import ResourceType +from scim2_models import User + +from scim2_client.engines.httpx import SyncSCIMClient + +scim2_server = pytest.importorskip("scim2_server") +from scim2_server.backend import InMemoryBackend # noqa: E402 +from scim2_server.provider import SCIMProvider # noqa: E402 + + +class OtherExtension(Extension): + schemas: Annotated[list[str], Required.true] = [ + "urn:ietf:params:scim:schemas:extension:Other:1.0:User" + ] + + test: str | None = None + test2: list[str] | None = None + + +def get_schemas(): + schemas = [ + User.to_schema(), + Group.to_schema(), + OtherExtension.to_schema(), + EnterpriseUser.to_schema(), + ] + + # SCIMProvider register_schema requires meta object to be set + for schema in schemas: + schema.meta = Meta(resource_type="Schema") + + return schemas + + +def get_resource_types(): + resource_types = [ + ResourceType.from_resource(User[Union[EnterpriseUser, OtherExtension]]), + ResourceType.from_resource(Group), + ] + + # SCIMProvider register_resource_type requires meta object to be set + for resource_type in resource_types: + resource_type.meta = Meta(resource_type="ResourceType") + + return resource_types + + +@pytest.fixture(scope="session") +def server(): + backend = InMemoryBackend() + provider = SCIMProvider(backend) + for schema in get_schemas(): + provider.register_schema(schema) + for resource_type in get_resource_types(): + provider.register_resource_type(resource_type) + + host = "localhost" + port = portpicker.pick_unused_port() + httpd = wsgiref.simple_server.make_server(host, port, provider) + + server_thread = threading.Thread(target=httpd.serve_forever) + server_thread.start() + try: + yield host, port + finally: + httpd.shutdown() + server_thread.join() + + +def test_discovery_resource_types_multiple_extensions(server): + host, port = server + client = Client(base_url=f"http://{host}:{port}") + scim_client = SyncSCIMClient(client) + + scim_client.discover() + assert scim_client.get_resource_model("User") + assert scim_client.get_resource_model("Group") From c6009dac93df38617d7d7c6ed2e1ec63b8120c30 Mon Sep 17 00:00:00 2001 From: Jan Burgmeier Date: Thu, 3 Jul 2025 11:30:18 +0200 Subject: [PATCH 3/5] Fix check_resource_model if resource_models was created from schemas --- scim2_client/client.py | 13 +++++++++++-- tests/test_discovery.py | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scim2_client/client.py b/scim2_client/client.py index c9333f0..cc47158 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -183,9 +183,18 @@ def get_resource_model(self, name: str) -> Optional[type[Resource]]: def check_resource_model( self, resource_model: type[Resource], payload=None ) -> None: + # We need to check the actual schema names, comapring the class + # types does not work because if the resource_models are + # discovered. The classes might differ: + # vs + schema_to_check = resource_model.model_fields["schemas"].default[0] + for element in self.resource_models: + schema = element.model_fields["schemas"].default[0] + if schema_to_check == schema: + return + if ( - resource_model not in self.resource_models - and resource_model not in CONFIG_RESOURCES + resource_model not in CONFIG_RESOURCES ): raise SCIMRequestError( f"Unknown resource type: '{resource_model}'", source=payload diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 8d6221a..dca5b6f 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -88,3 +88,7 @@ def test_discovery_resource_types_multiple_extensions(server): scim_client.discover() assert scim_client.get_resource_model("User") assert scim_client.get_resource_model("Group") + + # Try to create a user to see if discover filled everything correctly + user_request = User[Union[EnterpriseUser, OtherExtension]](user_name="bjensen@example.com") + scim_client.create(user_request) From e599f5326e35ddc5365942478121623522eaa1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Fri, 1 Aug 2025 14:40:17 +0200 Subject: [PATCH 4/5] fix: pre-commit --- scim2_client/client.py | 9 +++------ tests/test_discovery.py | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scim2_client/client.py b/scim2_client/client.py index 5e527ae..3b5ff34 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -2,7 +2,6 @@ import sys from collections.abc import Collection from dataclasses import dataclass -from typing import Any from typing import Optional from typing import TypeVar from typing import Union @@ -206,7 +205,7 @@ def get_resource_model(self, name: str) -> Optional[type[Resource]]: def _check_resource_model( self, resource_model: type[Resource], payload=None ) -> None: - # We need to check the actual schema names, comapring the class + # We need to check the actual schema names, comparing the class # types does not work because if the resource_models are # discovered. The classes might differ: # vs @@ -216,9 +215,7 @@ def _check_resource_model( if schema_to_check == schema: return - if ( - resource_model not in CONFIG_RESOURCES - ): + if resource_model not in CONFIG_RESOURCES: raise SCIMRequestError( f"Unknown resource type: '{resource_model}'", source=payload ) @@ -650,7 +647,7 @@ def build_resource_models( for schema, resource_type in resource_types_by_schema.items(): schema_obj = schema_objs_by_schema[schema] model = Resource.from_schema(schema_obj) - extensions: tuple[Any, ...] = () + extensions: tuple[type[Extension], ...] = () for ext_schema in resource_type.schema_extensions or []: schema_obj = schema_objs_by_schema[ext_schema.schema_] extension = Extension.from_schema(schema_obj) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index dca5b6f..9006ac1 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -90,5 +90,7 @@ def test_discovery_resource_types_multiple_extensions(server): assert scim_client.get_resource_model("Group") # Try to create a user to see if discover filled everything correctly - user_request = User[Union[EnterpriseUser, OtherExtension]](user_name="bjensen@example.com") + user_request = User[Union[EnterpriseUser, OtherExtension]]( + user_name="bjensen@example.com" + ) scim_client.create(user_request) From d7a0de45cfcec108cb8b7e3891c93e00375ca60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Fri, 1 Aug 2025 15:02:36 +0200 Subject: [PATCH 5/5] fix: unit tests --- scim2_client/client.py | 4 ---- tests/test_delete.py | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scim2_client/client.py b/scim2_client/client.py index 3b5ff34..f4615f5 100644 --- a/scim2_client/client.py +++ b/scim2_client/client.py @@ -205,10 +205,6 @@ def get_resource_model(self, name: str) -> Optional[type[Resource]]: def _check_resource_model( self, resource_model: type[Resource], payload=None ) -> None: - # We need to check the actual schema names, comparing the class - # types does not work because if the resource_models are - # discovered. The classes might differ: - # vs schema_to_check = resource_model.model_fields["schemas"].default[0] for element in self.resource_models: schema = element.model_fields["schemas"].default[0] diff --git a/tests/test_delete.py b/tests/test_delete.py index 4d17a22..6d6d4e2 100644 --- a/tests/test_delete.py +++ b/tests/test_delete.py @@ -1,12 +1,16 @@ import pytest from scim2_models import Error -from scim2_models import Group +from scim2_models import Resource from scim2_models import User from scim2_client import RequestNetworkError from scim2_client import SCIMRequestError +class UnregisteredResource(Resource): + schemas: list[str] = ["urn:test:schemas:UnregisteredResource"] + + def test_delete_user(httpserver, sync_client): """Nominal case for a User deletion.""" httpserver.expect_request( @@ -45,7 +49,7 @@ def test_errors(httpserver, code, sync_client): def test_invalid_resource_model(httpserver, sync_client): """Test that resource_models passed to the method must be part of SCIMClient.resource_models.""" with pytest.raises(SCIMRequestError, match=r"Unknown resource type"): - sync_client.delete(Group(display_name="foobar"), id="foobar") + sync_client.delete(UnregisteredResource, id="foobar") def test_dont_check_response_payload(httpserver, sync_client):