powerhome-scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +708 -0
  4. data/Rakefile +16 -0
  5. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +257 -0
  6. data/app/controllers/scimitar/application_controller.rb +157 -0
  7. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  8. data/app/controllers/scimitar/resources_controller.rb +203 -0
  9. data/app/controllers/scimitar/schemas_controller.rb +21 -0
  10. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  11. data/app/models/scimitar/authentication_error.rb +9 -0
  12. data/app/models/scimitar/authentication_scheme.rb +18 -0
  13. data/app/models/scimitar/bulk.rb +8 -0
  14. data/app/models/scimitar/complex_types/address.rb +12 -0
  15. data/app/models/scimitar/complex_types/base.rb +83 -0
  16. data/app/models/scimitar/complex_types/email.rb +12 -0
  17. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  18. data/app/models/scimitar/complex_types/ims.rb +12 -0
  19. data/app/models/scimitar/complex_types/name.rb +12 -0
  20. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  21. data/app/models/scimitar/complex_types/photo.rb +12 -0
  22. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  23. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  24. data/app/models/scimitar/complex_types/role.rb +12 -0
  25. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  26. data/app/models/scimitar/engine_configuration.rb +32 -0
  27. data/app/models/scimitar/error_response.rb +32 -0
  28. data/app/models/scimitar/errors.rb +14 -0
  29. data/app/models/scimitar/filter.rb +11 -0
  30. data/app/models/scimitar/filter_error.rb +22 -0
  31. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  32. data/app/models/scimitar/lists/count.rb +64 -0
  33. data/app/models/scimitar/lists/query_parser.rb +745 -0
  34. data/app/models/scimitar/meta.rb +7 -0
  35. data/app/models/scimitar/not_found_error.rb +10 -0
  36. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  37. data/app/models/scimitar/resource_type.rb +29 -0
  38. data/app/models/scimitar/resources/base.rb +190 -0
  39. data/app/models/scimitar/resources/group.rb +13 -0
  40. data/app/models/scimitar/resources/mixin.rb +1524 -0
  41. data/app/models/scimitar/resources/user.rb +13 -0
  42. data/app/models/scimitar/schema/address.rb +25 -0
  43. data/app/models/scimitar/schema/attribute.rb +132 -0
  44. data/app/models/scimitar/schema/base.rb +90 -0
  45. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  46. data/app/models/scimitar/schema/email.rb +10 -0
  47. data/app/models/scimitar/schema/entitlement.rb +10 -0
  48. data/app/models/scimitar/schema/group.rb +27 -0
  49. data/app/models/scimitar/schema/ims.rb +10 -0
  50. data/app/models/scimitar/schema/name.rb +20 -0
  51. data/app/models/scimitar/schema/phone_number.rb +10 -0
  52. data/app/models/scimitar/schema/photo.rb +10 -0
  53. data/app/models/scimitar/schema/reference_group.rb +23 -0
  54. data/app/models/scimitar/schema/reference_member.rb +21 -0
  55. data/app/models/scimitar/schema/role.rb +10 -0
  56. data/app/models/scimitar/schema/user.rb +52 -0
  57. data/app/models/scimitar/schema/vdtp.rb +18 -0
  58. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  59. data/app/models/scimitar/service_provider_configuration.rb +60 -0
  60. data/app/models/scimitar/supportable.rb +14 -0
  61. data/app/views/layouts/scimitar/application.html.erb +14 -0
  62. data/config/initializers/scimitar.rb +111 -0
  63. data/config/routes.rb +6 -0
  64. data/lib/scimitar/engine.rb +63 -0
  65. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +216 -0
  66. data/lib/scimitar/support/utilities.rb +51 -0
  67. data/lib/scimitar/version.rb +13 -0
  68. data/lib/scimitar.rb +29 -0
  69. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  70. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  71. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  72. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  73. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  74. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  75. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  76. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  77. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  78. data/spec/apps/dummy/app/models/mock_user.rb +132 -0
  79. data/spec/apps/dummy/config/application.rb +18 -0
  80. data/spec/apps/dummy/config/boot.rb +2 -0
  81. data/spec/apps/dummy/config/environment.rb +2 -0
  82. data/spec/apps/dummy/config/environments/test.rb +38 -0
  83. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  84. data/spec/apps/dummy/config/initializers/scimitar.rb +61 -0
  85. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/apps/dummy/config/routes.rb +45 -0
  87. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +24 -0
  88. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  89. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +13 -0
  90. data/spec/apps/dummy/db/schema.rb +48 -0
  91. data/spec/controllers/scimitar/application_controller_spec.rb +296 -0
  92. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  93. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  94. data/spec/controllers/scimitar/schemas_controller_spec.rb +83 -0
  95. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  96. data/spec/models/scimitar/complex_types/address_spec.rb +18 -0
  97. data/spec/models/scimitar/complex_types/email_spec.rb +21 -0
  98. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  99. data/spec/models/scimitar/lists/query_parser_spec.rb +830 -0
  100. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  101. data/spec/models/scimitar/resources/base_spec.rb +485 -0
  102. data/spec/models/scimitar/resources/base_validation_spec.rb +86 -0
  103. data/spec/models/scimitar/resources/mixin_spec.rb +3562 -0
  104. data/spec/models/scimitar/resources/user_spec.rb +68 -0
  105. data/spec/models/scimitar/schema/attribute_spec.rb +99 -0
  106. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  107. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  108. data/spec/models/scimitar/schema/user_spec.rb +720 -0
  109. data/spec/requests/active_record_backed_resources_controller_spec.rb +1354 -0
  110. data/spec/requests/application_controller_spec.rb +61 -0
  111. data/spec/requests/controller_configuration_spec.rb +17 -0
  112. data/spec/requests/engine_spec.rb +45 -0
  113. data/spec/spec_helper.rb +101 -0
  114. data/spec/spec_helper_spec.rb +30 -0
  115. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +169 -0
  116. metadata +321 -0
@@ -0,0 +1,1354 @@
1
+ require 'spec_helper'
2
+ require 'time'
3
+
4
+ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
5
+ before :each do
6
+ allow_any_instance_of(Scimitar::ApplicationController).to receive(:authenticated?).and_return(true)
7
+
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')
23
+ end
24
+
25
+ # ===========================================================================
26
+
27
+ context '#index' do
28
+ context 'with no items' do
29
+ before :each do
30
+ MockUser.delete_all
31
+ end
32
+
33
+ it 'returns empty list' do
34
+ expect_any_instance_of(MockUsersController).to receive(:index).once.and_call_original
35
+ get '/Users', params: { format: :scim }
36
+
37
+ expect(response.status ).to eql(200)
38
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
39
+
40
+ result = JSON.parse(response.body)
41
+
42
+ expect(result['totalResults']).to eql(0)
43
+ expect(result['startIndex' ]).to eql(1)
44
+ expect(result['itemsPerPage']).to eql(100)
45
+ end
46
+ end # "context 'with no items' do"
47
+
48
+ 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')
118
+
119
+ result = JSON.parse(response.body)
120
+
121
+ expect(result['totalResults']).to eql(3)
122
+ expect(result['Resources'].size).to eql(3)
123
+
124
+ keys = result['Resources'].map { |resource| resource.keys }.flatten.uniq
125
+ expect(keys).to match_array(%w[id meta name schemas urn:ietf:params:scim:schemas:extension:enterprise:2.0:User])
126
+ expect(result.dig('Resources', 0, 'id')).to eql @u1.primary_key.to_s
127
+ expect(result.dig('Resources', 0, 'name', 'givenName')).to eql 'Foo'
128
+ expect(result.dig('Resources', 0, 'name', 'familyName')).to eql 'Ark'
129
+ end
130
+
131
+ it 'applies a filter, with case-insensitive attribute matching (GitHub issue #37)' do
132
+ get '/Users', params: {
133
+ format: :scim,
134
+ filter: 'name.GIVENNAME eq "Foo" and name.Familyname pr and emails ne "home_1@test.com"'
135
+ }
136
+
137
+ expect(response.status ).to eql(200)
138
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
139
+
140
+ result = JSON.parse(response.body)
141
+
142
+ expect(result['totalResults']).to eql(1)
143
+ expect(result['Resources'].size).to eql(1)
144
+
145
+ ids = result['Resources'].map { |resource| resource['id'] }
146
+ expect(ids).to match_array([@u2.primary_key.to_s])
147
+
148
+ usernames = result['Resources'].map { |resource| resource['userName'] }
149
+ expect(usernames).to match_array(['2'])
150
+ end
151
+
152
+ # Strange attribute capitalisation in tests here builds on test coverage
153
+ # for now-fixed GitHub issue #37.
154
+ #
155
+ context '"meta" / IDs (GitHub issue #36)' do
156
+ it 'applies a filter on primary keys, using direct comparison (rather than e.g. case-insensitive operators)' do
157
+ get '/Users', params: {
158
+ format: :scim,
159
+ filter: "id eq \"#{@u3.primary_key}\""
160
+ }
161
+
162
+ expect(response.status ).to eql(200)
163
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
164
+
165
+ result = JSON.parse(response.body)
166
+
167
+ expect(result['totalResults']).to eql(1)
168
+ expect(result['Resources'].size).to eql(1)
169
+
170
+ ids = result['Resources'].map { |resource| resource['id'] }
171
+ expect(ids).to match_array([@u3.primary_key.to_s])
172
+
173
+ usernames = result['Resources'].map { |resource| resource['userName'] }
174
+ expect(usernames).to match_array(['3'])
175
+ end
176
+
177
+ it 'applies a filter on external IDs, using direct comparison' do
178
+ get '/Users', params: {
179
+ format: :scim,
180
+ filter: "externalID eq \"#{@u2.scim_uid}\""
181
+ }
182
+
183
+ expect(response.status ).to eql(200)
184
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
185
+
186
+ result = JSON.parse(response.body)
187
+
188
+ expect(result['totalResults']).to eql(1)
189
+ expect(result['Resources'].size).to eql(1)
190
+
191
+ ids = result['Resources'].map { |resource| resource['id'] }
192
+ expect(ids).to match_array([@u2.primary_key.to_s])
193
+
194
+ usernames = result['Resources'].map { |resource| resource['userName'] }
195
+ expect(usernames).to match_array(['2'])
196
+ end
197
+
198
+ it 'applies a filter on "meta" entries, using direct comparison' do
199
+ get '/Users', params: {
200
+ format: :scim,
201
+ filter: "Meta.LastModified eq \"#{@u3.updated_at}\""
202
+ }
203
+
204
+ expect(response.status ).to eql(200)
205
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
206
+
207
+ result = JSON.parse(response.body)
208
+
209
+ expect(result['totalResults']).to eql(1)
210
+ expect(result['Resources'].size).to eql(1)
211
+
212
+ ids = result['Resources'].map { |resource| resource['id'] }
213
+ expect(ids).to match_array([@u3.primary_key.to_s])
214
+
215
+ usernames = result['Resources'].map { |resource| resource['userName'] }
216
+ expect(usernames).to match_array(['3'])
217
+ end
218
+ end # "context '"meta" / IDs (GitHub issue #36)' do"
219
+
220
+ it 'obeys a page size' do
221
+ get '/Users', params: {
222
+ format: :scim,
223
+ count: 2
224
+ }
225
+
226
+ expect(response.status ).to eql(200)
227
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
228
+
229
+ result = JSON.parse(response.body)
230
+
231
+ expect(result['totalResults']).to eql(3)
232
+ expect(result['Resources'].size).to eql(2)
233
+
234
+ ids = result['Resources'].map { |resource| resource['id'] }
235
+ expect(ids).to match_array([@u1.primary_key.to_s, @u2.primary_key.to_s])
236
+
237
+ usernames = result['Resources'].map { |resource| resource['userName'] }
238
+ expect(usernames).to match_array(['1', '2'])
239
+ end
240
+
241
+ it 'obeys start-at-1 offsets' do
242
+ get '/Users', params: {
243
+ format: :scim,
244
+ startIndex: 2
245
+ }
246
+
247
+ expect(response.status ).to eql(200)
248
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
249
+
250
+ result = JSON.parse(response.body)
251
+
252
+ expect(result['totalResults']).to eql(3)
253
+ expect(result['Resources'].size).to eql(2)
254
+
255
+ ids = result['Resources'].map { |resource| resource['id'] }
256
+ expect(ids).to match_array([@u2.primary_key.to_s, @u3.primary_key.to_s])
257
+
258
+ usernames = result['Resources'].map { |resource| resource['userName'] }
259
+ expect(usernames).to match_array(['2', '3'])
260
+ end
261
+ end # "context 'with items' do"
262
+
263
+ context 'with bad calls' do
264
+ it 'complains about bad filters' do
265
+ get '/Users', params: {
266
+ format: :scim,
267
+ filter: 'name.givenName'
268
+ }
269
+
270
+ expect(response.status ).to eql(400)
271
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
272
+
273
+ result = JSON.parse(response.body)
274
+
275
+ expect(result['scimType']).to eql('invalidFilter')
276
+ end
277
+ end # "context 'with bad calls' do"
278
+ end # "context '#index' do"
279
+
280
+ # ===========================================================================
281
+
282
+ context '#show' do
283
+ context 'with a UUID, renamed primary key column' do
284
+ it 'shows an item' do
285
+ expect_any_instance_of(MockUsersController).to receive(:show).once.and_call_original
286
+ get "/Users/#{@u2.primary_key}", params: { format: :scim }
287
+
288
+ expect(response.status ).to eql(200)
289
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
290
+
291
+ result = JSON.parse(response.body)
292
+
293
+ expect(result['id']).to eql(@u2.primary_key.to_s)
294
+ expect(result['userName']).to eql('2')
295
+ expect(result['name']['familyName']).to eql('Bar')
296
+ expect(result['meta']['resourceType']).to eql('User')
297
+ end
298
+ end # "context 'with a UUID, renamed primary key column' do"
299
+
300
+ context 'with an integer, conventionally named primary key column' do
301
+ it 'shows an item' do
302
+ expect_any_instance_of(MockGroupsController).to receive(:show).once.and_call_original
303
+ get "/Groups/#{@g2.id}", params: { format: :scim }
304
+
305
+ expect(response.status ).to eql(200)
306
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
307
+
308
+ result = JSON.parse(response.body)
309
+
310
+ expect(result['id']).to eql(@g2.id.to_s) # Note - ID was converted String; not Integer
311
+ expect(result['displayName']).to eql('Group 2')
312
+ expect(result['meta']['resourceType']).to eql('Group')
313
+ end
314
+ end # "context 'with an integer, conventionally named primary key column' do"
315
+
316
+ it 'renders 404' do
317
+ get '/Users/xyz', params: { format: :scim }
318
+
319
+ expect(response.status ).to eql(404)
320
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
321
+
322
+ result = JSON.parse(response.body)
323
+
324
+ expect(result['status']).to eql('404')
325
+ end
326
+ end # "context '#show' do"
327
+
328
+ # ===========================================================================
329
+
330
+ context '#create' do
331
+ context 'creates an item' do
332
+ shared_examples 'a creator' do | force_upper_case: |
333
+ it 'with minimal parameters' do
334
+ mock_before = MockUser.all.to_a
335
+
336
+ attributes = { userName: '4' } # Minimum required by schema
337
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
338
+
339
+ # Prove that certain known pathways are called; can then unit test
340
+ # those if need be and be sure that this covers #create actions.
341
+ #
342
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
343
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
344
+
345
+ expect {
346
+ post "/Users", params: attributes.merge(format: :scim)
347
+ }.to change { MockUser.count }.by(1)
348
+
349
+ mock_after = MockUser.all.to_a
350
+ new_mock = (mock_after - mock_before).first
351
+
352
+ expect(response.status ).to eql(201)
353
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
354
+
355
+ result = JSON.parse(response.body)
356
+
357
+ expect(result['id']).to eql(new_mock.primary_key.to_s)
358
+ expect(result['meta']['resourceType']).to eql('User')
359
+ expect(new_mock.username).to eql('4')
360
+ end
361
+
362
+ # A bit of extra coverage just for general confidence.
363
+ #
364
+ it 'with more comprehensive parameters' do
365
+ mock_before = MockUser.all.to_a
366
+
367
+ attributes = {
368
+ userName: '4',
369
+ password: 'correcthorsebatterystaple',
370
+ name: {
371
+ givenName: 'Given',
372
+ familyName: 'Family'
373
+ },
374
+ meta: { resourceType: 'User' },
375
+ emails: [
376
+ {
377
+ type: 'work',
378
+ value: 'work_4@test.com'
379
+ },
380
+ {
381
+ type: 'home',
382
+ value: 'home_4@test.com'
383
+ }
384
+ ]
385
+ }
386
+
387
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
388
+
389
+ expect {
390
+ post "/Users", params: attributes.merge(format: :scim)
391
+ }.to change { MockUser.count }.by(1)
392
+
393
+ mock_after = MockUser.all.to_a
394
+ new_mock = (mock_after - mock_before).first
395
+
396
+ expect(response.status ).to eql(201)
397
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
398
+
399
+ result = JSON.parse(response.body)
400
+
401
+ expect(result['id']).to eql(new_mock.id.to_s)
402
+ expect(result['meta']['resourceType']).to eql('User')
403
+ expect(new_mock.username).to eql('4')
404
+ expect(new_mock.password).to eql('correcthorsebatterystaple')
405
+ expect(new_mock.first_name).to eql('Given')
406
+ expect(new_mock.last_name).to eql('Family')
407
+ expect(new_mock.home_email_address).to eql('home_4@test.com')
408
+ expect(new_mock.work_email_address).to eql('work_4@test.com')
409
+ end
410
+ end # "shared_examples 'a creator' do | force_upper_case: |"
411
+
412
+ context 'using schema-matched case' do
413
+ it_behaves_like 'a creator', force_upper_case: false
414
+ end # "context 'using schema-matched case' do"
415
+
416
+ context 'using upper case' do
417
+ it_behaves_like 'a creator', force_upper_case: true
418
+ end # "context 'using upper case' do"
419
+ end
420
+
421
+ it 'returns 409 for duplicates (by Rails validation)' do
422
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
423
+ expect {
424
+ post "/Users", params: {
425
+ format: :scim,
426
+ userName: '1' # Already exists
427
+ }
428
+ }.to_not change { MockUser.count }
429
+
430
+ expect(response.status ).to eql(409)
431
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
432
+
433
+ result = JSON.parse(response.body)
434
+
435
+ expect(result['scimType']).to eql('uniqueness')
436
+ expect(result['detail']).to include('already been taken')
437
+ end
438
+
439
+ it 'notes schema validation failures' do
440
+ expect {
441
+ post "/Users", params: {
442
+ format: :scim
443
+ # userName parameter is required by schema, but missing
444
+ }
445
+ }.to_not change { MockUser.count }
446
+
447
+ expect(response.status ).to eql(400)
448
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
449
+
450
+ result = JSON.parse(response.body)
451
+
452
+ expect(result['scimType']).to eql('invalidValue')
453
+ expect(result['detail']).to include('is required')
454
+ end
455
+
456
+ it 'notes Rails validation failures' do
457
+ expect {
458
+ post "/Users", params: {
459
+ format: :scim,
460
+ userName: MockUser::INVALID_USERNAME
461
+ }
462
+ }.to_not change { MockUser.count }
463
+
464
+ expect(response.status ).to eql(400)
465
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
466
+
467
+ result = JSON.parse(response.body)
468
+
469
+ expect(result['scimType']).to eql('invalidValue')
470
+ expect(result['detail']).to include('is reserved')
471
+ end
472
+
473
+ context 'with a block' do
474
+ it 'invokes the block' do
475
+ mock_before = MockUser.all.to_a
476
+
477
+ expect_any_instance_of(CustomCreateMockUsersController).to receive(:create).once.and_call_original
478
+ expect {
479
+ post "/CustomCreateUsers", params: {
480
+ format: :scim,
481
+ userName: '4' # Minimum required by schema
482
+ }
483
+ }.to change { MockUser.count }.by(1)
484
+
485
+ mock_after = MockUser.all.to_a
486
+ new_mock = (mock_after - mock_before).first
487
+
488
+ expect(response.status ).to eql(201)
489
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
490
+
491
+ result = JSON.parse(response.body)
492
+
493
+ expect(result['id']).to eql(new_mock.id.to_s)
494
+ expect(result['meta']['resourceType']).to eql('User')
495
+ expect(new_mock.first_name).to eql(CustomCreateMockUsersController::OVERRIDDEN_NAME)
496
+ end
497
+
498
+ it 'returns 409 for duplicates (by Rails validation)' do
499
+ existing_user = MockUser.create!(
500
+ username: '4',
501
+ first_name: 'Will Be Overridden',
502
+ last_name: 'Baz',
503
+ home_email_address: 'random@test.com',
504
+ scim_uid: '999'
505
+ )
506
+
507
+ expect_any_instance_of(CustomCreateMockUsersController).to receive(:create).once.and_call_original
508
+ expect {
509
+ post "/CustomCreateUsers", params: {
510
+ format: :scim,
511
+ userName: '4' # Already exists
512
+ }
513
+ }.to_not change { MockUser.count }
514
+
515
+ expect(response.status ).to eql(409)
516
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
517
+
518
+ result = JSON.parse(response.body)
519
+
520
+ expect(result['scimType']).to eql('uniqueness')
521
+ expect(result['detail']).to include('already been taken')
522
+ end
523
+
524
+ it 'notes Rails validation failures' do
525
+ expect_any_instance_of(CustomCreateMockUsersController).to receive(:create).once.and_call_original
526
+ expect {
527
+ post "/CustomCreateUsers", params: {
528
+ format: :scim,
529
+ userName: MockUser::INVALID_USERNAME
530
+ }
531
+ }.to_not change { MockUser.count }
532
+
533
+ expect(response.status ).to eql(400)
534
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
535
+
536
+ result = JSON.parse(response.body)
537
+
538
+ expect(result['scimType']).to eql('invalidValue')
539
+ expect(result['detail']).to include('is reserved')
540
+ end
541
+ end # "context 'with a block' do"
542
+ end # "context '#create' do"
543
+
544
+ # ===========================================================================
545
+
546
+ context '#replace' do
547
+ shared_examples 'a replacer' do | force_upper_case: |
548
+ it 'which replaces all attributes in an instance' do
549
+ attributes = { userName: '4' } # Minimum required by schema
550
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
551
+
552
+ # Prove that certain known pathways are called; can then unit test
553
+ # those if need be and be sure that this covers #replace actions.
554
+ #
555
+ expect_any_instance_of(MockUsersController).to receive(:replace).once.and_call_original
556
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
557
+ expect {
558
+ put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
559
+ }.to_not change { MockUser.count }
560
+
561
+ expect(response.status ).to eql(200)
562
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
563
+
564
+ result = JSON.parse(response.body)
565
+
566
+ expect(result['id']).to eql(@u2.primary_key.to_s)
567
+ expect(result['meta']['resourceType']).to eql('User')
568
+
569
+ expect(result).to have_key('name')
570
+ expect(result).to_not have_key('password')
571
+
572
+ @u2.reload
573
+
574
+ expect(@u2.username).to eql('4')
575
+ expect(@u2.first_name).to be_nil
576
+ expect(@u2.last_name).to be_nil
577
+ expect(@u2.home_email_address).to be_nil
578
+ expect(@u2.password).to be_nil
579
+ end
580
+
581
+ it 'can replace passwords' do
582
+ attributes = { userName: '4', password: 'correcthorsebatterystaple' }
583
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
584
+
585
+ expect {
586
+ put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
587
+ }.to_not change { MockUser.count }
588
+
589
+ expect(response.status ).to eql(200)
590
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
591
+
592
+ result = JSON.parse(response.body)
593
+
594
+ expect(result['id']).to eql(@u2.primary_key.to_s)
595
+ expect(result['meta']['resourceType']).to eql('User')
596
+
597
+ expect(result).to have_key('name')
598
+ expect(result).to_not have_key('password')
599
+
600
+ @u2.reload
601
+
602
+ expect(@u2.username).to eql('4')
603
+ expect(@u2.first_name).to be_nil
604
+ expect(@u2.last_name).to be_nil
605
+ expect(@u2.home_email_address).to be_nil
606
+ expect(@u2.password).to eql('correcthorsebatterystaple')
607
+ end
608
+ end # "shared_examples 'a replacer' do | force_upper_case: |"
609
+
610
+ context 'using schema-matched case' do
611
+ it_behaves_like 'a replacer', force_upper_case: false
612
+ end # "context 'using schema-matched case' do"
613
+
614
+ context 'using upper case' do
615
+ it_behaves_like 'a replacer', force_upper_case: true
616
+ end # "context 'using upper case' do"
617
+
618
+ it 'notes schema validation failures' do
619
+ expect {
620
+ put "/Users/#{@u2.primary_key}", params: {
621
+ format: :scim
622
+ # userName parameter is required by schema, but missing
623
+ }
624
+ }.to_not change { MockUser.count }
625
+
626
+ expect(response.status ).to eql(400)
627
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
628
+
629
+ result = JSON.parse(response.body)
630
+
631
+ expect(result['scimType']).to eql('invalidValue')
632
+ expect(result['detail']).to include('is required')
633
+
634
+ @u2.reload
635
+
636
+ expect(@u2.username).to eql('2')
637
+ expect(@u2.first_name).to eql('Foo')
638
+ expect(@u2.last_name).to eql('Bar')
639
+ expect(@u2.home_email_address).to eql('home_2@test.com')
640
+ end
641
+
642
+ it 'notes Rails validation failures' do
643
+ expect {
644
+ put "/Users/#{@u2.primary_key}", params: {
645
+ format: :scim,
646
+ userName: MockUser::INVALID_USERNAME
647
+ }
648
+ }.to_not change { MockUser.count }
649
+
650
+ expect(response.status ).to eql(400)
651
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
652
+
653
+ result = JSON.parse(response.body)
654
+
655
+ expect(result['scimType']).to eql('invalidValue')
656
+ expect(result['detail']).to include('is reserved')
657
+
658
+ @u2.reload
659
+
660
+ expect(@u2.username).to eql('2')
661
+ expect(@u2.first_name).to eql('Foo')
662
+ expect(@u2.last_name).to eql('Bar')
663
+ expect(@u2.home_email_address).to eql('home_2@test.com')
664
+ end
665
+
666
+ it 'returns 404 if ID is invalid' do
667
+ expect {
668
+ put '/Users/xyz', params: {
669
+ format: :scim,
670
+ userName: '4' # Minimum required by schema
671
+ }
672
+ }.to_not change { MockUser.count }
673
+
674
+ expect(response.status ).to eql(404)
675
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
676
+
677
+ result = JSON.parse(response.body)
678
+
679
+ expect(result['status']).to eql('404')
680
+ end
681
+
682
+ context 'with a block' do
683
+ it 'invokes the block' do
684
+ attributes = { userName: '4' } # Minimum required by schema
685
+
686
+ expect_any_instance_of(CustomReplaceMockUsersController).to receive(:replace).once.and_call_original
687
+ expect {
688
+ put "/CustomReplaceUsers/#{@u2.primary_key}", params: {
689
+ format: :scim,
690
+ userName: '4'
691
+ }
692
+ }.to_not change { MockUser.count }
693
+
694
+ expect(response.status ).to eql(200)
695
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
696
+
697
+ result = JSON.parse(response.body)
698
+
699
+ expect(result['id']).to eql(@u2.primary_key.to_s)
700
+ expect(result['meta']['resourceType']).to eql('User')
701
+
702
+ @u2.reload
703
+
704
+ expect(@u2.username ).to eql('4')
705
+ expect(@u2.first_name).to eql(CustomReplaceMockUsersController::OVERRIDDEN_NAME)
706
+ end
707
+
708
+ it 'notes Rails validation failures' do
709
+ expect_any_instance_of(CustomReplaceMockUsersController).to receive(:replace).once.and_call_original
710
+ expect {
711
+ put "/CustomReplaceUsers/#{@u2.primary_key}", params: {
712
+ format: :scim,
713
+ userName: MockUser::INVALID_USERNAME
714
+ }
715
+ }.to_not change { MockUser.count }
716
+
717
+ expect(response.status ).to eql(400)
718
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
719
+
720
+ result = JSON.parse(response.body)
721
+
722
+ expect(result['scimType']).to eql('invalidValue')
723
+ expect(result['detail']).to include('is reserved')
724
+
725
+ @u2.reload
726
+
727
+ expect(@u2.username).to eql('2')
728
+ expect(@u2.first_name).to eql('Foo')
729
+ expect(@u2.last_name).to eql('Bar')
730
+ expect(@u2.home_email_address).to eql('home_2@test.com')
731
+ end
732
+ end # "context 'with a block' do"
733
+ end # "context '#replace' do"
734
+
735
+ # ===========================================================================
736
+
737
+ context '#update' do
738
+ shared_examples 'an updater' do | force_upper_case: |
739
+ it 'which patches regular attributes' do
740
+ payload = {
741
+ Operations: [
742
+ {
743
+ op: 'add',
744
+ path: 'userName',
745
+ value: '4'
746
+ },
747
+ {
748
+ op: 'replace',
749
+ path: 'emails[type eq "work"]',
750
+ value: { type: 'work', value: 'work_4@test.com' }
751
+ }
752
+ ]
753
+ }
754
+
755
+ payload = spec_helper_hupcase(payload) if force_upper_case
756
+
757
+ # Prove that certain known pathways are called; can then unit test
758
+ # those if need be and be sure that this covers #update actions.
759
+ #
760
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
761
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
762
+
763
+ expect {
764
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
765
+ }.to_not change { MockUser.count }
766
+
767
+ expect(response.status ).to eql(200)
768
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
769
+
770
+ result = JSON.parse(response.body)
771
+
772
+ expect(result['id']).to eql(@u2.primary_key.to_s)
773
+ expect(result['meta']['resourceType']).to eql('User')
774
+
775
+ expect(result).to have_key('name')
776
+ expect(result).to_not have_key('password')
777
+
778
+ @u2.reload
779
+
780
+ expect(@u2.username).to eql('4')
781
+ expect(@u2.first_name).to eql('Foo')
782
+ expect(@u2.last_name).to eql('Bar')
783
+ expect(@u2.home_email_address).to eql('home_2@test.com')
784
+ expect(@u2.work_email_address).to eql('work_4@test.com')
785
+ expect(@u2.password).to eql('oldpassword')
786
+ end
787
+
788
+ it 'which patches "returned: \'never\'" fields' do
789
+ payload = {
790
+ Operations: [
791
+ {
792
+ op: 'replace',
793
+ path: 'password',
794
+ value: 'correcthorsebatterystaple'
795
+ }
796
+ ]
797
+ }
798
+
799
+ payload = spec_helper_hupcase(payload) if force_upper_case
800
+
801
+ expect {
802
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
803
+ }.to_not change { MockUser.count }
804
+
805
+ expect(response.status ).to eql(200)
806
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
807
+
808
+ result = JSON.parse(response.body)
809
+
810
+ expect(result['id']).to eql(@u2.primary_key.to_s)
811
+ expect(result['meta']['resourceType']).to eql('User')
812
+
813
+ expect(result).to have_key('name')
814
+ expect(result).to_not have_key('password')
815
+
816
+ @u2.reload
817
+
818
+ expect(@u2.username).to eql('2')
819
+ expect(@u2.first_name).to eql('Foo')
820
+ expect(@u2.last_name).to eql('Bar')
821
+ expect(@u2.home_email_address).to eql('home_2@test.com')
822
+ expect(@u2.work_email_address).to be_nil
823
+ expect(@u2.password).to eql('correcthorsebatterystaple')
824
+ end
825
+
826
+ context 'which clears attributes' do
827
+ before :each do
828
+ @u2.update!(work_email_address: 'work_2@test.com')
829
+ end
830
+
831
+ it 'with simple paths' do
832
+ payload = {
833
+ Operations: [
834
+ {
835
+ op: 'remove',
836
+ path: 'name.givenName'
837
+ }
838
+ ]
839
+ }
840
+
841
+ payload = spec_helper_hupcase(payload) if force_upper_case
842
+
843
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
844
+ expect {
845
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
846
+ }.to_not change { MockUser.count }
847
+
848
+ expect(response.status ).to eql(200)
849
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
850
+
851
+ result = JSON.parse(response.body)
852
+
853
+ expect(result['id']).to eql(@u2.primary_key.to_s)
854
+ expect(result['meta']['resourceType']).to eql('User')
855
+
856
+ @u2.reload
857
+
858
+ expect(@u2.username).to eql('2')
859
+ expect(@u2.first_name).to be_nil
860
+ expect(@u2.last_name).to eql('Bar')
861
+ expect(@u2.home_email_address).to eql('home_2@test.com')
862
+ expect(@u2.work_email_address).to eql('work_2@test.com')
863
+ end
864
+
865
+ it 'by array entry filter match' do
866
+ payload = {
867
+ Operations: [
868
+ {
869
+ op: 'remove',
870
+ path: 'emails[type eq "work"]'
871
+ }
872
+ ]
873
+ }
874
+
875
+ payload = spec_helper_hupcase(payload) if force_upper_case
876
+
877
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
878
+ expect {
879
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
880
+ }.to_not change { MockUser.count }
881
+
882
+ expect(response.status ).to eql(200)
883
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
884
+
885
+ result = JSON.parse(response.body)
886
+
887
+ expect(result['id']).to eql(@u2.primary_key.to_s)
888
+ expect(result['meta']['resourceType']).to eql('User')
889
+
890
+ @u2.reload
891
+
892
+ expect(@u2.username).to eql('2')
893
+ expect(@u2.first_name).to eql('Foo')
894
+ expect(@u2.last_name).to eql('Bar')
895
+ expect(@u2.home_email_address).to eql('home_2@test.com')
896
+ expect(@u2.work_email_address).to be_nil
897
+ end
898
+
899
+ it 'by whole collection' do
900
+ payload = {
901
+ Operations: [
902
+ {
903
+ op: 'remove',
904
+ path: 'emails'
905
+ }
906
+ ]
907
+ }
908
+
909
+ payload = spec_helper_hupcase(payload) if force_upper_case
910
+
911
+ expect_any_instance_of(MockUsersController).to receive(:update).once.and_call_original
912
+ expect {
913
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
914
+ }.to_not change { MockUser.count }
915
+
916
+ expect(response.status ).to eql(200)
917
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
918
+
919
+ result = JSON.parse(response.body)
920
+
921
+ expect(result['id']).to eql(@u2.primary_key.to_s)
922
+ expect(result['meta']['resourceType']).to eql('User')
923
+
924
+ @u2.reload
925
+
926
+ expect(@u2.username).to eql('2')
927
+ expect(@u2.first_name).to eql('Foo')
928
+ expect(@u2.last_name).to eql('Bar')
929
+ expect(@u2.home_email_address).to be_nil
930
+ expect(@u2.work_email_address).to be_nil
931
+ end
932
+ end # "context 'which clears attributes' do"
933
+ end # "shared_examples 'an updater' do | force_upper_case: |"
934
+
935
+ context 'using schema-matched case' do
936
+ it_behaves_like 'an updater', force_upper_case: false
937
+ end # "context 'using schema-matched case' do"
938
+
939
+ context 'using upper case' do
940
+ it_behaves_like 'an updater', force_upper_case: true
941
+ end # "context 'using upper case' do"
942
+
943
+ it 'notes Rails validation failures' do
944
+ expect {
945
+ patch "/Users/#{@u2.primary_key}", params: {
946
+ format: :scim,
947
+ Operations: [
948
+ {
949
+ op: 'add',
950
+ path: 'userName',
951
+ value: MockUser::INVALID_USERNAME
952
+ }
953
+ ]
954
+ }
955
+ }.to_not change { MockUser.count }
956
+
957
+ expect(response.status ).to eql(400)
958
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
959
+
960
+ result = JSON.parse(response.body)
961
+
962
+ expect(result['scimType']).to eql('invalidValue')
963
+ expect(result['detail']).to include('is reserved')
964
+
965
+ @u2.reload
966
+
967
+ expect(@u2.username).to eql('2')
968
+ expect(@u2.first_name).to eql('Foo')
969
+ expect(@u2.last_name).to eql('Bar')
970
+ expect(@u2.home_email_address).to eql('home_2@test.com')
971
+ end
972
+
973
+ it 'returns 404 if ID is invalid' do
974
+ expect {
975
+ patch '/Users/xyz', params: {
976
+ format: :scim,
977
+ Operations: [
978
+ {
979
+ op: 'add',
980
+ path: 'userName',
981
+ value: '4'
982
+ }
983
+ ]
984
+ }
985
+ }.to_not change { MockUser.count }
986
+
987
+ expect(response.status ).to eql(404)
988
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
989
+
990
+ result = JSON.parse(response.body)
991
+
992
+ expect(result['status']).to eql('404')
993
+ end
994
+
995
+ context 'when removing users from groups' do
996
+ before :each do
997
+ @g1.mock_users << @u1
998
+ @g1.mock_users << @u2
999
+ @g1.mock_users << @u3
1000
+
1001
+ # (Self-check) Verify group representation
1002
+ #
1003
+ get "/Groups/#{@g1.id}", params: { format: :scim }
1004
+
1005
+ expect(response.status).to eql(200)
1006
+ result = JSON.parse(response.body)
1007
+
1008
+ expect(result['members'].map { |m| m['value'] }.sort()).to eql(MockUser.pluck(:primary_key).sort())
1009
+ end
1010
+
1011
+ it 'can remove all users' do
1012
+ expect {
1013
+ expect {
1014
+ patch "/Groups/#{@g1.id}", params: {
1015
+ format: :scim,
1016
+ Operations: [
1017
+ {
1018
+ op: 'remove',
1019
+ path: 'members'
1020
+ }
1021
+ ]
1022
+ }
1023
+ }.to_not change { MockUser.count }
1024
+ }.to_not change { MockGroup.count }
1025
+
1026
+ get "/Groups/#{@g1.id}", params: { format: :scim }
1027
+
1028
+ expect(response.status ).to eql(200)
1029
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1030
+
1031
+ result = JSON.parse(response.body)
1032
+
1033
+ expect(result['members']).to be_empty
1034
+ expect(@g1.reload().mock_users).to be_empty
1035
+ end
1036
+
1037
+ # Define via 'let':
1038
+ #
1039
+ # * Hash 'payload', to send via 'patch'
1040
+ # * MockUser 'removed_user', which is the user that should be removed
1041
+ #
1042
+ shared_examples 'a user remover' do
1043
+ it 'which removes the identified user' do
1044
+ expect {
1045
+ expect {
1046
+ patch "/Groups/#{@g1.id}", params: payload()
1047
+ }.to_not change { MockUser.count }
1048
+ }.to_not change { MockGroup.count }
1049
+
1050
+ expected_remaining_user_ids = MockUser
1051
+ .where.not(primary_key: removed_user().id)
1052
+ .pluck(:primary_key)
1053
+ .sort()
1054
+
1055
+ get "/Groups/#{@g1.id}", params: { format: :scim }
1056
+
1057
+ expect(response.status ).to eql(200)
1058
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1059
+
1060
+ result = JSON.parse(response.body)
1061
+
1062
+ expect(result['members'].map { |m| m['value'] }.sort()).to eql(expected_remaining_user_ids)
1063
+ expect(@g1.reload().mock_users.map(&:primary_key).sort()).to eql(expected_remaining_user_ids)
1064
+ end
1065
+ end
1066
+
1067
+ # https://tools.ietf.org/html/rfc7644#section-3.5.2.2
1068
+ #
1069
+ context 'and using an RFC-compliant payload' do
1070
+ let(:removed_user) { @u2 }
1071
+ let(:payload) do
1072
+ {
1073
+ format: :scim,
1074
+ Operations: [
1075
+ {
1076
+ op: 'remove',
1077
+ path: "members[value eq \"#{removed_user().primary_key}\"]",
1078
+ }
1079
+ ]
1080
+ }
1081
+ end
1082
+
1083
+ it_behaves_like 'a user remover'
1084
+ end # context 'and using an RFC-compliant payload' do
1085
+
1086
+ # https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/use-scim-to-provision-users-and-groups#update-group-remove-members
1087
+ #
1088
+ context 'and using a Microsoft variant payload' do
1089
+ let(:removed_user) { @u2 }
1090
+ let(:payload) do
1091
+ {
1092
+ format: :scim,
1093
+ Operations: [
1094
+ {
1095
+ op: 'remove',
1096
+ path: 'members',
1097
+ value: [{
1098
+ '$ref' => nil,
1099
+ 'value' => removed_user().primary_key
1100
+ }]
1101
+ }
1102
+ ]
1103
+ }
1104
+ end
1105
+
1106
+ it_behaves_like 'a user remover'
1107
+ end # context 'and using a Microsoft variant payload' do
1108
+
1109
+ # https://help.salesforce.com/s/articleView?id=sf.identity_scim_manage_groups.htm&type=5
1110
+ #
1111
+ context 'and using a Salesforce variant payload' do
1112
+ let(:removed_user) { @u2 }
1113
+ let(:payload) do
1114
+ {
1115
+ format: :scim,
1116
+ Operations: [
1117
+ {
1118
+ op: 'remove',
1119
+ path: 'members',
1120
+ value: {
1121
+ 'members' => [{
1122
+ '$ref' => nil,
1123
+ 'value' => removed_user().primary_key
1124
+ }]
1125
+ }
1126
+ }
1127
+ ]
1128
+ }
1129
+ end
1130
+
1131
+ it_behaves_like 'a user remover'
1132
+ end # context 'and using a Salesforce variant payload' do
1133
+ end # "context 'when removing users from groups' do"
1134
+
1135
+ context 'with a block' do
1136
+ it 'invokes the block' do
1137
+ payload = {
1138
+ format: :scim,
1139
+ Operations: [
1140
+ {
1141
+ op: 'add',
1142
+ path: 'userName',
1143
+ value: '4'
1144
+ },
1145
+ {
1146
+ op: 'replace',
1147
+ path: 'emails[type eq "work"]',
1148
+ value: { type: 'work', value: 'work_4@test.com' }
1149
+ }
1150
+ ]
1151
+ }
1152
+
1153
+ expect_any_instance_of(CustomUpdateMockUsersController).to receive(:update).once.and_call_original
1154
+ expect {
1155
+ patch "/CustomUpdateUsers/#{@u2.primary_key}", params: payload
1156
+ }.to_not change { MockUser.count }
1157
+
1158
+ expect(response.status ).to eql(200)
1159
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1160
+
1161
+ result = JSON.parse(response.body)
1162
+
1163
+ expect(result['id']).to eql(@u2.primary_key.to_s)
1164
+ expect(result['meta']['resourceType']).to eql('User')
1165
+
1166
+ @u2.reload
1167
+
1168
+ expect(@u2.username ).to eql('4')
1169
+ expect(@u2.first_name ).to eql(CustomUpdateMockUsersController::OVERRIDDEN_NAME)
1170
+ expect(@u2.work_email_address).to eql('work_4@test.com')
1171
+ end
1172
+
1173
+ it 'notes Rails validation failures' do
1174
+ expect_any_instance_of(CustomUpdateMockUsersController).to receive(:update).once.and_call_original
1175
+ expect {
1176
+ patch "/CustomUpdateUsers/#{@u2.primary_key}", params: {
1177
+ format: :scim,
1178
+ Operations: [
1179
+ {
1180
+ op: 'add',
1181
+ path: 'userName',
1182
+ value: MockUser::INVALID_USERNAME
1183
+ }
1184
+ ]
1185
+ }
1186
+ }.to_not change { MockUser.count }
1187
+
1188
+ expect(response.status ).to eql(400)
1189
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1190
+
1191
+ result = JSON.parse(response.body)
1192
+
1193
+ expect(result['scimType']).to eql('invalidValue')
1194
+ expect(result['detail']).to include('is reserved')
1195
+
1196
+ @u2.reload
1197
+
1198
+ expect(@u2.username).to eql('2')
1199
+ expect(@u2.first_name).to eql('Foo')
1200
+ expect(@u2.last_name).to eql('Bar')
1201
+ expect(@u2.home_email_address).to eql('home_2@test.com')
1202
+ end
1203
+ end # "context 'with a block' do"
1204
+ end # "context '#update' do"
1205
+
1206
+ # ===========================================================================
1207
+ # In-passing parts of tests above show that #create, #replace and #update all
1208
+ # route through #save!, so now add some unit tests for that and for exception
1209
+ # handling overrides invoked via #save!.
1210
+ # ===========================================================================
1211
+
1212
+ context 'overriding #save!' do
1213
+ it 'invokes a block if given one' do
1214
+ mock_before = MockUser.all.to_a
1215
+ attributes = { userName: '5' } # Minimum required by schema
1216
+
1217
+ expect_any_instance_of(CustomSaveMockUsersController).to receive(:create).once.and_call_original
1218
+ expect {
1219
+ post "/CustomSaveUsers", params: attributes.merge(format: :scim)
1220
+ }.to change { MockUser.count }.by(1)
1221
+
1222
+ mock_after = MockUser.all.to_a
1223
+ new_mock = (mock_after - mock_before).first
1224
+
1225
+ expect(response.status ).to eql(201)
1226
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1227
+
1228
+ expect(new_mock.username).to eql(CustomSaveMockUsersController::CUSTOM_SAVE_BLOCK_USERNAME_INDICATOR)
1229
+ end
1230
+ end # "context 'overriding #save!' do
1231
+
1232
+ context 'custom on-save exceptions' do
1233
+ MockUsersController.new.send(:scimitar_rescuable_exceptions).each do | exception_class |
1234
+ it "handles out-of-box exception #{exception_class}" do
1235
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
1236
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
1237
+
1238
+ expect_any_instance_of(MockUser).to receive(:save!).once { raise exception_class }
1239
+
1240
+ expect {
1241
+ post "/Users", params: { format: :scim, userName: SecureRandom.uuid }
1242
+ }.to_not change { MockUser.count }
1243
+
1244
+ expected_status, expected_prefix = if exception_class == ActiveRecord::RecordNotUnique
1245
+ [409, 'Operation failed due to a uniqueness constraint: ']
1246
+ else
1247
+ [400, 'Operation failed since record has become invalid: ']
1248
+ end
1249
+
1250
+ expect(response.status ).to eql(expected_status)
1251
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1252
+
1253
+ result = JSON.parse(response.body)
1254
+
1255
+ # Check basic SCIM error rendering - good enough given other tests
1256
+ # elsewhere. Exact message varies by exception.
1257
+ #
1258
+ expect(result['detail']).to start_with(expected_prefix)
1259
+ end
1260
+ end
1261
+
1262
+ it 'handles custom exceptions' do
1263
+ exception_class = RuntimeError # (for testing only; usually, this would provoke a 500 response)
1264
+
1265
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
1266
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
1267
+
1268
+ expect_any_instance_of(MockUsersController).to receive(:scimitar_rescuable_exceptions).once { [ exception_class ] }
1269
+ expect_any_instance_of(MockUser ).to receive(:save! ).once { raise exception_class }
1270
+
1271
+ expect {
1272
+ post "/Users", params: { format: :scim, userName: SecureRandom.uuid }
1273
+ }.to_not change { MockUser.count }
1274
+
1275
+ expect(response.status ).to eql(400)
1276
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1277
+
1278
+ result = JSON.parse(response.body)
1279
+
1280
+ expect(result['detail']).to start_with('Operation failed since record has become invalid: ')
1281
+ end
1282
+
1283
+ it 'reports other exceptions as 500s' do
1284
+ expect_any_instance_of(MockUsersController).to receive(:create).once.and_call_original
1285
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
1286
+
1287
+ expect_any_instance_of(MockUser).to receive(:save!).once { raise RuntimeError }
1288
+
1289
+ expect {
1290
+ post "/Users", params: { format: :scim, userName: SecureRandom.uuid }
1291
+ }.to_not change { MockUser.count }
1292
+
1293
+ expect(response.status ).to eql(500)
1294
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1295
+
1296
+ result = JSON.parse(response.body)
1297
+
1298
+ expect(result['detail']).to eql('RuntimeError')
1299
+ end
1300
+ end
1301
+
1302
+ # ===========================================================================
1303
+
1304
+ context '#destroy' do
1305
+ it 'deletes an item if given no block' do
1306
+ expect_any_instance_of(MockUsersController).to receive(:destroy).once.and_call_original
1307
+ expect_any_instance_of(MockUser).to receive(:destroy!).once.and_call_original
1308
+ expect {
1309
+ delete "/Users/#{@u2.primary_key}", params: { format: :scim }
1310
+ }.to change { MockUser.count }.by(-1)
1311
+
1312
+ expect(response.status).to eql(204)
1313
+ expect(response.body).to be_empty
1314
+ end
1315
+
1316
+ it 'invokes a block if given one' do
1317
+ expect_any_instance_of(CustomDestroyMockUsersController).to receive(:destroy).once.and_call_original
1318
+ expect_any_instance_of(MockUser).to_not receive(:destroy!)
1319
+
1320
+ expect {
1321
+ delete "/CustomDestroyUsers/#{@u2.primary_key}", params: { format: :scim }
1322
+ }.to_not change { MockUser.count }
1323
+
1324
+ expect(response.status).to eql(204)
1325
+ expect(response.body).to be_empty
1326
+
1327
+ @u2.reload
1328
+ expect(@u2.username).to eql(CustomDestroyMockUsersController::NOT_REALLY_DELETED_USERNAME_INDICATOR)
1329
+ end
1330
+
1331
+ it 'returns 404 if ID is invalid' do
1332
+ expect {
1333
+ delete '/Users/xyz', params: { format: :scim }
1334
+ }.to_not change { MockUser.count }
1335
+
1336
+ expect(response.status ).to eql(404)
1337
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1338
+
1339
+ result = JSON.parse(response.body)
1340
+
1341
+ expect(result['status']).to eql('404')
1342
+ end
1343
+ end # "context '#destroy' do"
1344
+
1345
+ # ===========================================================================
1346
+
1347
+ context 'service methods' do
1348
+ context '#storage_scope' do
1349
+ it 'raises "not implemented" to warn subclass authors' do
1350
+ expect { described_class.new.send(:storage_scope) }.to raise_error(NotImplementedError)
1351
+ end
1352
+ end # "context '#storage_class' do"
1353
+ end # "context 'service methods' do"
1354
+ end