scimitar 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -20
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
  4. data/app/controllers/scimitar/resource_types_controller.rb +0 -2
  5. data/app/controllers/scimitar/resources_controller.rb +0 -2
  6. data/app/controllers/scimitar/schemas_controller.rb +361 -3
  7. data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
  8. data/app/models/scimitar/engine_configuration.rb +3 -1
  9. data/app/models/scimitar/lists/query_parser.rb +88 -3
  10. data/app/models/scimitar/resources/base.rb +48 -14
  11. data/app/models/scimitar/resources/mixin.rb +531 -71
  12. data/app/models/scimitar/schema/name.rb +2 -2
  13. data/app/models/scimitar/schema/user.rb +10 -10
  14. data/config/initializers/scimitar.rb +41 -0
  15. data/lib/scimitar/engine.rb +57 -12
  16. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
  17. data/lib/scimitar/support/utilities.rb +60 -0
  18. data/lib/scimitar/version.rb +2 -2
  19. data/spec/apps/dummy/app/models/mock_user.rb +18 -3
  20. data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
  21. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
  22. data/spec/apps/dummy/db/schema.rb +1 -0
  23. data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
  24. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  25. data/spec/models/scimitar/resources/base_spec.rb +20 -12
  26. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  27. data/spec/models/scimitar/resources/mixin_spec.rb +754 -122
  28. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  29. data/spec/requests/active_record_backed_resources_controller_spec.rb +312 -5
  30. data/spec/requests/engine_spec.rb +75 -0
  31. data/spec/spec_helper.rb +1 -1
  32. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
  33. metadata +22 -22
@@ -10,74 +10,362 @@ RSpec.describe Scimitar::SchemasController do
10
10
  super
11
11
  end
12
12
  end
13
+
13
14
  context '#index' do
14
- it 'returns a collection of supported schemas' do
15
- get :index, params: { format: :scim }
16
- expect(response).to be_ok
17
- parsed_body = JSON.parse(response.body)
18
- expect(parsed_body.length).to eql(3)
19
- schema_names = parsed_body.map {|schema| schema['name']}
20
- expect(schema_names).to match_array(['User', 'ExtendedUser', 'Group'])
21
- end
15
+ shared_examples 'a Schema list which' do
16
+ it 'returns a valid ListResponse' do
17
+ get :index, params: { format: :scim }
18
+ expect(response).to be_ok
22
19
 
23
- it 'returns only the User schema when its id is provided' do
24
- get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
25
- expect(response).to be_ok
26
- parsed_body = JSON.parse(response.body)
27
- expect(parsed_body['name']).to eql('User')
28
- end
20
+ parsed_body = JSON.parse(response.body)
21
+ schema_count = parsed_body['Resources']&.size
29
22
 
30
- it 'includes the controller customised schema location' do
31
- get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
32
- expect(response).to be_ok
33
- parsed_body = JSON.parse(response.body)
34
- expect(parsed_body.dig('meta', 'location')).to eq scim_schemas_url(name: Scimitar::Schema::User.id, test: 1)
35
- end
23
+ expect(parsed_body['schemas' ]).to match_array(['urn:ietf:params:scim:api:messages:2.0:ListResponse'])
24
+ expect(parsed_body['totalResults']).to eql(schema_count)
25
+ expect(parsed_body['itemsPerPage']).to eql(schema_count)
26
+ expect(parsed_body['startIndex' ]).to eql(1)
27
+ end
28
+
29
+ it 'returns a collection of supported schemas' do
30
+ get :index, params: { format: :scim }
31
+ expect(response).to be_ok
32
+
33
+ parsed_body = JSON.parse(response.body)
34
+ expect(parsed_body['Resources']&.size).to eql(4)
35
+
36
+ schema_names = parsed_body['Resources'].map {|schema| schema['name']}
37
+ expect(schema_names).to match_array(['User', 'EnterpriseExtendedUser', 'ManagementExtendedUser', 'Group'])
38
+ end
39
+
40
+ it 'returns only the User schema when its ID is provided' do
41
+ get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
42
+ expect(response).to be_ok
36
43
 
37
- it 'returns only the Group schema when its id is provided' do
38
- get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
39
- expect(response).to be_ok
40
- parsed_body = JSON.parse(response.body)
41
- expect(parsed_body['name']).to eql('Group')
44
+ parsed_body = JSON.parse(response.body)
45
+ expect(parsed_body.dig('Resources', 0, 'name')).to eql('User')
46
+ end
47
+
48
+ it 'includes the controller customised schema location' do
49
+ get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
50
+ expect(response).to be_ok
51
+
52
+ parsed_body = JSON.parse(response.body)
53
+ expect(parsed_body.dig('Resources', 0, 'meta', 'location')).to eq scim_schemas_url(name: Scimitar::Schema::User.id, test: 1)
54
+ end
55
+
56
+ it 'returns only the Group schema when its ID is provided' do
57
+ get :index, params: { name: Scimitar::Schema::Group.id, format: :scim }
58
+ expect(response).to be_ok
59
+
60
+ parsed_body = JSON.parse(response.body)
61
+
62
+ expect(parsed_body['Resources' ]&.size).to eql(1)
63
+ expect(parsed_body['totalResults'] ).to eql(1)
64
+ expect(parsed_body['itemsPerPage'] ).to eql(1)
65
+ expect(parsed_body['startIndex' ] ).to eql(1)
66
+
67
+ expect(parsed_body.dig('Resources', 0, 'name')).to eql('Group')
68
+ end
42
69
  end
43
70
 
44
- context 'with custom resource types' do
45
- around :each do | example |
46
- example.run()
47
- ensure
48
- Scimitar::Engine.reset_custom_resources
71
+ context 'with default engine configuration of schema_list_from_attribute_mappings undefined' do
72
+ it_behaves_like 'a Schema list which'
73
+
74
+ it 'returns all attributes' do
75
+ get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
76
+ expect(response).to be_ok
77
+
78
+ parsed_body = JSON.parse(response.body)
79
+ user_attrs = parsed_body['Resources'].find { | r | r['name'] == 'User' }
80
+
81
+ expect(user_attrs['attributes'].find { | a | a['name'] == 'ims' }).to be_present
82
+ expect(user_attrs['attributes'].find { | a | a['name'] == 'entitlements' }).to be_present
83
+ expect(user_attrs['attributes'].find { | a | a['name'] == 'x509Certificates' }).to be_present
84
+
85
+ name_attr = user_attrs['attributes'].find { | a | a['name'] == 'name' }
86
+
87
+ expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificPrefix' }).to be_present
88
+ expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificSuffix' }).to be_present
49
89
  end
50
90
 
51
- it 'returns only the License schemas when its id is provided' do
52
- license_schema = Class.new(Scimitar::Schema::Base) do
53
- def initialize(options = {})
54
- super(name: 'License',
55
- id: self.class.id,
56
- description: 'Represents a License')
57
- end
58
- def self.id
59
- 'License'
91
+ context 'with custom resource types' do
92
+ around :each do | example |
93
+ example.run()
94
+ ensure
95
+ Scimitar::Engine.reset_custom_resources
96
+ end
97
+
98
+ it 'returns only the License schemas when its ID is provided' do
99
+ license_schema = Class.new(Scimitar::Schema::Base) do
100
+ def initialize(options = {})
101
+ super(name: 'License', id: self.class.id(), description: 'Represents a License')
102
+ end
103
+ def self.id; 'urn:ietf:params:scim:schemas:license'; end
104
+ def self.scim_attributes; []; end
60
105
  end
61
- def self.scim_attributes
62
- []
106
+
107
+ license_resource = Class.new(Scimitar::Resources::Base) do
108
+ set_schema(license_schema)
109
+ def self.endpoint; '/License'; end
63
110
  end
111
+
112
+ Scimitar::Engine.add_custom_resource(license_resource)
113
+
114
+ get :index, params: { name: license_schema.id, format: :scim }
115
+ expect(response).to be_ok
116
+
117
+ parsed_body = JSON.parse(response.body)
118
+ expect(parsed_body.dig('Resources', 0, 'name')).to eql('License')
64
119
  end
120
+ end # "context 'with custom resource types' do"
121
+ end # "context 'with default engine configuration of schema_list_from_attribute_mappings undefined' do"
65
122
 
66
- license_resource = Class.new(Scimitar::Resources::Base) do
67
- set_schema license_schema
68
- def self.endopint
69
- '/Gaga'
70
- end
123
+ context 'with engine configuration of schema_list_from_attribute_mappings set' do
124
+ context 'standard resources' do
125
+ around :each do | example |
126
+ old_config = Scimitar.engine_configuration.schema_list_from_attribute_mappings
127
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = [
128
+ MockUser,
129
+ MockGroup
130
+ ]
131
+ example.run()
132
+ ensure
133
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = old_config
71
134
  end
72
135
 
73
- Scimitar::Engine.add_custom_resource(license_resource)
136
+ it_behaves_like 'a Schema list which'
74
137
 
75
- get :index, params: { name: license_schema.id, format: :scim }
76
- expect(response).to be_ok
77
- parsed_body = JSON.parse(response.body)
78
- expect(parsed_body['name']).to eql('License')
138
+ it 'returns only mapped attributes' do
139
+ get :index, params: { name: Scimitar::Schema::User.id, format: :scim }
140
+ expect(response).to be_ok
141
+
142
+ parsed_body = JSON.parse(response.body)
143
+ user_attrs = parsed_body['Resources'].find { | r | r['name'] == 'User' }
144
+ password_attr = user_attrs['attributes'].find { | a | a['name'] == 'password' }
145
+
146
+ expect(password_attr['mutability']).to eql('writeOnly')
147
+
148
+ expect(user_attrs['attributes'].find { | a | a['name'] == 'ims' }).to_not be_present
149
+ expect(user_attrs['attributes'].find { | a | a['name'] == 'entitlements' }).to_not be_present
150
+ expect(user_attrs['attributes'].find { | a | a['name'] == 'x509Certificates' }).to_not be_present
151
+
152
+ name_attr = user_attrs['attributes'].find { | a | a['name'] == 'name' }
153
+
154
+ expect(name_attr['subAttributes'].find { | s | s['name'] == 'givenName' }).to be_present
155
+ expect(name_attr['subAttributes'].find { | s | s['name'] == 'familyName' }).to be_present
156
+ expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificPrefix' }).to_not be_present
157
+ expect(name_attr['subAttributes'].find { | s | s['name'] == 'honorificSuffix' }).to_not be_present
158
+
159
+ emails_attr = user_attrs['attributes' ].find { | a | a['name'] == 'emails' }
160
+ value_attr = emails_attr['subAttributes'].find { | a | a['name'] == 'value' }
161
+ primary_attr = emails_attr['subAttributes'].find { | a | a['name'] == 'primary' }
162
+
163
+ expect( value_attr['mutability']).to eql('readWrite')
164
+ expect(primary_attr['mutability']).to eql('readOnly')
165
+
166
+ expect(emails_attr['subAttributes'].find { | s | s['name'] == 'type' }).to_not be_present
167
+ expect(emails_attr['subAttributes'].find { | s | s['name'] == 'display' }).to_not be_present
168
+
169
+ groups_attr = user_attrs['attributes' ].find { | a | a['name'] == 'groups' }
170
+ value_attr = groups_attr['subAttributes'].find { | a | a['name'] == 'value' }
171
+ display_attr = groups_attr['subAttributes'].find { | a | a['name'] == 'display' }
172
+
173
+ expect( value_attr['mutability']).to eql('readOnly')
174
+ expect(display_attr['mutability']).to eql('readOnly')
175
+ end
79
176
  end
80
- end
81
- end
82
- end
83
177
 
178
+ context 'with custom resource types' do
179
+ let(:license_schema) {
180
+ Class.new(Scimitar::Schema::Base) do
181
+ def initialize(options = {})
182
+ super(
183
+ id: self.class.id(),
184
+ name: 'License',
185
+ description: 'Represents a license',
186
+ scim_attributes: self.class.scim_attributes
187
+ )
188
+ end
189
+ def self.id; 'urn:ietf:params:scim:schemas:license'; end
190
+ def self.scim_attributes
191
+ [
192
+ Scimitar::Schema::Attribute.new(name: 'licenseNumber', type: 'string'),
193
+ Scimitar::Schema::Attribute.new(name: 'licenseExpired', type: 'boolean', mutability: 'readOnly'),
194
+ ]
195
+ end
196
+ end
197
+ }
198
+
199
+ let(:license_resource) {
200
+ local_var_license_schema = license_schema()
201
+
202
+ Class.new(Scimitar::Resources::Base) do
203
+ set_schema(local_var_license_schema)
204
+ def self.endpoint; '/License'; end
205
+ end
206
+ }
207
+
208
+ let(:license_model_base) {
209
+ local_var_license_resource = license_resource()
210
+
211
+ Class.new do
212
+ singleton_class.class_eval do
213
+ define_method(:scim_resource_type) { local_var_license_resource }
214
+ end
215
+ end
216
+ }
217
+
218
+ around :each do | example |
219
+ old_config = Scimitar.engine_configuration.schema_list_from_attribute_mappings
220
+ Scimitar::Engine.add_custom_resource(license_resource())
221
+ example.run()
222
+ ensure
223
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = old_config
224
+ Scimitar::Engine.reset_custom_resources
225
+ end
226
+
227
+ context 'with an empty attribute map' do
228
+ it 'returns no attributes' do
229
+ license_model = Class.new(license_model_base()) do
230
+ attr_accessor :license_number
231
+
232
+ def self.scim_mutable_attributes; nil; end
233
+ def self.scim_queryable_attributes; nil; end
234
+ def self.scim_attributes_map; {}; end # Empty map
235
+
236
+ include Scimitar::Resources::Mixin
237
+ end
238
+
239
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
240
+
241
+ get :index, params: { format: :scim }
242
+ expect(response).to be_ok
243
+
244
+ parsed_body = JSON.parse(response.body)
245
+
246
+ expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
247
+ expect(parsed_body.dig('Resources', 0, 'attributes')).to be_empty
248
+ end
249
+ end # "context 'with an empty attribute map' do"
250
+
251
+ context 'with a defined attribute map' do
252
+ it 'returns only the License schemas when its ID is provided' do
253
+ license_model = Class.new(license_model_base()) do
254
+ attr_accessor :license_number
255
+
256
+ def self.scim_mutable_attributes; nil; end
257
+ def self.scim_queryable_attributes; nil; end
258
+ def self.scim_attributes_map # Simple map
259
+ { licenseNumber: :license_number }
260
+ end
261
+
262
+ include Scimitar::Resources::Mixin
263
+ end
264
+
265
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
266
+
267
+ get :index, params: { format: :scim }
268
+ expect(response).to be_ok
269
+
270
+ parsed_body = JSON.parse(response.body)
271
+
272
+ expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
273
+ expect(parsed_body.dig('Resources', 0, 'attributes').size ).to eql(1)
274
+ expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'name' )).to eql('licenseNumber')
275
+ expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'mutability')).to eql('readWrite')
276
+ end
277
+ end # "context 'with a defined attribute map' do"
278
+
279
+ context 'with mutability overridden' do
280
+ it 'returns read-only when expected' do
281
+ license_model = Class.new(license_model_base()) do
282
+ attr_accessor :license_number
283
+
284
+ def self.scim_mutable_attributes; []; end # Note empty array, NOT "nil" - no mutable attributes
285
+ def self.scim_queryable_attributes; nil; end
286
+ def self.scim_attributes_map
287
+ { licenseNumber: :license_number }
288
+ end
289
+
290
+ include Scimitar::Resources::Mixin
291
+ end
292
+
293
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
294
+
295
+ get :index, params: { format: :scim }
296
+ expect(response).to be_ok
297
+
298
+ parsed_body = JSON.parse(response.body)
299
+
300
+ expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
301
+ expect(parsed_body.dig('Resources', 0, 'attributes').size ).to eql(1)
302
+ expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'name' )).to eql('licenseNumber')
303
+ expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'mutability')).to eql('readOnly')
304
+ end
305
+
306
+ it 'returns write-only when expected' do
307
+ license_model = Class.new(license_model_base()) do
308
+ attr_writer :license_number # Writer only, no reader
309
+
310
+ def self.scim_mutable_attributes; nil; end
311
+ def self.scim_queryable_attributes; nil; end
312
+ def self.scim_attributes_map
313
+ { licenseNumber: :license_number }
314
+ end
315
+
316
+ include Scimitar::Resources::Mixin
317
+ end
318
+
319
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
320
+
321
+ get :index, params: { format: :scim }
322
+ expect(response).to be_ok
323
+
324
+ parsed_body = JSON.parse(response.body)
325
+
326
+ expect(parsed_body.dig('Resources', 0, 'name' )).to eql('License')
327
+ expect(parsed_body.dig('Resources', 0, 'attributes').size ).to eql(1)
328
+ expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'name' )).to eql('licenseNumber')
329
+ expect(parsed_body.dig('Resources', 0, 'attributes', 0, 'mutability')).to eql('writeOnly')
330
+ end
331
+
332
+ it 'handles conflicts via reality-wins' do
333
+ license_model = Class.new(license_model_base()) do
334
+ def self.scim_mutable_attributes; [:licence_expired]; end
335
+ def self.scim_queryable_attributes; nil; end
336
+ def self.scim_attributes_map
337
+ { licenseNumber: :license_number, licenseExpired: :licence_expired }
338
+ end
339
+
340
+ include Scimitar::Resources::Mixin
341
+ end
342
+
343
+ Scimitar.engine_configuration.schema_list_from_attribute_mappings = [license_model]
344
+
345
+ get :index, params: { format: :scim }
346
+ expect(response).to be_ok
347
+
348
+ parsed_body = JSON.parse(response.body)
349
+ attributes = parsed_body.dig('Resources', 0, 'attributes')
350
+
351
+ expect(parsed_body.dig('Resources', 0, 'name')).to eql('License')
352
+ expect(attributes.size).to eql(2)
353
+
354
+ number_attr = attributes.find { | a | a['name'] == 'licenseNumber' }
355
+ expiry_attr = attributes.find { | a | a['name'] == 'licenseExpired' }
356
+
357
+ # Number attribute - no reader or writer, so code has to shrug and
358
+ # say "it's broken, so I'll quote the schema verbatim'.
359
+ #
360
+ # Expiry attribute - is read-only in schema, but we declare it as a
361
+ # writable attribute and provide no reader. This clashes badly; the
362
+ # schema read-only declaration is ignored in favour of reality.
363
+ #
364
+ expect(number_attr['mutability']).to eql('readWrite')
365
+ expect(expiry_attr['mutability']).to eql('writeOnly')
366
+ end
367
+ end # "context 'with mutability overridden' do"
368
+ end # "context 'with custom resource types' do"
369
+ end # "context 'with engine configuration of schema_list_from_attribute_mappings: true' do"
370
+ end # "context '#index' do
371
+ end
@@ -60,6 +60,15 @@ RSpec.describe Scimitar::Lists::QueryParser do
60
60
  expect(%Q("O'Malley")).to eql(tree[2])
61
61
  end
62
62
 
63
+ it "extended attribute equals" do
64
+ @instance.parse(%Q(primaryEmail eq "foo@bar.com"))
65
+
66
+ rpn = @instance.rpn
67
+ expect('primaryEmail').to eql(rpn[0])
68
+ expect(%Q("foo@bar.com")).to eql(rpn[1])
69
+ expect('eq').to eql(rpn[2])
70
+ end
71
+
63
72
  it "user name starts with" do
64
73
  @instance.parse(%Q(userName sw "J"))
65
74
 
@@ -337,6 +346,67 @@ RSpec.describe Scimitar::Lists::QueryParser do
337
346
  result = @instance.send(:flatten_filter, 'emails[type eq "work" and value co "@example.com" ] or userType eq "Admin" or ims[type eq "xmpp" and value co "@foo.com"]')
338
347
  expect(result).to eql('emails.type eq "work" and emails.value co "@example.com" or userType eq "Admin" or ims.type eq "xmpp" and ims.value co "@foo.com"')
339
348
  end
349
+
350
+ it 'handles an example previously described as unsupported in README.md' do
351
+ result = @instance.send(:flatten_filter, 'filter=userType eq "Employee" and emails[type eq "work" and value co "@example.com"]')
352
+ expect(result).to eql('filter=userType eq "Employee" and emails.type eq "work" and emails.value co "@example.com"')
353
+ end
354
+
355
+ # https://github.com/RIPAGlobal/scimitar/issues/116
356
+ #
357
+ context 'with schema IDs (GitHub issue #116)' do
358
+ it 'handles simple attributes' do
359
+ result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeId eq "gsar"')
360
+ expect(result).to eql('employeeId eq "gsar"')
361
+ end
362
+
363
+ it 'handles dotted attribute paths' do
364
+ result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary.path eq "gsar"')
365
+ expect(result).to eql('imaginary.path eq "gsar"')
366
+ end
367
+
368
+ it 'replaces all examples' do
369
+ result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeId eq "gsar" or urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary.path eq "gsar"')
370
+ expect(result).to eql('employeeId eq "gsar" or imaginary.path eq "gsar"')
371
+ end
372
+
373
+ it 'handles the square bracket form with schema ID at the root' do
374
+ result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User[employeeId eq "gsar"')
375
+ expect(result).to eql('employeeId eq "gsar"')
376
+ end
377
+
378
+ it 'handles the square bracket form with schema ID and attribute at the root' do
379
+ result = @instance.send(:flatten_filter, 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:imaginary[path eq "gsar"')
380
+ expect(result).to eql('imaginary.path eq "gsar"')
381
+ end
382
+ end
383
+
384
+ # https://github.com/RIPAGlobal/scimitar/issues/115
385
+ #
386
+ context 'broken filters from Microsoft (GitHub issue #115)' do
387
+ it 'work with "eq"' do
388
+ result = @instance.send(:flatten_filter, 'emails[type eq "work"].value eq "foo@bar.com"')
389
+ expect(result).to eql('emails.type eq "work" and emails.value eq "foo@bar.com"')
390
+ end
391
+
392
+ it 'work with "ne"' do # (just check a couple of operators, not all!)
393
+ result = @instance.send(:flatten_filter, 'emails[type eq "work"].value ne "foo@bar.com"')
394
+ expect(result).to eql('emails.type eq "work" and emails.value ne "foo@bar.com"')
395
+ end
396
+
397
+ it 'preserve input case' do
398
+ result = @instance.send(:flatten_filter, 'emaiLs[TYPE eq "work"].valUE eq "FOO@bar.com"')
399
+ expect(result).to eql('emaiLs.TYPE eq "work" and emaiLs.valUE eq "FOO@bar.com"')
400
+ end
401
+
402
+ # At the time of writing, this was used in a "belt and braces" request
403
+ # spec in 'active_record_backed_resources_controller_spec.rb'.
404
+ #
405
+ it 'handles more complex, hypothetical cases' do
406
+ result = @instance.send(:flatten_filter, 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"')
407
+ expect(result).to eql('name.givenName eq "FOO" and name.familyName pr and emails ne "home_1@test.com"')
408
+ end
409
+ end # "context 'broken filters from Microsoft' do"
340
410
  end # "context 'when flattening is needed' do"
341
411
 
342
412
  context 'with bad filters' do
@@ -4,7 +4,7 @@ RSpec.describe Scimitar::Resources::Base do
4
4
  context 'basic operation' do
5
5
  FirstCustomSchema = Class.new(Scimitar::Schema::Base) do
6
6
  def self.id
7
- 'custom-id'
7
+ 'urn:ietf:params:scim:schemas:custom-id'
8
8
  end
9
9
 
10
10
  def self.scim_attributes
@@ -16,7 +16,7 @@ RSpec.describe Scimitar::Resources::Base do
16
16
  name: 'names', multiValued: true, complexType: Scimitar::ComplexTypes::Name, required: false
17
17
  ),
18
18
  Scimitar::Schema::Attribute.new(
19
- name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: false
19
+ name: 'privateName', complexType: Scimitar::ComplexTypes::Name, required: false, returned: 'never'
20
20
  ),
21
21
  ]
22
22
  end
@@ -27,6 +27,14 @@ RSpec.describe Scimitar::Resources::Base do
27
27
  end
28
28
 
29
29
  context '#initialize' do
30
+ it 'accepts nil for non-required attributes' do
31
+ resource = CustomResourse.new(name: nil, names: nil, privateName: nil)
32
+
33
+ expect(resource.name).to be_nil
34
+ expect(resource.names).to be_nil
35
+ expect(resource.privateName).to be_nil
36
+ end
37
+
30
38
  shared_examples 'an initializer' do | force_upper_case: |
31
39
  it 'which builds the nested type' do
32
40
  attributes = {
@@ -117,7 +125,7 @@ RSpec.describe Scimitar::Resources::Base do
117
125
 
118
126
  result = resource.as_json
119
127
 
120
- expect(result['schemas'] ).to eql(['custom-id'])
128
+ expect(result['schemas'] ).to eql(['urn:ietf:params:scim:schemas:custom-id'])
121
129
  expect(result['meta']['resourceType']).to eql('CustomResourse')
122
130
  expect(result['errors'] ).to be_nil
123
131
  end
@@ -136,7 +144,7 @@ RSpec.describe Scimitar::Resources::Base do
136
144
 
137
145
  result = resource.as_json
138
146
 
139
- expect(result['schemas'] ).to eql(['custom-id'])
147
+ expect(result['schemas'] ).to eql(['urn:ietf:params:scim:schemas:custom-id'])
140
148
  expect(result['meta']['resourceType']).to eql('CustomResourse')
141
149
  expect(result['errors'] ).to be_nil
142
150
  expect(result['name'] ).to be_present
@@ -287,7 +295,7 @@ RSpec.describe Scimitar::Resources::Base do
287
295
  context 'of custom schema' do
288
296
  ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
289
297
  def self.id
290
- 'custom-id'
298
+ 'urn:ietf:params:scim:schemas:custom-id'
291
299
  end
292
300
 
293
301
  def self.scim_attributes
@@ -297,7 +305,7 @@ RSpec.describe Scimitar::Resources::Base do
297
305
 
298
306
  ExtensionSchema = Class.new(Scimitar::Schema::Base) do
299
307
  def self.id
300
- 'extension-id'
308
+ 'urn:ietf:params:scim:schemas:extension'
301
309
  end
302
310
 
303
311
  def self.scim_attributes
@@ -325,13 +333,13 @@ RSpec.describe Scimitar::Resources::Base do
325
333
 
326
334
  context '#initialize' do
327
335
  it 'allows setting extension attributes' do
328
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
336
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {relationship: 'GAGA'})
329
337
  expect(resource.relationship).to eql('GAGA')
330
338
  end
331
339
 
332
340
  it 'allows setting complex extension attributes' do
333
341
  user_groups = [{ value: '123' }, { value: '456'}]
334
- resource = resource_class.new('extension-id' => {userGroups: user_groups})
342
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {userGroups: user_groups})
335
343
  expect(resource.userGroups.map(&:value)).to eql(['123', '456'])
336
344
  end
337
345
  end # "context '#initialize' do"
@@ -340,8 +348,8 @@ RSpec.describe Scimitar::Resources::Base do
340
348
  it 'namespaces the extension attributes' do
341
349
  resource = resource_class.new(relationship: 'GAGA')
342
350
  hash = resource.as_json
343
- expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
344
- expect(hash["extension-id"]).to eql("relationship" => 'GAGA')
351
+ expect(hash["schemas"]).to eql(['urn:ietf:params:scim:schemas:custom-id', 'urn:ietf:params:scim:schemas:extension'])
352
+ expect(hash["urn:ietf:params:scim:schemas:extension"]).to eql("relationship" => 'GAGA')
345
353
  end
346
354
  end # "context '#as_json' do"
347
355
 
@@ -354,10 +362,10 @@ RSpec.describe Scimitar::Resources::Base do
354
362
 
355
363
  context 'validation' do
356
364
  it 'validates into custom schema' do
357
- resource = resource_class.new('extension-id' => {})
365
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {})
358
366
  expect(resource.valid?).to eql(false)
359
367
 
360
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
368
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {relationship: 'GAGA'})
361
369
  expect(resource.relationship).to eql('GAGA')
362
370
  expect(resource.valid?).to eql(true)
363
371
  end
@@ -4,7 +4,20 @@ RSpec.describe Scimitar::Resources::Base do
4
4
  context '#valid?' do
5
5
  MyCustomSchema = Class.new(Scimitar::Schema::Base) do
6
6
  def self.id
7
- 'custom-id'
7
+ 'urn:ietf:params:scim:schemas:custom-id'
8
+ end
9
+
10
+ class NameWithRequirementsSchema < Scimitar::Schema::Base
11
+ def self.scim_attributes
12
+ @scim_attributes ||= [
13
+ Scimitar::Schema::Attribute.new(name: 'familyName', type: 'string', required: true),
14
+ Scimitar::Schema::Attribute.new(name: 'givenName', type: 'string', required: true),
15
+ ]
16
+ end
17
+ end
18
+
19
+ class NameWithRequirementsComplexType < Scimitar::ComplexTypes::Base
20
+ set_schema NameWithRequirementsSchema
8
21
  end
9
22
 
10
23
  def self.scim_attributes
@@ -16,10 +29,10 @@ RSpec.describe Scimitar::Resources::Base do
16
29
  name: 'enforce', type: 'boolean', required: true
17
30
  ),
18
31
  Scimitar::Schema::Attribute.new(
19
- name: 'complexName', complexType: Scimitar::ComplexTypes::Name, required: false
32
+ name: 'complexName', complexType: NameWithRequirementsComplexType, required: false
20
33
  ),
21
34
  Scimitar::Schema::Attribute.new(
22
- name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued:true, required: false
35
+ name: 'complexNames', complexType: Scimitar::ComplexTypes::Name, multiValued: true, required: false
23
36
  ),
24
37
  Scimitar::Schema::Attribute.new(
25
38
  name: 'vdtpTestByEmail', complexType: Scimitar::ComplexTypes::Email, required: false