scimitar 1.10.0 → 2.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +23 -98
  3. data/app/controllers/scimitar/application_controller.rb +13 -41
  4. data/app/controllers/scimitar/resource_types_controller.rb +2 -0
  5. data/app/controllers/scimitar/resources_controller.rb +2 -0
  6. data/app/controllers/scimitar/schemas_controller.rb +3 -366
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +1 -0
  8. data/app/models/scimitar/complex_types/address.rb +6 -0
  9. data/app/models/scimitar/engine_configuration.rb +5 -15
  10. data/app/models/scimitar/error_response.rb +0 -12
  11. data/app/models/scimitar/lists/query_parser.rb +13 -113
  12. data/app/models/scimitar/resource_invalid_error.rb +1 -1
  13. data/app/models/scimitar/resources/base.rb +9 -53
  14. data/app/models/scimitar/resources/mixin.rb +59 -646
  15. data/app/models/scimitar/schema/address.rb +0 -1
  16. data/app/models/scimitar/schema/attribute.rb +5 -14
  17. data/app/models/scimitar/schema/base.rb +1 -1
  18. data/app/models/scimitar/schema/name.rb +2 -2
  19. data/app/models/scimitar/schema/user.rb +10 -10
  20. data/app/models/scimitar/schema/vdtp.rb +1 -1
  21. data/app/models/scimitar/service_provider_configuration.rb +3 -14
  22. data/config/initializers/scimitar.rb +3 -69
  23. data/lib/scimitar/engine.rb +12 -57
  24. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +10 -140
  25. data/lib/scimitar/version.rb +2 -2
  26. data/lib/scimitar.rb +2 -7
  27. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +1 -1
  28. data/spec/apps/dummy/app/models/mock_group.rb +1 -1
  29. data/spec/apps/dummy/app/models/mock_user.rb +9 -52
  30. data/spec/apps/dummy/config/application.rb +1 -0
  31. data/spec/apps/dummy/config/environments/test.rb +28 -5
  32. data/spec/apps/dummy/config/initializers/scimitar.rb +10 -90
  33. data/spec/apps/dummy/config/routes.rb +7 -28
  34. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -11
  35. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +3 -8
  36. data/spec/apps/dummy/db/schema.rb +4 -12
  37. data/spec/controllers/scimitar/application_controller_spec.rb +3 -126
  38. data/spec/controllers/scimitar/resource_types_controller_spec.rb +2 -2
  39. data/spec/controllers/scimitar/schemas_controller_spec.rb +48 -344
  40. data/spec/models/scimitar/complex_types/address_spec.rb +4 -3
  41. data/spec/models/scimitar/complex_types/email_spec.rb +2 -0
  42. data/spec/models/scimitar/lists/query_parser_spec.rb +9 -146
  43. data/spec/models/scimitar/resources/base_spec.rb +71 -217
  44. data/spec/models/scimitar/resources/base_validation_spec.rb +5 -43
  45. data/spec/models/scimitar/resources/mixin_spec.rb +129 -1508
  46. data/spec/models/scimitar/schema/attribute_spec.rb +3 -22
  47. data/spec/models/scimitar/schema/base_spec.rb +1 -1
  48. data/spec/models/scimitar/schema/user_spec.rb +2 -12
  49. data/spec/requests/active_record_backed_resources_controller_spec.rb +66 -1016
  50. data/spec/requests/application_controller_spec.rb +3 -16
  51. data/spec/requests/engine_spec.rb +0 -75
  52. data/spec/spec_helper.rb +1 -9
  53. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +0 -108
  54. metadata +26 -37
  55. data/LICENSE.txt +0 -21
  56. data/README.md +0 -717
  57. data/lib/scimitar/support/utilities.rb +0 -111
  58. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +0 -25
  59. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +0 -25
  60. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +0 -24
  61. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +0 -25
@@ -1,42 +1,25 @@
1
1
  require 'spec_helper'
2
- require 'time'
3
2
 
4
3
  RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
5
4
  before :each do
6
5
  allow_any_instance_of(Scimitar::ApplicationController).to receive(:authenticated?).and_return(true)
7
6
 
8
- # If a sort order is unspecified, the controller defaults to ID ascending.
9
- # With UUID based IDs, testing life is made easier by ensuring that the
10
- # creation order matches an ascending UUID sort order (which is what would
11
- # happen if we were using integer primary keys).
12
- #
13
- lmt = Time.parse("2023-01-09 14:25:00 +1300")
14
- ids = 3.times.map { SecureRandom.uuid }.sort()
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, 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
-
20
- @g1 = MockGroup.create!(display_name: 'Group 1')
21
- @g2 = MockGroup.create!(display_name: 'Group 2')
22
- @g3 = MockGroup.create!(display_name: 'Group 3')
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')
23
10
  end
24
11
 
25
12
  # ===========================================================================
26
13
 
27
14
  context '#index' do
28
15
  context 'with no items' do
29
- before :each do
16
+ it 'returns empty list' do
30
17
  MockUser.delete_all
31
- end
32
18
 
33
- it 'returns empty list' do
34
19
  expect_any_instance_of(MockUsersController).to receive(:index).once.and_call_original
35
20
  get '/Users', params: { format: :scim }
36
21
 
37
- expect(response.status ).to eql(200)
38
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
39
-
22
+ expect(response.status).to eql(200)
40
23
  result = JSON.parse(response.body)
41
24
 
42
25
  expect(result['totalResults']).to eql(0)
@@ -46,227 +29,55 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
46
29
  end # "context 'with no items' do"
47
30
 
48
31
  context 'with items' do
49
- context 'with a UUID, renamed primary key column' do
50
- it 'returns all items' do
51
- get '/Users', params: { format: :scim }
52
-
53
- expect(response.status ).to eql(200)
54
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
55
-
56
- result = JSON.parse(response.body)
57
-
58
- expect(result['totalResults']).to eql(3)
59
- expect(result['Resources'].size).to eql(3)
60
-
61
- ids = result['Resources'].map { |resource| resource['id'] }
62
- expect(ids).to match_array([@u1.primary_key.to_s, @u2.primary_key.to_s, @u3.primary_key.to_s])
63
-
64
- usernames = result['Resources'].map { |resource| resource['userName'] }
65
- expect(usernames).to match_array(['1', '2', '3'])
66
- end
67
- end # "context 'with a UUID, renamed primary key column' do"
68
-
69
- context 'with an integer, conventionally named primary key column' do
70
- it 'returns all items' do
71
- get '/Groups', params: { format: :scim }
72
-
73
- expect(response.status ).to eql(200)
74
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
75
-
76
- result = JSON.parse(response.body)
77
-
78
- expect(result['totalResults']).to eql(3)
79
- expect(result['Resources'].size).to eql(3)
80
-
81
- ids = result['Resources'].map { |resource| resource['id'] }
82
- expect(ids).to match_array([@g1.id.to_s, @g2.id.to_s, @g3.id.to_s])
83
-
84
- usernames = result['Resources'].map { |resource| resource['displayName'] }
85
- expect(usernames).to match_array(['Group 1', 'Group 2', 'Group 3'])
86
- end
87
- end # "context 'with an integer, conventionally named primary key column' do"
88
-
89
- it 'applies a filter, with case-insensitive value comparison' do
90
- get '/Users', params: {
91
- format: :scim,
92
- filter: 'name.givenName eq "FOO" and name.familyName pr and emails ne "home_1@test.com"'
93
- }
94
-
95
- expect(response.status ).to eql(200)
96
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
97
-
98
- result = JSON.parse(response.body)
99
-
100
- expect(result['totalResults']).to eql(1)
101
- expect(result['Resources'].size).to eql(1)
102
-
103
- ids = result['Resources'].map { |resource| resource['id'] }
104
- expect(ids).to match_array([@u2.primary_key.to_s])
105
-
106
- usernames = result['Resources'].map { |resource| resource['userName'] }
107
- expect(usernames).to match_array(['2'])
108
- end
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')
32
+ it 'returns all items' do
33
+ get '/Users', params: { format: :scim }
118
34
 
35
+ expect(response.status).to eql(200)
119
36
  result = JSON.parse(response.body)
120
37
 
121
38
  expect(result['totalResults']).to eql(3)
122
39
  expect(result['Resources'].size).to eql(3)
123
40
 
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/RIPAGlobal/scimitar/issues/37
140
- #
141
- it 'applies a filter, with case-insensitive attribute matching (GitHub issue #37)' do
142
- get '/Users', params: {
143
- format: :scim,
144
- filter: 'name.GIVENNAME eq "Foo" and name.Familyname pr and emails ne "home_1@test.com"'
145
- }
146
-
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
41
  ids = result['Resources'].map { |resource| resource['id'] }
156
- expect(ids).to match_array([@u2.primary_key.to_s])
42
+ expect(ids).to match_array([@u1.id.to_s, @u2.id.to_s, @u3.id.to_s])
157
43
 
158
44
  usernames = result['Resources'].map { |resource| resource['userName'] }
159
- expect(usernames).to match_array(['2'])
45
+ expect(usernames).to match_array(['1', '2', '3'])
160
46
  end
161
47
 
162
- # https://github.com/RIPAGlobal/scimitar/issues/115
163
- #
164
- it 'handles broken Microsoft filters (GitHub issue #115)' do
48
+ it 'applies a filter, with case-insensitive value comparison' do
165
49
  get '/Users', params: {
166
50
  format: :scim,
167
- filter: 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"'
51
+ filter: 'name.givenName eq "Foo" and name.familyName pr and emails ne "home_1@TEST.COM"'
168
52
  }
169
53
 
170
- expect(response.status ).to eql(200)
171
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
172
-
54
+ expect(response.status).to eql(200)
173
55
  result = JSON.parse(response.body)
174
56
 
175
57
  expect(result['totalResults']).to eql(1)
176
58
  expect(result['Resources'].size).to eql(1)
177
59
 
178
60
  ids = result['Resources'].map { |resource| resource['id'] }
179
- expect(ids).to match_array([@u2.primary_key.to_s])
61
+ expect(ids).to match_array([@u2.id.to_s])
180
62
 
181
63
  usernames = result['Resources'].map { |resource| resource['userName'] }
182
64
  expect(usernames).to match_array(['2'])
183
65
  end
184
66
 
185
-
186
- # Strange attribute capitalisation in tests here builds on test coverage
187
- # for now-fixed GitHub issue #37.
188
- #
189
- context '"meta" / IDs (GitHub issue #36)' do
190
- it 'applies a filter on primary keys, using direct comparison (rather than e.g. case-insensitive operators)' do
191
- get '/Users', params: {
192
- format: :scim,
193
- filter: "id eq \"#{@u3.primary_key}\""
194
- }
195
-
196
- expect(response.status ).to eql(200)
197
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
198
-
199
- result = JSON.parse(response.body)
200
-
201
- expect(result['totalResults']).to eql(1)
202
- expect(result['Resources'].size).to eql(1)
203
-
204
- ids = result['Resources'].map { |resource| resource['id'] }
205
- expect(ids).to match_array([@u3.primary_key.to_s])
206
-
207
- usernames = result['Resources'].map { |resource| resource['userName'] }
208
- expect(usernames).to match_array(['3'])
209
- end
210
-
211
- it 'applies a filter on external IDs, using direct comparison' do
212
- get '/Users', params: {
213
- format: :scim,
214
- filter: "externalID eq \"#{@u2.scim_uid}\""
215
- }
216
-
217
- expect(response.status ).to eql(200)
218
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
219
-
220
- result = JSON.parse(response.body)
221
-
222
- expect(result['totalResults']).to eql(1)
223
- expect(result['Resources'].size).to eql(1)
224
-
225
- ids = result['Resources'].map { |resource| resource['id'] }
226
- expect(ids).to match_array([@u2.primary_key.to_s])
227
-
228
- usernames = result['Resources'].map { |resource| resource['userName'] }
229
- expect(usernames).to match_array(['2'])
230
- end
231
-
232
- it 'applies a filter on "meta" entries, using direct comparison' do
233
- get '/Users', params: {
234
- format: :scim,
235
- filter: "Meta.LastModified eq \"#{@u3.updated_at}\""
236
- }
237
-
238
- expect(response.status ).to eql(200)
239
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
240
-
241
- result = JSON.parse(response.body)
242
-
243
- expect(result['totalResults']).to eql(1)
244
- expect(result['Resources'].size).to eql(1)
245
-
246
- ids = result['Resources'].map { |resource| resource['id'] }
247
- expect(ids).to match_array([@u3.primary_key.to_s])
248
-
249
- usernames = result['Resources'].map { |resource| resource['userName'] }
250
- expect(usernames).to match_array(['3'])
251
- end
252
- end # "context '"meta" / IDs (GitHub issue #36)' do"
253
-
254
67
  it 'obeys a page size' do
255
68
  get '/Users', params: {
256
69
  format: :scim,
257
70
  count: 2
258
71
  }
259
72
 
260
- expect(response.status ).to eql(200)
261
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
262
-
73
+ expect(response.status).to eql(200)
263
74
  result = JSON.parse(response.body)
264
75
 
265
76
  expect(result['totalResults']).to eql(3)
266
77
  expect(result['Resources'].size).to eql(2)
267
78
 
268
79
  ids = result['Resources'].map { |resource| resource['id'] }
269
- expect(ids).to match_array([@u1.primary_key.to_s, @u2.primary_key.to_s])
80
+ expect(ids).to match_array([@u1.id.to_s, @u2.id.to_s])
270
81
 
271
82
  usernames = result['Resources'].map { |resource| resource['userName'] }
272
83
  expect(usernames).to match_array(['1', '2'])
@@ -278,16 +89,14 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
278
89
  startIndex: 2
279
90
  }
280
91
 
281
- expect(response.status ).to eql(200)
282
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
283
-
92
+ expect(response.status).to eql(200)
284
93
  result = JSON.parse(response.body)
285
94
 
286
95
  expect(result['totalResults']).to eql(3)
287
96
  expect(result['Resources'].size).to eql(2)
288
97
 
289
98
  ids = result['Resources'].map { |resource| resource['id'] }
290
- expect(ids).to match_array([@u2.primary_key.to_s, @u3.primary_key.to_s])
99
+ expect(ids).to match_array([@u2.id.to_s, @u3.id.to_s])
291
100
 
292
101
  usernames = result['Resources'].map { |resource| resource['userName'] }
293
102
  expect(usernames).to match_array(['2', '3'])
@@ -301,11 +110,8 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
301
110
  filter: 'name.givenName'
302
111
  }
303
112
 
304
- expect(response.status ).to eql(400)
305
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
306
-
113
+ expect(response.status).to eql(400)
307
114
  result = JSON.parse(response.body)
308
-
309
115
  expect(result['scimType']).to eql('invalidFilter')
310
116
  end
311
117
  end # "context 'with bad calls' do"
@@ -314,47 +120,24 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
314
120
  # ===========================================================================
315
121
 
316
122
  context '#show' do
317
- context 'with a UUID, renamed primary key column' do
318
- it 'shows an item' do
319
- expect_any_instance_of(MockUsersController).to receive(:show).once.and_call_original
320
- get "/Users/#{@u2.primary_key}", params: { format: :scim }
321
-
322
- expect(response.status ).to eql(200)
323
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
324
-
325
- result = JSON.parse(response.body)
326
-
327
- expect(result['id']).to eql(@u2.primary_key.to_s)
328
- expect(result['userName']).to eql('2')
329
- expect(result['name']['familyName']).to eql('Bar')
330
- expect(result['meta']['resourceType']).to eql('User')
331
- end
332
- end # "context 'with a UUID, renamed primary key column' do"
333
-
334
- context 'with an integer, conventionally named primary key column' do
335
- it 'shows an item' do
336
- expect_any_instance_of(MockGroupsController).to receive(:show).once.and_call_original
337
- get "/Groups/#{@g2.id}", params: { format: :scim }
338
-
339
- expect(response.status ).to eql(200)
340
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
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 }
341
126
 
342
- result = JSON.parse(response.body)
127
+ expect(response.status).to eql(200)
128
+ result = JSON.parse(response.body)
343
129
 
344
- expect(result['id']).to eql(@g2.id.to_s) # Note - ID was converted String; not Integer
345
- expect(result['displayName']).to eql('Group 2')
346
- expect(result['meta']['resourceType']).to eql('Group')
347
- end
348
- end # "context 'with an integer, conventionally named primary key column' do"
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
349
135
 
350
136
  it 'renders 404' do
351
137
  get '/Users/xyz', params: { format: :scim }
352
138
 
353
- expect(response.status ).to eql(404)
354
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
355
-
139
+ expect(response.status).to eql(404)
356
140
  result = JSON.parse(response.body)
357
-
358
141
  expect(result['status']).to eql('404')
359
142
  end
360
143
  end # "context '#show' do"
@@ -367,15 +150,10 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
367
150
  it 'with minimal parameters' do
368
151
  mock_before = MockUser.all.to_a
369
152
 
370
- attributes = { userName: '4' } # Minimum required by schema
153
+ attributes = { userName: '4' } # Minimum required by schema
371
154
  attributes = spec_helper_hupcase(attributes) if force_upper_case
372
155
 
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
- #
376
156
  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
-
379
157
  expect {
380
158
  post "/Users", params: attributes.merge(format: :scim)
381
159
  }.to change { MockUser.count }.by(1)
@@ -383,12 +161,10 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
383
161
  mock_after = MockUser.all.to_a
384
162
  new_mock = (mock_after - mock_before).first
385
163
 
386
- expect(response.status ).to eql(201)
387
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
388
-
164
+ expect(response.status).to eql(201)
389
165
  result = JSON.parse(response.body)
390
166
 
391
- expect(result['id']).to eql(new_mock.primary_key.to_s)
167
+ expect(result['id']).to eql(new_mock.id.to_s)
392
168
  expect(result['meta']['resourceType']).to eql('User')
393
169
  expect(new_mock.username).to eql('4')
394
170
  end
@@ -400,12 +176,10 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
400
176
 
401
177
  attributes = {
402
178
  userName: '4',
403
- password: 'correcthorsebatterystaple',
404
179
  name: {
405
180
  givenName: 'Given',
406
181
  familyName: 'Family'
407
182
  },
408
- meta: { resourceType: 'User' },
409
183
  emails: [
410
184
  {
411
185
  type: 'work',
@@ -427,90 +201,17 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
427
201
  mock_after = MockUser.all.to_a
428
202
  new_mock = (mock_after - mock_before).first
429
203
 
430
- expect(response.status ).to eql(201)
431
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
432
-
204
+ expect(response.status).to eql(201)
433
205
  result = JSON.parse(response.body)
434
206
 
435
207
  expect(result['id']).to eql(new_mock.id.to_s)
436
208
  expect(result['meta']['resourceType']).to eql('User')
437
209
  expect(new_mock.username).to eql('4')
438
- expect(new_mock.password).to eql('correcthorsebatterystaple')
439
210
  expect(new_mock.first_name).to eql('Given')
440
211
  expect(new_mock.last_name).to eql('Family')
441
212
  expect(new_mock.home_email_address).to eql('home_4@test.com')
442
213
  expect(new_mock.work_email_address).to eql('work_4@test.com')
443
214
  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
514
215
  end # "shared_examples 'a creator' do | force_upper_case: |"
515
216
 
516
217
  context 'using schema-matched case' do
@@ -531,11 +232,8 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
531
232
  }
532
233
  }.to_not change { MockUser.count }
533
234
 
534
- expect(response.status ).to eql(409)
535
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
536
-
235
+ expect(response.status).to eql(409)
537
236
  result = JSON.parse(response.body)
538
-
539
237
  expect(result['scimType']).to eql('uniqueness')
540
238
  expect(result['detail']).to include('already been taken')
541
239
  end
@@ -548,11 +246,8 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
548
246
  }
549
247
  }.to_not change { MockUser.count }
550
248
 
551
- expect(response.status ).to eql(400)
552
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
553
-
249
+ expect(response.status).to eql(400)
554
250
  result = JSON.parse(response.body)
555
-
556
251
  expect(result['scimType']).to eql('invalidValue')
557
252
  expect(result['detail']).to include('is required')
558
253
  end
@@ -565,84 +260,12 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
565
260
  }
566
261
  }.to_not change { MockUser.count }
567
262
 
568
- expect(response.status ).to eql(400)
569
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
570
-
263
+ expect(response.status).to eql(400)
571
264
  result = JSON.parse(response.body)
572
265
 
573
266
  expect(result['scimType']).to eql('invalidValue')
574
267
  expect(result['detail']).to include('is reserved')
575
268
  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"
646
269
  end # "context '#create' do"
647
270
 
648
271
  # ===========================================================================
@@ -650,64 +273,26 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
650
273
  context '#replace' do
651
274
  shared_examples 'a replacer' do | force_upper_case: |
652
275
  it 'which replaces all attributes in an instance' do
653
- attributes = { userName: '4' } # Minimum required by schema
276
+ attributes = { userName: '4' } # Minimum required by schema
654
277
  attributes = spec_helper_hupcase(attributes) if force_upper_case
655
278
 
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
- #
659
279
  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
661
- expect {
662
- put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
663
- }.to_not change { MockUser.count }
664
-
665
- expect(response.status ).to eql(200)
666
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
667
-
668
- result = JSON.parse(response.body)
669
-
670
- expect(result['id']).to eql(@u2.primary_key.to_s)
671
- expect(result['meta']['resourceType']).to eql('User')
672
-
673
- expect(result).to have_key('name')
674
- expect(result).to_not have_key('password')
675
-
676
- @u2.reload
677
-
678
- expect(@u2.username).to eql('4')
679
- expect(@u2.first_name).to be_nil
680
- expect(@u2.last_name).to be_nil
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
280
  expect {
690
- put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
281
+ put "/Users/#{@u2.id}", params: attributes.merge(format: :scim)
691
282
  }.to_not change { MockUser.count }
692
283
 
693
- expect(response.status ).to eql(200)
694
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
695
-
284
+ expect(response.status).to eql(200)
696
285
  result = JSON.parse(response.body)
697
286
 
698
- expect(result['id']).to eql(@u2.primary_key.to_s)
287
+ expect(result['id']).to eql(@u2.id.to_s)
699
288
  expect(result['meta']['resourceType']).to eql('User')
700
289
 
701
- expect(result).to have_key('name')
702
- expect(result).to_not have_key('password')
703
-
704
290
  @u2.reload
705
291
 
706
292
  expect(@u2.username).to eql('4')
707
293
  expect(@u2.first_name).to be_nil
708
294
  expect(@u2.last_name).to be_nil
709
295
  expect(@u2.home_email_address).to be_nil
710
- expect(@u2.password).to eql('correcthorsebatterystaple')
711
296
  end
712
297
  end # "shared_examples 'a replacer' do | force_upper_case: |"
713
298
 
@@ -721,17 +306,14 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
721
306
 
722
307
  it 'notes schema validation failures' do
723
308
  expect {
724
- put "/Users/#{@u2.primary_key}", params: {
309
+ put "/Users/#{@u2.id}", params: {
725
310
  format: :scim
726
311
  # userName parameter is required by schema, but missing
727
312
  }
728
313
  }.to_not change { MockUser.count }
729
314
 
730
- expect(response.status ).to eql(400)
731
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
732
-
315
+ expect(response.status).to eql(400)
733
316
  result = JSON.parse(response.body)
734
-
735
317
  expect(result['scimType']).to eql('invalidValue')
736
318
  expect(result['detail']).to include('is required')
737
319
 
@@ -745,15 +327,13 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
745
327
 
746
328
  it 'notes Rails validation failures' do
747
329
  expect {
748
- put "/Users/#{@u2.primary_key}", params: {
330
+ post "/Users", params: {
749
331
  format: :scim,
750
332
  userName: MockUser::INVALID_USERNAME
751
333
  }
752
334
  }.to_not change { MockUser.count }
753
335
 
754
- expect(response.status ).to eql(400)
755
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
756
-
336
+ expect(response.status).to eql(400)
757
337
  result = JSON.parse(response.body)
758
338
 
759
339
  expect(result['scimType']).to eql('invalidValue')
@@ -775,72 +355,17 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
775
355
  }
776
356
  }.to_not change { MockUser.count }
777
357
 
778
- expect(response.status ).to eql(404)
779
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
780
-
358
+ expect(response.status).to eql(404)
781
359
  result = JSON.parse(response.body)
782
-
783
360
  expect(result['status']).to eql('404')
784
361
  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"
837
362
  end # "context '#replace' do"
838
363
 
839
364
  # ===========================================================================
840
365
 
841
366
  context '#update' do
842
367
  shared_examples 'an updater' do | force_upper_case: |
843
- it 'which patches regular attributes' do
368
+ it 'which patches specific attributes' do
844
369
  payload = {
845
370
  Operations: [
846
371
  {
@@ -858,27 +383,17 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
858
383
 
859
384
  payload = spec_helper_hupcase(payload) if force_upper_case
860
385
 
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
- #
864
386
  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
-
867
387
  expect {
868
- patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
388
+ patch "/Users/#{@u2.id}", params: payload.merge(format: :scim)
869
389
  }.to_not change { MockUser.count }
870
390
 
871
- expect(response.status ).to eql(200)
872
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
873
-
391
+ expect(response.status).to eql(200)
874
392
  result = JSON.parse(response.body)
875
393
 
876
- expect(result['id']).to eql(@u2.primary_key.to_s)
394
+ expect(result['id']).to eql(@u2.id.to_s)
877
395
  expect(result['meta']['resourceType']).to eql('User')
878
396
 
879
- expect(result).to have_key('name')
880
- expect(result).to_not have_key('password')
881
-
882
397
  @u2.reload
883
398
 
884
399
  expect(@u2.username).to eql('4')
@@ -886,151 +401,6 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
886
401
  expect(@u2.last_name).to eql('Bar')
887
402
  expect(@u2.home_email_address).to eql('home_2@test.com')
888
403
  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')
1034
404
  end
1035
405
 
1036
406
  context 'which clears attributes' do
@@ -1052,15 +422,13 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1052
422
 
1053
423
  expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
1054
424
  expect {
1055
- patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
425
+ patch "/Users/#{@u2.id}", params: payload.merge(format: :scim)
1056
426
  }.to_not change { MockUser.count }
1057
427
 
1058
- expect(response.status ).to eql(200)
1059
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1060
-
428
+ expect(response.status).to eql(200)
1061
429
  result = JSON.parse(response.body)
1062
430
 
1063
- expect(result['id']).to eql(@u2.primary_key.to_s)
431
+ expect(result['id']).to eql(@u2.id.to_s)
1064
432
  expect(result['meta']['resourceType']).to eql('User')
1065
433
 
1066
434
  @u2.reload
@@ -1086,15 +454,13 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1086
454
 
1087
455
  expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
1088
456
  expect {
1089
- patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
457
+ patch "/Users/#{@u2.id}", params: payload.merge(format: :scim)
1090
458
  }.to_not change { MockUser.count }
1091
459
 
1092
- expect(response.status ).to eql(200)
1093
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1094
-
460
+ expect(response.status).to eql(200)
1095
461
  result = JSON.parse(response.body)
1096
462
 
1097
- expect(result['id']).to eql(@u2.primary_key.to_s)
463
+ expect(result['id']).to eql(@u2.id.to_s)
1098
464
  expect(result['meta']['resourceType']).to eql('User')
1099
465
 
1100
466
  @u2.reload
@@ -1120,15 +486,13 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1120
486
 
1121
487
  expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
1122
488
  expect {
1123
- patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
489
+ patch "/Users/#{@u2.id}", params: payload.merge(format: :scim)
1124
490
  }.to_not change { MockUser.count }
1125
491
 
1126
- expect(response.status ).to eql(200)
1127
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1128
-
492
+ expect(response.status).to eql(200)
1129
493
  result = JSON.parse(response.body)
1130
494
 
1131
- expect(result['id']).to eql(@u2.primary_key.to_s)
495
+ expect(result['id']).to eql(@u2.id.to_s)
1132
496
  expect(result['meta']['resourceType']).to eql('User')
1133
497
 
1134
498
  @u2.reload
@@ -1152,7 +516,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1152
516
 
1153
517
  it 'notes Rails validation failures' do
1154
518
  expect {
1155
- patch "/Users/#{@u2.primary_key}", params: {
519
+ patch "/Users/#{@u2.id}", params: {
1156
520
  format: :scim,
1157
521
  Operations: [
1158
522
  {
@@ -1164,9 +528,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1164
528
  }
1165
529
  }.to_not change { MockUser.count }
1166
530
 
1167
- expect(response.status ).to eql(400)
1168
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1169
-
531
+ expect(response.status).to eql(400)
1170
532
  result = JSON.parse(response.body)
1171
533
 
1172
534
  expect(result['scimType']).to eql('invalidValue')
@@ -1194,329 +556,20 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1194
556
  }
1195
557
  }.to_not change { MockUser.count }
1196
558
 
1197
- expect(response.status ).to eql(404)
1198
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1199
-
559
+ expect(response.status).to eql(404)
1200
560
  result = JSON.parse(response.body)
1201
-
1202
561
  expect(result['status']).to eql('404')
1203
562
  end
1204
-
1205
- context 'when removing users from groups' do
1206
- before :each do
1207
- @g1.mock_users << @u1
1208
- @g1.mock_users << @u2
1209
- @g1.mock_users << @u3
1210
-
1211
- # (Self-check) Verify group representation
1212
- #
1213
- get "/Groups/#{@g1.id}", params: { format: :scim }
1214
-
1215
- expect(response.status).to eql(200)
1216
- result = JSON.parse(response.body)
1217
-
1218
- expect(result['members'].map { |m| m['value'] }.sort()).to eql(MockUser.pluck(:primary_key).sort())
1219
- end
1220
-
1221
- it 'can remove all users' do
1222
- expect {
1223
- expect {
1224
- patch "/Groups/#{@g1.id}", params: {
1225
- format: :scim,
1226
- Operations: [
1227
- {
1228
- op: 'remove',
1229
- path: 'members'
1230
- }
1231
- ]
1232
- }
1233
- }.to_not change { MockUser.count }
1234
- }.to_not change { MockGroup.count }
1235
-
1236
- get "/Groups/#{@g1.id}", params: { format: :scim }
1237
-
1238
- expect(response.status ).to eql(200)
1239
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1240
-
1241
- result = JSON.parse(response.body)
1242
-
1243
- expect(result['members']).to be_empty
1244
- expect(@g1.reload().mock_users).to be_empty
1245
- end
1246
-
1247
- # Define via 'let':
1248
- #
1249
- # * Hash 'payload', to send via 'patch'
1250
- # * MockUser 'removed_user', which is the user that should be removed
1251
- #
1252
- shared_examples 'a user remover' do
1253
- it 'which removes the identified user' do
1254
- expect {
1255
- expect {
1256
- patch "/Groups/#{@g1.id}", params: payload()
1257
- }.to_not change { MockUser.count }
1258
- }.to_not change { MockGroup.count }
1259
-
1260
- expected_remaining_user_ids = MockUser
1261
- .where.not(primary_key: removed_user().id)
1262
- .pluck(:primary_key)
1263
- .sort()
1264
-
1265
- get "/Groups/#{@g1.id}", params: { format: :scim }
1266
-
1267
- expect(response.status ).to eql(200)
1268
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1269
-
1270
- result = JSON.parse(response.body)
1271
-
1272
- expect(result['members'].map { |m| m['value'] }.sort()).to eql(expected_remaining_user_ids)
1273
- expect(@g1.reload().mock_users.map(&:primary_key).sort()).to eql(expected_remaining_user_ids)
1274
- end
1275
- end
1276
-
1277
- # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
1278
- #
1279
- context 'and using an RFC-compliant payload' do
1280
- let(:removed_user) { @u2 }
1281
- let(:payload) do
1282
- {
1283
- format: :scim,
1284
- Operations: [
1285
- {
1286
- op: 'remove',
1287
- path: "members[value eq \"#{removed_user().primary_key}\"]",
1288
- }
1289
- ]
1290
- }
1291
- end
1292
-
1293
- it_behaves_like 'a user remover'
1294
- end # context 'and using an RFC-compliant payload' do
1295
-
1296
- # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
1297
- #
1298
- context 'and using a Microsoft variant payload' do
1299
- let(:removed_user) { @u2 }
1300
- let(:payload) do
1301
- {
1302
- format: :scim,
1303
- Operations: [
1304
- {
1305
- op: 'remove',
1306
- path: 'members',
1307
- value: [{
1308
- '$ref' => nil,
1309
- 'value' => removed_user().primary_key
1310
- }]
1311
- }
1312
- ]
1313
- }
1314
- end
1315
-
1316
- it_behaves_like 'a user remover'
1317
- end # context 'and using a Microsoft variant payload' do
1318
-
1319
- # https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
1320
- #
1321
- context 'and using a Salesforce variant payload' do
1322
- let(:removed_user) { @u2 }
1323
- let(:payload) do
1324
- {
1325
- format: :scim,
1326
- Operations: [
1327
- {
1328
- op: 'remove',
1329
- path: 'members',
1330
- value: {
1331
- 'members' => [{
1332
- '$ref' => nil,
1333
- 'value' => removed_user().primary_key
1334
- }]
1335
- }
1336
- }
1337
- ]
1338
- }
1339
- end
1340
-
1341
- it_behaves_like 'a user remover'
1342
- end # context 'and using a Salesforce variant payload' do
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"
1414
563
  end # "context '#update' do"
1415
564
 
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
-
1512
565
  # ===========================================================================
1513
566
 
1514
567
  context '#destroy' do
1515
- it 'deletes an item if given no block' do
568
+ it 'deletes an item if given no blok' do
1516
569
  expect_any_instance_of(MockUsersController).to receive(:destroy).once.and_call_original
1517
570
  expect_any_instance_of(MockUser).to receive(:destroy!).once.and_call_original
1518
571
  expect {
1519
- delete "/Users/#{@u2.primary_key}", params: { format: :scim }
572
+ delete "/Users/#{@u2.id}", params: { format: :scim }
1520
573
  }.to change { MockUser.count }.by(-1)
1521
574
 
1522
575
  expect(response.status).to eql(204)
@@ -1528,7 +581,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1528
581
  expect_any_instance_of(MockUser).to_not receive(:destroy!)
1529
582
 
1530
583
  expect {
1531
- delete "/CustomDestroyUsers/#{@u2.primary_key}", params: { format: :scim }
584
+ delete "/CustomDestroyUsers/#{@u2.id}", params: { format: :scim }
1532
585
  }.to_not change { MockUser.count }
1533
586
 
1534
587
  expect(response.status).to eql(204)
@@ -1543,11 +596,8 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
1543
596
  delete '/Users/xyz', params: { format: :scim }
1544
597
  }.to_not change { MockUser.count }
1545
598
 
1546
- expect(response.status ).to eql(404)
1547
- expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1548
-
599
+ expect(response.status).to eql(404)
1549
600
  result = JSON.parse(response.body)
1550
-
1551
601
  expect(result['status']).to eql('404')
1552
602
  end
1553
603
  end # "context '#destroy' do"