-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathtest_model_attributes.py
More file actions
391 lines (325 loc) · 12.3 KB
/
test_model_attributes.py
File metadata and controls
391 lines (325 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import uuid
from typing import Annotated
from scim2_models.annotations import Required
from scim2_models.annotations import Returned
from scim2_models.attributes import ComplexAttribute
from scim2_models.base import BaseModel
from scim2_models.context import Context
from scim2_models.messages.error import Error
from scim2_models.messages.patch_op import PatchOp
from scim2_models.reference import Reference
from scim2_models.resources.enterprise_user import EnterpriseUser
from scim2_models.resources.group import Group
from scim2_models.resources.group import GroupMember
from scim2_models.resources.resource import Extension
from scim2_models.resources.resource import Meta
from scim2_models.resources.resource import Resource
from scim2_models.resources.user import User
from scim2_models.urn import _validate_attribute_urn
class Sub(ComplexAttribute):
dummy: str
class Sup(Resource):
schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:Sup"]
dummy: str
sub: Sub
subs: list[Sub]
ref: Reference[Sub]
refunion: Reference[Sub | User]
def test_guess_root_type():
assert Sup.get_field_root_type("dummy") is str
assert Sup.get_field_root_type("sub") == Sub
assert Sup.get_field_root_type("subs") == Sub
assert Sup.get_field_root_type("ref") == Reference[Sub]
assert Sup.get_field_root_type("refunion") == Reference[Sub | User]
class ReturnedModel(BaseModel):
always: Annotated[str | None, Returned.always] = None
never: Annotated[str | None, Returned.never] = None
default: Annotated[str | None, Returned.default] = None
request: Annotated[str | None, Returned.request] = None
class Baz(ComplexAttribute):
baz_snake_case: str
class Foo(Resource):
schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:Foo"]
sub: Annotated[ReturnedModel, Returned.default]
bar: str
snake_case: str
baz: Baz | None = None
class Bar(Resource):
schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:Bar"]
sub: Annotated[ReturnedModel, Returned.default]
bar: str
snake_case: str
baz: Baz | None = None
class MyExtension(Extension):
schemas: Annotated[list[str], Required.true] = ["urn:example:2.0:MyExtension"]
baz: str
def test_validate_attribute_urn():
"""Test the method that validates and normalizes attribute URNs."""
assert _validate_attribute_urn("bar", Foo) == "urn:example:2.0:Foo:bar"
assert (
_validate_attribute_urn("urn:example:2.0:Foo:bar", Foo)
== "urn:example:2.0:Foo:bar"
)
assert _validate_attribute_urn("sub", Foo) == "urn:example:2.0:Foo:sub"
assert (
_validate_attribute_urn("urn:example:2.0:Foo:sub", Foo)
== "urn:example:2.0:Foo:sub"
)
assert (
_validate_attribute_urn("sub.always", Foo) == "urn:example:2.0:Foo:sub.always"
)
assert (
_validate_attribute_urn("urn:example:2.0:Foo:sub.always", Foo)
== "urn:example:2.0:Foo:sub.always"
)
assert _validate_attribute_urn("snakeCase", Foo) == "urn:example:2.0:Foo:snakeCase"
assert (
_validate_attribute_urn("urn:example:2.0:Foo:snakeCase", Foo)
== "urn:example:2.0:Foo:snakeCase"
)
assert (
_validate_attribute_urn("urn:example:2.0:MyExtension:baz", Foo[MyExtension])
== "urn:example:2.0:MyExtension:baz"
)
assert _validate_attribute_urn("urn:InvalidResource:bar", Foo) is None
assert _validate_attribute_urn("urn:example:2.0:Foo:invalid", Foo) is None
assert _validate_attribute_urn("bar.invalid", Foo) is None
assert (
_validate_attribute_urn("urn:example:2.0:MyExtension:invalid", Foo[MyExtension])
is None
)
def test_payload_attribute_case_sensitivity():
"""RFC7643 §2.1 indicates that attribute names should be case insensitive.
Attribute names are case insensitive and are often "camel-cased"
(e.g., "camelCase").
Reported by issue #39.
"""
payload = {
"UserName": "UserName123",
"Active": True,
"displayname": "BobIsAmazing",
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"externalId": uuid.uuid4().hex,
"name": {
"formatted": "Ryan Leenay",
"familyName": "Leenay",
"givenName": "Ryan",
},
"emails": [
{"Primary": True, "type": "work", "value": "testing@bob.com"},
{"Primary": False, "type": "home", "value": "testinghome@bob.com"},
],
}
user = User.model_validate(payload)
assert user.user_name == "UserName123"
assert user.display_name == "BobIsAmazing"
def test_attribute_inclusion_case_sensitivity():
"""Test that attribute inclusion supports any attribute case.
Reported by #45.
"""
user = User.model_validate({"userName": "foobar"})
assert user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE, attributes=["userName"]
) == {
"userName": "foobar",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
}
assert user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE, attributes=["username"]
) == {
"userName": "foobar",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
}
assert user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE, attributes=["USERNAME"]
) == {
"userName": "foobar",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
}
assert user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=["urn:ietf:params:scim:schemas:core:2.0:User:userName"],
) == {
"userName": "foobar",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
}
assert user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=["urn:ietf:params:scim:schemas:core:2.0:User:username"],
) == {
"userName": "foobar",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
}
assert user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=["URN:IETF:PARAMS:SCIM:SCHEMAS:CORE:2.0:USER:USERNAME"],
) == {
"userName": "foobar",
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
}
def test_attribute_inclusion_schema_extensions():
"""Verifies that attributes from schema extensions work."""
user = User[EnterpriseUser].model_validate(
{
"userName": "foobar",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "12345"
},
}
)
expected = {
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
],
"userName": "foobar",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "12345",
},
}
assert (
user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=[
"urn:ietf:params:scim:schemas:core:2.0:User:userName",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber",
],
)
== expected
)
assert (
user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=[
"urn:ietf:params:scim:schemas:core:2.0:User:userName",
"URN:IETF:PARAMS:SCIM:SCHEMAS:EXTENSION:ENTERPRISE:2.0:USER:EMPLOYEENUMBER",
],
)
== expected
)
def test_dump_after_assignment():
"""Test that attribute assignment does not break model dump."""
user = User(id="1", user_name="ABC")
user.meta = Meta(
resource_type="User",
location="/v2/Users/foo",
)
assert user.model_dump(scim_ctx=Context.RESOURCE_CREATION_RESPONSE) == {
"id": "1",
"meta": {
"location": "/v2/Users/foo",
"resourceType": "User",
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
],
"userName": "ABC",
}
def test_binary_attributes():
decoded = b"This is a very long line with a lot of characters, enough to create newlines when encoded."
encoded = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdoIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu"
user = User.model_validate(
{"userName": "foobar", "x509Certificates": [{"value": encoded}]}
)
assert user.x509_certificates[0].value == decoded
assert user.model_dump()["x509Certificates"][0]["value"] == encoded
encoded_without_newlines = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdoIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu"
user = User.model_validate(
{
"userName": "foobar",
"x509Certificates": [{"value": encoded_without_newlines}],
}
)
assert user.x509_certificates[0].value == decoded
assert user.model_dump()["x509Certificates"][0]["value"] == encoded
encoded_with_padding = "VGhpcyBpcyBhIHZlcnkgbG9uZyBsaW5lIHdpdGggYSBsb3Qgb2YgY2hhcmFjdGVycywgZW5vdWdoIHRvIGNyZWF0ZSBuZXdsaW5lcyB3aGVuIGVuY29kZWQu=================="
user = User.model_validate(
{"userName": "foobar", "x509Certificates": [{"value": encoded_with_padding}]}
)
assert user.x509_certificates[0].value == decoded
assert user.model_dump()["x509Certificates"][0]["value"] == encoded
def test_scim_object_model_dump_coverage():
"""Test ScimObject.model_dump for coverage of mode setting."""
# Test with scim_ctx=None (no mode setting)
error = Error(status="400", detail="Test error")
result = error.model_dump(scim_ctx=None)
assert isinstance(result, dict)
# Test model_dump_json coverage
json_result = error.model_dump_json(scim_ctx=None)
assert isinstance(json_result, str)
def test_patch_op_preserves_case_in_value_fields():
"""Test that PatchOp preserves original case in operation values."""
# Test data from the GitHub issue
patch_data = {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"value": {
"streetAddress": "911 Universal City Plaza",
},
}
],
}
patch_op = PatchOp[User].model_validate(patch_data)
result = patch_op.model_dump()
value = result["Operations"][0]["value"]
assert value["streetAddress"] == "911 Universal City Plaza"
def test_patch_op_preserves_case_in_sub_value_fields():
"""Test that nested objects within Any fields are still normalized according to their schema."""
patch_data = {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"value": {
"name": {"givenName": "John"},
},
}
],
}
patch_op = PatchOp[User].model_validate(patch_data)
result = patch_op.model_dump()
value = result["Operations"][0]["value"]
assert value["name"]["givenName"] == "John"
def test_complex_attribute_inclusion_includes_sub_attributes():
"""When a complex attribute is requested, its sub-attributes should be included."""
user = User(
user_name="bjensen",
name={"given_name": "Barbara", "family_name": "Jensen"},
)
result = user.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=["name"],
)
assert result["name"] == {"givenName": "Barbara", "familyName": "Jensen"}
def test_multivalued_complex_attribute_inclusion_includes_sub_attributes():
"""When a multi-valued complex attribute is requested, its sub-attributes should be included."""
group = Group(
id="group-123",
display_name="Engineering",
members=[
GroupMember(value="user-1", type="User"),
GroupMember(value="user-2", type="User"),
],
)
result = group.model_dump(
scim_ctx=Context.RESOURCE_QUERY_RESPONSE,
attributes=["members"],
)
assert result["members"] == [
{"value": "user-1", "type": "User"},
{"value": "user-2", "type": "User"},
]