main
  1# frozen_string_literal: true
  2
  3RSpec.describe Scim::Kit::V2::Resource do
  4  subject { described_class.new(schemas: schemas, location: resource_location) }
  5
  6  let(:schemas) { [schema] }
  7  let(:schema) { Scim::Kit::V2::Schema.new(id: Scim::Kit::V2::Schemas::USER, name: 'User', location: FFaker::Internet.uri('https')) }
  8  let(:resource_location) { FFaker::Internet.uri('https') }
  9
 10  context 'when the schemas is empty' do
 11    let(:schemas) { [] }
 12
 13    specify { expect { subject }.not_to raise_error }
 14    specify { expect(subject.meta.resource_type).to eql('Unknown') }
 15  end
 16
 17  context 'with common attributes' do
 18    let(:id) { SecureRandom.uuid }
 19    let(:external_id) { SecureRandom.uuid }
 20    let(:created_at) { Time.now }
 21    let(:updated_at) { Time.now }
 22    let(:version) { SecureRandom.uuid }
 23
 24    before do
 25      subject.id = id
 26      subject.external_id = external_id
 27      subject.meta.created = created_at
 28      subject.meta.last_modified = updated_at
 29      subject.meta.version = version
 30    end
 31
 32    specify { expect(subject.id).to eql(id) }
 33    specify { expect(subject.external_id).to eql(external_id) }
 34    specify { expect(subject.meta.resource_type).to eql('User') }
 35    specify { expect(subject.meta.location).to eql(resource_location) }
 36    specify { expect(subject.meta.created).to eql(created_at) }
 37    specify { expect(subject.meta.last_modified).to eql(updated_at) }
 38    specify { expect(subject.meta.version).to eql(version) }
 39
 40    describe '#as_json' do
 41      specify { expect(subject.as_json[:schemas]).to match_array([schema.id]) }
 42      specify { expect(subject.as_json[:id]).to eql(id) }
 43      specify { expect(subject.as_json[:externalId]).to be_nil } # only render in client mode
 44      specify { expect(subject.as_json[:meta][:resourceType]).to eql('User') }
 45      specify { expect(subject.as_json[:meta][:location]).to eql(resource_location) }
 46      specify { expect(subject.as_json[:meta][:created]).to eql(created_at.iso8601) }
 47      specify { expect(subject.as_json[:meta][:lastModified]).to eql(updated_at.iso8601) }
 48      specify { expect(subject.as_json[:meta][:version]).to eql(version) }
 49    end
 50  end
 51
 52  context 'with attribute named "members"' do
 53    before do
 54      schema.add_attribute(name: 'members') do |attribute|
 55        attribute.mutability = :read_only
 56        attribute.multi_valued = true
 57        attribute.add_attribute(name: 'value') do |z|
 58          z.mutability = :immutable
 59        end
 60        attribute.add_attribute(name: '$ref') do |z|
 61          z.reference_types = %w[User Group]
 62          z.mutability = :immutable
 63        end
 64        attribute.add_attribute(name: 'type') do |z|
 65          z.canonical_values = %w[User Group]
 66          z.mutability = :immutable
 67        end
 68      end
 69      subject.members << { value: SecureRandom.uuid, '$ref' => FFaker::Internet.uri('https'), type: 'User' }
 70    end
 71
 72    specify { expect(subject.members[0][:type]).to eql('User') }
 73    specify { expect(subject.as_json[:members][0][:type]).to eql('User') }
 74    specify { expect(subject.to_h[:members][0][:type]).to eql('User') }
 75  end
 76
 77  context 'with custom string attribute' do
 78    let(:user_name) { FFaker::Internet.user_name }
 79
 80    before do
 81      schema.add_attribute(name: 'userName')
 82      subject.user_name = user_name
 83    end
 84
 85    specify { expect(subject.user_name).to eql(user_name) }
 86  end
 87
 88  context 'with attribute named "type"' do
 89    before do
 90      schema.add_attribute(name: 'type')
 91      subject.type = 'User'
 92    end
 93
 94    specify { expect(subject.type).to eql('User') }
 95    specify { expect(subject.as_json[:type]).to eql('User') }
 96    specify { expect(subject.send(:attribute_for, :type)._type).to be_instance_of(Scim::Kit::V2::AttributeType) }
 97  end
 98
 99  context 'with attribute named $ref' do
100    before do
101      schema.add_attribute(name: '$ref')
102      subject.write_attribute('$ref', 'User')
103    end
104
105    specify { expect(subject.read_attribute('$ref')).to eql('User') }
106    specify { expect(subject.as_json['$ref']).to eql('User') }
107    specify { expect(subject.send(:attribute_for, '$ref')._type).to be_instance_of(Scim::Kit::V2::AttributeType) }
108  end
109
110  context 'with a complex attribute' do
111    before do
112      schema.add_attribute(name: 'name') do |x|
113        x.add_attribute(name: 'familyName')
114        x.add_attribute(name: 'givenName')
115      end
116      subject.name.family_name = 'Garrett'
117      subject.name.given_name = 'Tsuyoshi'
118    end
119
120    specify { expect(subject.name.family_name).to eql('Garrett') }
121    specify { expect(subject.name.given_name).to eql('Tsuyoshi') }
122
123    describe '#as_json' do
124      specify { expect(subject.as_json[:name][:familyName]).to eql('Garrett') }
125      specify { expect(subject.as_json[:name][:givenName]).to eql('Tsuyoshi') }
126    end
127  end
128
129  context 'with a complex multi valued attribute' do
130    let(:email) { FFaker::Internet.email }
131    let(:other_email) { FFaker::Internet.email }
132
133    before do
134      schema.add_attribute(name: 'emails', type: :complex) do |x|
135        x.multi_valued = true
136        x.add_attribute(name: 'value') do |y|
137          y.required = true
138        end
139        x.add_attribute(name: 'primary', type: :boolean)
140      end
141      subject.emails = [
142        { value: email, primary: true },
143        { value: other_email, primary: false }
144      ]
145    end
146
147    specify { expect(subject.emails).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) }
148    specify { expect(subject.as_json[:emails]).to match_array([{ value: email, primary: true }, { value: other_email, primary: false }]) }
149
150    context 'when one attribute has an invalid type' do
151      before do
152        subject.emails = [{ value: email, primary: 'q' }]
153        subject.valid?
154      end
155
156      specify { expect(subject).not_to be_valid }
157      specify { expect(subject.errors[:primary]).to be_present }
158    end
159
160    context 'when a required attribute is missing' do
161      before do
162        subject.emails = [{ primary: true }]
163        subject.valid?
164      end
165
166      specify { expect(subject).not_to be_valid }
167      specify { expect(subject.errors[:value]).to be_present }
168    end
169  end
170
171  context 'with multiple schemas' do
172    let(:schemas) { [schema, extension] }
173    let(:extension) { Scim::Kit::V2::Schema.new(id: extension_id, name: 'Extension', location: FFaker::Internet.uri('https')) }
174    let(:extension_id) { 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' }
175
176    before do
177      schema.add_attribute(name: :country)
178      extension.add_attribute(name: :province)
179    end
180
181    context 'without any collisions' do
182      before do
183        subject.country = 'canada'
184        subject.province = 'alberta'
185      end
186
187      specify { expect(subject.country).to eql('canada') }
188      specify { expect(subject.province).to eql('alberta') }
189      specify { expect(subject.as_json[:country]).to eql('canada') }
190      specify { expect(subject.as_json[extension_id][:province]).to eql('alberta') }
191    end
192
193    context 'with an extension attribute with the same name as a core attribute' do
194      before do
195        extension.add_attribute(name: :country)
196
197        subject.country = 'canada'
198        subject.write_attribute("#{extension_id}#country", 'usa')
199      end
200
201      specify { expect(subject.country).to eql('canada') }
202      specify { expect(subject.read_attribute("#{extension_id}#country")).to eql('usa') }
203      specify { expect(subject.as_json[:country]).to eql('canada') }
204      specify { expect(subject.as_json[extension_id][:country]).to eql('usa') }
205    end
206  end
207
208  describe '#valid?' do
209    context 'when valid' do
210      before { subject.id = SecureRandom.uuid }
211
212      specify { expect(subject).to be_valid }
213    end
214
215    context 'when a required simple attribute is blank' do
216      before do
217        schema.add_attribute(name: 'userName') do |x|
218          x.required = true
219        end
220        subject.id = SecureRandom.uuid
221        subject.valid?
222      end
223
224      specify { expect(subject).not_to be_valid }
225      specify { expect(subject.errors[:user_name]).to be_present }
226    end
227
228    context 'when not matching a canonical value' do
229      before do
230        schema.add_attribute(name: 'hero') do |x|
231          x.canonical_values = %w[batman robin]
232        end
233        subject.id = SecureRandom.uuid
234        subject.hero = 'spiderman'
235        subject.valid?
236      end
237
238      specify { expect(subject).not_to be_valid }
239      specify { expect(subject.errors[:hero]).to be_present }
240    end
241
242    context 'when validating a complex type' do
243      before do
244        schema.add_attribute(name: :manager, type: :complex) do |x|
245          x.multi_valued = false
246          x.required = false
247          x.mutability = :read_write
248          x.returned = :default
249          x.add_attribute(name: :value, type: :string) do |y|
250            y.multi_valued = false
251            y.required = false
252            y.case_exact = false
253            y.mutability = :read_write
254            y.returned = :default
255            y.uniqueness = :none
256          end
257          x.add_attribute(name: '$ref', type: :reference) do |y|
258            y.multi_valued = false
259            y.required = false
260            y.case_exact = false
261            y.mutability = :read_write
262            y.returned = :default
263            y.uniqueness = :none
264          end
265          x.add_attribute(name: :display_name, type: :string) do |y|
266            y.multi_valued = false
267            y.required = true
268            y.case_exact = false
269            y.mutability = :read_only
270            y.returned = :default
271            y.uniqueness = :none
272          end
273        end
274      end
275
276      context 'when valid' do
277        before do
278          subject.manager.value = SecureRandom.uuid
279          subject.manager.write_attribute('$ref', FFaker::Internet.uri('https'))
280          subject.manager.display_name = SecureRandom.uuid
281        end
282
283        specify { expect(subject).to be_valid }
284      end
285
286      context 'when invalid' do
287        before do
288          subject.manager.value = SecureRandom.uuid
289          subject.manager.write_attribute('$ref', SecureRandom.uuid)
290          subject.manager.display_name = nil
291          subject.valid?
292        end
293
294        specify { expect(subject).not_to be_valid }
295        specify { expect(subject.errors[:display_name]).to be_present }
296      end
297    end
298  end
299
300  context 'when building a new resource' do
301    subject { described_class.new(schemas: schemas) }
302
303    before do
304      schema.add_attribute(name: 'userName') do |attribute|
305        attribute.required = true
306        attribute.uniqueness = :server
307      end
308      schema.add_attribute(name: 'name') do |attribute|
309        attribute.add_attribute(name: 'formatted') do |x|
310          x.mutability = :read_only
311        end
312        attribute.add_attribute(name: 'familyName')
313        attribute.add_attribute(name: 'givenName')
314      end
315      schema.add_attribute(name: 'displayName') do |attribute|
316        attribute.mutability = :read_only
317      end
318      schema.add_attribute(name: 'locale')
319      schema.add_attribute(name: 'timezone')
320      schema.add_attribute(name: 'active', type: :boolean)
321      schema.add_attribute(name: 'password') do |attribute|
322        attribute.mutability = :write_only
323        attribute.returned = :never
324      end
325      schema.add_attribute(name: 'emails') do |attribute|
326        attribute.multi_valued = true
327        attribute.add_attribute(name: 'value')
328        attribute.add_attribute(name: 'primary', type: :boolean)
329      end
330      schema.add_attribute(name: 'groups') do |attribute|
331        attribute.multi_valued = true
332        attribute.mutability = :read_only
333        attribute.add_attribute(name: 'value') do |x|
334          x.mutability = :read_only
335        end
336        attribute.add_attribute(name: '$ref') do |x|
337          x.reference_types = %w[User Group]
338          x.mutability = :read_only
339        end
340        attribute.add_attribute(name: 'display') do |x|
341          x.mutability = :read_only
342        end
343      end
344    end
345
346    specify { expect(subject.as_json.key?(:meta)).to be(false) }
347    specify { expect(subject.as_json.key?(:id)).to be(false) }
348    specify { expect(subject.as_json.key?(:externalId)).to be(false) }
349
350    context 'when using a simplified API' do
351      let(:user_name) { FFaker::Internet.user_name }
352      let(:resource) do
353        described_class.new(schemas: schemas) do |x|
354          x.user_name = user_name
355          x.name.given_name = 'Barbara'
356          x.name.family_name = 'Jensen'
357          x.emails = [
358            { value: FFaker::Internet.email, primary: true },
359            { value: FFaker::Internet.email, primary: false }
360          ]
361          x.locale = 'en'
362          x.timezone = 'Etc/UTC'
363        end
364      end
365
366      specify { expect(resource.user_name).to eql(user_name) }
367      specify { expect(resource.name.given_name).to eql('Barbara') }
368      specify { expect(resource.name.family_name).to eql('Jensen') }
369      specify { expect(resource.emails[0][:value]).to be_present }
370      specify { expect(resource.emails[0][:primary]).to be(true) }
371      specify { expect(resource.emails[1][:value]).to be_present }
372      specify { expect(resource.emails[1][:primary]).to be(false) }
373      specify { expect(resource.locale).to eql('en') }
374      specify { expect(resource.timezone).to eql('Etc/UTC') }
375
376      specify { expect(resource.to_h[:userName]).to eql(user_name) }
377      specify { expect(resource.to_h[:name][:givenName]).to eql('Barbara') }
378      specify { expect(resource.to_h[:name][:familyName]).to eql('Jensen') }
379      specify { expect(resource.to_h[:emails][0][:value]).to be_present }
380      specify { expect(resource.to_h[:emails][0][:primary]).to be(true) }
381      specify { expect(resource.to_h[:emails][1][:value]).to be_present }
382      specify { expect(resource.to_h[:emails][1][:primary]).to be(false) }
383      specify { expect(resource.to_h[:locale]).to eql('en') }
384      specify { expect(resource.to_h[:timezone]).to eql('Etc/UTC') }
385      specify { expect(resource.to_h.key?(:meta)).to be(false) }
386      specify { expect(resource.to_h.key?(:id)).to be(false) }
387      specify { expect(resource.to_h.key?(:external_id)).to be(false) }
388    end
389
390    context 'when building in client mode' do
391      subject { described_class.new(schemas: schemas) }
392
393      let(:external_id) { SecureRandom.uuid }
394
395      before do
396        subject.password = FFaker::Internet.password
397        subject.external_id = external_id
398      end
399
400      specify { expect(subject.to_h.key?(:id)).to be(false) }
401      specify { expect(subject.to_h.key?(:externalId)).to be(true) }
402      specify { expect(subject.to_h[:externalId]).to eql(external_id) }
403      specify { expect(subject.to_h.key?(:meta)).to be(false) }
404      specify { expect(subject.to_h.key?(:userName)).to be(true) }
405      specify { expect(subject.to_h[:name].key?(:formatted)).to be(false) }
406      specify { expect(subject.to_h[:name].key?(:familyName)).to be(true) }
407      specify { expect(subject.to_h[:name].key?(:givenName)).to be(true) }
408      specify { expect(subject.to_h.key?(:displayName)).to be(false) }
409      specify { expect(subject.to_h.key?(:locale)).to be(true) }
410      specify { expect(subject.to_h.key?(:timezone)).to be(true) }
411      specify { expect(subject.to_h.key?(:active)).to be(true) }
412      specify { expect(subject.to_h.key?(:password)).to be(true) }
413      specify { expect(subject.to_h.key?(:emails)).to be(true) }
414      specify { expect(subject.to_h.key?(:groups)).to be(false) }
415    end
416
417    context 'when building in server mode' do
418      subject { described_class.new(schemas: schemas, location: resource_location) }
419
420      before do
421        subject.external_id = SecureRandom.uuid
422      end
423
424      specify { expect(subject.to_h.key?(:id)).to be(true) }
425      specify { expect(subject.to_h.key?(:externalId)).to be(false) }
426      specify { expect(subject.to_h.key?(:meta)).to be(true) }
427      specify { expect(subject.to_h.key?(:userName)).to be(true) }
428      specify { expect(subject.to_h[:name].key?(:formatted)).to be(true) }
429      specify { expect(subject.to_h[:name].key?(:familyName)).to be(true) }
430      specify { expect(subject.to_h[:name].key?(:givenName)).to be(true) }
431      specify { expect(subject.to_h.key?(:displayName)).to be(true) }
432      specify { expect(subject.to_h.key?(:locale)).to be(true) }
433      specify { expect(subject.to_h.key?(:timezone)).to be(true) }
434      specify { expect(subject.to_h.key?(:active)).to be(true) }
435      specify { expect(subject.to_h.key?(:password)).to be(false) }
436      specify { expect(subject.to_h.key?(:emails)).to be(true) }
437      specify { expect(subject.to_h.key?(:groups)).to be(true) }
438    end
439  end
440
441  describe '#mode?' do
442    context 'when server mode' do
443      subject { described_class.new(schemas: schemas, location: resource_location) }
444
445      specify { expect(subject).to be_mode(:server) }
446      specify { expect(subject).not_to be_mode(:client) }
447    end
448
449    context 'when client mode' do
450      subject { described_class.new(schemas: schemas) }
451
452      specify { expect(subject).not_to be_mode(:server) }
453      specify { expect(subject).to be_mode(:client) }
454    end
455  end
456
457  describe '#assign_attributes' do
458    context 'with a simple string attribute' do
459      let(:user_name) { FFaker::Internet.user_name }
460
461      before do
462        schema.add_attribute(name: 'userName')
463        subject.assign_attributes('schemas' => schemas.map(&:id), userName: user_name)
464      end
465
466      specify { expect(subject.user_name).to eql(user_name) }
467    end
468
469    context 'with a simple integer attribute' do
470      before do
471        schema.add_attribute(name: 'age', type: :integer)
472        subject.assign_attributes(schemas: schemas.map(&:id), age: 34)
473      end
474
475      specify { expect(subject.age).to be(34) }
476    end
477
478    context 'with a multi-valued simple string attribute' do
479      before do
480        schema.add_attribute(name: 'colours', type: :string) do |x|
481          x.multi_valued = true
482        end
483        subject.assign_attributes(schemas: schemas.map(&:id), colours: ['red', 'green', :blue])
484      end
485
486      specify { expect(subject.colours).to match_array(%w[red green blue]) }
487    end
488
489    context 'with a single complex attribute' do
490      before do
491        schema.add_attribute(name: :name) do |x|
492          x.add_attribute(name: :given_name)
493          x.add_attribute(name: :family_name)
494        end
495        subject.assign_attributes(schemas: schemas.map(&:id), name: { givenName: 'Tsuyoshi', familyName: 'Garrett' })
496      end
497
498      specify { expect(subject.name.given_name).to eql('Tsuyoshi') }
499      specify { expect(subject.name.family_name).to eql('Garrett') }
500    end
501
502    context 'with a multi-valued complex attribute' do
503      let(:email) { FFaker::Internet.email }
504      let(:other_email) { FFaker::Internet.email }
505
506      before do
507        schema.add_attribute(name: :emails) do |x|
508          x.multi_valued = true
509          x.add_attribute(name: :value)
510          x.add_attribute(name: :primary, type: :boolean)
511        end
512        subject.assign_attributes(schemas: schemas.map(&:id), emails: [
513          { value: email, primary: true },
514          { value: other_email, primary: false }
515        ])
516      end
517
518      specify do
519        expect(subject.emails).to match_array([
520          { value: email, primary: true },
521          { value: other_email, primary: false }
522        ])
523      end
524
525      specify { expect(subject.emails[0][:value]).to eql(email) }
526      specify { expect(subject.emails[0][:primary]).to be(true) }
527      specify { expect(subject.emails[1][:value]).to eql(other_email) }
528      specify { expect(subject.emails[1][:primary]).to be(false) }
529    end
530
531    context 'with an extension schema' do
532      let(:schemas) { [schema, extension] }
533      let(:extension) { Scim::Kit::V2::Schema.new(id: extension_id, name: 'Extension', location: FFaker::Internet.uri('https')) }
534      let(:extension_id) { Scim::Kit::V2::Schemas::ENTERPRISE_USER }
535
536      before do
537        extension.add_attribute(name: :preferred_name)
538        subject.assign_attributes(
539          schemas: schemas.map(&:id),
540          extension_id => { preferredName: 'hunk' }
541        )
542      end
543
544      specify { expect(subject.preferred_name).to eql('hunk') }
545    end
546
547    context 'when initializing the resource with attributes' do
548      subject { described_class.new(schemas: schemas, attributes: attributes) }
549
550      let(:user_name) { FFaker::Internet.user_name }
551      let(:email) { FFaker::Internet.email }
552      let(:attributes) do
553        {
554          schemas: schemas.map(&:id),
555          userName: user_name,
556          age: 34,
557          colours: %w[red green blue],
558          name: { given_name: 'Tsuyoshi', family_name: 'Garrett' },
559          emails: [{ value: email, primary: true }]
560        }
561      end
562
563      before do
564        schema.add_attribute(name: :user_name)
565        schema.add_attribute(name: :age, type: :integer)
566        schema.add_attribute(name: :colours, type: :string) do |x|
567          x.multi_valued = true
568        end
569        schema.add_attribute(name: :name) do |x|
570          x.add_attribute(name: :given_name)
571          x.add_attribute(name: :family_name)
572        end
573        schema.add_attribute(name: :emails) do |x|
574          x.multi_valued = true
575          x.add_attribute(name: :value)
576          x.add_attribute(name: :primary, type: :boolean)
577        end
578      end
579
580      specify { expect(subject.raw_attributes).to eql(attributes) }
581      specify { expect(subject.user_name).to eql(user_name) }
582      specify { expect(subject.age).to be(34) }
583      specify { expect(subject.colours).to match_array(%w[red green blue]) }
584      specify { expect(subject.name.given_name).to eql('Tsuyoshi') }
585      specify { expect(subject.name.family_name).to eql('Garrett') }
586      specify { expect(subject.emails[0][:value]).to eql(email) }
587      specify { expect(subject.emails[0][:primary]).to be(true) }
588
589      specify do
590        attributes = { schemas: schemas.map(&:id), unknown: 'unknown' }
591        expect do
592          described_class.new(schemas: schemas, attributes: attributes)
593        end.to raise_error(Scim::Kit::UnknownAttributeError)
594      end
595    end
596  end
597
598  describe 'Errors' do
599    subject { described_class.new(schemas: schemas) }
600
601    let(:schemas) { [Scim::Kit::V2::Error.default_schema] }
602
603    before do
604      subject.scim_type = :invalidSyntax
605      subject.detail = 'error'
606      subject.status = 400
607    end
608
609    specify { expect(subject.to_h[:schemas]).to match_array([Scim::Kit::V2::Messages::ERROR]) }
610    specify { expect(subject.to_h[:scimType]).to eql('invalidSyntax') }
611    specify { expect(subject.to_h[:detail]).to eql('error') }
612    specify { expect(subject.to_h[:status]).to eql('400') }
613  end
614end