scimitar 2.5.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +721 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +72 -18
- data/app/controllers/scimitar/application_controller.rb +17 -9
- data/app/controllers/scimitar/resource_types_controller.rb +7 -3
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +366 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +3 -2
- data/app/models/scimitar/complex_types/address.rb +0 -6
- data/app/models/scimitar/complex_types/base.rb +2 -2
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +97 -12
- data/app/models/scimitar/resource_invalid_error.rb +1 -1
- data/app/models/scimitar/resource_type.rb +4 -6
- data/app/models/scimitar/resources/base.rb +52 -8
- data/app/models/scimitar/resources/mixin.rb +539 -76
- data/app/models/scimitar/schema/attribute.rb +18 -8
- data/app/models/scimitar/schema/base.rb +2 -2
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +49 -3
- data/lib/scimitar/engine.rb +57 -12
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
- data/lib/scimitar/support/utilities.rb +111 -0
- data/lib/scimitar/version.rb +2 -2
- data/lib/scimitar.rb +1 -0
- data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
- data/spec/apps/dummy/app/models/mock_user.rb +20 -3
- data/spec/apps/dummy/config/application.rb +8 -0
- data/spec/apps/dummy/config/initializers/scimitar.rb +40 -3
- data/spec/apps/dummy/config/routes.rb +18 -1
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +2 -0
- data/spec/apps/dummy/db/schema.rb +3 -1
- data/spec/controllers/scimitar/application_controller_spec.rb +56 -2
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +8 -4
- data/spec/controllers/scimitar/schemas_controller_spec.rb +344 -48
- data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +1 -0
- data/spec/models/scimitar/complex_types/address_spec.rb +3 -4
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +55 -13
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +781 -124
- data/spec/models/scimitar/schema/attribute_spec.rb +22 -0
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +723 -40
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +10 -2
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
- metadata +42 -34
@@ -13,9 +13,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
13
13
|
lmt = Time.parse("2023-01-09 14:25:00 +1300")
|
14
14
|
ids = 3.times.map { SecureRandom.uuid }.sort()
|
15
15
|
|
16
|
-
@u1 = MockUser.create(primary_key: ids.shift(), username: '1', first_name: 'Foo', last_name: 'Ark', home_email_address: 'home_1@test.com', scim_uid: '001', created_at: lmt, updated_at: lmt + 1)
|
17
|
-
@u2 = MockUser.create(primary_key: ids.shift(), username: '2', first_name: 'Foo', last_name: 'Bar', home_email_address: 'home_2@test.com', scim_uid: '002', created_at: lmt, updated_at: lmt + 2)
|
18
|
-
@u3 = MockUser.create(primary_key: ids.shift(), username: '3', first_name: 'Foo', home_email_address: 'home_3@test.com', scim_uid: '003', created_at: lmt, updated_at: lmt + 3)
|
16
|
+
@u1 = MockUser.create!(primary_key: ids.shift(), username: '1', first_name: 'Foo', last_name: 'Ark', home_email_address: 'home_1@test.com', scim_uid: '001', created_at: lmt, updated_at: lmt + 1)
|
17
|
+
@u2 = MockUser.create!(primary_key: ids.shift(), username: '2', first_name: 'Foo', last_name: 'Bar', home_email_address: 'home_2@test.com', scim_uid: '002', created_at: lmt, updated_at: lmt + 2, password: 'oldpassword')
|
18
|
+
@u3 = MockUser.create!(primary_key: ids.shift(), username: '3', first_name: 'Foo', home_email_address: 'home_3@test.com', scim_uid: '003', created_at: lmt, updated_at: lmt + 3)
|
19
19
|
|
20
20
|
@g1 = MockGroup.create!(display_name: 'Group 1')
|
21
21
|
@g2 = MockGroup.create!(display_name: 'Group 2')
|
@@ -26,13 +26,17 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
26
26
|
|
27
27
|
context '#index' do
|
28
28
|
context 'with no items' do
|
29
|
-
|
29
|
+
before :each do
|
30
30
|
MockUser.delete_all
|
31
|
+
end
|
31
32
|
|
33
|
+
it 'returns empty list' do
|
32
34
|
expect_any_instance_of(MockUsersController).to receive(:index).once.and_call_original
|
33
35
|
get '/Users', params: { format: :scim }
|
34
36
|
|
35
|
-
expect(response.status).to eql(200)
|
37
|
+
expect(response.status ).to eql(200)
|
38
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
39
|
+
|
36
40
|
result = JSON.parse(response.body)
|
37
41
|
|
38
42
|
expect(result['totalResults']).to eql(0)
|
@@ -46,7 +50,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
46
50
|
it 'returns all items' do
|
47
51
|
get '/Users', params: { format: :scim }
|
48
52
|
|
49
|
-
expect(response.status).to eql(200)
|
53
|
+
expect(response.status ).to eql(200)
|
54
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
55
|
+
|
50
56
|
result = JSON.parse(response.body)
|
51
57
|
|
52
58
|
expect(result['totalResults']).to eql(3)
|
@@ -64,7 +70,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
64
70
|
it 'returns all items' do
|
65
71
|
get '/Groups', params: { format: :scim }
|
66
72
|
|
67
|
-
expect(response.status).to eql(200)
|
73
|
+
expect(response.status ).to eql(200)
|
74
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
75
|
+
|
68
76
|
result = JSON.parse(response.body)
|
69
77
|
|
70
78
|
expect(result['totalResults']).to eql(3)
|
@@ -84,7 +92,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
84
92
|
filter: 'name.givenName eq "FOO" and name.familyName pr and emails ne "home_1@test.com"'
|
85
93
|
}
|
86
94
|
|
87
|
-
expect(response.status).to eql(200)
|
95
|
+
expect(response.status ).to eql(200)
|
96
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
97
|
+
|
88
98
|
result = JSON.parse(response.body)
|
89
99
|
|
90
100
|
expect(result['totalResults']).to eql(1)
|
@@ -97,13 +107,69 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
97
107
|
expect(usernames).to match_array(['2'])
|
98
108
|
end
|
99
109
|
|
110
|
+
it 'returns only the requested attributes' do
|
111
|
+
get '/Users', params: {
|
112
|
+
format: :scim,
|
113
|
+
attributes: "id,name"
|
114
|
+
}
|
115
|
+
|
116
|
+
expect(response.status ).to eql(200)
|
117
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
118
|
+
|
119
|
+
result = JSON.parse(response.body)
|
120
|
+
|
121
|
+
expect(result['totalResults']).to eql(3)
|
122
|
+
expect(result['Resources'].size).to eql(3)
|
123
|
+
|
124
|
+
keys = result['Resources'].map { |resource| resource.keys }.flatten.uniq
|
125
|
+
|
126
|
+
expect(keys).to match_array(%w[
|
127
|
+
id
|
128
|
+
meta
|
129
|
+
name
|
130
|
+
schemas
|
131
|
+
urn:ietf:params:scim:schemas:extension:enterprise:2.0:User
|
132
|
+
urn:ietf:params:scim:schemas:extension:manager:1.0:User
|
133
|
+
])
|
134
|
+
expect(result.dig('Resources', 0, 'id')).to eql @u1.primary_key.to_s
|
135
|
+
expect(result.dig('Resources', 0, 'name', 'givenName')).to eql 'Foo'
|
136
|
+
expect(result.dig('Resources', 0, 'name', 'familyName')).to eql 'Ark'
|
137
|
+
end
|
138
|
+
|
139
|
+
# https://github.com/pond/scimitar/issues/37
|
140
|
+
#
|
100
141
|
it 'applies a filter, with case-insensitive attribute matching (GitHub issue #37)' do
|
101
142
|
get '/Users', params: {
|
102
143
|
format: :scim,
|
103
144
|
filter: 'name.GIVENNAME eq "Foo" and name.Familyname pr and emails ne "home_1@test.com"'
|
104
145
|
}
|
105
146
|
|
106
|
-
expect(response.status).to eql(200)
|
147
|
+
expect(response.status ).to eql(200)
|
148
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
149
|
+
|
150
|
+
result = JSON.parse(response.body)
|
151
|
+
|
152
|
+
expect(result['totalResults']).to eql(1)
|
153
|
+
expect(result['Resources'].size).to eql(1)
|
154
|
+
|
155
|
+
ids = result['Resources'].map { |resource| resource['id'] }
|
156
|
+
expect(ids).to match_array([@u2.primary_key.to_s])
|
157
|
+
|
158
|
+
usernames = result['Resources'].map { |resource| resource['userName'] }
|
159
|
+
expect(usernames).to match_array(['2'])
|
160
|
+
end
|
161
|
+
|
162
|
+
# https://github.com/pond/scimitar/issues/115
|
163
|
+
#
|
164
|
+
it 'handles broken Microsoft filters (GitHub issue #115)' do
|
165
|
+
get '/Users', params: {
|
166
|
+
format: :scim,
|
167
|
+
filter: 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"'
|
168
|
+
}
|
169
|
+
|
170
|
+
expect(response.status ).to eql(200)
|
171
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
172
|
+
|
107
173
|
result = JSON.parse(response.body)
|
108
174
|
|
109
175
|
expect(result['totalResults']).to eql(1)
|
@@ -116,6 +182,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
116
182
|
expect(usernames).to match_array(['2'])
|
117
183
|
end
|
118
184
|
|
185
|
+
|
119
186
|
# Strange attribute capitalisation in tests here builds on test coverage
|
120
187
|
# for now-fixed GitHub issue #37.
|
121
188
|
#
|
@@ -126,7 +193,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
126
193
|
filter: "id eq \"#{@u3.primary_key}\""
|
127
194
|
}
|
128
195
|
|
129
|
-
expect(response.status).to eql(200)
|
196
|
+
expect(response.status ).to eql(200)
|
197
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
198
|
+
|
130
199
|
result = JSON.parse(response.body)
|
131
200
|
|
132
201
|
expect(result['totalResults']).to eql(1)
|
@@ -145,7 +214,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
145
214
|
filter: "externalID eq \"#{@u2.scim_uid}\""
|
146
215
|
}
|
147
216
|
|
148
|
-
expect(response.status).to eql(200)
|
217
|
+
expect(response.status ).to eql(200)
|
218
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
219
|
+
|
149
220
|
result = JSON.parse(response.body)
|
150
221
|
|
151
222
|
expect(result['totalResults']).to eql(1)
|
@@ -164,7 +235,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
164
235
|
filter: "Meta.LastModified eq \"#{@u3.updated_at}\""
|
165
236
|
}
|
166
237
|
|
167
|
-
expect(response.status).to eql(200)
|
238
|
+
expect(response.status ).to eql(200)
|
239
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
240
|
+
|
168
241
|
result = JSON.parse(response.body)
|
169
242
|
|
170
243
|
expect(result['totalResults']).to eql(1)
|
@@ -184,7 +257,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
184
257
|
count: 2
|
185
258
|
}
|
186
259
|
|
187
|
-
expect(response.status).to eql(200)
|
260
|
+
expect(response.status ).to eql(200)
|
261
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
262
|
+
|
188
263
|
result = JSON.parse(response.body)
|
189
264
|
|
190
265
|
expect(result['totalResults']).to eql(3)
|
@@ -203,7 +278,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
203
278
|
startIndex: 2
|
204
279
|
}
|
205
280
|
|
206
|
-
expect(response.status).to eql(200)
|
281
|
+
expect(response.status ).to eql(200)
|
282
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
283
|
+
|
207
284
|
result = JSON.parse(response.body)
|
208
285
|
|
209
286
|
expect(result['totalResults']).to eql(3)
|
@@ -224,8 +301,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
224
301
|
filter: 'name.givenName'
|
225
302
|
}
|
226
303
|
|
227
|
-
expect(response.status).to eql(400)
|
304
|
+
expect(response.status ).to eql(400)
|
305
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
306
|
+
|
228
307
|
result = JSON.parse(response.body)
|
308
|
+
|
229
309
|
expect(result['scimType']).to eql('invalidFilter')
|
230
310
|
end
|
231
311
|
end # "context 'with bad calls' do"
|
@@ -239,7 +319,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
239
319
|
expect_any_instance_of(MockUsersController).to receive(:show).once.and_call_original
|
240
320
|
get "/Users/#{@u2.primary_key}", params: { format: :scim }
|
241
321
|
|
242
|
-
expect(response.status).to eql(200)
|
322
|
+
expect(response.status ).to eql(200)
|
323
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
324
|
+
|
243
325
|
result = JSON.parse(response.body)
|
244
326
|
|
245
327
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -254,7 +336,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
254
336
|
expect_any_instance_of(MockGroupsController).to receive(:show).once.and_call_original
|
255
337
|
get "/Groups/#{@g2.id}", params: { format: :scim }
|
256
338
|
|
257
|
-
expect(response.status).to eql(200)
|
339
|
+
expect(response.status ).to eql(200)
|
340
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
341
|
+
|
258
342
|
result = JSON.parse(response.body)
|
259
343
|
|
260
344
|
expect(result['id']).to eql(@g2.id.to_s) # Note - ID was converted String; not Integer
|
@@ -266,8 +350,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
266
350
|
it 'renders 404' do
|
267
351
|
get '/Users/xyz', params: { format: :scim }
|
268
352
|
|
269
|
-
expect(response.status).to eql(404)
|
353
|
+
expect(response.status ).to eql(404)
|
354
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
355
|
+
|
270
356
|
result = JSON.parse(response.body)
|
357
|
+
|
271
358
|
expect(result['status']).to eql('404')
|
272
359
|
end
|
273
360
|
end # "context '#show' do"
|
@@ -283,7 +370,12 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
283
370
|
attributes = { userName: '4' } # Minimum required by schema
|
284
371
|
attributes = spec_helper_hupcase(attributes) if force_upper_case
|
285
372
|
|
373
|
+
# Prove that certain known pathways are called; can then unit test
|
374
|
+
# those if need be and be sure that this covers #create actions.
|
375
|
+
#
|
286
376
|
expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
|
377
|
+
expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
|
378
|
+
|
287
379
|
expect {
|
288
380
|
post "/Users", params: attributes.merge(format: :scim)
|
289
381
|
}.to change { MockUser.count }.by(1)
|
@@ -291,7 +383,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
291
383
|
mock_after = MockUser.all.to_a
|
292
384
|
new_mock = (mock_after - mock_before).first
|
293
385
|
|
294
|
-
expect(response.status).to eql(201)
|
386
|
+
expect(response.status ).to eql(201)
|
387
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
388
|
+
|
295
389
|
result = JSON.parse(response.body)
|
296
390
|
|
297
391
|
expect(result['id']).to eql(new_mock.primary_key.to_s)
|
@@ -306,6 +400,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
306
400
|
|
307
401
|
attributes = {
|
308
402
|
userName: '4',
|
403
|
+
password: 'correcthorsebatterystaple',
|
309
404
|
name: {
|
310
405
|
givenName: 'Given',
|
311
406
|
familyName: 'Family'
|
@@ -332,17 +427,90 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
332
427
|
mock_after = MockUser.all.to_a
|
333
428
|
new_mock = (mock_after - mock_before).first
|
334
429
|
|
335
|
-
expect(response.status).to eql(201)
|
430
|
+
expect(response.status ).to eql(201)
|
431
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
432
|
+
|
336
433
|
result = JSON.parse(response.body)
|
337
434
|
|
338
435
|
expect(result['id']).to eql(new_mock.id.to_s)
|
339
436
|
expect(result['meta']['resourceType']).to eql('User')
|
340
437
|
expect(new_mock.username).to eql('4')
|
438
|
+
expect(new_mock.password).to eql('correcthorsebatterystaple')
|
341
439
|
expect(new_mock.first_name).to eql('Given')
|
342
440
|
expect(new_mock.last_name).to eql('Family')
|
343
441
|
expect(new_mock.home_email_address).to eql('home_4@test.com')
|
344
442
|
expect(new_mock.work_email_address).to eql('work_4@test.com')
|
345
443
|
end
|
444
|
+
|
445
|
+
it 'with schema ID value keys without inline attributes' do
|
446
|
+
mock_before = MockUser.all.to_a
|
447
|
+
|
448
|
+
attributes = {
|
449
|
+
userName: '4',
|
450
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
|
451
|
+
organization: 'Foo Bar!',
|
452
|
+
department: 'Bar Foo!'
|
453
|
+
},
|
454
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User': {
|
455
|
+
manager: 'Foo Baz!'
|
456
|
+
}
|
457
|
+
}
|
458
|
+
|
459
|
+
attributes = spec_helper_hupcase(attributes) if force_upper_case
|
460
|
+
|
461
|
+
expect {
|
462
|
+
post "/Users", params: attributes.merge(format: :scim)
|
463
|
+
}.to change { MockUser.count }.by(1)
|
464
|
+
|
465
|
+
mock_after = MockUser.all.to_a
|
466
|
+
new_mock = (mock_after - mock_before).first
|
467
|
+
|
468
|
+
expect(response.status ).to eql(201)
|
469
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
470
|
+
|
471
|
+
result = JSON.parse(response.body)
|
472
|
+
|
473
|
+
expect(new_mock.organization).to eql('Foo Bar!')
|
474
|
+
expect(new_mock.department ).to eql('Bar Foo!')
|
475
|
+
expect(new_mock.manager ).to eql('Foo Baz!')
|
476
|
+
|
477
|
+
expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization']).to eql(new_mock.organization)
|
478
|
+
expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['department' ]).to eql(new_mock.department )
|
479
|
+
expect(result['urn:ietf:params:scim:schemas:extension:manager:1.0:User' ]['manager' ]).to eql(new_mock.manager )
|
480
|
+
end
|
481
|
+
|
482
|
+
it 'with schema ID value keys that have inline attributes' do
|
483
|
+
mock_before = MockUser.all.to_a
|
484
|
+
|
485
|
+
attributes = {
|
486
|
+
userName: '4',
|
487
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization': 'Foo Bar!',
|
488
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department': 'Bar Foo!',
|
489
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User:manager': 'Foo Baz!'
|
490
|
+
}
|
491
|
+
|
492
|
+
attributes = spec_helper_hupcase(attributes) if force_upper_case
|
493
|
+
|
494
|
+
expect {
|
495
|
+
post "/Users", params: attributes.merge(format: :scim)
|
496
|
+
}.to change { MockUser.count }.by(1)
|
497
|
+
|
498
|
+
mock_after = MockUser.all.to_a
|
499
|
+
new_mock = (mock_after - mock_before).first
|
500
|
+
|
501
|
+
expect(response.status ).to eql(201)
|
502
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
503
|
+
|
504
|
+
result = JSON.parse(response.body)
|
505
|
+
|
506
|
+
expect(new_mock.organization).to eql('Foo Bar!')
|
507
|
+
expect(new_mock.department ).to eql('Bar Foo!')
|
508
|
+
expect(new_mock.manager ).to eql('Foo Baz!')
|
509
|
+
|
510
|
+
expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization']).to eql(new_mock.organization)
|
511
|
+
expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['department' ]).to eql(new_mock.department )
|
512
|
+
expect(result['urn:ietf:params:scim:schemas:extension:manager:1.0:User' ]['manager' ]).to eql(new_mock.manager )
|
513
|
+
end
|
346
514
|
end # "shared_examples 'a creator' do | force_upper_case: |"
|
347
515
|
|
348
516
|
context 'using schema-matched case' do
|
@@ -363,8 +531,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
363
531
|
}
|
364
532
|
}.to_not change { MockUser.count }
|
365
533
|
|
366
|
-
expect(response.status).to eql(409)
|
534
|
+
expect(response.status ).to eql(409)
|
535
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
536
|
+
|
367
537
|
result = JSON.parse(response.body)
|
538
|
+
|
368
539
|
expect(result['scimType']).to eql('uniqueness')
|
369
540
|
expect(result['detail']).to include('already been taken')
|
370
541
|
end
|
@@ -377,8 +548,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
377
548
|
}
|
378
549
|
}.to_not change { MockUser.count }
|
379
550
|
|
380
|
-
expect(response.status).to eql(400)
|
551
|
+
expect(response.status ).to eql(400)
|
552
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
553
|
+
|
381
554
|
result = JSON.parse(response.body)
|
555
|
+
|
382
556
|
expect(result['scimType']).to eql('invalidValue')
|
383
557
|
expect(result['detail']).to include('is required')
|
384
558
|
end
|
@@ -391,12 +565,84 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
391
565
|
}
|
392
566
|
}.to_not change { MockUser.count }
|
393
567
|
|
394
|
-
expect(response.status).to eql(400)
|
568
|
+
expect(response.status ).to eql(400)
|
569
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
570
|
+
|
395
571
|
result = JSON.parse(response.body)
|
396
572
|
|
397
573
|
expect(result['scimType']).to eql('invalidValue')
|
398
574
|
expect(result['detail']).to include('is reserved')
|
399
575
|
end
|
576
|
+
|
577
|
+
context 'with a block' do
|
578
|
+
it 'invokes the block' do
|
579
|
+
mock_before = MockUser.all.to_a
|
580
|
+
|
581
|
+
expect_any_instance_of(CustomCreateMockUsersController).to receive(:create).once.and_call_original
|
582
|
+
expect {
|
583
|
+
post "/CustomCreateUsers", params: {
|
584
|
+
format: :scim,
|
585
|
+
userName: '4' # Minimum required by schema
|
586
|
+
}
|
587
|
+
}.to change { MockUser.count }.by(1)
|
588
|
+
|
589
|
+
mock_after = MockUser.all.to_a
|
590
|
+
new_mock = (mock_after - mock_before).first
|
591
|
+
|
592
|
+
expect(response.status ).to eql(201)
|
593
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
594
|
+
|
595
|
+
result = JSON.parse(response.body)
|
596
|
+
|
597
|
+
expect(result['id']).to eql(new_mock.id.to_s)
|
598
|
+
expect(result['meta']['resourceType']).to eql('User')
|
599
|
+
expect(new_mock.first_name).to eql(CustomCreateMockUsersController::OVERRIDDEN_NAME)
|
600
|
+
end
|
601
|
+
|
602
|
+
it 'returns 409 for duplicates (by Rails validation)' do
|
603
|
+
existing_user = MockUser.create!(
|
604
|
+
username: '4',
|
605
|
+
first_name: 'Will Be Overridden',
|
606
|
+
last_name: 'Baz',
|
607
|
+
home_email_address: 'random@test.com',
|
608
|
+
scim_uid: '999'
|
609
|
+
)
|
610
|
+
|
611
|
+
expect_any_instance_of(CustomCreateMockUsersController).to receive(:create).once.and_call_original
|
612
|
+
expect {
|
613
|
+
post "/CustomCreateUsers", params: {
|
614
|
+
format: :scim,
|
615
|
+
userName: '4' # Already exists
|
616
|
+
}
|
617
|
+
}.to_not change { MockUser.count }
|
618
|
+
|
619
|
+
expect(response.status ).to eql(409)
|
620
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
621
|
+
|
622
|
+
result = JSON.parse(response.body)
|
623
|
+
|
624
|
+
expect(result['scimType']).to eql('uniqueness')
|
625
|
+
expect(result['detail']).to include('already been taken')
|
626
|
+
end
|
627
|
+
|
628
|
+
it 'notes Rails validation failures' do
|
629
|
+
expect_any_instance_of(CustomCreateMockUsersController).to receive(:create).once.and_call_original
|
630
|
+
expect {
|
631
|
+
post "/CustomCreateUsers", params: {
|
632
|
+
format: :scim,
|
633
|
+
userName: MockUser::INVALID_USERNAME
|
634
|
+
}
|
635
|
+
}.to_not change { MockUser.count }
|
636
|
+
|
637
|
+
expect(response.status ).to eql(400)
|
638
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
639
|
+
|
640
|
+
result = JSON.parse(response.body)
|
641
|
+
|
642
|
+
expect(result['scimType']).to eql('invalidValue')
|
643
|
+
expect(result['detail']).to include('is reserved')
|
644
|
+
end
|
645
|
+
end # "context 'with a block' do"
|
400
646
|
end # "context '#create' do"
|
401
647
|
|
402
648
|
# ===========================================================================
|
@@ -404,26 +650,64 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
404
650
|
context '#replace' do
|
405
651
|
shared_examples 'a replacer' do | force_upper_case: |
|
406
652
|
it 'which replaces all attributes in an instance' do
|
407
|
-
attributes = { userName: '4' }
|
653
|
+
attributes = { userName: '4' } # Minimum required by schema
|
408
654
|
attributes = spec_helper_hupcase(attributes) if force_upper_case
|
409
655
|
|
656
|
+
# Prove that certain known pathways are called; can then unit test
|
657
|
+
# those if need be and be sure that this covers #replace actions.
|
658
|
+
#
|
410
659
|
expect_any_instance_of(MockUsersController).to receive(:replace).once.and_call_original
|
660
|
+
expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
|
411
661
|
expect {
|
412
662
|
put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
|
413
663
|
}.to_not change { MockUser.count }
|
414
664
|
|
415
|
-
expect(response.status).to eql(200)
|
665
|
+
expect(response.status ).to eql(200)
|
666
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
667
|
+
|
416
668
|
result = JSON.parse(response.body)
|
417
669
|
|
418
670
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
419
671
|
expect(result['meta']['resourceType']).to eql('User')
|
420
672
|
|
673
|
+
expect(result).to have_key('name')
|
674
|
+
expect(result).to_not have_key('password')
|
675
|
+
|
421
676
|
@u2.reload
|
422
677
|
|
423
678
|
expect(@u2.username).to eql('4')
|
424
679
|
expect(@u2.first_name).to be_nil
|
425
680
|
expect(@u2.last_name).to be_nil
|
426
681
|
expect(@u2.home_email_address).to be_nil
|
682
|
+
expect(@u2.password).to be_nil
|
683
|
+
end
|
684
|
+
|
685
|
+
it 'can replace passwords' do
|
686
|
+
attributes = { userName: '4', password: 'correcthorsebatterystaple' }
|
687
|
+
attributes = spec_helper_hupcase(attributes) if force_upper_case
|
688
|
+
|
689
|
+
expect {
|
690
|
+
put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
|
691
|
+
}.to_not change { MockUser.count }
|
692
|
+
|
693
|
+
expect(response.status ).to eql(200)
|
694
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
695
|
+
|
696
|
+
result = JSON.parse(response.body)
|
697
|
+
|
698
|
+
expect(result['id']).to eql(@u2.primary_key.to_s)
|
699
|
+
expect(result['meta']['resourceType']).to eql('User')
|
700
|
+
|
701
|
+
expect(result).to have_key('name')
|
702
|
+
expect(result).to_not have_key('password')
|
703
|
+
|
704
|
+
@u2.reload
|
705
|
+
|
706
|
+
expect(@u2.username).to eql('4')
|
707
|
+
expect(@u2.first_name).to be_nil
|
708
|
+
expect(@u2.last_name).to be_nil
|
709
|
+
expect(@u2.home_email_address).to be_nil
|
710
|
+
expect(@u2.password).to eql('correcthorsebatterystaple')
|
427
711
|
end
|
428
712
|
end # "shared_examples 'a replacer' do | force_upper_case: |"
|
429
713
|
|
@@ -443,8 +727,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
443
727
|
}
|
444
728
|
}.to_not change { MockUser.count }
|
445
729
|
|
446
|
-
expect(response.status).to eql(400)
|
730
|
+
expect(response.status ).to eql(400)
|
731
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
732
|
+
|
447
733
|
result = JSON.parse(response.body)
|
734
|
+
|
448
735
|
expect(result['scimType']).to eql('invalidValue')
|
449
736
|
expect(result['detail']).to include('is required')
|
450
737
|
|
@@ -458,13 +745,15 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
458
745
|
|
459
746
|
it 'notes Rails validation failures' do
|
460
747
|
expect {
|
461
|
-
|
748
|
+
put "/Users/#{@u2.primary_key}", params: {
|
462
749
|
format: :scim,
|
463
750
|
userName: MockUser::INVALID_USERNAME
|
464
751
|
}
|
465
752
|
}.to_not change { MockUser.count }
|
466
753
|
|
467
|
-
expect(response.status).to eql(400)
|
754
|
+
expect(response.status ).to eql(400)
|
755
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
756
|
+
|
468
757
|
result = JSON.parse(response.body)
|
469
758
|
|
470
759
|
expect(result['scimType']).to eql('invalidValue')
|
@@ -486,17 +775,72 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
486
775
|
}
|
487
776
|
}.to_not change { MockUser.count }
|
488
777
|
|
489
|
-
expect(response.status).to eql(404)
|
778
|
+
expect(response.status ).to eql(404)
|
779
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
780
|
+
|
490
781
|
result = JSON.parse(response.body)
|
782
|
+
|
491
783
|
expect(result['status']).to eql('404')
|
492
784
|
end
|
785
|
+
|
786
|
+
context 'with a block' do
|
787
|
+
it 'invokes the block' do
|
788
|
+
attributes = { userName: '4' } # Minimum required by schema
|
789
|
+
|
790
|
+
expect_any_instance_of(CustomReplaceMockUsersController).to receive(:replace).once.and_call_original
|
791
|
+
expect {
|
792
|
+
put "/CustomReplaceUsers/#{@u2.primary_key}", params: {
|
793
|
+
format: :scim,
|
794
|
+
userName: '4'
|
795
|
+
}
|
796
|
+
}.to_not change { MockUser.count }
|
797
|
+
|
798
|
+
expect(response.status ).to eql(200)
|
799
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
800
|
+
|
801
|
+
result = JSON.parse(response.body)
|
802
|
+
|
803
|
+
expect(result['id']).to eql(@u2.primary_key.to_s)
|
804
|
+
expect(result['meta']['resourceType']).to eql('User')
|
805
|
+
|
806
|
+
@u2.reload
|
807
|
+
|
808
|
+
expect(@u2.username ).to eql('4')
|
809
|
+
expect(@u2.first_name).to eql(CustomReplaceMockUsersController::OVERRIDDEN_NAME)
|
810
|
+
end
|
811
|
+
|
812
|
+
it 'notes Rails validation failures' do
|
813
|
+
expect_any_instance_of(CustomReplaceMockUsersController).to receive(:replace).once.and_call_original
|
814
|
+
expect {
|
815
|
+
put "/CustomReplaceUsers/#{@u2.primary_key}", params: {
|
816
|
+
format: :scim,
|
817
|
+
userName: MockUser::INVALID_USERNAME
|
818
|
+
}
|
819
|
+
}.to_not change { MockUser.count }
|
820
|
+
|
821
|
+
expect(response.status ).to eql(400)
|
822
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
823
|
+
|
824
|
+
result = JSON.parse(response.body)
|
825
|
+
|
826
|
+
expect(result['scimType']).to eql('invalidValue')
|
827
|
+
expect(result['detail']).to include('is reserved')
|
828
|
+
|
829
|
+
@u2.reload
|
830
|
+
|
831
|
+
expect(@u2.username).to eql('2')
|
832
|
+
expect(@u2.first_name).to eql('Foo')
|
833
|
+
expect(@u2.last_name).to eql('Bar')
|
834
|
+
expect(@u2.home_email_address).to eql('home_2@test.com')
|
835
|
+
end
|
836
|
+
end # "context 'with a block' do"
|
493
837
|
end # "context '#replace' do"
|
494
838
|
|
495
839
|
# ===========================================================================
|
496
840
|
|
497
841
|
context '#update' do
|
498
842
|
shared_examples 'an updater' do | force_upper_case: |
|
499
|
-
it 'which patches
|
843
|
+
it 'which patches regular attributes' do
|
500
844
|
payload = {
|
501
845
|
Operations: [
|
502
846
|
{
|
@@ -514,17 +858,27 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
514
858
|
|
515
859
|
payload = spec_helper_hupcase(payload) if force_upper_case
|
516
860
|
|
861
|
+
# Prove that certain known pathways are called; can then unit test
|
862
|
+
# those if need be and be sure that this covers #update actions.
|
863
|
+
#
|
517
864
|
expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
|
865
|
+
expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
|
866
|
+
|
518
867
|
expect {
|
519
868
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
520
869
|
}.to_not change { MockUser.count }
|
521
870
|
|
522
|
-
expect(response.status).to eql(200)
|
871
|
+
expect(response.status ).to eql(200)
|
872
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
873
|
+
|
523
874
|
result = JSON.parse(response.body)
|
524
875
|
|
525
876
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
526
877
|
expect(result['meta']['resourceType']).to eql('User')
|
527
878
|
|
879
|
+
expect(result).to have_key('name')
|
880
|
+
expect(result).to_not have_key('password')
|
881
|
+
|
528
882
|
@u2.reload
|
529
883
|
|
530
884
|
expect(@u2.username).to eql('4')
|
@@ -532,6 +886,151 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
532
886
|
expect(@u2.last_name).to eql('Bar')
|
533
887
|
expect(@u2.home_email_address).to eql('home_2@test.com')
|
534
888
|
expect(@u2.work_email_address).to eql('work_4@test.com')
|
889
|
+
expect(@u2.password).to eql('oldpassword')
|
890
|
+
end
|
891
|
+
|
892
|
+
context 'which' do
|
893
|
+
shared_examples 'it handles not-to-spec in-value Azure/Entra dotted attribute paths' do | operation |
|
894
|
+
it "and performs operation" do
|
895
|
+
payload = {
|
896
|
+
Operations: [
|
897
|
+
{
|
898
|
+
op: 'add',
|
899
|
+
value: {
|
900
|
+
'name.givenName' => 'Foo!',
|
901
|
+
'name.familyName' => 'Bar!',
|
902
|
+
'name.formatted' => 'Foo! Bar!' # Unrecognised; should be ignored
|
903
|
+
},
|
904
|
+
},
|
905
|
+
]
|
906
|
+
}
|
907
|
+
|
908
|
+
payload = spec_helper_hupcase(payload) if force_upper_case
|
909
|
+
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
910
|
+
|
911
|
+
expect(response.status ).to eql(200)
|
912
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
913
|
+
|
914
|
+
@u2.reload
|
915
|
+
result = JSON.parse(response.body)
|
916
|
+
|
917
|
+
expect(@u2.first_name).to eql('Foo!')
|
918
|
+
expect(@u2.last_name ).to eql('Bar!')
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
it_behaves_like 'it handles not-to-spec in-value Azure/Entra dotted attribute paths', 'add'
|
923
|
+
it_behaves_like 'it handles not-to-spec in-value Azure/Entra dotted attribute paths', 'replace'
|
924
|
+
|
925
|
+
shared_examples 'it handles schema ID value keys without inline attributes' do | operation |
|
926
|
+
it "and performs operation" do
|
927
|
+
payload = {
|
928
|
+
Operations: [
|
929
|
+
{
|
930
|
+
op: operation,
|
931
|
+
value: {
|
932
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
|
933
|
+
'organization' => 'Foo Bar!',
|
934
|
+
'department' => 'Bar Foo!'
|
935
|
+
},
|
936
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User': {
|
937
|
+
'manager' => 'Foo Baz!'
|
938
|
+
}
|
939
|
+
},
|
940
|
+
},
|
941
|
+
]
|
942
|
+
}
|
943
|
+
|
944
|
+
@u2.update!(organization: 'Old org')
|
945
|
+
payload = spec_helper_hupcase(payload) if force_upper_case
|
946
|
+
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
947
|
+
|
948
|
+
expect(response.status ).to eql(200)
|
949
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
950
|
+
|
951
|
+
@u2.reload
|
952
|
+
result = JSON.parse(response.body)
|
953
|
+
|
954
|
+
expect(@u2.organization).to eql('Foo Bar!')
|
955
|
+
expect(@u2.department ).to eql('Bar Foo!')
|
956
|
+
expect(@u2.manager ).to eql('Foo Baz!')
|
957
|
+
end
|
958
|
+
end
|
959
|
+
|
960
|
+
it_behaves_like 'it handles schema ID value keys without inline attributes', 'add'
|
961
|
+
it_behaves_like 'it handles schema ID value keys without inline attributes', 'replace'
|
962
|
+
|
963
|
+
shared_examples 'it handles schema ID value keys with inline attributes' do
|
964
|
+
it "and performs operation" do
|
965
|
+
payload = {
|
966
|
+
Operations: [
|
967
|
+
{
|
968
|
+
op: 'add',
|
969
|
+
value: {
|
970
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization' => 'Foo Bar!',
|
971
|
+
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department' => 'Bar Foo!',
|
972
|
+
'urn:ietf:params:scim:schemas:extension:manager:1.0:User:manager' => 'Foo Baz!'
|
973
|
+
},
|
974
|
+
},
|
975
|
+
]
|
976
|
+
}
|
977
|
+
|
978
|
+
@u2.update!(organization: 'Old org')
|
979
|
+
payload = spec_helper_hupcase(payload) if force_upper_case
|
980
|
+
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
981
|
+
|
982
|
+
expect(response.status ).to eql(200)
|
983
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
984
|
+
|
985
|
+
@u2.reload
|
986
|
+
result = JSON.parse(response.body)
|
987
|
+
|
988
|
+
expect(@u2.organization).to eql('Foo Bar!')
|
989
|
+
expect(@u2.department ).to eql('Bar Foo!')
|
990
|
+
expect(@u2.manager ).to eql('Foo Baz!')
|
991
|
+
end
|
992
|
+
end
|
993
|
+
|
994
|
+
it_behaves_like 'it handles schema ID value keys with inline attributes', 'add'
|
995
|
+
it_behaves_like 'it handles schema ID value keys with inline attributes', 'replace'
|
996
|
+
end
|
997
|
+
|
998
|
+
it 'which patches "returned: \'never\'" fields' do
|
999
|
+
payload = {
|
1000
|
+
Operations: [
|
1001
|
+
{
|
1002
|
+
op: 'replace',
|
1003
|
+
path: 'password',
|
1004
|
+
value: 'correcthorsebatterystaple'
|
1005
|
+
}
|
1006
|
+
]
|
1007
|
+
}
|
1008
|
+
|
1009
|
+
payload = spec_helper_hupcase(payload) if force_upper_case
|
1010
|
+
|
1011
|
+
expect {
|
1012
|
+
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
1013
|
+
}.to_not change { MockUser.count }
|
1014
|
+
|
1015
|
+
expect(response.status ).to eql(200)
|
1016
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1017
|
+
|
1018
|
+
result = JSON.parse(response.body)
|
1019
|
+
|
1020
|
+
expect(result['id']).to eql(@u2.primary_key.to_s)
|
1021
|
+
expect(result['meta']['resourceType']).to eql('User')
|
1022
|
+
|
1023
|
+
expect(result).to have_key('name')
|
1024
|
+
expect(result).to_not have_key('password')
|
1025
|
+
|
1026
|
+
@u2.reload
|
1027
|
+
|
1028
|
+
expect(@u2.username).to eql('2')
|
1029
|
+
expect(@u2.first_name).to eql('Foo')
|
1030
|
+
expect(@u2.last_name).to eql('Bar')
|
1031
|
+
expect(@u2.home_email_address).to eql('home_2@test.com')
|
1032
|
+
expect(@u2.work_email_address).to be_nil
|
1033
|
+
expect(@u2.password).to eql('correcthorsebatterystaple')
|
535
1034
|
end
|
536
1035
|
|
537
1036
|
context 'which clears attributes' do
|
@@ -556,7 +1055,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
556
1055
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
557
1056
|
}.to_not change { MockUser.count }
|
558
1057
|
|
559
|
-
expect(response.status).to eql(200)
|
1058
|
+
expect(response.status ).to eql(200)
|
1059
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1060
|
+
|
560
1061
|
result = JSON.parse(response.body)
|
561
1062
|
|
562
1063
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -588,7 +1089,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
588
1089
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
589
1090
|
}.to_not change { MockUser.count }
|
590
1091
|
|
591
|
-
expect(response.status).to eql(200)
|
1092
|
+
expect(response.status ).to eql(200)
|
1093
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1094
|
+
|
592
1095
|
result = JSON.parse(response.body)
|
593
1096
|
|
594
1097
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -620,7 +1123,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
620
1123
|
patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
|
621
1124
|
}.to_not change { MockUser.count }
|
622
1125
|
|
623
|
-
expect(response.status).to eql(200)
|
1126
|
+
expect(response.status ).to eql(200)
|
1127
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1128
|
+
|
624
1129
|
result = JSON.parse(response.body)
|
625
1130
|
|
626
1131
|
expect(result['id']).to eql(@u2.primary_key.to_s)
|
@@ -659,7 +1164,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
659
1164
|
}
|
660
1165
|
}.to_not change { MockUser.count }
|
661
1166
|
|
662
|
-
expect(response.status).to eql(400)
|
1167
|
+
expect(response.status ).to eql(400)
|
1168
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1169
|
+
|
663
1170
|
result = JSON.parse(response.body)
|
664
1171
|
|
665
1172
|
expect(result['scimType']).to eql('invalidValue')
|
@@ -687,8 +1194,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
687
1194
|
}
|
688
1195
|
}.to_not change { MockUser.count }
|
689
1196
|
|
690
|
-
expect(response.status).to eql(404)
|
1197
|
+
expect(response.status ).to eql(404)
|
1198
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1199
|
+
|
691
1200
|
result = JSON.parse(response.body)
|
1201
|
+
|
692
1202
|
expect(result['status']).to eql('404')
|
693
1203
|
end
|
694
1204
|
|
@@ -725,7 +1235,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
725
1235
|
|
726
1236
|
get "/Groups/#{@g1.id}", params: { format: :scim }
|
727
1237
|
|
728
|
-
expect(response.status).to eql(200)
|
1238
|
+
expect(response.status ).to eql(200)
|
1239
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1240
|
+
|
729
1241
|
result = JSON.parse(response.body)
|
730
1242
|
|
731
1243
|
expect(result['members']).to be_empty
|
@@ -752,7 +1264,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
752
1264
|
|
753
1265
|
get "/Groups/#{@g1.id}", params: { format: :scim }
|
754
1266
|
|
755
|
-
expect(response.status).to eql(200)
|
1267
|
+
expect(response.status ).to eql(200)
|
1268
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1269
|
+
|
756
1270
|
result = JSON.parse(response.body)
|
757
1271
|
|
758
1272
|
expect(result['members'].map { |m| m['value'] }.sort()).to eql(expected_remaining_user_ids)
|
@@ -827,12 +1341,178 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
827
1341
|
it_behaves_like 'a user remover'
|
828
1342
|
end # context 'and using a Salesforce variant payload' do
|
829
1343
|
end # "context 'when removing users from groups' do"
|
1344
|
+
|
1345
|
+
context 'with a block' do
|
1346
|
+
it 'invokes the block' do
|
1347
|
+
payload = {
|
1348
|
+
format: :scim,
|
1349
|
+
Operations: [
|
1350
|
+
{
|
1351
|
+
op: 'add',
|
1352
|
+
path: 'userName',
|
1353
|
+
value: '4'
|
1354
|
+
},
|
1355
|
+
{
|
1356
|
+
op: 'replace',
|
1357
|
+
path: 'emails[type eq "work"]',
|
1358
|
+
value: { type: 'work', value: 'work_4@test.com' }
|
1359
|
+
}
|
1360
|
+
]
|
1361
|
+
}
|
1362
|
+
|
1363
|
+
expect_any_instance_of(CustomUpdateMockUsersController).to receive(:update).once.and_call_original
|
1364
|
+
expect {
|
1365
|
+
patch "/CustomUpdateUsers/#{@u2.primary_key}", params: payload
|
1366
|
+
}.to_not change { MockUser.count }
|
1367
|
+
|
1368
|
+
expect(response.status ).to eql(200)
|
1369
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1370
|
+
|
1371
|
+
result = JSON.parse(response.body)
|
1372
|
+
|
1373
|
+
expect(result['id']).to eql(@u2.primary_key.to_s)
|
1374
|
+
expect(result['meta']['resourceType']).to eql('User')
|
1375
|
+
|
1376
|
+
@u2.reload
|
1377
|
+
|
1378
|
+
expect(@u2.username ).to eql('4')
|
1379
|
+
expect(@u2.first_name ).to eql(CustomUpdateMockUsersController::OVERRIDDEN_NAME)
|
1380
|
+
expect(@u2.work_email_address).to eql('work_4@test.com')
|
1381
|
+
end
|
1382
|
+
|
1383
|
+
it 'notes Rails validation failures' do
|
1384
|
+
expect_any_instance_of(CustomUpdateMockUsersController).to receive(:update).once.and_call_original
|
1385
|
+
expect {
|
1386
|
+
patch "/CustomUpdateUsers/#{@u2.primary_key}", params: {
|
1387
|
+
format: :scim,
|
1388
|
+
Operations: [
|
1389
|
+
{
|
1390
|
+
op: 'add',
|
1391
|
+
path: 'userName',
|
1392
|
+
value: MockUser::INVALID_USERNAME
|
1393
|
+
}
|
1394
|
+
]
|
1395
|
+
}
|
1396
|
+
}.to_not change { MockUser.count }
|
1397
|
+
|
1398
|
+
expect(response.status ).to eql(400)
|
1399
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1400
|
+
|
1401
|
+
result = JSON.parse(response.body)
|
1402
|
+
|
1403
|
+
expect(result['scimType']).to eql('invalidValue')
|
1404
|
+
expect(result['detail']).to include('is reserved')
|
1405
|
+
|
1406
|
+
@u2.reload
|
1407
|
+
|
1408
|
+
expect(@u2.username).to eql('2')
|
1409
|
+
expect(@u2.first_name).to eql('Foo')
|
1410
|
+
expect(@u2.last_name).to eql('Bar')
|
1411
|
+
expect(@u2.home_email_address).to eql('home_2@test.com')
|
1412
|
+
end
|
1413
|
+
end # "context 'with a block' do"
|
830
1414
|
end # "context '#update' do"
|
831
1415
|
|
1416
|
+
# ===========================================================================
|
1417
|
+
# In-passing parts of tests above show that #create, #replace and #update all
|
1418
|
+
# route through #save!, so now add some unit tests for that and for exception
|
1419
|
+
# handling overrides invoked via #save!.
|
1420
|
+
# ===========================================================================
|
1421
|
+
|
1422
|
+
context 'overriding #save!' do
|
1423
|
+
it 'invokes a block if given one' do
|
1424
|
+
mock_before = MockUser.all.to_a
|
1425
|
+
attributes = { userName: '5' } # Minimum required by schema
|
1426
|
+
|
1427
|
+
expect_any_instance_of(CustomSaveMockUsersController).to receive(:create).once.and_call_original
|
1428
|
+
expect {
|
1429
|
+
post "/CustomSaveUsers", params: attributes.merge(format: :scim)
|
1430
|
+
}.to change { MockUser.count }.by(1)
|
1431
|
+
|
1432
|
+
mock_after = MockUser.all.to_a
|
1433
|
+
new_mock = (mock_after - mock_before).first
|
1434
|
+
|
1435
|
+
expect(response.status ).to eql(201)
|
1436
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1437
|
+
|
1438
|
+
expect(new_mock.username).to eql(CustomSaveMockUsersController::CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
|
1439
|
+
end
|
1440
|
+
end # "context 'overriding #save!' do
|
1441
|
+
|
1442
|
+
context 'custom on-save exceptions' do
|
1443
|
+
MockUsersController.new.send(:scimitar_rescuable_exceptions).each do | exception_class |
|
1444
|
+
it "handles out-of-box exception #{exception_class}" do
|
1445
|
+
expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
|
1446
|
+
expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
|
1447
|
+
|
1448
|
+
expect_any_instance_of(MockUser).to receive(:save!).once { raise exception_class }
|
1449
|
+
|
1450
|
+
expect {
|
1451
|
+
post "/Users", params: { format: :scim, userName: SecureRandom.uuid }
|
1452
|
+
}.to_not change { MockUser.count }
|
1453
|
+
|
1454
|
+
expected_status, expected_prefix = if exception_class == ActiveRecord::RecordNotUnique
|
1455
|
+
[409, 'Operation failed due to a uniqueness constraint: ']
|
1456
|
+
else
|
1457
|
+
[400, 'Operation failed since record has become invalid: ']
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
expect(response.status ).to eql(expected_status)
|
1461
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1462
|
+
|
1463
|
+
result = JSON.parse(response.body)
|
1464
|
+
|
1465
|
+
# Check basic SCIM error rendering - good enough given other tests
|
1466
|
+
# elsewhere. Exact message varies by exception.
|
1467
|
+
#
|
1468
|
+
expect(result['detail']).to start_with(expected_prefix)
|
1469
|
+
end
|
1470
|
+
end
|
1471
|
+
|
1472
|
+
it 'handles custom exceptions' do
|
1473
|
+
exception_class = RuntimeError # (for testing only; usually, this would provoke a 500 response)
|
1474
|
+
|
1475
|
+
expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
|
1476
|
+
expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
|
1477
|
+
|
1478
|
+
expect_any_instance_of(MockUsersController).to receive(:scimitar_rescuable_exceptions).once { [ exception_class ] }
|
1479
|
+
expect_any_instance_of(MockUser ).to receive(:save! ).once { raise exception_class }
|
1480
|
+
|
1481
|
+
expect {
|
1482
|
+
post "/Users", params: { format: :scim, userName: SecureRandom.uuid }
|
1483
|
+
}.to_not change { MockUser.count }
|
1484
|
+
|
1485
|
+
expect(response.status ).to eql(400)
|
1486
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1487
|
+
|
1488
|
+
result = JSON.parse(response.body)
|
1489
|
+
|
1490
|
+
expect(result['detail']).to start_with('Operation failed since record has become invalid: ')
|
1491
|
+
end
|
1492
|
+
|
1493
|
+
it 'reports other exceptions as 500s' do
|
1494
|
+
expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
|
1495
|
+
expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
|
1496
|
+
|
1497
|
+
expect_any_instance_of(MockUser).to receive(:save!).once { raise RuntimeError }
|
1498
|
+
|
1499
|
+
expect {
|
1500
|
+
post "/Users", params: { format: :scim, userName: SecureRandom.uuid }
|
1501
|
+
}.to_not change { MockUser.count }
|
1502
|
+
|
1503
|
+
expect(response.status ).to eql(500)
|
1504
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1505
|
+
|
1506
|
+
result = JSON.parse(response.body)
|
1507
|
+
|
1508
|
+
expect(result['detail']).to eql('RuntimeError')
|
1509
|
+
end
|
1510
|
+
end
|
1511
|
+
|
832
1512
|
# ===========================================================================
|
833
1513
|
|
834
1514
|
context '#destroy' do
|
835
|
-
it 'deletes an item if given no
|
1515
|
+
it 'deletes an item if given no block' do
|
836
1516
|
expect_any_instance_of(MockUsersController).to receive(:destroy).once.and_call_original
|
837
1517
|
expect_any_instance_of(MockUser).to receive(:destroy!).once.and_call_original
|
838
1518
|
expect {
|
@@ -863,8 +1543,11 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
|
|
863
1543
|
delete '/Users/xyz', params: { format: :scim }
|
864
1544
|
}.to_not change { MockUser.count }
|
865
1545
|
|
866
|
-
expect(response.status).to eql(404)
|
1546
|
+
expect(response.status ).to eql(404)
|
1547
|
+
expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
|
1548
|
+
|
867
1549
|
result = JSON.parse(response.body)
|
1550
|
+
|
868
1551
|
expect(result['status']).to eql('404')
|
869
1552
|
end
|
870
1553
|
end # "context '#destroy' do"
|