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
@@ -27,7 +27,7 @@ RSpec.describe Scimitar::Schema::User do
27
27
  "subAttributes": [
28
28
  {
29
29
  "multiValued": false,
30
- "required": true,
30
+ "required": false,
31
31
  "caseExact": false,
32
32
  "mutability": "readWrite",
33
33
  "uniqueness": "none",
@@ -37,7 +37,7 @@ RSpec.describe Scimitar::Schema::User do
37
37
  },
38
38
  {
39
39
  "multiValued": false,
40
- "required": true,
40
+ "required": false,
41
41
  "caseExact": false,
42
42
  "mutability": "readWrite",
43
43
  "uniqueness": "none",
@@ -14,7 +14,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
14
14
  ids = 3.times.map { SecureRandom.uuid }.sort()
15
15
 
16
16
  @u1 = MockUser.create!(primary_key: ids.shift(), username: '1', first_name: 'Foo', last_name: 'Ark', home_email_address: 'home_1@test.com', scim_uid: '001', created_at: lmt, updated_at: lmt + 1)
17
- @u2 = MockUser.create!(primary_key: ids.shift(), username: '2', first_name: 'Foo', last_name: 'Bar', home_email_address: 'home_2@test.com', scim_uid: '002', created_at: lmt, updated_at: lmt + 2)
17
+ @u2 = MockUser.create!(primary_key: ids.shift(), username: '2', first_name: 'Foo', last_name: 'Bar', home_email_address: 'home_2@test.com', scim_uid: '002', created_at: lmt, updated_at: lmt + 2, password: 'oldpassword')
18
18
  @u3 = MockUser.create!(primary_key: ids.shift(), username: '3', first_name: 'Foo', home_email_address: 'home_3@test.com', scim_uid: '003', created_at: lmt, updated_at: lmt + 3)
19
19
 
20
20
  @g1 = MockGroup.create!(display_name: 'Group 1')
@@ -107,6 +107,37 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
107
107
  expect(usernames).to match_array(['2'])
108
108
  end
109
109
 
110
+ it 'returns only the requested attributes' do
111
+ get '/Users', params: {
112
+ format: :scim,
113
+ attributes: "id,name"
114
+ }
115
+
116
+ expect(response.status ).to eql(200)
117
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
118
+
119
+ result = JSON.parse(response.body)
120
+
121
+ expect(result['totalResults']).to eql(3)
122
+ expect(result['Resources'].size).to eql(3)
123
+
124
+ keys = result['Resources'].map { |resource| resource.keys }.flatten.uniq
125
+
126
+ expect(keys).to match_array(%w[
127
+ id
128
+ meta
129
+ name
130
+ schemas
131
+ urn:ietf:params:scim:schemas:extension:enterprise:2.0:User
132
+ urn:ietf:params:scim:schemas:extension:manager:1.0:User
133
+ ])
134
+ expect(result.dig('Resources', 0, 'id')).to eql @u1.primary_key.to_s
135
+ expect(result.dig('Resources', 0, 'name', 'givenName')).to eql 'Foo'
136
+ expect(result.dig('Resources', 0, 'name', 'familyName')).to eql 'Ark'
137
+ end
138
+
139
+ # https://github.com/RIPAGlobal/scimitar/issues/37
140
+ #
110
141
  it 'applies a filter, with case-insensitive attribute matching (GitHub issue #37)' do
111
142
  get '/Users', params: {
112
143
  format: :scim,
@@ -128,6 +159,30 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
128
159
  expect(usernames).to match_array(['2'])
129
160
  end
130
161
 
162
+ # https://github.com/RIPAGlobal/scimitar/issues/115
163
+ #
164
+ it 'handles broken Microsoft filters (GitHub issue #115)' do
165
+ get '/Users', params: {
166
+ format: :scim,
167
+ filter: 'name[givenName eq "FOO"].familyName pr and emails ne "home_1@test.com"'
168
+ }
169
+
170
+ expect(response.status ).to eql(200)
171
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
172
+
173
+ result = JSON.parse(response.body)
174
+
175
+ expect(result['totalResults']).to eql(1)
176
+ expect(result['Resources'].size).to eql(1)
177
+
178
+ ids = result['Resources'].map { |resource| resource['id'] }
179
+ expect(ids).to match_array([@u2.primary_key.to_s])
180
+
181
+ usernames = result['Resources'].map { |resource| resource['userName'] }
182
+ expect(usernames).to match_array(['2'])
183
+ end
184
+
185
+
131
186
  # Strange attribute capitalisation in tests here builds on test coverage
132
187
  # for now-fixed GitHub issue #37.
133
188
  #
@@ -345,6 +400,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
345
400
 
346
401
  attributes = {
347
402
  userName: '4',
403
+ password: 'correcthorsebatterystaple',
348
404
  name: {
349
405
  givenName: 'Given',
350
406
  familyName: 'Family'
@@ -379,11 +435,82 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
379
435
  expect(result['id']).to eql(new_mock.id.to_s)
380
436
  expect(result['meta']['resourceType']).to eql('User')
381
437
  expect(new_mock.username).to eql('4')
438
+ expect(new_mock.password).to eql('correcthorsebatterystaple')
382
439
  expect(new_mock.first_name).to eql('Given')
383
440
  expect(new_mock.last_name).to eql('Family')
384
441
  expect(new_mock.home_email_address).to eql('home_4@test.com')
385
442
  expect(new_mock.work_email_address).to eql('work_4@test.com')
386
443
  end
444
+
445
+ it 'with schema ID value keys without inline attributes' do
446
+ mock_before = MockUser.all.to_a
447
+
448
+ attributes = {
449
+ userName: '4',
450
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
451
+ organization: 'Foo Bar!',
452
+ department: 'Bar Foo!'
453
+ },
454
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User': {
455
+ manager: 'Foo Baz!'
456
+ }
457
+ }
458
+
459
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
460
+
461
+ expect {
462
+ post "/Users", params: attributes.merge(format: :scim)
463
+ }.to change { MockUser.count }.by(1)
464
+
465
+ mock_after = MockUser.all.to_a
466
+ new_mock = (mock_after - mock_before).first
467
+
468
+ expect(response.status ).to eql(201)
469
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
470
+
471
+ result = JSON.parse(response.body)
472
+
473
+ expect(new_mock.organization).to eql('Foo Bar!')
474
+ expect(new_mock.department ).to eql('Bar Foo!')
475
+ expect(new_mock.manager ).to eql('Foo Baz!')
476
+
477
+ expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization']).to eql(new_mock.organization)
478
+ expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['department' ]).to eql(new_mock.department )
479
+ expect(result['urn:ietf:params:scim:schemas:extension:manager:1.0:User' ]['manager' ]).to eql(new_mock.manager )
480
+ end
481
+
482
+ it 'with schema ID value keys that have inline attributes' do
483
+ mock_before = MockUser.all.to_a
484
+
485
+ attributes = {
486
+ userName: '4',
487
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization': 'Foo Bar!',
488
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department': 'Bar Foo!',
489
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User:manager': 'Foo Baz!'
490
+ }
491
+
492
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
493
+
494
+ expect {
495
+ post "/Users", params: attributes.merge(format: :scim)
496
+ }.to change { MockUser.count }.by(1)
497
+
498
+ mock_after = MockUser.all.to_a
499
+ new_mock = (mock_after - mock_before).first
500
+
501
+ expect(response.status ).to eql(201)
502
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
503
+
504
+ result = JSON.parse(response.body)
505
+
506
+ expect(new_mock.organization).to eql('Foo Bar!')
507
+ expect(new_mock.department ).to eql('Bar Foo!')
508
+ expect(new_mock.manager ).to eql('Foo Baz!')
509
+
510
+ expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['organization']).to eql(new_mock.organization)
511
+ expect(result['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']['department' ]).to eql(new_mock.department )
512
+ expect(result['urn:ietf:params:scim:schemas:extension:manager:1.0:User' ]['manager' ]).to eql(new_mock.manager )
513
+ end
387
514
  end # "shared_examples 'a creator' do | force_upper_case: |"
388
515
 
389
516
  context 'using schema-matched case' do
@@ -511,6 +638,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
511
638
  expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
512
639
 
513
640
  result = JSON.parse(response.body)
641
+
514
642
  expect(result['scimType']).to eql('invalidValue')
515
643
  expect(result['detail']).to include('is reserved')
516
644
  end
@@ -522,14 +650,41 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
522
650
  context '#replace' do
523
651
  shared_examples 'a replacer' do | force_upper_case: |
524
652
  it 'which replaces all attributes in an instance' do
525
- attributes = { userName: '4' } # Minimum required by schema
653
+ attributes = { userName: '4' } # Minimum required by schema
526
654
  attributes = spec_helper_hupcase(attributes) if force_upper_case
527
655
 
528
656
  # Prove that certain known pathways are called; can then unit test
529
657
  # those if need be and be sure that this covers #replace actions.
530
658
  #
531
659
  expect_any_instance_of(MockUsersController).to receive(:replace).once.and_call_original
532
- expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
660
+ expect_any_instance_of(MockUsersController).to receive(:save! ).once.and_call_original
661
+ expect {
662
+ put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
663
+ }.to_not change { MockUser.count }
664
+
665
+ expect(response.status ).to eql(200)
666
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
667
+
668
+ result = JSON.parse(response.body)
669
+
670
+ expect(result['id']).to eql(@u2.primary_key.to_s)
671
+ expect(result['meta']['resourceType']).to eql('User')
672
+
673
+ expect(result).to have_key('name')
674
+ expect(result).to_not have_key('password')
675
+
676
+ @u2.reload
677
+
678
+ expect(@u2.username).to eql('4')
679
+ expect(@u2.first_name).to be_nil
680
+ expect(@u2.last_name).to be_nil
681
+ expect(@u2.home_email_address).to be_nil
682
+ expect(@u2.password).to be_nil
683
+ end
684
+
685
+ it 'can replace passwords' do
686
+ attributes = { userName: '4', password: 'correcthorsebatterystaple' }
687
+ attributes = spec_helper_hupcase(attributes) if force_upper_case
533
688
 
534
689
  expect {
535
690
  put "/Users/#{@u2.primary_key}", params: attributes.merge(format: :scim)
@@ -543,12 +698,16 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
543
698
  expect(result['id']).to eql(@u2.primary_key.to_s)
544
699
  expect(result['meta']['resourceType']).to eql('User')
545
700
 
701
+ expect(result).to have_key('name')
702
+ expect(result).to_not have_key('password')
703
+
546
704
  @u2.reload
547
705
 
548
706
  expect(@u2.username).to eql('4')
549
707
  expect(@u2.first_name).to be_nil
550
708
  expect(@u2.last_name).to be_nil
551
709
  expect(@u2.home_email_address).to be_nil
710
+ expect(@u2.password).to eql('correcthorsebatterystaple')
552
711
  end
553
712
  end # "shared_examples 'a replacer' do | force_upper_case: |"
554
713
 
@@ -626,7 +785,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
626
785
 
627
786
  context 'with a block' do
628
787
  it 'invokes the block' do
629
- attributes = { userName: '4' } # Minimum required by schema
788
+ attributes = { userName: '4' } # Minimum required by schema
630
789
 
631
790
  expect_any_instance_of(CustomReplaceMockUsersController).to receive(:replace).once.and_call_original
632
791
  expect {
@@ -681,7 +840,7 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
681
840
 
682
841
  context '#update' do
683
842
  shared_examples 'an updater' do | force_upper_case: |
684
- it 'which patches specific attributes' do
843
+ it 'which patches regular attributes' do
685
844
  payload = {
686
845
  Operations: [
687
846
  {
@@ -717,6 +876,9 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
717
876
  expect(result['id']).to eql(@u2.primary_key.to_s)
718
877
  expect(result['meta']['resourceType']).to eql('User')
719
878
 
879
+ expect(result).to have_key('name')
880
+ expect(result).to_not have_key('password')
881
+
720
882
  @u2.reload
721
883
 
722
884
  expect(@u2.username).to eql('4')
@@ -724,6 +886,151 @@ RSpec.describe Scimitar::ActiveRecordBackedResourcesController do
724
886
  expect(@u2.last_name).to eql('Bar')
725
887
  expect(@u2.home_email_address).to eql('home_2@test.com')
726
888
  expect(@u2.work_email_address).to eql('work_4@test.com')
889
+ expect(@u2.password).to eql('oldpassword')
890
+ end
891
+
892
+ context 'which' do
893
+ shared_examples 'it handles not-to-spec in-value Azure/Entra dotted attribute paths' do | operation |
894
+ it "and performs operation" do
895
+ payload = {
896
+ Operations: [
897
+ {
898
+ op: 'add',
899
+ value: {
900
+ 'name.givenName' => 'Foo!',
901
+ 'name.familyName' => 'Bar!',
902
+ 'name.formatted' => 'Foo! Bar!' # Unrecognised; should be ignored
903
+ },
904
+ },
905
+ ]
906
+ }
907
+
908
+ payload = spec_helper_hupcase(payload) if force_upper_case
909
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
910
+
911
+ expect(response.status ).to eql(200)
912
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
913
+
914
+ @u2.reload
915
+ result = JSON.parse(response.body)
916
+
917
+ expect(@u2.first_name).to eql('Foo!')
918
+ expect(@u2.last_name ).to eql('Bar!')
919
+ end
920
+ end
921
+
922
+ it_behaves_like 'it handles not-to-spec in-value Azure/Entra dotted attribute paths', 'add'
923
+ it_behaves_like 'it handles not-to-spec in-value Azure/Entra dotted attribute paths', 'replace'
924
+
925
+ shared_examples 'it handles schema ID value keys without inline attributes' do | operation |
926
+ it "and performs operation" do
927
+ payload = {
928
+ Operations: [
929
+ {
930
+ op: operation,
931
+ value: {
932
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': {
933
+ 'organization' => 'Foo Bar!',
934
+ 'department' => 'Bar Foo!'
935
+ },
936
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User': {
937
+ 'manager' => 'Foo Baz!'
938
+ }
939
+ },
940
+ },
941
+ ]
942
+ }
943
+
944
+ @u2.update!(organization: 'Old org')
945
+ payload = spec_helper_hupcase(payload) if force_upper_case
946
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
947
+
948
+ expect(response.status ).to eql(200)
949
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
950
+
951
+ @u2.reload
952
+ result = JSON.parse(response.body)
953
+
954
+ expect(@u2.organization).to eql('Foo Bar!')
955
+ expect(@u2.department ).to eql('Bar Foo!')
956
+ expect(@u2.manager ).to eql('Foo Baz!')
957
+ end
958
+ end
959
+
960
+ it_behaves_like 'it handles schema ID value keys without inline attributes', 'add'
961
+ it_behaves_like 'it handles schema ID value keys without inline attributes', 'replace'
962
+
963
+ shared_examples 'it handles schema ID value keys with inline attributes' do
964
+ it "and performs operation" do
965
+ payload = {
966
+ Operations: [
967
+ {
968
+ op: 'add',
969
+ value: {
970
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization' => 'Foo Bar!',
971
+ 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department' => 'Bar Foo!',
972
+ 'urn:ietf:params:scim:schemas:extension:manager:1.0:User:manager' => 'Foo Baz!'
973
+ },
974
+ },
975
+ ]
976
+ }
977
+
978
+ @u2.update!(organization: 'Old org')
979
+ payload = spec_helper_hupcase(payload) if force_upper_case
980
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
981
+
982
+ expect(response.status ).to eql(200)
983
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
984
+
985
+ @u2.reload
986
+ result = JSON.parse(response.body)
987
+
988
+ expect(@u2.organization).to eql('Foo Bar!')
989
+ expect(@u2.department ).to eql('Bar Foo!')
990
+ expect(@u2.manager ).to eql('Foo Baz!')
991
+ end
992
+ end
993
+
994
+ it_behaves_like 'it handles schema ID value keys with inline attributes', 'add'
995
+ it_behaves_like 'it handles schema ID value keys with inline attributes', 'replace'
996
+ end
997
+
998
+ it 'which patches "returned: \'never\'" fields' do
999
+ payload = {
1000
+ Operations: [
1001
+ {
1002
+ op: 'replace',
1003
+ path: 'password',
1004
+ value: 'correcthorsebatterystaple'
1005
+ }
1006
+ ]
1007
+ }
1008
+
1009
+ payload = spec_helper_hupcase(payload) if force_upper_case
1010
+
1011
+ expect {
1012
+ patch "/Users/#{@u2.primary_key}", params: payload.merge(format: :scim)
1013
+ }.to_not change { MockUser.count }
1014
+
1015
+ expect(response.status ).to eql(200)
1016
+ expect(response.headers['Content-Type']).to eql('application/scim+json; charset=utf-8')
1017
+
1018
+ result = JSON.parse(response.body)
1019
+
1020
+ expect(result['id']).to eql(@u2.primary_key.to_s)
1021
+ expect(result['meta']['resourceType']).to eql('User')
1022
+
1023
+ expect(result).to have_key('name')
1024
+ expect(result).to_not have_key('password')
1025
+
1026
+ @u2.reload
1027
+
1028
+ expect(@u2.username).to eql('2')
1029
+ expect(@u2.first_name).to eql('Foo')
1030
+ expect(@u2.last_name).to eql('Bar')
1031
+ expect(@u2.home_email_address).to eql('home_2@test.com')
1032
+ expect(@u2.work_email_address).to be_nil
1033
+ expect(@u2.password).to eql('correcthorsebatterystaple')
727
1034
  end
728
1035
 
729
1036
  context 'which clears attributes' do
@@ -42,4 +42,79 @@ RSpec.describe Scimitar::Engine do
42
42
  expect(JSON.parse(response.body)['name']['familyName']).to eql('baz')
43
43
  end
44
44
  end # "context 'parameter parser' do"
45
+
46
+ # These are unit tests rather than request tests; seems like a reasonable
47
+ # place to put them in the absence of a standardised RSpec "engine" location.
48
+ #
49
+ context 'engine unit tests' do
50
+ around :each do | example |
51
+ license_schema = Class.new(Scimitar::Schema::Base) do
52
+ def initialize(options = {})
53
+ super(name: 'License', id: self.class.id(), description: 'Represents a License')
54
+ end
55
+ def self.id; 'urn:ietf:params:scim:schemas:license'; end
56
+ def self.scim_attributes; []; end
57
+ end
58
+
59
+ @license_resource = Class.new(Scimitar::Resources::Base) do
60
+ self.set_schema(license_schema)
61
+ def self.endpoint; '/License'; end
62
+ end
63
+
64
+ example.run()
65
+ ensure
66
+ Scimitar::Engine.reset_default_resources()
67
+ Scimitar::Engine.reset_custom_resources()
68
+ end
69
+
70
+ context '::resources, :add_custom_resource, ::set_default_resources' do
71
+ it 'returns default resources' do
72
+ expect(Scimitar::Engine.resources()).to match_array([Scimitar::Resources::User, Scimitar::Resources::Group])
73
+ end
74
+
75
+ it 'includes custom resources' do
76
+ Scimitar::Engine.add_custom_resource(@license_resource)
77
+ expect(Scimitar::Engine.resources()).to match_array([Scimitar::Resources::User, Scimitar::Resources::Group, @license_resource])
78
+ end
79
+
80
+ it 'notes changes to defaults' do
81
+ Scimitar::Engine::set_default_resources([Scimitar::Resources::User])
82
+ expect(Scimitar::Engine.resources()).to match_array([Scimitar::Resources::User])
83
+ end
84
+
85
+ it 'notes changes to defaults with custom resources added' do
86
+ Scimitar::Engine::set_default_resources([Scimitar::Resources::User])
87
+ Scimitar::Engine.add_custom_resource(@license_resource)
88
+ expect(Scimitar::Engine.resources()).to match_array([Scimitar::Resources::User, @license_resource])
89
+ end
90
+
91
+ it 'rejects bad defaults' do
92
+ expect {
93
+ Scimitar::Engine::set_default_resources([@license_resource])
94
+ }.to raise_error('Scimitar::Engine::set_default_resources: Only Scimitar::Resources::User, Scimitar::Resources::Group are supported')
95
+ end
96
+
97
+ it 'rejects empty defaults' do
98
+ expect {
99
+ Scimitar::Engine::set_default_resources([])
100
+ }.to raise_error('Scimitar::Engine::set_default_resources: At least one resource must be given')
101
+ end
102
+ end # "context '::resources, :add_custom_resource, ::set_default_resources' do"
103
+
104
+ context '#schemas' do
105
+ it 'returns schema instances from ::resources' do
106
+ expect(Scimitar::Engine).to receive(:resources).and_return([Scimitar::Resources::User, @license_resource])
107
+
108
+ schema_instances = Scimitar::Engine.schemas()
109
+ schema_classes = schema_instances.map(&:class)
110
+
111
+ expect(schema_classes).to match_array([
112
+ Scimitar::Schema::User,
113
+ ScimSchemaExtensions::User::Enterprise,
114
+ ScimSchemaExtensions::User::Manager,
115
+ @license_resource.schemas.first
116
+ ])
117
+ end
118
+ end # "context '#schemas' do"
119
+ end # "context 'engine unit tests' do"
45
120
  end
data/spec/spec_helper.rb CHANGED
@@ -19,7 +19,7 @@ require File.expand_path('../apps/dummy/config/environment', __FILE__)
19
19
  abort("The Rails environment is running in production mode!") if Rails.env.production?
20
20
 
21
21
  require 'rspec/rails'
22
- require 'byebug'
22
+ require 'debug'
23
23
  require 'scimitar'
24
24
 
25
25
  # ============================================================================
@@ -37,6 +37,114 @@ RSpec.describe Scimitar::Support::HashWithIndifferentCaseInsensitiveAccess do
37
37
  expect(subject()).to_not have_key('bar')
38
38
  end
39
39
  end # "context 'where keys set as symbols' do"
40
+
41
+ context 'access and merging' do
42
+ before :each do
43
+ @original_subject = subject().to_h().dup()
44
+ end
45
+
46
+ it 'returns keys as Strings' do
47
+ subject()[:foo] = 1
48
+ subject()[:BAR] = 2
49
+
50
+ expect(subject().keys).to match_array(@original_subject.keys + ['foo', 'BAR'])
51
+ end
52
+
53
+ it 'retains original case of keys' do
54
+ subject()[:foo ] = 1
55
+ subject()['FoO'] = 40 # (first-time-set case preservation test in passing)
56
+ subject()[:BAR ] = 2
57
+ subject()['Baz'] = 3
58
+
59
+ expectation = @original_subject.merge({
60
+ 'foo' => 40,
61
+ 'BAR' => 2,
62
+ 'Baz' => 3
63
+ })
64
+
65
+ expect(subject()).to eql(expectation)
66
+ end
67
+
68
+ it '#merge does not mutate the receiver and retains case of first-set keys' do
69
+ subject()[:foo] = 1
70
+ subject()[:BAR] = 2
71
+
72
+ pre_merge_subject = subject().dup()
73
+
74
+ result = subject().merge({:FOO => { 'onE' => 40 }, :Baz => 3})
75
+ expectation = @original_subject.merge({
76
+ 'foo' => { 'onE' => 40 },
77
+ 'BAR' => 2,
78
+ 'Baz' => 3
79
+ })
80
+
81
+ expect(subject()).to eql(pre_merge_subject)
82
+ expect(result).to eql(expectation)
83
+ end
84
+
85
+ it '#merge! mutates the receiver retains case of first-set keys' do
86
+ subject()[:foo] = 1
87
+ subject()[:BAR] = 2
88
+
89
+ subject().merge!({:FOO => { 'onE' => 40 }, :Baz => 3})
90
+
91
+ expectation = @original_subject.merge({
92
+ 'foo' => { 'onE' => 40 },
93
+ 'BAR' => 2,
94
+ 'Baz' => 3
95
+ })
96
+
97
+ expect(subject()).to eql(expectation)
98
+ end
99
+
100
+ it '#deep_merge does not mutate the receiver and retains nested key cases' do
101
+ subject()[:foo] = { :one => 10 }
102
+ subject()[:BAR] = 2
103
+
104
+ pre_merge_subject = subject().dup()
105
+
106
+ result = subject().deep_merge({:FOO => { 'ONE' => 40, :TWO => 20 }, :Baz => 3})
107
+ expectation = @original_subject.merge({
108
+ 'foo' => { 'one' => 40, 'TWO' => 20 },
109
+ 'BAR' => 2,
110
+ 'Baz' => 3
111
+ })
112
+
113
+ expect(subject()).to eql(pre_merge_subject)
114
+ expect(result).to eql(expectation)
115
+ end
116
+
117
+ it '#deep_merge! mutates the receiver and retains nested key cases' do
118
+ subject()[:foo] = { :one => 10 }
119
+ subject()[:BAR] = 2
120
+
121
+ subject().deep_merge!({:FOO => { 'ONE' => 40, :TWO => 20 }, :Baz => 3})
122
+
123
+ expectation = @original_subject.merge({
124
+ 'foo' => { 'one' => 40, 'TWO' => 20 },
125
+ 'BAR' => 2,
126
+ 'Baz' => 3
127
+ })
128
+
129
+ expect(subject()).to eql(expectation)
130
+ end
131
+
132
+ it 'retains indifferent behaviour after duplication' do
133
+ subject()[:foo] = { 'onE' => 40 }
134
+ subject()[:BAR] = 2
135
+
136
+ duplicate = subject().dup()
137
+ duplicate.merge!({ 'FOO' => true, 'baz' => 3 })
138
+
139
+ expectation = @original_subject.merge({
140
+ 'foo' => true,
141
+ 'BAR' => 2,
142
+ 'baz' => 3
143
+ })
144
+
145
+ expect(duplicate.to_h).to eql(expectation.to_h)
146
+ end
147
+ end # "context 'access and merging' do"
40
148
  end # "shared_examples 'an indifferent access, case insensitive Hash' do"
41
149
 
42
150
  context 'when created directly' do