scimitar 1.0.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 (106) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +16 -0
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
  4. data/app/controllers/scimitar/application_controller.rb +129 -0
  5. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  6. data/app/controllers/scimitar/resources_controller.rb +203 -0
  7. data/app/controllers/scimitar/schemas_controller.rb +16 -0
  8. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  9. data/app/models/scimitar/authentication_error.rb +9 -0
  10. data/app/models/scimitar/authentication_scheme.rb +18 -0
  11. data/app/models/scimitar/bulk.rb +8 -0
  12. data/app/models/scimitar/complex_types/address.rb +18 -0
  13. data/app/models/scimitar/complex_types/base.rb +41 -0
  14. data/app/models/scimitar/complex_types/email.rb +12 -0
  15. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  16. data/app/models/scimitar/complex_types/ims.rb +12 -0
  17. data/app/models/scimitar/complex_types/name.rb +12 -0
  18. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  19. data/app/models/scimitar/complex_types/photo.rb +12 -0
  20. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  21. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  22. data/app/models/scimitar/complex_types/role.rb +12 -0
  23. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  24. data/app/models/scimitar/engine_configuration.rb +24 -0
  25. data/app/models/scimitar/error_response.rb +20 -0
  26. data/app/models/scimitar/errors.rb +14 -0
  27. data/app/models/scimitar/filter.rb +11 -0
  28. data/app/models/scimitar/filter_error.rb +22 -0
  29. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  30. data/app/models/scimitar/lists/count.rb +64 -0
  31. data/app/models/scimitar/lists/query_parser.rb +730 -0
  32. data/app/models/scimitar/meta.rb +7 -0
  33. data/app/models/scimitar/not_found_error.rb +10 -0
  34. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  35. data/app/models/scimitar/resource_type.rb +29 -0
  36. data/app/models/scimitar/resources/base.rb +159 -0
  37. data/app/models/scimitar/resources/group.rb +13 -0
  38. data/app/models/scimitar/resources/mixin.rb +964 -0
  39. data/app/models/scimitar/resources/user.rb +13 -0
  40. data/app/models/scimitar/schema/address.rb +24 -0
  41. data/app/models/scimitar/schema/attribute.rb +123 -0
  42. data/app/models/scimitar/schema/base.rb +86 -0
  43. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  44. data/app/models/scimitar/schema/email.rb +10 -0
  45. data/app/models/scimitar/schema/entitlement.rb +10 -0
  46. data/app/models/scimitar/schema/group.rb +27 -0
  47. data/app/models/scimitar/schema/ims.rb +10 -0
  48. data/app/models/scimitar/schema/name.rb +20 -0
  49. data/app/models/scimitar/schema/phone_number.rb +10 -0
  50. data/app/models/scimitar/schema/photo.rb +10 -0
  51. data/app/models/scimitar/schema/reference_group.rb +23 -0
  52. data/app/models/scimitar/schema/reference_member.rb +21 -0
  53. data/app/models/scimitar/schema/role.rb +10 -0
  54. data/app/models/scimitar/schema/user.rb +52 -0
  55. data/app/models/scimitar/schema/vdtp.rb +18 -0
  56. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  57. data/app/models/scimitar/service_provider_configuration.rb +49 -0
  58. data/app/models/scimitar/supportable.rb +14 -0
  59. data/app/views/layouts/scimitar/application.html.erb +14 -0
  60. data/config/initializers/scimitar.rb +82 -0
  61. data/config/routes.rb +6 -0
  62. data/lib/scimitar.rb +23 -0
  63. data/lib/scimitar/engine.rb +63 -0
  64. data/lib/scimitar/version.rb +13 -0
  65. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  66. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  67. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  68. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  69. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  70. data/spec/apps/dummy/app/models/mock_user.rb +104 -0
  71. data/spec/apps/dummy/config/application.rb +17 -0
  72. data/spec/apps/dummy/config/boot.rb +2 -0
  73. data/spec/apps/dummy/config/environment.rb +2 -0
  74. data/spec/apps/dummy/config/environments/test.rb +15 -0
  75. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
  77. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  78. data/spec/apps/dummy/config/routes.rb +24 -0
  79. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
  80. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  81. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
  82. data/spec/apps/dummy/db/schema.rb +42 -0
  83. data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
  84. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  85. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  86. data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
  87. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  88. data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
  89. data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
  90. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  91. data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
  92. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  93. data/spec/models/scimitar/resources/base_spec.rb +289 -0
  94. data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
  95. data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
  96. data/spec/models/scimitar/resources/user_spec.rb +55 -0
  97. data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
  98. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  99. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  100. data/spec/models/scimitar/schema/user_spec.rb +710 -0
  101. data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
  102. data/spec/requests/application_controller_spec.rb +49 -0
  103. data/spec/requests/controller_configuration_spec.rb +17 -0
  104. data/spec/requests/engine_spec.rb +20 -0
  105. data/spec/spec_helper.rb +66 -0
  106. metadata +315 -0
@@ -0,0 +1,569 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
4
+ before :each do
5
+ allow_any_instance_of(Scimitar::ApplicationController).to receive(:authenticated?).and_return(true)
6
+
7
+ @u1 = MockUser.create(username: '1', first_name: 'Foo', last_name: 'Ark', home_email_address: 'home_1@test.com')
8
+ @u2 = MockUser.create(username: '2', first_name: 'Foo', last_name: 'Bar', home_email_address: 'home_2@test.com')
9
+ @u3 = MockUser.create(username: '3', first_name: 'Foo', home_email_address: 'home_3@test.com')
10
+ end
11
+
12
+ # ===========================================================================
13
+
14
+ context '#index' do
15
+ context 'with no items' do
16
+ it 'returns empty list' do
17
+ MockUser.delete_all
18
+
19
+ expect_any_instance_of(MockUsersController).to receive(:index).once.and_call_original
20
+ get '/Users', params: { format: :scim }
21
+
22
+ expect(response.status).to eql(200)
23
+ result = JSON.parse(response.body)
24
+
25
+ expect(result['totalResults']).to eql(0)
26
+ expect(result['startIndex' ]).to eql(1)
27
+ expect(result['itemsPerPage']).to eql(100)
28
+ end
29
+ end # "context 'with no items' do"
30
+
31
+ context 'with items' do
32
+ it 'returns all items' do
33
+ get '/Users', params: { format: :scim }
34
+
35
+ expect(response.status).to eql(200)
36
+ result = JSON.parse(response.body)
37
+
38
+ expect(result['totalResults']).to eql(3)
39
+ expect(result['Resources'].size).to eql(3)
40
+
41
+ ids = result['Resources'].map { |resource| resource['id'] }
42
+ expect(ids).to match_array([@u1.id.to_s, @u2.id.to_s, @u3.id.to_s])
43
+
44
+ usernames = result['Resources'].map { |resource| resource['userName'] }
45
+ expect(usernames).to match_array(['1', '2', '3'])
46
+ end
47
+
48
+ it 'applies a filter, with case-insensitive value comparison' do
49
+ get '/Users', params: {
50
+ format: :scim,
51
+ filter: 'name.givenName eq "Foo" and name.familyName pr and emails ne "home_1@TEST.COM"'
52
+ }
53
+
54
+ expect(response.status).to eql(200)
55
+ result = JSON.parse(response.body)
56
+
57
+ expect(result['totalResults']).to eql(1)
58
+ expect(result['Resources'].size).to eql(1)
59
+
60
+ ids = result['Resources'].map { |resource| resource['id'] }
61
+ expect(ids).to match_array([@u2.id.to_s])
62
+
63
+ usernames = result['Resources'].map { |resource| resource['userName'] }
64
+ expect(usernames).to match_array(['2'])
65
+ end
66
+
67
+ it 'obeys a page size' do
68
+ get '/Users', params: {
69
+ format: :scim,
70
+ count: 2
71
+ }
72
+
73
+ expect(response.status).to eql(200)
74
+ result = JSON.parse(response.body)
75
+
76
+ expect(result['totalResults']).to eql(3)
77
+ expect(result['Resources'].size).to eql(2)
78
+
79
+ ids = result['Resources'].map { |resource| resource['id'] }
80
+ expect(ids).to match_array([@u1.id.to_s, @u2.id.to_s])
81
+
82
+ usernames = result['Resources'].map { |resource| resource['userName'] }
83
+ expect(usernames).to match_array(['1', '2'])
84
+ end
85
+
86
+ it 'obeys start-at-1 offsets' do
87
+ get '/Users', params: {
88
+ format: :scim,
89
+ startIndex: 2
90
+ }
91
+
92
+ expect(response.status).to eql(200)
93
+ result = JSON.parse(response.body)
94
+
95
+ expect(result['totalResults']).to eql(3)
96
+ expect(result['Resources'].size).to eql(2)
97
+
98
+ ids = result['Resources'].map { |resource| resource['id'] }
99
+ expect(ids).to match_array([@u2.id.to_s, @u3.id.to_s])
100
+
101
+ usernames = result['Resources'].map { |resource| resource['userName'] }
102
+ expect(usernames).to match_array(['2', '3'])
103
+ end
104
+ end # "context 'with items' do"
105
+
106
+ context 'with bad calls' do
107
+ it 'complains about bad filters' do
108
+ get '/Users', params: {
109
+ format: :scim,
110
+ filter: 'name.givenName'
111
+ }
112
+
113
+ expect(response.status).to eql(400)
114
+ result = JSON.parse(response.body)
115
+ expect(result['scimType']).to eql('invalidFilter')
116
+ end
117
+ end # "context 'with bad calls' do"
118
+ end # "context '#index' do"
119
+
120
+ # ===========================================================================
121
+
122
+ context '#show' do
123
+ it 'shows an item' do
124
+ expect_any_instance_of(MockUsersController).to receive(:show).once.and_call_original
125
+ get "/Users/#{@u2.id}", params: { format: :scim }
126
+
127
+ expect(response.status).to eql(200)
128
+ result = JSON.parse(response.body)
129
+
130
+ expect(result['id']).to eql(@u2.id.to_s) # Note - ID was converted String; not Integer
131
+ expect(result['userName']).to eql('2')
132
+ expect(result['name']['familyName']).to eql('Bar')
133
+ expect(result['meta']['resourceType']).to eql('User')
134
+ end
135
+
136
+ it 'renders 404' do
137
+ get '/Users/xyz', params: { format: :scim }
138
+
139
+ expect(response.status).to eql(404)
140
+ result = JSON.parse(response.body)
141
+ expect(result['status']).to eql('404')
142
+ end
143
+ end # "context '#show' do"
144
+
145
+ # ===========================================================================
146
+
147
+ context '#create' do
148
+ context 'creates an item' do
149
+ it 'with minimal parameters' do
150
+ mock_before = MockUser.all.to_a
151
+
152
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
153
+ expect {
154
+ post "/Users", params: {
155
+ format: :scim,
156
+ userName: '4' # Minimum required by schema
157
+ }
158
+ }.to change { MockUser.count }.by(1)
159
+
160
+ mock_after = MockUser.all.to_a
161
+ new_mock = (mock_after - mock_before).first
162
+
163
+ expect(response.status).to eql(201)
164
+ result = JSON.parse(response.body)
165
+
166
+ expect(result['id']).to eql(new_mock.id.to_s)
167
+ expect(result['meta']['resourceType']).to eql('User')
168
+ expect(new_mock.username).to eql('4')
169
+ end
170
+
171
+ # A bit of extra coverage just for general confidence.
172
+ #
173
+ it 'with more comprehensive parameters' do
174
+ mock_before = MockUser.all.to_a
175
+
176
+ expect {
177
+ post "/Users", params: {
178
+ format: :scim,
179
+ userName: '4',
180
+ name: {
181
+ givenName: 'Given',
182
+ familyName: 'Family'
183
+ },
184
+ emails: [
185
+ {
186
+ type: 'work',
187
+ value: 'work_4@test.com'
188
+ },
189
+ {
190
+ type: 'home',
191
+ value: 'home_4@test.com'
192
+ }
193
+ ]
194
+ }
195
+ }.to change { MockUser.count }.by(1)
196
+
197
+ mock_after = MockUser.all.to_a
198
+ new_mock = (mock_after - mock_before).first
199
+
200
+ expect(response.status).to eql(201)
201
+ result = JSON.parse(response.body)
202
+
203
+ expect(result['id']).to eql(new_mock.id.to_s)
204
+ expect(result['meta']['resourceType']).to eql('User')
205
+ expect(new_mock.username).to eql('4')
206
+ expect(new_mock.first_name).to eql('Given')
207
+ expect(new_mock.last_name).to eql('Family')
208
+ expect(new_mock.home_email_address).to eql('home_4@test.com')
209
+ expect(new_mock.work_email_address).to eql('work_4@test.com')
210
+ end
211
+ end
212
+
213
+ it 'returns 409 for duplicates (by Rails validation)' do
214
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
215
+ expect {
216
+ post "/Users", params: {
217
+ format: :scim,
218
+ userName: '1' # Already exists
219
+ }
220
+ }.to_not change { MockUser.count }
221
+
222
+ expect(response.status).to eql(409)
223
+ result = JSON.parse(response.body)
224
+ expect(result['scimType']).to eql('uniqueness')
225
+ expect(result['detail']).to include('already been taken')
226
+ end
227
+
228
+ it 'notes schema validation failures' do
229
+ expect {
230
+ post "/Users", params: {
231
+ format: :scim
232
+ # userName parameter is required by schema, but missing
233
+ }
234
+ }.to_not change { MockUser.count }
235
+
236
+ expect(response.status).to eql(400)
237
+ result = JSON.parse(response.body)
238
+ expect(result['scimType']).to eql('invalidValue')
239
+ expect(result['detail']).to include('is required')
240
+ end
241
+
242
+ it 'notes Rails validation failures' do
243
+ expect {
244
+ post "/Users", params: {
245
+ format: :scim,
246
+ userName: MockUser::INVALID_USERNAME
247
+ }
248
+ }.to_not change { MockUser.count }
249
+
250
+ expect(response.status).to eql(400)
251
+ result = JSON.parse(response.body)
252
+
253
+ expect(result['scimType']).to eql('invalidValue')
254
+ expect(result['detail']).to include('is reserved')
255
+ end
256
+ end # "context '#create' do"
257
+
258
+ # ===========================================================================
259
+
260
+ context '#replace' do
261
+ it 'replaces all attributes in an instance' do
262
+ expect_any_instance_of(MockUsersController).to receive(:replace).once.and_call_original
263
+ expect {
264
+ put "/Users/#{@u2.id}", params: {
265
+ format: :scim,
266
+ userName: '4' # Minimum required by schema
267
+ }
268
+ }.to_not change { MockUser.count }
269
+
270
+ expect(response.status).to eql(200)
271
+ result = JSON.parse(response.body)
272
+
273
+ expect(result['id']).to eql(@u2.id.to_s)
274
+ expect(result['meta']['resourceType']).to eql('User')
275
+
276
+ @u2.reload
277
+
278
+ expect(@u2.username).to eql('4')
279
+ expect(@u2.first_name).to be_nil
280
+ expect(@u2.last_name).to be_nil
281
+ expect(@u2.home_email_address).to be_nil
282
+ end
283
+
284
+ it 'notes schema validation failures' do
285
+ expect {
286
+ put "/Users/#{@u2.id}", params: {
287
+ format: :scim
288
+ # userName parameter is required by schema, but missing
289
+ }
290
+ }.to_not change { MockUser.count }
291
+
292
+ expect(response.status).to eql(400)
293
+ result = JSON.parse(response.body)
294
+ expect(result['scimType']).to eql('invalidValue')
295
+ expect(result['detail']).to include('is required')
296
+
297
+ @u2.reload
298
+
299
+ expect(@u2.username).to eql('2')
300
+ expect(@u2.first_name).to eql('Foo')
301
+ expect(@u2.last_name).to eql('Bar')
302
+ expect(@u2.home_email_address).to eql('home_2@test.com')
303
+ end
304
+
305
+ it 'notes Rails validation failures' do
306
+ expect {
307
+ post "/Users", params: {
308
+ format: :scim,
309
+ userName: MockUser::INVALID_USERNAME
310
+ }
311
+ }.to_not change { MockUser.count }
312
+
313
+ expect(response.status).to eql(400)
314
+ result = JSON.parse(response.body)
315
+
316
+ expect(result['scimType']).to eql('invalidValue')
317
+ expect(result['detail']).to include('is reserved')
318
+
319
+ @u2.reload
320
+
321
+ expect(@u2.username).to eql('2')
322
+ expect(@u2.first_name).to eql('Foo')
323
+ expect(@u2.last_name).to eql('Bar')
324
+ expect(@u2.home_email_address).to eql('home_2@test.com')
325
+ end
326
+
327
+ it 'returns 404 if ID is invalid' do
328
+ expect {
329
+ put '/Users/xyz', params: {
330
+ format: :scim,
331
+ userName: '4' # Minimum required by schema
332
+ }
333
+ }.to_not change { MockUser.count }
334
+
335
+ expect(response.status).to eql(404)
336
+ result = JSON.parse(response.body)
337
+ expect(result['status']).to eql('404')
338
+ end
339
+ end # "context '#replace' do"
340
+
341
+ # ===========================================================================
342
+
343
+ context '#update' do
344
+ it 'patches specific attributes' do
345
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
346
+ expect {
347
+ patch "/Users/#{@u2.id}", params: {
348
+ format: :scim,
349
+ Operations: [
350
+ {
351
+ op: 'add',
352
+ path: 'userName',
353
+ value: '4'
354
+ },
355
+ {
356
+ op: 'replace',
357
+ path: 'emails[type eq "work"]',
358
+ value: { type: 'work', value: 'work_4@test.com' }
359
+ }
360
+ ]
361
+ }
362
+ }.to_not change { MockUser.count }
363
+
364
+ expect(response.status).to eql(200)
365
+ result = JSON.parse(response.body)
366
+
367
+ expect(result['id']).to eql(@u2.id.to_s)
368
+ expect(result['meta']['resourceType']).to eql('User')
369
+
370
+ @u2.reload
371
+
372
+ expect(@u2.username).to eql('4')
373
+ expect(@u2.first_name).to eql('Foo')
374
+ expect(@u2.last_name).to eql('Bar')
375
+ expect(@u2.home_email_address).to eql('home_2@test.com')
376
+ expect(@u2.work_email_address).to eql('work_4@test.com')
377
+ end
378
+
379
+ context 'clears attributes' do
380
+ before :each do
381
+ @u2.update!(work_email_address: 'work_2@test.com')
382
+ end
383
+
384
+ it 'with simple paths' do
385
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
386
+ expect {
387
+ patch "/Users/#{@u2.id}", params: {
388
+ format: :scim,
389
+ Operations: [
390
+ {
391
+ op: 'remove',
392
+ path: 'name.givenName'
393
+ }
394
+ ]
395
+ }
396
+ }.to_not change { MockUser.count }
397
+
398
+ expect(response.status).to eql(200)
399
+ result = JSON.parse(response.body)
400
+
401
+ expect(result['id']).to eql(@u2.id.to_s)
402
+ expect(result['meta']['resourceType']).to eql('User')
403
+
404
+ @u2.reload
405
+
406
+ expect(@u2.username).to eql('2')
407
+ expect(@u2.first_name).to be_nil
408
+ expect(@u2.last_name).to eql('Bar')
409
+ expect(@u2.home_email_address).to eql('home_2@test.com')
410
+ expect(@u2.work_email_address).to eql('work_2@test.com')
411
+ end
412
+
413
+ it 'by array entry filter match' do
414
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
415
+ expect {
416
+ patch "/Users/#{@u2.id}", params: {
417
+ format: :scim,
418
+ Operations: [
419
+ {
420
+ op: 'remove',
421
+ path: 'emails[type eq "work"]'
422
+ }
423
+ ]
424
+ }
425
+ }.to_not change { MockUser.count }
426
+
427
+ expect(response.status).to eql(200)
428
+ result = JSON.parse(response.body)
429
+
430
+ expect(result['id']).to eql(@u2.id.to_s)
431
+ expect(result['meta']['resourceType']).to eql('User')
432
+
433
+ @u2.reload
434
+
435
+ expect(@u2.username).to eql('2')
436
+ expect(@u2.first_name).to eql('Foo')
437
+ expect(@u2.last_name).to eql('Bar')
438
+ expect(@u2.home_email_address).to eql('home_2@test.com')
439
+ expect(@u2.work_email_address).to be_nil
440
+ end
441
+
442
+ it 'by whole collection' do
443
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
444
+ expect {
445
+ patch "/Users/#{@u2.id}", params: {
446
+ format: :scim,
447
+ Operations: [
448
+ {
449
+ op: 'remove',
450
+ path: 'emails'
451
+ }
452
+ ]
453
+ }
454
+ }.to_not change { MockUser.count }
455
+
456
+ expect(response.status).to eql(200)
457
+ result = JSON.parse(response.body)
458
+
459
+ expect(result['id']).to eql(@u2.id.to_s)
460
+ expect(result['meta']['resourceType']).to eql('User')
461
+
462
+ @u2.reload
463
+
464
+ expect(@u2.username).to eql('2')
465
+ expect(@u2.first_name).to eql('Foo')
466
+ expect(@u2.last_name).to eql('Bar')
467
+ expect(@u2.home_email_address).to be_nil
468
+ expect(@u2.work_email_address).to be_nil
469
+ end
470
+ end # "context 'clears attributes' do"
471
+
472
+ it 'notes Rails validation failures' do
473
+ expect {
474
+ patch "/Users/#{@u2.id}", params: {
475
+ format: :scim,
476
+ Operations: [
477
+ {
478
+ op: 'add',
479
+ path: 'userName',
480
+ value: MockUser::INVALID_USERNAME
481
+ }
482
+ ]
483
+ }
484
+ }.to_not change { MockUser.count }
485
+
486
+ expect(response.status).to eql(400)
487
+ result = JSON.parse(response.body)
488
+
489
+ expect(result['scimType']).to eql('invalidValue')
490
+ expect(result['detail']).to include('is reserved')
491
+
492
+ @u2.reload
493
+
494
+ expect(@u2.username).to eql('2')
495
+ expect(@u2.first_name).to eql('Foo')
496
+ expect(@u2.last_name).to eql('Bar')
497
+ expect(@u2.home_email_address).to eql('home_2@test.com')
498
+ end
499
+
500
+ it 'returns 404 if ID is invalid' do
501
+ expect {
502
+ patch '/Users/xyz', params: {
503
+ format: :scim,
504
+ Operations: [
505
+ {
506
+ op: 'add',
507
+ path: 'userName',
508
+ value: '4'
509
+ }
510
+ ]
511
+ }
512
+ }.to_not change { MockUser.count }
513
+
514
+ expect(response.status).to eql(404)
515
+ result = JSON.parse(response.body)
516
+ expect(result['status']).to eql('404')
517
+ end
518
+ end # "context '#update' do"
519
+
520
+ # ===========================================================================
521
+
522
+ context '#destroy' do
523
+ it 'deletes an item if given no blok' do
524
+ expect_any_instance_of(MockUsersController).to receive(:destroy).once.and_call_original
525
+ expect_any_instance_of(MockUser).to receive(:destroy!).once.and_call_original
526
+ expect {
527
+ delete "/Users/#{@u2.id}", params: { format: :scim }
528
+ }.to change { MockUser.count }.by(-1)
529
+
530
+ expect(response.status).to eql(204)
531
+ expect(response.body).to be_empty
532
+ end
533
+
534
+ it 'invokes a block if given one' do
535
+ expect_any_instance_of(CustomDestroyMockUsersController).to receive(:destroy).once.and_call_original
536
+ expect_any_instance_of(MockUser).to_not receive(:destroy!)
537
+
538
+ expect {
539
+ delete "/CustomDestroyUsers/#{@u2.id}", params: { format: :scim }
540
+ }.to_not change { MockUser.count }
541
+
542
+ expect(response.status).to eql(204)
543
+ expect(response.body).to be_empty
544
+
545
+ @u2.reload
546
+ expect(@u2.username).to eql(CustomDestroyMockUsersController::NOT_REALLY_DELETED_USERNAME_INDICATOR)
547
+ end
548
+
549
+ it 'returns 404 if ID is invalid' do
550
+ expect {
551
+ delete '/Users/xyz', params: { format: :scim }
552
+ }.to_not change { MockUser.count }
553
+
554
+ expect(response.status).to eql(404)
555
+ result = JSON.parse(response.body)
556
+ expect(result['status']).to eql('404')
557
+ end
558
+ end # "context '#destroy' do"
559
+
560
+ # ===========================================================================
561
+
562
+ context 'service methods' do
563
+ context '#storage_scope' do
564
+ it 'raises "not implemented" to warn subclass authors' do
565
+ expect { described_class.new.send(:storage_scope) }.to raise_error(NotImplementedError)
566
+ end
567
+ end # "context '#storage_class' do"
568
+ end # "context 'service methods' do"
569
+ end