-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathtest_patch_op_replace.py
More file actions
368 lines (308 loc) · 10.8 KB
/
test_patch_op_replace.py
File metadata and controls
368 lines (308 loc) · 10.8 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
from typing import Annotated
import pytest
from scim2_models import URN
from scim2_models import MutabilityException
from scim2_models import PatchOp
from scim2_models import PatchOperation
from scim2_models import User
from scim2_models.annotations import Mutability
from scim2_models.resources.resource import Resource
def test_replace_operation_single_attribute():
"""Test replacing a single-valued attribute.
:rfc:`RFC7644 §3.5.2.3 <7644#section-3.5.2.3>`: "The 'replace' operation replaces
the value at the target location specified by the 'path'."
"""
user = User(nick_name="OldNick")
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_, path="nickName", value="NewNick"
)
]
)
result = patch.patch(user)
assert result is True
assert user.nick_name == "NewNick"
def test_replace_operation_single_attribute_none_to_value():
"""Test replacing a None single-valued attribute with a value."""
user = User(nick_name=None)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_, path="nickName", value="NewNick"
)
]
)
result = patch.patch(user)
assert result is True
assert user.nick_name == "NewNick"
def test_replace_operation_nonexistent_attribute():
"""Test replacing a nonexistent attribute should be treated as add."""
user = User()
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_, path="nickName", value="NewNick"
)
]
)
result = patch.patch(user)
assert result is True
assert user.nick_name == "NewNick"
def test_replace_operation_same_value():
"""Test replace operation with same value should return False."""
user = User(nick_name="Test")
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_, path="nickName", value="Test"
)
]
)
result = patch.patch(user)
assert result is False
assert user.nick_name == "Test"
def test_replace_operation_sub_attribute():
"""Test replacing a sub-attribute of a complex attribute."""
user = User(name={"familyName": "OldName", "givenName": "Barbara"})
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_, path="name.familyName", value="NewName"
)
]
)
result = patch.patch(user)
assert result is True
assert user.name.family_name == "NewName"
assert user.name.given_name == "Barbara"
def test_replace_operation_complex_attribute():
"""Test replacing an entire complex attribute."""
user = User(name={"familyName": "OldName", "givenName": "Barbara"})
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_,
path="name",
value={"familyName": "NewName", "givenName": "John"},
)
]
)
result = patch.patch(user)
assert result is True
assert user.name.family_name == "NewName"
assert user.name.given_name == "John"
def test_replace_operation_sub_attribute_parent_none():
"""Test replacing a sub-attribute when parent is None (should create parent)."""
user = User(name=None)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_, path="name.familyName", value="NewName"
)
]
)
result = patch.patch(user)
assert result is True
assert user.name is not None
assert user.name.family_name == "NewName"
def test_replace_operation_multiple_attribute_all():
"""Test replacing all items in a multi-valued attribute."""
user = User(
emails=[
{"value": "old1@example.com", "type": "work"},
{"value": "old2@example.com", "type": "home"},
]
)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_,
path="emails",
value=[{"value": "new@example.com", "type": "work"}],
)
]
)
result = patch.patch(user)
assert result is True
assert len(user.emails) == 1
assert user.emails[0].value == "new@example.com"
assert user.emails[0].type == "work"
def test_replace_operation_no_path():
"""Test replacing multiple attributes when no path is specified.
:rfc:`RFC7644 §3.5.2.3 <7644#section-3.5.2.3>`: "If the 'path' parameter is omitted,
the target is assumed to be the resource itself, and the 'value' parameter
SHALL contain the replacement attributes."
"""
user = User(nick_name="OldNick", display_name="Old Display")
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_,
value={
"nickName": "NewNick",
"displayName": "New Display",
},
)
]
)
result = patch.patch(user)
assert result is True
assert user.nick_name == "NewNick"
assert user.display_name == "New Display"
def test_replace_operation_no_path_same_attributes():
"""Test replace operation with no path but same attribute values should return False."""
user = User(nick_name="Test", display_name="Display")
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_,
value={"nickName": "Test", "displayName": "Display"},
)
]
)
result = patch.patch(user)
assert result is False
assert user.nick_name == "Test"
assert user.display_name == "Display"
def test_replace_operation_with_non_dict_value_no_path():
"""Test replace operation with no path and non-dict value should return False."""
user = User(nick_name="Test")
patch = PatchOp[User](
operations=[
PatchOperation[User](op=PatchOperation.Op.replace_, value="invalid_value")
]
)
result = patch.patch(user)
assert result is False
assert user.nick_name == "Test"
def test_immutable_field():
"""Test that replace operations on immutable fields raise mutability errors."""
class Dummy(Resource):
__schema__ = URN("urn:test:TestResource")
immutable: Annotated[str, Mutability.immutable]
resource = Dummy.model_construct(immutable="original")
patch = PatchOp[Dummy](
operations=[
PatchOperation[Dummy](
op=PatchOperation.Op.replace_, path="immutable", value="new_value"
)
]
)
with pytest.raises(MutabilityException):
patch.patch(resource)
def test_primary_auto_exclusion_on_add():
"""Test that adding an element with primary=true auto-excludes other primary values.
:rfc:`RFC 7644 §3.5.2 <7644#section-3.5.2>`: "a PATCH operation that sets a
value's 'primary' sub-attribute to 'true' SHALL cause the server to
automatically set 'primary' to 'false' for any other values in the array."
"""
from scim2_models import Email
user = User(
emails=[
Email(value="existing@example.com", primary=True),
]
)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.add,
path="emails",
value={"value": "new@example.com", "primary": True},
)
]
)
result = patch.patch(user)
assert result is True
assert user.emails[0].primary is False
assert user.emails[1].primary is True
def test_primary_auto_exclusion_on_replace_list():
"""Test that replacing a list with a new primary auto-excludes the old one."""
from scim2_models import Email
user = User(
emails=[
Email(value="old@example.com", primary=True),
]
)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_,
path="emails",
value=[
{"value": "old@example.com", "primary": False},
{"value": "new@example.com", "primary": True},
],
)
]
)
result = patch.patch(user)
assert result is True
assert user.emails[0].primary is False
assert user.emails[1].primary is True
def test_primary_no_change_when_single_primary():
"""Test that no change occurs when there's only one primary after patch."""
from scim2_models import Email
user = User(
emails=[
Email(value="a@example.com", primary=True),
Email(value="b@example.com", primary=False),
]
)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.add,
path="emails",
value={"value": "c@example.com", "primary": False},
)
]
)
result = patch.patch(user)
assert result is True
assert user.emails[0].primary is True
assert user.emails[1].primary is False
assert user.emails[2].primary is False
def test_primary_auto_exclusion_rejects_multiple_new_primaries():
"""Test that setting multiple new primaries in one operation raises an error."""
from scim2_models import Email
user = User(
emails=[
Email(value="a@example.com", primary=False),
Email(value="b@example.com", primary=False),
]
)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.replace_,
path="emails",
value=[
{"value": "a@example.com", "primary": True},
{"value": "b@example.com", "primary": True},
],
)
]
)
with pytest.raises(Exception, match="Multiple values marked as primary"):
patch.patch(user)
def test_primary_auto_exclusion_rejects_preexisting_multiple_primaries():
"""Test that patching data with preexisting multiple primaries raises an error."""
from scim2_models import Email
user = User(
emails=[
Email(value="a@example.com", primary=True),
Email(value="b@example.com", primary=True),
]
)
patch = PatchOp[User](
operations=[
PatchOperation[User](
op=PatchOperation.Op.add,
path="emails",
value={"value": "c@example.com", "primary": False},
)
]
)
with pytest.raises(Exception, match="Multiple primary values already exist"):
patch.patch(user)