Skip to content

Commit 5305a5e

Browse files
committed
feat: discover the server configuration endpoints
1 parent a731ead commit 5305a5e

File tree

5 files changed

+114
-28
lines changed

5 files changed

+114
-28
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ classifiers = [
2727
requires-python = ">= 3.10"
2828
dependencies = [
2929
"click>=8.1.7",
30-
"scim2-client>=0.4.0",
30+
"scim2-client>=0.4.2",
3131
"scim2-tester[httpx]>=0.1.10",
3232
"sphinx-click-rst-to-ansi-formatter>=0.1.0",
3333
"pydanclick>=0.3.0",

scim2_cli/__init__.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,36 @@ def _is_pydantic_model(model: Any) -> TypeGuard[type[BaseModel]]:
4444
patch_pydanclick()
4545

4646

47+
def load_config_files(
48+
schemas_fd, resource_types_fd, service_provider_config_fd
49+
) -> tuple[
50+
list[type[Resource]], list[ResourceType] | None, ServiceProviderConfig | None
51+
]:
52+
if schemas_fd:
53+
schemas_payload = json.load(schemas_fd)
54+
schemas_obj = [Schema.model_validate(schema) for schema in schemas_payload]
55+
resource_models = [Resource.from_schema(schema) for schema in schemas_obj]
56+
57+
else:
58+
resource_models = [User, Group]
59+
60+
if resource_types_fd:
61+
resource_types_payload = json.load(resource_types_fd)
62+
resource_types = [
63+
ResourceType.model_validate(item) for item in resource_types_payload
64+
]
65+
else:
66+
resource_types = None
67+
68+
if service_provider_config_fd:
69+
spc_payload = json.load(service_provider_config_fd)
70+
service_provider_config = ServiceProviderConfig.model_validate(spc_payload)
71+
else:
72+
service_provider_config = None
73+
74+
return resource_models, resource_types, service_provider_config
75+
76+
4777
@click.group(cls=make_rst_to_ansi_formatter(DOC_URL, group=True))
4878
@click.option("-u", "--url", help="The SCIM server endpoint.", envvar="SCIM_CLI_URL")
4979
@click.option(
@@ -86,36 +116,21 @@ def cli(
86116
headers_dict = split_headers(header)
87117
client = Client(base_url=ctx.obj["URL"], headers=headers_dict)
88118

89-
if schemas:
90-
schemas_payload = json.load(schemas)
91-
schemas_obj = [Schema.model_validate(schema) for schema in schemas_payload]
92-
resource_models = [Resource.from_schema(schema) for schema in schemas_obj]
93-
94-
else:
95-
resource_models = [User, Group]
96-
97-
if resource_types:
98-
resource_types_payload = json.load(resource_types)
99-
resource_types_obj = [
100-
ResourceType.model_validate(item) for item in resource_types_payload
101-
]
102-
else:
103-
resource_types_obj = None
104-
105-
if service_provider_config:
106-
spc_payload = json.load(service_provider_config)
107-
spc_obj = ServiceProviderConfig.model_validate(spc_payload)
108-
else:
109-
spc_obj = None
119+
resource_models, resource_types_obj, spc_obj = load_config_files(
120+
schemas, resource_types, service_provider_config
121+
)
110122

111123
scim_client = SyncSCIMClient(
112124
client,
113125
resource_models=resource_models,
114126
resource_types=resource_types_obj,
115127
service_provider_config=spc_obj,
116128
)
117-
if not resource_types:
118-
scim_client.register_naive_resource_types()
129+
scim_client.discover(
130+
schemas=not bool(schemas),
131+
resource_types=not bool(resource_types),
132+
service_provider_config=not bool(service_provider_config),
133+
)
119134

120135
ctx.obj["client"] = scim_client
121136
ctx.obj["resource_models"] = {

tests/conftest.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,82 @@
11
import pytest
22
from click.testing import CliRunner
3+
from scim2_models import AuthenticationScheme
4+
from scim2_models import Bulk
5+
from scim2_models import ChangePassword
6+
from scim2_models import ETag
7+
from scim2_models import Filter
8+
from scim2_models import ListResponse
9+
from scim2_models import Patch
10+
from scim2_models import ResourceType
11+
from scim2_models import Schema
12+
from scim2_models import ServiceProviderConfig
13+
from scim2_models import Sort
14+
from scim2_models import User
315

416

517
@pytest.fixture
618
def runner():
719
return CliRunner()
820

921

22+
@pytest.fixture
23+
def httpserver(httpserver):
24+
httpserver.expect_request("/ServiceProviderConfig").respond_with_json(
25+
ServiceProviderConfig(
26+
documentation_uri="https://scim.test",
27+
patch=Patch(supported=False),
28+
bulk=Bulk(supported=False, max_operations=0, max_payload_size=0),
29+
change_password=ChangePassword(supported=True),
30+
filter=Filter(supported=False, max_results=0),
31+
sort=Sort(supported=False),
32+
etag=ETag(supported=False),
33+
authentication_schemes=[
34+
AuthenticationScheme(
35+
name="OAuth Bearer Token",
36+
description="Authentication scheme using the OAuth Bearer Token Standard",
37+
spec_uri="http://www.rfc-editor.org/info/rfc6750",
38+
documentation_uri="https://scim.test",
39+
type="oauthbearertoken",
40+
primary=True,
41+
),
42+
],
43+
).model_dump(),
44+
status=200,
45+
content_type="application/scim+json",
46+
)
47+
48+
httpserver.expect_request("/ResourceTypes").respond_with_json(
49+
ListResponse[ResourceType](
50+
total_results=1,
51+
start_index=1,
52+
items_per_page=1,
53+
resources=[
54+
ResourceType(
55+
name="User",
56+
id="User",
57+
endpoint="/Users",
58+
schema_="urn:ietf:params:scim:schemas:core:2.0:User",
59+
)
60+
],
61+
).model_dump(),
62+
status=200,
63+
content_type="application/scim+json",
64+
)
65+
66+
httpserver.expect_request("/Schemas").respond_with_json(
67+
ListResponse[Schema](
68+
total_results=1,
69+
start_index=1,
70+
items_per_page=1,
71+
resources=[User.to_schema()],
72+
).model_dump(),
73+
status=200,
74+
content_type="application/scim+json",
75+
)
76+
77+
return httpserver
78+
79+
1080
@pytest.fixture
1181
def simple_user_payload(httpserver):
1282
def wrapped(id):

tests/test_cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_stdin_bad_json(runner, httpserver):
2727
cli,
2828
["--url", httpserver.url_for("/"), "query", "user"],
2929
input="invalid",
30+
catch_exceptions=False,
3031
)
3132
assert result.exit_code == 1
3233
assert "Invalid JSON input." in result.stdout
@@ -99,8 +100,9 @@ def test_env_vars(runner, httpserver, simple_user_payload):
99100

100101

101102
def test_custom_configuration(runner, httpserver, simple_user_payload, tmp_path):
103+
"""Test passing custom .JSON configuration files to the command."""
102104
spc = ServiceProviderConfig(
103-
documentation_uri="https://canaille.readthedocs.io",
105+
documentation_uri="https://scim.test",
104106
patch=Patch(supported=False),
105107
bulk=Bulk(supported=False, max_operations=0, max_payload_size=0),
106108
change_password=ChangePassword(supported=True),
@@ -112,7 +114,7 @@ def test_custom_configuration(runner, httpserver, simple_user_payload, tmp_path)
112114
name="OAuth Bearer Token",
113115
description="Authentication scheme using the OAuth Bearer Token Standard",
114116
spec_uri="http://www.rfc-editor.org/info/rfc6750",
115-
documentation_uri="https://canaille.readthedocs.io",
117+
documentation_uri="https://scim.test",
116118
type="oauthbearertoken",
117119
primary=True,
118120
),
@@ -141,7 +143,6 @@ def test_custom_configuration(runner, httpserver, simple_user_payload, tmp_path)
141143
with open(resource_types_path, "w") as fd:
142144
json.dump(resource_types, fd)
143145

144-
"""Test passing custom .JSON configuration files to the command."""
145146
httpserver.expect_request(
146147
"/somewhere-different/foobar",
147148
method="GET",

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)