scimitar 1.8.1 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
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