scimitar 1.8.2 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) 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 +36 -5
  11. data/app/models/scimitar/resources/mixin.rb +133 -43
  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/utilities.rb +60 -0
  17. data/lib/scimitar/version.rb +2 -2
  18. data/spec/apps/dummy/app/models/mock_user.rb +18 -3
  19. data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
  20. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
  21. data/spec/apps/dummy/db/schema.rb +1 -0
  22. data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
  23. data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
  24. data/spec/models/scimitar/resources/base_spec.rb +11 -11
  25. data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
  26. data/spec/models/scimitar/resources/mixin_spec.rb +71 -10
  27. data/spec/models/scimitar/schema/user_spec.rb +2 -2
  28. data/spec/requests/active_record_backed_resources_controller_spec.rb +231 -0
  29. data/spec/requests/engine_spec.rb +75 -0
  30. data/spec/spec_helper.rb +1 -1
  31. 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
@@ -125,7 +125,7 @@ RSpec.describe Scimitar::Resources::Base do
125
125
 
126
126
  result = resource.as_json
127
127
 
128
- expect(result['schemas'] ).to eql(['custom-id'])
128
+ expect(result['schemas'] ).to eql(['urn:ietf:params:scim:schemas:custom-id'])
129
129
  expect(result['meta']['resourceType']).to eql('CustomResourse')
130
130
  expect(result['errors'] ).to be_nil
131
131
  end
@@ -144,7 +144,7 @@ RSpec.describe Scimitar::Resources::Base do
144
144
 
145
145
  result = resource.as_json
146
146
 
147
- expect(result['schemas'] ).to eql(['custom-id'])
147
+ expect(result['schemas'] ).to eql(['urn:ietf:params:scim:schemas:custom-id'])
148
148
  expect(result['meta']['resourceType']).to eql('CustomResourse')
149
149
  expect(result['errors'] ).to be_nil
150
150
  expect(result['name'] ).to be_present
@@ -295,7 +295,7 @@ RSpec.describe Scimitar::Resources::Base do
295
295
  context 'of custom schema' do
296
296
  ThirdCustomSchema = Class.new(Scimitar::Schema::Base) do
297
297
  def self.id
298
- 'custom-id'
298
+ 'urn:ietf:params:scim:schemas:custom-id'
299
299
  end
300
300
 
301
301
  def self.scim_attributes
@@ -305,7 +305,7 @@ RSpec.describe Scimitar::Resources::Base do
305
305
 
306
306
  ExtensionSchema = Class.new(Scimitar::Schema::Base) do
307
307
  def self.id
308
- 'extension-id'
308
+ 'urn:ietf:params:scim:schemas:extension'
309
309
  end
310
310
 
311
311
  def self.scim_attributes
@@ -333,13 +333,13 @@ RSpec.describe Scimitar::Resources::Base do
333
333
 
334
334
  context '#initialize' do
335
335
  it 'allows setting extension attributes' do
336
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
336
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {relationship: 'GAGA'})
337
337
  expect(resource.relationship).to eql('GAGA')
338
338
  end
339
339
 
340
340
  it 'allows setting complex extension attributes' do
341
341
  user_groups = [{ value: '123' }, { value: '456'}]
342
- resource = resource_class.new('extension-id' => {userGroups: user_groups})
342
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {userGroups: user_groups})
343
343
  expect(resource.userGroups.map(&:value)).to eql(['123', '456'])
344
344
  end
345
345
  end # "context '#initialize' do"
@@ -348,8 +348,8 @@ RSpec.describe Scimitar::Resources::Base do
348
348
  it 'namespaces the extension attributes' do
349
349
  resource = resource_class.new(relationship: 'GAGA')
350
350
  hash = resource.as_json
351
- expect(hash["schemas"]).to eql(['custom-id', 'extension-id'])
352
- 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')
353
353
  end
354
354
  end # "context '#as_json' do"
355
355
 
@@ -362,10 +362,10 @@ RSpec.describe Scimitar::Resources::Base do
362
362
 
363
363
  context 'validation' do
364
364
  it 'validates into custom schema' do
365
- resource = resource_class.new('extension-id' => {})
365
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {})
366
366
  expect(resource.valid?).to eql(false)
367
367
 
368
- resource = resource_class.new('extension-id' => {relationship: 'GAGA'})
368
+ resource = resource_class.new('urn:ietf:params:scim:schemas:extension' => {relationship: 'GAGA'})
369
369
  expect(resource.relationship).to eql('GAGA')
370
370
  expect(resource.valid?).to eql(true)
371
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