From 1cdd9465ab58f5605c37456e49b0f81641722822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Sun, 20 Sep 2026 20:46:37 +0200 Subject: [PATCH 1/4] fix: apply the test command response check options --- doc/changelog.rst | 8 ++++++++ scim2_cli/test.py | 4 ++-- tests/test_test.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 077e981..ece203d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,14 @@ Changelog ========= +[Unreleased] +------------ + +Fixed +^^^^^ +- The :ref:`test` ``--dont-check-status-code`` and ``--dont-check-content-type`` options + were not applied on the client. + [0.2.4] - 2026-01-25 -------------------- diff --git a/scim2_cli/test.py b/scim2_cli/test.py index ca76f1c..23dc944 100644 --- a/scim2_cli/test.py +++ b/scim2_cli/test.py @@ -32,8 +32,8 @@ def test_cli(ctx, verbose, check_status_code, check_content_type): test """ client = ctx.obj["client"] - client.check_status_code = check_status_code - client.check_content_type = check_content_type + client.check_response_status_codes = check_status_code + client.check_response_content_type = check_content_type results = check_server(client) click.echo(f"Performing a SCIM compliance check on {client.client.base_url} ...") success = True diff --git a/tests/test_test.py b/tests/test_test.py index 201d6cd..4bdf2b3 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -1,5 +1,6 @@ from unittest.mock import patch +import pytest from scim2_tester import CheckResult from scim2_tester import Status @@ -81,3 +82,30 @@ def test_failure(runner, httpserver): ["--url", "http://scim.invalid", "test"], ) assert result.exit_code == 1, result.output + + +@pytest.mark.parametrize( + ("options", "expected"), + [ + ([], True), + (["--dont-check-status-code", "--dont-check-content-type"], False), + ], +) +def test_response_checks_options(runner, httpserver, options, expected): + """Test that the response check options are applied on the client.""" + checked = {} + + def check_server(client): + checked["status_codes"] = client.check_response_status_codes + checked["content_type"] = client.check_response_content_type + return [] + + with patch("scim2_cli.test.check_server", check_server): + result = runner.invoke( + cli, + ["--url", httpserver.url_for("/"), "test", *options], + catch_exceptions=False, + ) + + assert result.exit_code == 0, result.output + assert checked == {"status_codes": expected, "content_type": expected} From eb0d25aa5f06bf6503a0c6774137c76efd3bf5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Sun, 20 Sep 2026 20:46:57 +0200 Subject: [PATCH 2/4] chore: bump to scim2-tester 0.3.0 --- doc/changelog.rst | 9 ++ pyproject.toml | 4 +- scim2_cli/__init__.py | 8 +- scim2_cli/create.py | 4 +- scim2_cli/delete.py | 4 +- scim2_cli/query.py | 6 +- scim2_cli/replace.py | 4 +- scim2_cli/search.py | 4 +- scim2_cli/utils.py | 7 ++ tests/test_cli.py | 19 ++++ tests/test_create.py | 19 ++++ uv.lock | 231 ++++++++++++++++++++++-------------------- 12 files changed, 194 insertions(+), 125 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index ece203d..001d915 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -4,8 +4,17 @@ Changelog [Unreleased] ------------ +Changed +^^^^^^^ +- scim2-client 0.8.0 and scim2-tester 0.3.0 are now the minimum supported versions. +- Requests are performed with `httpx2 `_ instead of + httpx, following the scim2-client 0.8 engine rename. + Fixed ^^^^^ +- Server SCIM errors and invalid request payloads are reported as readable messages + instead of a traceback. scim2-client 0.8 raises the scim2-models exceptions for those, + which do not belong to its own exception hierarchy. - The :ref:`test` ``--dont-check-status-code`` and ``--dont-check-content-type`` options were not applied on the client. diff --git a/pyproject.toml b/pyproject.toml index a7715fc..59e8d74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,8 @@ classifiers = [ requires-python = ">= 3.10" dependencies = [ "click>=8.1.7", - "scim2-client>=0.7.1", - "scim2-tester[httpx]>=0.2.5", + "scim2-client[httpx2]>=0.8.0", + "scim2-tester>=0.3.0", "sphinx-click-rst-to-ansi-formatter>=0.1.0", "pydanclick>=0.4.0", "pygments>=2.18.0", diff --git a/scim2_cli/__init__.py b/scim2_cli/__init__.py index a921120..01095fb 100644 --- a/scim2_cli/__init__.py +++ b/scim2_cli/__init__.py @@ -2,9 +2,8 @@ import re import click -from httpx import Client -from scim2_client import SCIMClientError -from scim2_client.engines.httpx import SyncSCIMClient +from httpx2 import Client +from scim2_client.engines.httpx2 import SyncSCIMClient from scim2_models import Group from scim2_models import ListResponse from scim2_models import Resource @@ -21,6 +20,7 @@ from scim2_cli.search import search_cli from scim2_cli.test import test_cli from scim2_cli.utils import DOC_URL +from scim2_cli.utils import SCIM_EXCEPTIONS from scim2_cli.utils import HeaderType from scim2_cli.utils import exception_to_click_error from scim2_cli.utils import split_headers @@ -138,7 +138,7 @@ def cli( resource_types=not bool(resource_types), service_provider_config=not bool(service_provider_config), ) - except SCIMClientError as exc: + except SCIM_EXCEPTIONS as exc: raise exception_to_click_error(exc) from exc ctx.obj["client"] = scim_client diff --git a/scim2_cli/create.py b/scim2_cli/create.py index fe275a9..85db058 100644 --- a/scim2_cli/create.py +++ b/scim2_cli/create.py @@ -1,11 +1,11 @@ import click from click import ClickException from pydanclick import from_pydantic -from scim2_client import SCIMClientError from scim2_models import Context from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter from scim2_cli.utils import DOC_URL +from scim2_cli.utils import SCIM_EXCEPTIONS from scim2_cli.utils import ModelCommand from scim2_cli.utils import exception_to_click_error from scim2_cli.utils import formatted_payload @@ -16,7 +16,7 @@ def create_payload(client, payload, indent): try: response = client.create(payload, raise_scim_errors=False) - except SCIMClientError as scim_exc: + except SCIM_EXCEPTIONS as scim_exc: raise exception_to_click_error(scim_exc) from scim_exc payload = formatted_payload(response.model_dump(), indent) diff --git a/scim2_cli/delete.py b/scim2_cli/delete.py index b496ec6..74133d6 100644 --- a/scim2_cli/delete.py +++ b/scim2_cli/delete.py @@ -1,6 +1,5 @@ import click from click import ClickException -from scim2_client import SCIMClientError from scim2_models import Message from scim2_models import Resource from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter @@ -8,6 +7,7 @@ from scim2_cli.utils import exception_to_click_error from .utils import DOC_URL +from .utils import SCIM_EXCEPTIONS from .utils import formatted_payload @@ -39,7 +39,7 @@ def delete_cli(ctx, resource_type, id, indent): try: response = ctx.obj["client"].delete(resource_model, id, raise_scim_errors=False) - except SCIMClientError as scim_exc: + except SCIM_EXCEPTIONS as scim_exc: raise exception_to_click_error(scim_exc) from scim_exc if response: diff --git a/scim2_cli/query.py b/scim2_cli/query.py index ca74036..6ebfde2 100644 --- a/scim2_cli/query.py +++ b/scim2_cli/query.py @@ -1,6 +1,5 @@ import click from click import ClickException -from scim2_client import SCIMClientError from scim2_models import ResourceType from scim2_models import Schema from scim2_models import SearchRequest @@ -10,6 +9,7 @@ from scim2_cli.utils import exception_to_click_error from .utils import DOC_URL +from .utils import SCIM_EXCEPTIONS from .utils import formatted_payload @@ -112,12 +112,12 @@ def query_cli( response = ctx.obj["client"].query( resource_type, id, - search_request=payload, + query_parameters=payload, check_request_payload=check_request_payload, raise_scim_errors=False, ) - except SCIMClientError as scim_exc: + except SCIM_EXCEPTIONS as scim_exc: raise exception_to_click_error(scim_exc) from scim_exc payload = formatted_payload(response.model_dump(), indent) diff --git a/scim2_cli/replace.py b/scim2_cli/replace.py index 291fbf6..343cc9a 100644 --- a/scim2_cli/replace.py +++ b/scim2_cli/replace.py @@ -1,13 +1,13 @@ import click from click import ClickException from pydanclick import from_pydantic -from scim2_client import SCIMClientError from scim2_models import Context from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter from scim2_cli.utils import exception_to_click_error from .utils import DOC_URL +from .utils import SCIM_EXCEPTIONS from .utils import ModelCommand from .utils import formatted_payload from .utils import unacceptable_fields @@ -17,7 +17,7 @@ def replace_payload(client, payload, indent): try: response = client.replace(payload, raise_scim_errors=False) - except SCIMClientError as scim_exc: + except SCIM_EXCEPTIONS as scim_exc: raise exception_to_click_error(scim_exc) from scim_exc payload = formatted_payload(response.model_dump(), indent) diff --git a/scim2_cli/search.py b/scim2_cli/search.py index 4d4e296..0a491d1 100644 --- a/scim2_cli/search.py +++ b/scim2_cli/search.py @@ -1,11 +1,11 @@ import click -from scim2_client import SCIMClientError from scim2_models import SearchRequest from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter from scim2_cli.utils import exception_to_click_error from .utils import DOC_URL +from .utils import SCIM_EXCEPTIONS from .utils import formatted_payload @@ -91,7 +91,7 @@ def search_cli( raise_scim_errors=False, ) - except SCIMClientError as scim_exc: + except SCIM_EXCEPTIONS as scim_exc: raise exception_to_click_error(scim_exc) from scim_exc payload = formatted_payload(response.model_dump(), indent) diff --git a/scim2_cli/utils.py b/scim2_cli/utils.py index c8231c9..6b95877 100644 --- a/scim2_cli/utils.py +++ b/scim2_cli/utils.py @@ -3,11 +3,18 @@ from enum import Enum import click +from scim2_client import SCIMClientException +from scim2_models import SCIMException from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter DOC_URL = "https://scim2-cli.readthedocs.io/" INDENTATION_SIZE = 4 +# scim2-client raises its own exceptions for transport and response issues, and +# scim2-models SCIM exceptions for invalid request payloads and server errors. +# The two hierarchies are unrelated, so both must be caught. +SCIM_EXCEPTIONS = (SCIMClientException, SCIMException) + class HeaderType(click.ParamType): envvar_list_splitter = ";" diff --git a/tests/test_cli.py b/tests/test_cli.py index 3d5f93f..47e2611 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,6 +5,7 @@ from scim2_models import AuthenticationScheme from scim2_models import Bulk from scim2_models import ChangePassword +from scim2_models import Error from scim2_models import ETag from scim2_models import Filter from scim2_models import ListResponse @@ -325,3 +326,21 @@ def test_custom_configuration_by_env( del os.environ["SCIM_CLI_SERVICE_PROVIDER_CONFIG"] del os.environ["SCIM_CLI_SCHEMAS"] del os.environ["SCIM_CLI_RESOURCE_TYPES"] + + +def test_discovery_scim_error(runner, httpserver): + """Test that the discovery step displays a readable error when the server answers a SCIM error.""" + httpserver.clear_all_handlers() + httpserver.expect_request("/ResourceTypes").respond_with_json( + Error(status=403, detail="Insufficient permissions").model_dump(), + status=403, + content_type="application/scim+json", + ) + + result = runner.invoke( + cli, + ["--url", httpserver.url_for("/"), "query"], + catch_exceptions=False, + ) + assert result.exit_code == 1, result.output + assert "Error: Insufficient permissions" in result.output diff --git a/tests/test_create.py b/tests/test_create.py index b1d0b47..44e02cc 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -219,3 +219,22 @@ def test_command_validation_error(runner, httpserver, simple_user_payload): ) assert result.exit_code == 1, result.output assert "Expected type User but got undefined object with no schema" in result.output + + +def test_no_command_unknown_schema(runner, httpserver): + """Test that a payload with an unknown schema displays a readable error.""" + payload = { + "schemas": [ + "urn:ietf:params:scim:schemas:core:2.0:Unknown", + ], + "userName": "new-user@example.com", + } + + result = runner.invoke( + cli, + ["--url", httpserver.url_for("/"), "create"], + input=json.dumps(payload), + catch_exceptions=False, + ) + assert result.exit_code == 1, result.output + assert "Cannot guess resource type from the payload" in result.output diff --git a/uv.lock b/uv.lock index 5f2cc4f..88a51f9 100644 --- a/uv.lock +++ b/uv.lock @@ -511,7 +511,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -537,31 +537,42 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8c/e925b1c92018abb3a1863ce1549d76d2381e334d21d65d4ac8f65dabd78a/httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db", size = 67740, upload-time = "2026-09-14T14:18:04.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/117a771a2bb91df334b66bf4da14cd02f21aefbcfe53180f336ce55e8f90/httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e", size = 83162, upload-time = "2026-09-14T14:18:02.529Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a0/e9deef4654132857b5a5dbe4eddd0ac59c2814500e11f2f5044cd81103ee/httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44", size = 100290, upload-time = "2026-09-14T14:18:05.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/a0c72b0e006df654709fbc366cc5bcb53e5aee13e1e3395152c6dd293376/httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a", size = 95565, upload-time = "2026-09-14T14:18:03.553Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] @@ -753,7 +764,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.11'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ @@ -770,7 +781,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.11'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -968,12 +979,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "docutils", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, - { name = "pyyaml", marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ @@ -990,12 +1001,12 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, - { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" } }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } @@ -1476,8 +1487,8 @@ dependencies = [ { name = "click" }, { name = "pydanclick" }, { name = "pygments" }, - { name = "scim2-client" }, - { name = "scim2-tester", extra = ["httpx"] }, + { name = "scim2-client", extra = ["httpx2"] }, + { name = "scim2-tester" }, { name = "sphinx-click-rst-to-ansi-formatter" }, ] @@ -1514,8 +1525,8 @@ requires-dist = [ { name = "click", specifier = ">=8.1.7" }, { name = "pydanclick", specifier = ">=0.4.0" }, { name = "pygments", specifier = ">=2.18.0" }, - { name = "scim2-client", specifier = ">=0.7.1" }, - { name = "scim2-tester", extras = ["httpx"], specifier = ">=0.2.5" }, + { name = "scim2-client", extras = ["httpx2"], specifier = ">=0.8.0" }, + { name = "scim2-tester", specifier = ">=0.3.0" }, { name = "sphinx-click-rst-to-ansi-formatter", specifier = ">=0.1.0" }, ] @@ -1542,49 +1553,44 @@ doc = [ [[package]] name = "scim2-client" -version = "0.7.5" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "scim2-models" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/af/55a8d97acee4f3488a0628f5a4538a2e7ccb40e33acdbca84a92ed14ab11/scim2_client-0.7.5.tar.gz", hash = "sha256:6667f4b39c5cd8fcd4caf59e8bad136bb6f92ba1f30ece04948f36728b291e20", size = 18526, upload-time = "2026-04-02T12:05:30.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/a6/f31df435773d1399cdeabd9a6a697d69c0c83917746a7ccd8aff70dd9e49/scim2_client-0.8.0.tar.gz", hash = "sha256:b807b51f38db0f519b74873024c7efbbebcbe79fc3c688cc2a8ef82a2a497c28", size = 21796, upload-time = "2026-09-20T15:29:31.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/05/ac0ebd3aef5cbc889f0de9c506b92add9f835d9821da4cd324d5f7930f0e/scim2_client-0.7.5-py3-none-any.whl", hash = "sha256:bb2d3437027e6b13f2d620038a288c6b718284d3a7249f121c1ce43f5cf7fe1e", size = 20518, upload-time = "2026-04-02T12:05:29.055Z" }, + { url = "https://files.pythonhosted.org/packages/86/ed/28235414f6fd5c5dbd336937f93182ebcf08e63806f2f5cc7dde58cd0def/scim2_client-0.8.0-py3-none-any.whl", hash = "sha256:25934fe9a3f08bde13749ddc87da1405a40390c92262021df4b4b8c4eda7662c", size = 23456, upload-time = "2026-09-20T15:29:30.78Z" }, ] [package.optional-dependencies] -httpx = [ - { name = "httpx" }, +httpx2 = [ + { name = "httpx2" }, ] [[package]] name = "scim2-models" -version = "0.6.12" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", extra = ["email"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/67/121a60b8433839fb19a9986e8d9f5416941e42ea6827bd7e520783477cca/scim2_models-0.6.12.tar.gz", hash = "sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b", size = 48631, upload-time = "2026-04-13T12:37:11.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/3d/b967a207f4182b6985f2128304cd69016c5ec5c29dc0bec2212848112331/scim2_models-0.7.0.tar.gz", hash = "sha256:ac5e59944a87c0862464d160fc4426cf59a99740bdd14ddf459d621ee4fa7aac", size = 49250, upload-time = "2026-09-05T10:35:43.35Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/b0/04d4b4e10ce59e813a3000d3ccb97537e88ab0341aacebc51abee50ed637/scim2_models-0.6.12-py3-none-any.whl", hash = "sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a", size = 61012, upload-time = "2026-04-13T12:37:10.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/09f7b8e5fc32c62af5434d88f47106205a1deb81952c14438cbd3d287065/scim2_models-0.7.0-py3-none-any.whl", hash = "sha256:4212d8b74fc744700c23d4f5c87866b984449201873f2b6f7bb44c6c5b6f8088", size = 62805, upload-time = "2026-09-05T10:35:41.917Z" }, ] [[package]] name = "scim2-tester" -version = "0.2.8" +version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "scim2-client" }, { name = "scim2-models" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/21/46a05e13d04f48bd274217019f6a57286f0849d40134bd27fd4ee7301a9b/scim2_tester-0.2.8.tar.gz", hash = "sha256:d8b8f3d18d452cafbe3eb966c7013579c78c75794eba31ca161e68e6f6544051", size = 25848, upload-time = "2026-04-02T11:55:28.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/7f/736a93c80c06572a3381cf13ba3a5e1e8c92d0b1b8473217d07a5d6d4a53/scim2_tester-0.3.0.tar.gz", hash = "sha256:6b22b682809f9e08b4cef4349a2c7b53f86c95e4c2fbfbd5d70c99f11c19aa7c", size = 26498, upload-time = "2026-09-20T17:55:16.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/7d/6d4fcb04e7875880466851056b906e98c317b3b079fd3c00498c3f458a62/scim2_tester-0.2.8-py3-none-any.whl", hash = "sha256:fbd2cce113cd9c0c14caf2660dc252d3b360a5461b931aa88f96ac7f0bd74ae6", size = 38460, upload-time = "2026-04-02T11:55:26.773Z" }, -] - -[package.optional-dependencies] -httpx = [ - { name = "scim2-client", extra = ["httpx"] }, + { url = "https://files.pythonhosted.org/packages/3e/61/acc6e5f793ef12d68ff05f9b207de7e5315c64c1d382e12790bb6c537785/scim2_tester-0.3.0-py3-none-any.whl", hash = "sha256:80831dc64b13e829b32ae926c086859a9b1e11f9fa838b79b46f174d6c53bffb", size = 38842, upload-time = "2026-09-20T17:55:14.751Z" }, ] [[package]] @@ -1628,23 +1634,23 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -1659,23 +1665,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version == '3.11.*'" }, - { name = "babel", marker = "python_full_version == '3.11.*'" }, - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version == '3.11.*'" }, - { name = "imagesize", marker = "python_full_version == '3.11.*'" }, - { name = "jinja2", marker = "python_full_version == '3.11.*'" }, - { name = "packaging", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "requests", marker = "python_full_version == '3.11.*'" }, - { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -1691,23 +1697,23 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.15'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -1752,7 +1758,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } wheels = [ @@ -1769,7 +1775,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } @@ -1813,10 +1819,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "beartype", marker = "python_full_version < '3.11'" }, - { name = "docutils", marker = "python_full_version < '3.11'" }, - { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "beartype" }, + { name = "docutils" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/3e/a82aa5fed0d06161a89dc2f6971b160f837cad44f196c467fc6b2132acaa/sphinx_substitution_extensions-2026.1.12.tar.gz", hash = "sha256:25e0c6c40fbf9e1df593883da946879044a3bf8d85652c8c58f354a53575d736", size = 31676, upload-time = "2026-01-12T06:19:35.324Z" } wheels = [ @@ -1833,10 +1839,10 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "beartype", marker = "python_full_version >= '3.11'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "beartype" }, + { name = "docutils" }, + { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" } }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/bc/50d6cc6ddb2249843edaa15857f4292c486304d6ea957d752a5a0118f19a/sphinx_substitution_extensions-2026.8.13.1.tar.gz", hash = "sha256:a4c64ed45614af028b6e397f001852e3968d8d06db2e02f79cceec8de53623a8", size = 44974, upload-time = "2026-08-13T10:01:38.054Z" } @@ -2010,6 +2016,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/0a/6dc462e4fb543305283a6157c80f43e3d12ca4702da6ae6521d541c6b55c/tox_uv_bare-1.36.0-py3-none-any.whl", hash = "sha256:ba397dd0396df95a75744d4e42a50ee27207c0ffcf277b62ffba9c3de455a939", size = 22489, upload-time = "2026-07-21T13:09:55.389Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" From 07d62f565aee1654680ec5eefe0d1e17a87d0379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Sun, 20 Sep 2026 20:47:24 +0200 Subject: [PATCH 3/4] fix: restrict query parameters on single resources --- doc/changelog.rst | 4 ++ scim2_cli/query.py | 29 +++++++++++++++ tests/test_query.py | 90 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 001d915..8d8eff0 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,6 +9,10 @@ Changed - scim2-client 0.8.0 and scim2-tester 0.3.0 are now the minimum supported versions. - Requests are performed with `httpx2 `_ instead of httpx, following the scim2-client 0.8 engine rename. +- :ref:`query` only sends the ``attributes`` and ``excludedAttributes`` parameters when a + single resource is queried, as :rfc:`RFC7644 §3.4.1 <7644#section-3.4.1>` defines those + as the sole parameters of that request. ``--start-index``, ``--count``, ``--filter``, + ``--sort-by`` and ``--sort-order`` are refused in that case, instead of being sent along. Fixed ^^^^^ diff --git a/scim2_cli/query.py b/scim2_cli/query.py index 6ebfde2..c824f2d 100644 --- a/scim2_cli/query.py +++ b/scim2_cli/query.py @@ -1,6 +1,7 @@ import click from click import ClickException from scim2_models import ResourceType +from scim2_models import ResponseParameters from scim2_models import Schema from scim2_models import SearchRequest from scim2_models import ServiceProviderConfig @@ -73,6 +74,9 @@ def query_cli( - If :code:`RESOURCE_TYPE` is :code:`user` and :code:`id` is not set, then the request will made on the :code:`/Users` endpoint. - If :code:`RESOURCE_TYPE` is not set, then the request will made on the :code:`/` endpoint. + When a single resource is queried, only :code:`--attribute` and :code:`--excluded-attribute` are + available, as defined in `RFC7644 §3.4.1 `_. + Data passed in JSON format to stdin is sent as request arguments and all the other query arguments are ignored: .. code-block:: bash @@ -92,10 +96,35 @@ def query_cli( f"Unknown resource type '{resource_type}. Available values are: {ok_values}'" ) from exc + # ServiceProviderConfig is a singleton endpoint, so it is reached without an id. + single_resource = bool(id) or resource_type is ServiceProviderConfig + listing_options = [ + name + for name, value in ( + ("--start-index", start_index), + ("--count", count), + ("--filter", filter), + ("--sort-by", sort_by), + ("--sort-order", sort_order), + ) + if value is not None + ] + if single_resource and listing_options: + raise ClickException( + f"{', '.join(listing_options)} cannot be used when querying a single resource." + ) + if ctx.obj.get("stdin"): check_request_payload = False payload = ctx.obj.get("stdin") + elif single_resource: + check_request_payload = True + payload = ResponseParameters( + attributes=attribute, + excluded_attributes=excluded_attribute, + ) + else: check_request_payload = True payload = SearchRequest( diff --git a/tests/test_query.py b/tests/test_query.py index 53b4782..c88c6b9 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -24,6 +24,7 @@ def httpserver(httpserver, simple_user_payload): httpserver.expect_request( "/Users", + query_string="attributes=userName&attributes=displayName&filter=userName+Eq+%22john%22&sortBy=userName&sortOrder=ascending&startIndex=1&count=10", method="GET", ).respond_with_json( { @@ -31,37 +32,42 @@ def httpserver(httpserver, simple_user_payload): "itemsPerPage": 10, "startIndex": 1, "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], - "Resources": [simple_user_payload("all-users")], + "Resources": [simple_user_payload("full-qs")], }, status=200, content_type="application/scim+json", ) httpserver.expect_request( - "/Users/one-by-id", + "/Users", method="GET", ).respond_with_json( - simple_user_payload("one-by-id"), + { + "totalResults": 1, + "itemsPerPage": 10, + "startIndex": 1, + "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + "Resources": [simple_user_payload("all-users")], + }, status=200, content_type="application/scim+json", ) httpserver.expect_request( - "/Users/user-name-qs", - query_string="attributes=userName", + "/Users/one-by-id", method="GET", ).respond_with_json( - simple_user_payload("user-name-qs"), + simple_user_payload("one-by-id"), status=200, content_type="application/scim+json", ) httpserver.expect_request( - "/Users/full-qs", - query_string="attributes=userName&attributes=displayName&filter=userName+Eq+%22john%22&sortBy=userName&sortOrder=ascending&startIndex=1&count=10", + "/Users/user-name-qs", + query_string="attributes=userName", method="GET", ).respond_with_json( - simple_user_payload("full-qs"), + simple_user_payload("user-name-qs"), status=200, content_type="application/scim+json", ) @@ -185,7 +191,7 @@ def test_stdin(runner, httpserver, simple_user_payload): def test_search_request_payload(runner, httpserver, simple_user_payload): - """Test that most of the arguments are passed in the payload.""" + """Test that most of the arguments are passed in the payload when listing resources.""" result = runner.invoke( cli, [ @@ -193,7 +199,6 @@ def test_search_request_payload(runner, httpserver, simple_user_payload): httpserver.url_for("/"), "query", "user", - "full-qs", "--attribute", "userName", "--attribute", @@ -214,7 +219,52 @@ def test_search_request_payload(runner, httpserver, simple_user_payload): assert result.exit_code == 0, result.output json_output = json.loads(result.output) - assert json_output == simple_user_payload("full-qs") + assert json_output["Resources"] == [simple_user_payload("full-qs")] + + +def test_listing_options_on_a_single_resource(runner, httpserver): + """Test that the listing arguments are refused when an id is passed.""" + result = runner.invoke( + cli, + [ + "--url", + httpserver.url_for("/"), + "query", + "user", + "one-by-id", + "--filter", + 'userName Eq "john"', + "--count", + "10", + ], + catch_exceptions=False, + ) + assert result.exit_code == 1, result.output + assert ( + "Error: --count, --filter cannot be used when querying a single resource." + in result.output + ) + + +def test_listing_options_on_the_service_provider_config(runner, httpserver): + """Test that the listing arguments are refused on the ServiceProviderConfig endpoint.""" + result = runner.invoke( + cli, + [ + "--url", + httpserver.url_for("/"), + "query", + "serviceproviderconfig", + "--filter", + 'userName Eq "john"', + ], + catch_exceptions=False, + ) + assert result.exit_code == 1, result.output + assert ( + "Error: --filter cannot be used when querying a single resource." + in result.output + ) def test_unknown_resource_type( @@ -269,3 +319,19 @@ def test_validation_error(runner, httpserver, simple_user_payload): ) assert result.exit_code == 1, result.output assert "Expected type User but got undefined object with no schema" in result.output + + +def test_service_provider_config(runner, httpserver): + """Test querying the ServiceProviderConfig singleton endpoint.""" + result = runner.invoke( + cli, + ["--url", httpserver.url_for("/"), "query", "serviceproviderconfig"], + catch_exceptions=False, + ) + assert result.exit_code == 0, result.output + + json_output = json.loads(result.output) + assert json_output["schemas"] == [ + "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" + ] + assert json_output["documentationUri"] == "https://scim.test" From 43430696914130774f31892889b057119ac729f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Sun, 20 Sep 2026 20:47:40 +0200 Subject: [PATCH 4/4] doc: suppress the changelog autosectionlabel warnings --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index 29104a2..37ba73b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -50,6 +50,7 @@ pygments_style = "sphinx" todo_include_todos = True toctree_collapse = False +suppress_warnings = ["autosectionlabel.changelog"] intersphinx_mapping = { "python": ("https://docs.python.org/3", None),