scimitar 1.10.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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"