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.
- checksums.yaml +4 -4
- data/README.md +27 -20
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +5 -4
- data/app/controllers/scimitar/resource_types_controller.rb +0 -2
- data/app/controllers/scimitar/resources_controller.rb +0 -2
- data/app/controllers/scimitar/schemas_controller.rb +361 -3
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +0 -1
- data/app/models/scimitar/engine_configuration.rb +3 -1
- data/app/models/scimitar/lists/query_parser.rb +88 -3
- data/app/models/scimitar/resources/base.rb +48 -14
- data/app/models/scimitar/resources/mixin.rb +531 -71
- data/app/models/scimitar/schema/name.rb +2 -2
- data/app/models/scimitar/schema/user.rb +10 -10
- data/config/initializers/scimitar.rb +41 -0
- data/lib/scimitar/engine.rb +57 -12
- data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +140 -10
- data/lib/scimitar/support/utilities.rb +60 -0
- data/lib/scimitar/version.rb +2 -2
- data/spec/apps/dummy/app/models/mock_user.rb +18 -3
- data/spec/apps/dummy/config/initializers/scimitar.rb +31 -2
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +1 -0
- data/spec/apps/dummy/db/schema.rb +1 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +342 -54
- data/spec/models/scimitar/lists/query_parser_spec.rb +70 -0
- data/spec/models/scimitar/resources/base_spec.rb +20 -12
- data/spec/models/scimitar/resources/base_validation_spec.rb +16 -3
- data/spec/models/scimitar/resources/mixin_spec.rb +754 -122
- data/spec/models/scimitar/schema/user_spec.rb +2 -2
- data/spec/requests/active_record_backed_resources_controller_spec.rb +312 -5
- data/spec/requests/engine_spec.rb +75 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +108 -0
- metadata +22 -22
@@ -27,7 +27,7 @@ RSpec.describe Scimitar::Schema::User do
|
|
27
27
|
"subAttributes": [
|
28
28
|
{
|
29
29
|
"multiValued": false,
|
30
|
-
"required":
|
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":
|
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' }
|
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!
|
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' }
|
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
|
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 '
|
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
|