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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -0
  3. data/README.md +721 -0
  4. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +72 -18
  5. data/app/controllers/scimitar/application_controller.rb +17 -9
  6. data/app/controllers/scimitar/resource_types_controller.rb +7 -3
  7. data/app/controllers/scimitar/resources_controller.rb +0 -2
  8. data/app/controllers/scimitar/schemas_controller.rb +366 -3
  9. data/app/controllers/scimitar/service_provider_configurations_controller.rb +3 -2
  10. data/app/models/scimitar/complex_types/address.rb +0 -6
  11. data/app/models/scimitar/complex_types/base.rb +2 -2
  12. data/app/models/scimitar/engine_configuration.rb +3 -1
  13. data/app/models/scimitar/lists/query_parser.rb +97 -12
  14. data/app/models/scimitar/resource_invalid_error.rb +1 -1
  15. data/app/models/scimitar/resource_type.rb +4 -6
  16. data/app/models/scimitar/resources/base.rb +52 -8
  17. data/app/models/scimitar/resources/mixin.rb +539 -76
  18. data/app/models/scimitar/schema/attribute.rb +18 -8
  19. data/app/models/scimitar/schema/base.rb +2 -2
  20. data/app/models/scimitar/schema/name.rb +2 -2
  21. data/app/models/scimitar/schema/user.rb +10 -10
  22. data/config/initializers/scimitar.rb +49 -3
  23. data/lib/scimitar/engine.rb +57 -12
  24. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
  25. data/lib/scimitar/support/utilities.rb +111 -0
  26. data/lib/scimitar/version.rb +2 -2
  27. data/lib/scimitar.rb +1 -0
  28. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  29. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  30. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  31. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  32. data/spec/apps/dummy/app/models/mock_user.rb +20 -3
  33. data/spec/apps/dummy/config/application.rb +8 -0
  34. data/spec/apps/dummy/config/initializers/scimitar.rb +40 -3
  35. data/spec/apps/dummy/config/routes.rb +18 -1
  36. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +2 -0
  37. data/spec/apps/dummy/db/schema.rb +3 -1
  38. data/spec/controllers/scimitar/application_controller_spec.rb +56 -2
  39. data/spec/controllers/scimitar/resource_types_controller_spec.rb +8 -4
  40. data/spec/controllers/scimitar/schemas_controller_spec.rb +344 -48
  41. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +1 -0
  42. data/spec/models/scimitar/complex_types/address_spec.rb +3 -4
  43. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  44. data/spec/models/scimitar/resources/base_spec.rb +55 -13
  45. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  46. data/spec/models/scimitar/resources/mixin_spec.rb +781 -124
  47. data/spec/models/scimitar/schema/attribute_spec.rb +22 -0
  48. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  49. data/spec/requests/active_record_backed_resources_controller_spec.rb +723 -40
  50. data/spec/requests/engine_spec.rb +75 -0
  51. data/spec/spec_helper.rb +10 -2
  52. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
  53. 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
- it 'returns empty list' do
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' } # Minimum required by schema
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
- post "/Users", params: {
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 specific attributes' do
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 blok' do
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"