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