scimitar 1.0.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 +7 -0
- data/Rakefile +16 -0
- data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
- data/app/controllers/scimitar/application_controller.rb +129 -0
- data/app/controllers/scimitar/resource_types_controller.rb +28 -0
- data/app/controllers/scimitar/resources_controller.rb +203 -0
- data/app/controllers/scimitar/schemas_controller.rb +16 -0
- data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
- data/app/models/scimitar/authentication_error.rb +9 -0
- data/app/models/scimitar/authentication_scheme.rb +18 -0
- data/app/models/scimitar/bulk.rb +8 -0
- data/app/models/scimitar/complex_types/address.rb +18 -0
- data/app/models/scimitar/complex_types/base.rb +41 -0
- data/app/models/scimitar/complex_types/email.rb +12 -0
- data/app/models/scimitar/complex_types/entitlement.rb +12 -0
- data/app/models/scimitar/complex_types/ims.rb +12 -0
- data/app/models/scimitar/complex_types/name.rb +12 -0
- data/app/models/scimitar/complex_types/phone_number.rb +12 -0
- data/app/models/scimitar/complex_types/photo.rb +12 -0
- data/app/models/scimitar/complex_types/reference_group.rb +12 -0
- data/app/models/scimitar/complex_types/reference_member.rb +12 -0
- data/app/models/scimitar/complex_types/role.rb +12 -0
- data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
- data/app/models/scimitar/engine_configuration.rb +24 -0
- data/app/models/scimitar/error_response.rb +20 -0
- data/app/models/scimitar/errors.rb +14 -0
- data/app/models/scimitar/filter.rb +11 -0
- data/app/models/scimitar/filter_error.rb +22 -0
- data/app/models/scimitar/invalid_syntax_error.rb +9 -0
- data/app/models/scimitar/lists/count.rb +64 -0
- data/app/models/scimitar/lists/query_parser.rb +730 -0
- data/app/models/scimitar/meta.rb +7 -0
- data/app/models/scimitar/not_found_error.rb +10 -0
- data/app/models/scimitar/resource_invalid_error.rb +9 -0
- data/app/models/scimitar/resource_type.rb +29 -0
- data/app/models/scimitar/resources/base.rb +159 -0
- data/app/models/scimitar/resources/group.rb +13 -0
- data/app/models/scimitar/resources/mixin.rb +964 -0
- data/app/models/scimitar/resources/user.rb +13 -0
- data/app/models/scimitar/schema/address.rb +24 -0
- data/app/models/scimitar/schema/attribute.rb +123 -0
- data/app/models/scimitar/schema/base.rb +86 -0
- data/app/models/scimitar/schema/derived_attributes.rb +24 -0
- data/app/models/scimitar/schema/email.rb +10 -0
- data/app/models/scimitar/schema/entitlement.rb +10 -0
- data/app/models/scimitar/schema/group.rb +27 -0
- data/app/models/scimitar/schema/ims.rb +10 -0
- data/app/models/scimitar/schema/name.rb +20 -0
- data/app/models/scimitar/schema/phone_number.rb +10 -0
- data/app/models/scimitar/schema/photo.rb +10 -0
- data/app/models/scimitar/schema/reference_group.rb +23 -0
- data/app/models/scimitar/schema/reference_member.rb +21 -0
- data/app/models/scimitar/schema/role.rb +10 -0
- data/app/models/scimitar/schema/user.rb +52 -0
- data/app/models/scimitar/schema/vdtp.rb +18 -0
- data/app/models/scimitar/schema/x509_certificate.rb +22 -0
- data/app/models/scimitar/service_provider_configuration.rb +49 -0
- data/app/models/scimitar/supportable.rb +14 -0
- data/app/views/layouts/scimitar/application.html.erb +14 -0
- data/config/initializers/scimitar.rb +82 -0
- data/config/routes.rb +6 -0
- data/lib/scimitar.rb +23 -0
- data/lib/scimitar/engine.rb +63 -0
- data/lib/scimitar/version.rb +13 -0
- data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
- data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
- data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
- data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
- data/spec/apps/dummy/app/models/mock_group.rb +83 -0
- data/spec/apps/dummy/app/models/mock_user.rb +104 -0
- data/spec/apps/dummy/config/application.rb +17 -0
- data/spec/apps/dummy/config/boot.rb +2 -0
- data/spec/apps/dummy/config/environment.rb +2 -0
- data/spec/apps/dummy/config/environments/test.rb +15 -0
- data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
- data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
- data/spec/apps/dummy/config/routes.rb +24 -0
- data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
- data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
- data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
- data/spec/apps/dummy/db/schema.rb +42 -0
- data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
- data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
- data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
- data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
- data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
- data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
- data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
- data/spec/models/scimitar/lists/count_spec.rb +147 -0
- data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
- data/spec/models/scimitar/resource_type_spec.rb +21 -0
- data/spec/models/scimitar/resources/base_spec.rb +289 -0
- data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
- data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
- data/spec/models/scimitar/resources/user_spec.rb +55 -0
- data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
- data/spec/models/scimitar/schema/base_spec.rb +64 -0
- data/spec/models/scimitar/schema/group_spec.rb +87 -0
- data/spec/models/scimitar/schema/user_spec.rb +710 -0
- data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
- data/spec/requests/application_controller_spec.rb +49 -0
- data/spec/requests/controller_configuration_spec.rb +17 -0
- data/spec/requests/engine_spec.rb +20 -0
- data/spec/spec_helper.rb +66 -0
- metadata +315 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Scimitar::ComplexTypes::Email do
|
|
4
|
+
context '#as_json' do
|
|
5
|
+
it 'assumes no defaults' do
|
|
6
|
+
expect(described_class.new.as_json).to eq({})
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'allows a custom email type' do
|
|
10
|
+
expect(described_class.new(type: 'home').as_json).to eq('type' => 'home')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'allows a non-primary email' do
|
|
14
|
+
expect(described_class.new(primary: false).as_json).to eq('primary' => false)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'shows the set email' do
|
|
18
|
+
expect(described_class.new(value: 'a@b.c').as_json).to eq('value' => 'a@b.c')
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Scimitar::Lists::Count do
|
|
4
|
+
before :each do
|
|
5
|
+
@instance = described_class.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# ===========================================================================
|
|
9
|
+
# LIMIT
|
|
10
|
+
# ===========================================================================
|
|
11
|
+
|
|
12
|
+
context '#limit' do
|
|
13
|
+
it 'defaults to 100' do
|
|
14
|
+
expect(@instance.limit).to eql(100)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'converts input strings to integers' do
|
|
18
|
+
@instance.limit = '50'
|
|
19
|
+
expect(@instance.limit).to eql(50)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'ignores "nil"' do
|
|
23
|
+
expect { @instance.limit = nil }.to_not raise_error
|
|
24
|
+
expect(@instance.limit).to eql(100)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'ignores blank' do
|
|
28
|
+
expect { @instance.limit = ' ' }.to_not raise_error
|
|
29
|
+
expect(@instance.limit).to eql(100)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'error checking' do
|
|
33
|
+
it 'complains about attempts to set non-numeric values' do
|
|
34
|
+
expect { @instance.limit = 'A' }.to raise_error(RuntimeError)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'complains about attempts to set zero values' do
|
|
38
|
+
expect { @instance.limit = '0' }.to raise_error(RuntimeError)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'complains about attempts to set zero values' do
|
|
42
|
+
|
|
43
|
+
expect { @instance.limit = '-10' }.to raise_error(RuntimeError)
|
|
44
|
+
end
|
|
45
|
+
end # "context 'on-read error checking' do"
|
|
46
|
+
end # "context '#limit' do"
|
|
47
|
+
|
|
48
|
+
# ===========================================================================
|
|
49
|
+
# START INDEX
|
|
50
|
+
# ===========================================================================
|
|
51
|
+
|
|
52
|
+
context '#start_index' do
|
|
53
|
+
it 'defaults to 1' do
|
|
54
|
+
expect(@instance.start_index).to eql(1)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'accepts input integers' do
|
|
58
|
+
@instance.start_index = 12
|
|
59
|
+
expect(@instance.start_index).to eql(12)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'converts input strings to integers' do
|
|
63
|
+
@instance.start_index = '12'
|
|
64
|
+
expect(@instance.start_index).to eql(12)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'bounds zero values to 1' do
|
|
68
|
+
@instance.start_index = '0'
|
|
69
|
+
expect(@instance.start_index).to eql(1)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'ignores "nil"' do
|
|
73
|
+
expect { @instance.start_index = nil }.to_not raise_error
|
|
74
|
+
expect(@instance.start_index).to eql(1)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'ignores blank' do
|
|
78
|
+
expect { @instance.start_index = ' ' }.to_not raise_error
|
|
79
|
+
expect(@instance.start_index).to eql(1)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context 'error checking' do
|
|
83
|
+
it 'complains about attempts to set non-numeric values' do
|
|
84
|
+
|
|
85
|
+
expect { @instance.start_index = 'A' }.to raise_error(RuntimeError)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'complains about attempts to set negative values' do
|
|
89
|
+
expect { @instance.start_index = '-10' }.to raise_error(RuntimeError)
|
|
90
|
+
end
|
|
91
|
+
end # "context 'on-read error checking' do"
|
|
92
|
+
end # "context '#start_index' do"
|
|
93
|
+
|
|
94
|
+
# ===========================================================================
|
|
95
|
+
# OFFSET
|
|
96
|
+
# ===========================================================================
|
|
97
|
+
|
|
98
|
+
context '#offset' do
|
|
99
|
+
it 'defaults to 0' do
|
|
100
|
+
expect(@instance.offset).to eql(0)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'returns the #start_index minus one (set by integer)' do
|
|
104
|
+
@instance.start_index = 12
|
|
105
|
+
expect(@instance.offset).to eql(11)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'returns the #start_index minus one by (set by string)' do
|
|
109
|
+
@instance.start_index = '12'
|
|
110
|
+
expect(@instance.offset).to eql(11)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'is read-only' do
|
|
114
|
+
expect { @instance.offset = 42 }.to raise_error(NoMethodError)
|
|
115
|
+
end
|
|
116
|
+
end # "context '#offset' do"
|
|
117
|
+
|
|
118
|
+
# ===========================================================================
|
|
119
|
+
# TOTAL
|
|
120
|
+
# ===========================================================================
|
|
121
|
+
|
|
122
|
+
context '#total' do
|
|
123
|
+
it 'defaults to "nil" as "unknown"' do
|
|
124
|
+
expect(@instance.total).to be_nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'is read/write' do
|
|
128
|
+
@instance.total = 42
|
|
129
|
+
expect(@instance.total).to eql(42)
|
|
130
|
+
end
|
|
131
|
+
end # "context '#total' do"
|
|
132
|
+
|
|
133
|
+
# ===========================================================================
|
|
134
|
+
# INSTANTIATION
|
|
135
|
+
# ===========================================================================
|
|
136
|
+
|
|
137
|
+
context 'instantiation' do
|
|
138
|
+
it 'instantiates with parameters' do
|
|
139
|
+
instance = described_class.new(start_index: '5', total: 45)
|
|
140
|
+
|
|
141
|
+
expect(instance.limit ).to eql(100)
|
|
142
|
+
expect(instance.start_index).to eql(5)
|
|
143
|
+
expect(instance.offset ).to eql(4)
|
|
144
|
+
expect(instance.total ).to eql(45)
|
|
145
|
+
end
|
|
146
|
+
end # "context 'instantiation' do"
|
|
147
|
+
end # "RSpec.describe Scimitar::Lists::Count do"
|
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Note that #
|
|
4
|
+
|
|
5
|
+
RSpec.describe Scimitar::Lists::QueryParser do
|
|
6
|
+
|
|
7
|
+
# We use the dummy app's MockUser class, so need a database connection from
|
|
8
|
+
# that app too. ActiveRecord can then escape column values, generate SQL and
|
|
9
|
+
# so-forth, and we can run tests to check on that output to verify that
|
|
10
|
+
# the gem has instructed ActiveRecord appropriately.
|
|
11
|
+
#
|
|
12
|
+
require_relative '../../../apps/dummy/app/models/mock_user.rb'
|
|
13
|
+
|
|
14
|
+
before :each do
|
|
15
|
+
@instance = described_class.new(MockUser.new.scim_queryable_attributes())
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ===========================================================================
|
|
19
|
+
# BASIC PARSING
|
|
20
|
+
#
|
|
21
|
+
# Adapted from SCIM Query Filter Parser's non-RSpec tests.
|
|
22
|
+
# ===========================================================================
|
|
23
|
+
|
|
24
|
+
context 'basic parsing' do
|
|
25
|
+
it "empty string" do
|
|
26
|
+
@instance.parse("")
|
|
27
|
+
|
|
28
|
+
rpn = @instance.rpn
|
|
29
|
+
expect(rpn).to be_empty
|
|
30
|
+
|
|
31
|
+
tree = @instance.tree
|
|
32
|
+
expect(tree).to be_empty
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "user name equals" do
|
|
36
|
+
@instance.parse(%Q(userName eq "bjensen"))
|
|
37
|
+
|
|
38
|
+
rpn = @instance.rpn
|
|
39
|
+
expect('userName').to eql(rpn[0])
|
|
40
|
+
expect('"bjensen"').to eql(rpn[1])
|
|
41
|
+
expect('eq').to eql(rpn[2])
|
|
42
|
+
|
|
43
|
+
tree = @instance.tree
|
|
44
|
+
expect('eq').to eql(tree[0])
|
|
45
|
+
expect('userName').to eql(tree[1])
|
|
46
|
+
expect('"bjensen"').to eql(tree[2])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "family name equals" do
|
|
50
|
+
@instance.parse(%Q(name.familyName co "O'Malley"))
|
|
51
|
+
|
|
52
|
+
rpn = @instance.rpn
|
|
53
|
+
expect('name.familyName').to eql(rpn[0])
|
|
54
|
+
expect(%Q("O'Malley")).to eql(rpn[1])
|
|
55
|
+
expect('co').to eql(rpn[2])
|
|
56
|
+
|
|
57
|
+
tree = @instance.tree
|
|
58
|
+
expect('co').to eql(tree[0])
|
|
59
|
+
expect('name.familyName').to eql(tree[1])
|
|
60
|
+
expect(%Q("O'Malley")).to eql(tree[2])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "user name starts with" do
|
|
64
|
+
@instance.parse(%Q(userName sw "J"))
|
|
65
|
+
|
|
66
|
+
rpn = @instance.rpn
|
|
67
|
+
expect('userName').to eql(rpn[0])
|
|
68
|
+
expect(%Q("J")).to eql(rpn[1])
|
|
69
|
+
expect('sw').to eql(rpn[2])
|
|
70
|
+
|
|
71
|
+
tree = @instance.tree
|
|
72
|
+
expect('sw').to eql(tree[0])
|
|
73
|
+
expect('userName').to eql(tree[1])
|
|
74
|
+
expect('"J"').to eql(tree[2])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "title present" do
|
|
78
|
+
@instance.parse(%Q(title pr))
|
|
79
|
+
|
|
80
|
+
rpn = @instance.rpn
|
|
81
|
+
expect('title').to eql(rpn[0])
|
|
82
|
+
expect('pr').to eql(rpn[1])
|
|
83
|
+
|
|
84
|
+
tree = @instance.tree
|
|
85
|
+
expect('pr').to eql(tree[0])
|
|
86
|
+
expect('title').to eql(tree[1])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "last modified greater than" do
|
|
90
|
+
@instance.parse(%Q(meta.lastModified gt "2011-05-13T04:42:34Z"))
|
|
91
|
+
|
|
92
|
+
rpn = @instance.rpn
|
|
93
|
+
expect('meta.lastModified').to eql(rpn[0])
|
|
94
|
+
expect('"2011-05-13T04:42:34Z"').to eql(rpn[1])
|
|
95
|
+
expect('gt').to eql(rpn[2])
|
|
96
|
+
|
|
97
|
+
tree = @instance.tree
|
|
98
|
+
expect('gt').to eql(tree[0])
|
|
99
|
+
expect('meta.lastModified').to eql(tree[1])
|
|
100
|
+
expect('"2011-05-13T04:42:34Z"').to eql(tree[2])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "last modified greater than or equal to" do
|
|
104
|
+
@instance.parse(%Q(meta.lastModified ge "2011-05-13T04:42:34Z"))
|
|
105
|
+
|
|
106
|
+
rpn = @instance.rpn
|
|
107
|
+
|
|
108
|
+
expect('meta.lastModified').to eql(rpn[0])
|
|
109
|
+
expect('"2011-05-13T04:42:34Z"').to eql(rpn[1])
|
|
110
|
+
expect('ge').to eql(rpn[2])
|
|
111
|
+
|
|
112
|
+
tree = @instance.tree
|
|
113
|
+
expect('ge').to eql(tree[0])
|
|
114
|
+
expect('meta.lastModified').to eql(tree[1])
|
|
115
|
+
expect('"2011-05-13T04:42:34Z"').to eql(tree[2])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "last modified less than" do
|
|
119
|
+
@instance.parse(%Q(meta.lastModified lt "2011-05-13T04:42:34Z"))
|
|
120
|
+
|
|
121
|
+
rpn = @instance.rpn
|
|
122
|
+
|
|
123
|
+
expect('meta.lastModified').to eql(rpn[0])
|
|
124
|
+
expect('"2011-05-13T04:42:34Z"').to eql(rpn[1])
|
|
125
|
+
expect('lt').to eql(rpn[2])
|
|
126
|
+
|
|
127
|
+
tree = @instance.tree
|
|
128
|
+
expect('lt').to eql(tree[0])
|
|
129
|
+
expect('meta.lastModified').to eql(tree[1])
|
|
130
|
+
expect('"2011-05-13T04:42:34Z"').to eql(tree[2])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "last modified less than or equal to" do
|
|
134
|
+
@instance.parse(%Q(meta.lastModified le "2011-05-13T04:42:34Z"))
|
|
135
|
+
|
|
136
|
+
rpn = @instance.rpn
|
|
137
|
+
|
|
138
|
+
expect('meta.lastModified').to eql(rpn[0])
|
|
139
|
+
expect('"2011-05-13T04:42:34Z"').to eql(rpn[1])
|
|
140
|
+
expect('le').to eql(rpn[2])
|
|
141
|
+
|
|
142
|
+
tree = @instance.tree
|
|
143
|
+
expect('le').to eql(tree[0])
|
|
144
|
+
expect('meta.lastModified').to eql(tree[1])
|
|
145
|
+
expect('"2011-05-13T04:42:34Z"').to eql(tree[2])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "title and user type equal" do
|
|
149
|
+
@instance.parse(%Q(title pr and userType eq "Employee"))
|
|
150
|
+
|
|
151
|
+
rpn = @instance.rpn
|
|
152
|
+
|
|
153
|
+
expect('title').to eql(rpn[0])
|
|
154
|
+
expect('pr').to eql(rpn[1])
|
|
155
|
+
expect('userType').to eql(rpn[2])
|
|
156
|
+
expect('"Employee"').to eql(rpn[3])
|
|
157
|
+
expect('eq').to eql(rpn[4])
|
|
158
|
+
expect('and').to eql(rpn[5])
|
|
159
|
+
|
|
160
|
+
tree = @instance.tree
|
|
161
|
+
expect(3).to eql(tree.count)
|
|
162
|
+
expect('and').to eql(tree[0])
|
|
163
|
+
|
|
164
|
+
sub = tree[1]
|
|
165
|
+
expect(2).to eql(sub.count)
|
|
166
|
+
expect('pr').to eql(sub[0])
|
|
167
|
+
expect('title').to eql(sub[1])
|
|
168
|
+
|
|
169
|
+
sub = tree[2]
|
|
170
|
+
expect(3).to eql(sub.count)
|
|
171
|
+
expect('eq').to eql(sub[0])
|
|
172
|
+
expect('userType').to eql(sub[1])
|
|
173
|
+
expect('"Employee"').to eql(sub[2])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "title or user type equal" do
|
|
177
|
+
@instance.parse(%Q(title pr or userType eq "Intern"))
|
|
178
|
+
|
|
179
|
+
rpn = @instance.rpn
|
|
180
|
+
|
|
181
|
+
expect('title').to eql(rpn[0])
|
|
182
|
+
expect('pr').to eql(rpn[1])
|
|
183
|
+
expect('userType').to eql(rpn[2])
|
|
184
|
+
expect('"Intern"').to eql(rpn[3])
|
|
185
|
+
expect('eq').to eql(rpn[4])
|
|
186
|
+
expect('or').to eql(rpn[5])
|
|
187
|
+
|
|
188
|
+
tree = @instance.tree
|
|
189
|
+
expect(3).to eql(tree.count)
|
|
190
|
+
expect('or').to eql(tree[0])
|
|
191
|
+
|
|
192
|
+
sub = tree[1]
|
|
193
|
+
expect(2).to eql(sub.count)
|
|
194
|
+
expect('pr').to eql(sub[0])
|
|
195
|
+
expect('title').to eql(sub[1])
|
|
196
|
+
|
|
197
|
+
sub = tree[2]
|
|
198
|
+
expect(3).to eql(sub.count)
|
|
199
|
+
expect('eq').to eql(sub[0])
|
|
200
|
+
expect('userType').to eql(sub[1])
|
|
201
|
+
expect('"Intern"').to eql(sub[2])
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "compound filter" do
|
|
205
|
+
@instance.parse(%Q{userType eq "Employee" and (emails co "example.com" or emails co "example.org")})
|
|
206
|
+
|
|
207
|
+
rpn = @instance.rpn
|
|
208
|
+
|
|
209
|
+
expect('userType').to eql(rpn[0])
|
|
210
|
+
expect('"Employee"').to eql(rpn[1])
|
|
211
|
+
expect('eq').to eql(rpn[2])
|
|
212
|
+
expect('emails').to eql(rpn[3])
|
|
213
|
+
expect('"example.com"').to eql(rpn[4])
|
|
214
|
+
expect('co').to eql(rpn[5])
|
|
215
|
+
expect('emails').to eql(rpn[6])
|
|
216
|
+
expect('"example.org"').to eql(rpn[7])
|
|
217
|
+
expect('co').to eql(rpn[8])
|
|
218
|
+
expect('or').to eql(rpn[9])
|
|
219
|
+
expect('and').to eql(rpn[10])
|
|
220
|
+
|
|
221
|
+
tree = @instance.tree
|
|
222
|
+
expect(3).to eql(tree.count)
|
|
223
|
+
expect('and').to eql(tree[0])
|
|
224
|
+
|
|
225
|
+
sub = tree[1]
|
|
226
|
+
expect(3).to eql(sub.count)
|
|
227
|
+
expect('eq').to eql(sub[0])
|
|
228
|
+
expect('userType').to eql(sub[1])
|
|
229
|
+
expect('"Employee"').to eql(sub[2])
|
|
230
|
+
|
|
231
|
+
sub = tree[2]
|
|
232
|
+
expect(3).to eql(sub.count)
|
|
233
|
+
expect('or').to eql(sub[0])
|
|
234
|
+
|
|
235
|
+
expect(3).to eql(sub[1].count)
|
|
236
|
+
expect('co').to eql(sub[1][0])
|
|
237
|
+
expect('emails').to eql(sub[1][1])
|
|
238
|
+
expect('"example.com"').to eql(sub[1][2])
|
|
239
|
+
|
|
240
|
+
expect(3).to eql(sub[2].count)
|
|
241
|
+
expect('co').to eql(sub[2][0])
|
|
242
|
+
expect('emails').to eql(sub[2][1])
|
|
243
|
+
expect('"example.org"').to eql(sub[2][2])
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
context 'with errors' do
|
|
247
|
+
it 'unsupported operator' do
|
|
248
|
+
expect { @instance.parse('userName zz "Foo"') }.to raise_error(Scimitar::FilterError)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it 'misplaced operator' do
|
|
252
|
+
expect(@instance).to receive(:assert_not_op).twice.and_call_original
|
|
253
|
+
expect(@instance).to receive(:assert_op).once.and_call_original
|
|
254
|
+
expect { @instance.parse('userName eq pr') }.to raise_error(Scimitar::FilterError)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'missing logical operator' do
|
|
258
|
+
expect(@instance).to receive(:assert_op).twice.and_call_original
|
|
259
|
+
expect(@instance).to receive(:assert_not_op).once.and_call_original
|
|
260
|
+
expect { @instance.parse('userName pr userType eq "Foo"') }.to raise_error(Scimitar::FilterError)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'missing closing bracket' do
|
|
264
|
+
expect(@instance).to receive(:assert_close).once.and_call_original
|
|
265
|
+
expect { @instance.parse('userName pr and (userType eq "Foo"') }.to raise_error(Scimitar::FilterError)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'trailing junk' do
|
|
269
|
+
expect(@instance).to receive(:assert_eos).once.and_call_original
|
|
270
|
+
expect { @instance.parse('userName eq "Foo" )') }.to raise_error(Scimitar::FilterError)
|
|
271
|
+
end
|
|
272
|
+
end # "context 'with errors' do"
|
|
273
|
+
end # "context 'basic parsing' do"
|
|
274
|
+
|
|
275
|
+
# ===========================================================================
|
|
276
|
+
# INTERNAL FILTER FLATTENING
|
|
277
|
+
#
|
|
278
|
+
# Attempts to reduce query parser complexity while tolerating a wider range
|
|
279
|
+
# of input "styles" of filter
|
|
280
|
+
# ===========================================================================
|
|
281
|
+
|
|
282
|
+
context '#flatten_filter (private)' do
|
|
283
|
+
context 'when flattening is not needed' do
|
|
284
|
+
it 'and with one filter, binary operator' do
|
|
285
|
+
result = @instance.send(:flatten_filter, 'userType eq "Admin"')
|
|
286
|
+
expect(result).to eql('userType eq "Admin"')
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'and with one filter, unary operator' do
|
|
290
|
+
result = @instance.send(:flatten_filter, 'userType pr')
|
|
291
|
+
expect(result).to eql('userType pr')
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'and two filters, unary then binary operator' do
|
|
295
|
+
result = @instance.send(:flatten_filter, 'userType pr and userName eq "Foo"')
|
|
296
|
+
expect(result).to eql('userType pr and userName eq "Foo"')
|
|
297
|
+
end
|
|
298
|
+
end # "context 'when flattening is not needed' do"
|
|
299
|
+
|
|
300
|
+
context 'when flattening is needed' do
|
|
301
|
+
it 'flattens simple cases' do
|
|
302
|
+
result = @instance.send(:flatten_filter, 'userType eq "Employee" and emails[type eq "work" and value co "@example.com"]')
|
|
303
|
+
expect(result).to eql('userType eq "Employee" and emails.type eq "work" and emails.value co "@example.com"')
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
it 'correctly processes more than one inner filter' do
|
|
307
|
+
result = @instance.send(:flatten_filter, 'emails[type eq "work" and value co "@example.com"] or userType eq "Admin" or ims[type eq "xmpp" and value co "@foo.com"]')
|
|
308
|
+
expect(result).to eql('emails.type eq "work" and emails.value co "@example.com" or userType eq "Admin" or ims.type eq "xmpp" and ims.value co "@foo.com"')
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it 'flattens nested cases' do
|
|
312
|
+
result = @instance.send(:flatten_filter, 'userType ne "Employee" and not (emails[value co "example.com" or (value co "example.org")]) and userName="foo"')
|
|
313
|
+
expect(result).to eql('userType ne "Employee" and not (emails.value co "example.com" or (emails.value co "example.org")) and userName="foo"')
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it 'handles spaces in quoted values' do
|
|
317
|
+
result = @instance.send(:flatten_filter, 'userType eq "Employee spaces" or userName pr and emails[type eq "with spaces" and value co "@example.com"]')
|
|
318
|
+
expect(result).to eql('userType eq "Employee spaces" or userName pr and emails.type eq "with spaces" and emails.value co "@example.com"')
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it 'handles escaped quotes in quoted values' do
|
|
322
|
+
result = @instance.send(:flatten_filter, 'userType eq "Emplo\\"yee" and emails[type eq "\\"work\\"" and value co "@example.com"]')
|
|
323
|
+
expect(result).to eql('userType eq "Emplo\\"yee" and emails.type eq "\\"work\\"" and emails.value co "@example.com"')
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it 'handles escaped opening square brackets' do
|
|
327
|
+
result = @instance.send(:flatten_filter, 'userType eq \\[Employee and emails[type eq "work" and value co "@example.com"]')
|
|
328
|
+
expect(result).to eql('userType eq \\[Employee and emails.type eq "work" and emails.value co "@example.com"')
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it 'handles escaped closing square brackets' do
|
|
332
|
+
result = @instance.send(:flatten_filter, 'userType eq "Employee" and emails[type eq "work" and value co Unquoted\\]]')
|
|
333
|
+
expect(result).to eql('userType eq "Employee" and emails.type eq "work" and emails.value co Unquoted\\]')
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
it 'handles spaces before closing square brackets' do
|
|
337
|
+
result = @instance.send(:flatten_filter, 'emails[type eq "work" and value co "@example.com" ] or userType eq "Admin" or ims[type eq "xmpp" and value co "@foo.com"]')
|
|
338
|
+
expect(result).to eql('emails.type eq "work" and emails.value co "@example.com" or userType eq "Admin" or ims.type eq "xmpp" and ims.value co "@foo.com"')
|
|
339
|
+
end
|
|
340
|
+
end # "context 'when flattening is needed' do"
|
|
341
|
+
|
|
342
|
+
context 'with bad filters' do
|
|
343
|
+
it 'missing operator' do
|
|
344
|
+
expect { @instance.send(:flatten_filter, 'emails.type "work"') }.to raise_error(RuntimeError, 'Expected operator')
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it 'unexpected closing "]"' do
|
|
348
|
+
expect { @instance.send(:flatten_filter, 'emails.type eq "work"]') }.to raise_error(RuntimeError, 'Unexpected closing "]"')
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it 'logic operator is neither "and" nor "or"' do
|
|
352
|
+
expect { @instance.send(:flatten_filter, 'userName pr nand userType pr') }.to raise_error(RuntimeError, 'Expected "and" or "or"')
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end # "context '#flatten_filter (private)' do"
|
|
356
|
+
|
|
357
|
+
# ===========================================================================
|
|
358
|
+
# ACTIVERECORD QUERIES
|
|
359
|
+
#
|
|
360
|
+
# If you have issues here, check that private method unit tests are passing
|
|
361
|
+
# before worrying about these higher-level checks.
|
|
362
|
+
# ===========================================================================
|
|
363
|
+
|
|
364
|
+
context '#to_activerecord_query' do
|
|
365
|
+
|
|
366
|
+
# Means we don't need to iterate over every SCIM operator here, as we can
|
|
367
|
+
# have confidence that the lower level unit tests provide coverage.
|
|
368
|
+
#
|
|
369
|
+
it 'uses heavily-unit-tested #apply_scim_filter under the hood' do
|
|
370
|
+
@instance.parse("name.familyName EQ \"BAZ\"") # Note "EQ" upper case
|
|
371
|
+
|
|
372
|
+
expect(@instance).to receive(:apply_scim_filter).with(
|
|
373
|
+
base_scope: MockUser.all,
|
|
374
|
+
scim_attribute: 'name.familyName',
|
|
375
|
+
scim_operator: 'eq', # Note 'eq' lower case
|
|
376
|
+
scim_parameter: '"BAZ"',
|
|
377
|
+
case_sensitive: false
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@instance.to_activerecord_query(MockUser.all)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Technically tests #parse :-) but I hit this when writing the test that
|
|
384
|
+
# immediately follows - this location will do for now, since OK in context.
|
|
385
|
+
#
|
|
386
|
+
it 'complains about incorrectly quoted queries' do
|
|
387
|
+
expect { @instance.parse('name.familyName co B%_AZ') }.to raise_error(Scimitar::FilterError)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
it 'escapes values sent into ILIKE statements' do
|
|
391
|
+
@instance.parse('name.familyName co "B%_AZ"')
|
|
392
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
393
|
+
|
|
394
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE '%B\%\_AZ%'})
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'operates correctly with a few hand-chosen basic queries' do
|
|
398
|
+
user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Doe')
|
|
399
|
+
user_2 = MockUser.create(username: '2', first_name: 'John', last_name: 'Smithe')
|
|
400
|
+
user_3 = MockUser.create(username: '3', last_name: 'Davis')
|
|
401
|
+
|
|
402
|
+
# Test the various "LIKE" wildcards
|
|
403
|
+
|
|
404
|
+
@instance.parse('name.familyName co o') # Last name contains 'o'
|
|
405
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
406
|
+
|
|
407
|
+
expect(query.count).to eql(1)
|
|
408
|
+
expect(query.pluck(:id)).to eql([user_1.id])
|
|
409
|
+
|
|
410
|
+
@instance.parse('name.givenName sw J') # First name starts with 'J'
|
|
411
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
412
|
+
|
|
413
|
+
expect(query.count).to eql(2)
|
|
414
|
+
expect(query.pluck(:id)).to match_array([user_1.id, user_2.id])
|
|
415
|
+
|
|
416
|
+
@instance.parse('name.familyName ew he') # Last name ends with 'he'
|
|
417
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
418
|
+
|
|
419
|
+
expect(query.count).to eql(1)
|
|
420
|
+
expect(query.pluck(:id)).to eql([user_2.id])
|
|
421
|
+
|
|
422
|
+
# Test presence
|
|
423
|
+
|
|
424
|
+
@instance.parse('name.givenName pr') # First name is present
|
|
425
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
426
|
+
|
|
427
|
+
expect(query.count).to eql(2)
|
|
428
|
+
expect(query.pluck(:id)).to match_array([user_1.id, user_2.id])
|
|
429
|
+
|
|
430
|
+
# Test a simple not-equals, but use a custom starting scope. Note that
|
|
431
|
+
# the query would find "user_3" *except* there is no first name defined
|
|
432
|
+
# at all, and in SQL, "foo != bar" is *not* a match if foo IS NULL.
|
|
433
|
+
|
|
434
|
+
@instance.parse('name.givenName ne Bob') # First name is not 'Bob'
|
|
435
|
+
query = @instance.to_activerecord_query(MockUser.where.not('first_name' => 'John'))
|
|
436
|
+
|
|
437
|
+
expect(query.count).to eql(1)
|
|
438
|
+
expect(query.pluck(:id)).to match_array([user_1.id])
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
context 'when mapped to multiple columns' do
|
|
442
|
+
context 'with binary operators' do
|
|
443
|
+
it 'reads across all using OR' do
|
|
444
|
+
@instance.parse('emails eq "any@test.com"')
|
|
445
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
446
|
+
|
|
447
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."work_email_address" ILIKE 'any@test.com' OR "mock_users"."home_email_address" ILIKE 'any@test.com')})
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
it 'works with other query elements using correct precedence' do
|
|
451
|
+
@instance.parse('name.familyName eq "John" and emails eq "any@test.com"')
|
|
452
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
453
|
+
|
|
454
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE 'John' AND ("mock_users"."work_email_address" ILIKE 'any@test.com' OR "mock_users"."home_email_address" ILIKE 'any@test.com')})
|
|
455
|
+
end
|
|
456
|
+
end # "context 'with binary operators' do"
|
|
457
|
+
|
|
458
|
+
context 'with unary operators' do
|
|
459
|
+
it 'reads across all using OR' do
|
|
460
|
+
@instance.parse('emails pr')
|
|
461
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
462
|
+
|
|
463
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE (("mock_users"."work_email_address" != '' AND "mock_users"."work_email_address" IS NOT NULL) OR ("mock_users"."home_email_address" != '' AND "mock_users"."home_email_address" IS NOT NULL))})
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it 'works with other query elements using correct precedence' do
|
|
467
|
+
@instance.parse('name.familyName eq "John" and emails pr')
|
|
468
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
469
|
+
|
|
470
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE 'John' AND (("mock_users"."work_email_address" != '' AND "mock_users"."work_email_address" IS NOT NULL) OR ("mock_users"."home_email_address" != '' AND "mock_users"."home_email_address" IS NOT NULL))})
|
|
471
|
+
end
|
|
472
|
+
end # "context 'with unary operators' do
|
|
473
|
+
end # "context 'when mapped to multiple columns' do"
|
|
474
|
+
|
|
475
|
+
context 'when instructed to ignore an attribute' do
|
|
476
|
+
it 'ignores it' do
|
|
477
|
+
@instance.parse('emails.type eq "work"')
|
|
478
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
479
|
+
|
|
480
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users"})
|
|
481
|
+
end
|
|
482
|
+
end # "context 'when instructed to ignore an attribute' do"
|
|
483
|
+
|
|
484
|
+
context 'with complex cases' do
|
|
485
|
+
context 'using AND' do
|
|
486
|
+
it 'generates expected SQL' do
|
|
487
|
+
@instance.parse('name.givenName pr AND name.familyName ne "Doe"')
|
|
488
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
489
|
+
|
|
490
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."first_name" != '' AND "mock_users"."first_name" IS NOT NULL) AND "mock_users"."last_name" NOT ILIKE 'Doe'})
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
it 'finds expected items' do
|
|
494
|
+
user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis')
|
|
495
|
+
user_2 = MockUser.create(username: '2', first_name: 'John', last_name: 'Doe')
|
|
496
|
+
user_3 = MockUser.create(username: '3', last_name: 'Doe')
|
|
497
|
+
|
|
498
|
+
@instance.parse('name.givenName pr AND name.familyName eq "Doe"')
|
|
499
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
500
|
+
|
|
501
|
+
expect(query.count).to eql(1)
|
|
502
|
+
expect(query.pluck(:id)).to match_array([user_2.id])
|
|
503
|
+
end
|
|
504
|
+
end # "context 'simple AND' do"
|
|
505
|
+
|
|
506
|
+
context 'using OR' do
|
|
507
|
+
it 'generates expected SQL' do
|
|
508
|
+
@instance.parse('name.givenName pr OR name.familyName eq "Doe"')
|
|
509
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
510
|
+
|
|
511
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE (("mock_users"."first_name" != '' AND "mock_users"."first_name" IS NOT NULL) OR "mock_users"."last_name" ILIKE 'Doe')})
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
it 'finds expected items' do
|
|
515
|
+
user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis')
|
|
516
|
+
user_2 = MockUser.create(username: '2', last_name: 'Doe')
|
|
517
|
+
user_3 = MockUser.create(username: '3', last_name: 'Smith')
|
|
518
|
+
|
|
519
|
+
@instance.parse('name.givenName pr OR name.familyName eq "Doe"')
|
|
520
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
521
|
+
|
|
522
|
+
expect(query.count).to eql(2)
|
|
523
|
+
expect(query.pluck(:id)).to match_array([user_1.id, user_2.id])
|
|
524
|
+
end
|
|
525
|
+
end # "context 'simple OR' do"
|
|
526
|
+
|
|
527
|
+
context 'combined AND, OR and parentheses' do
|
|
528
|
+
it 'generates expected SQL' do
|
|
529
|
+
@instance.parse('name.givenName eq "Jane" and (name.familyName co "avi" or name.familyName ew "ith")')
|
|
530
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
531
|
+
|
|
532
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."first_name" ILIKE 'Jane' AND ("mock_users"."last_name" ILIKE '%avi%' OR "mock_users"."last_name" ILIKE '%ith')})
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it 'finds expected items' do
|
|
536
|
+
user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis') # Match
|
|
537
|
+
user_2 = MockUser.create(username: '2', first_name: 'Jane', last_name: 'Smith') # Match
|
|
538
|
+
user_3 = MockUser.create(username: '3', first_name: 'Jane', last_name: 'Moreith') # Match
|
|
539
|
+
user_4 = MockUser.create(username: '4', first_name: 'Jane', last_name: 'Doe') # No last name match
|
|
540
|
+
user_5 = MockUser.create(username: '5', first_name: 'Doe', last_name: 'Smith') # No first name match
|
|
541
|
+
user_6 = MockUser.create(username: '6', first_name: 'Bill', last_name: 'Davis') # No first name match
|
|
542
|
+
user_7 = MockUser.create(username: '7', last_name: 'Davis') # Missing first name
|
|
543
|
+
user_8 = MockUser.create(username: '8', last_name: 'Smith') # Missing first name
|
|
544
|
+
|
|
545
|
+
@instance.parse('name.givenName eq "Jane" and (name.familyName co "avi" or name.familyName ew "ith")')
|
|
546
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
547
|
+
|
|
548
|
+
expect(query.count).to eql(3)
|
|
549
|
+
expect(query.pluck(:id)).to match_array([user_1.id, user_2.id, user_3.id])
|
|
550
|
+
end
|
|
551
|
+
end # "context 'combined AND and OR' do"
|
|
552
|
+
|
|
553
|
+
context 'when flattening is needed' do
|
|
554
|
+
it 'generates expected SQL' do
|
|
555
|
+
@instance.parse('name[givenName eq "Jane" and (familyName co "avi" or familyName ew "ith")]')
|
|
556
|
+
query = @instance.to_activerecord_query(MockUser.all)
|
|
557
|
+
|
|
558
|
+
expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."first_name" ILIKE 'Jane' AND ("mock_users"."last_name" ILIKE '%avi%' OR "mock_users"."last_name" ILIKE '%ith')})
|
|
559
|
+
end
|
|
560
|
+
end # "context 'when flattening is needed' do"
|
|
561
|
+
end # "context 'complex cases' do"
|
|
562
|
+
end # "context '#to_activerecord_query' do"
|
|
563
|
+
|
|
564
|
+
# ===========================================================================
|
|
565
|
+
# PRIVATE METHODS
|
|
566
|
+
# ===========================================================================
|
|
567
|
+
|
|
568
|
+
context 'internal method' do
|
|
569
|
+
|
|
570
|
+
# =========================================================================
|
|
571
|
+
# Attributes
|
|
572
|
+
# =========================================================================
|
|
573
|
+
|
|
574
|
+
context '#activerecord_columns' do
|
|
575
|
+
it 'returns a column in an array' do
|
|
576
|
+
expect(@instance.send(:activerecord_columns, 'name.familyName')).to eql([:last_name])
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
it 'returns multiple column in an array' do
|
|
580
|
+
expect(@instance.send(:activerecord_columns, 'emails')).to eql([:work_email_address, :home_email_address])
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
it 'returns empty for "ignore"' do
|
|
584
|
+
expect(@instance.send(:activerecord_columns, 'emails.type')).to be_empty
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
it 'complains if there is no column present' do
|
|
588
|
+
expect { @instance.send(:activerecord_columns, nil) }.to raise_error(Scimitar::FilterError)
|
|
589
|
+
expect { @instance.send(:activerecord_columns, '' ) }.to raise_error(Scimitar::FilterError)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
it 'complains if there is no column mapping available' do
|
|
593
|
+
expect { @instance.send(:activerecord_columns, 'externalId') }.to raise_error(Scimitar::FilterError)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
it 'complains about malformed declarations' do
|
|
597
|
+
local_instance = described_class.new(
|
|
598
|
+
{
|
|
599
|
+
'name.givenName' => { wut: true }
|
|
600
|
+
}
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
expect { local_instance.send(:activerecord_columns, 'name.givenName' ) }.to raise_error(RuntimeError)
|
|
604
|
+
end
|
|
605
|
+
end # "context '#activerecord_columns' do"
|
|
606
|
+
|
|
607
|
+
# =========================================================================
|
|
608
|
+
# Parameters
|
|
609
|
+
# =========================================================================
|
|
610
|
+
|
|
611
|
+
context '#activerecord_parameter' do
|
|
612
|
+
it 'returns a blank string if a parameter is missing' do
|
|
613
|
+
expect(@instance.send(:activerecord_parameter, nil )).to eql('')
|
|
614
|
+
expect(@instance.send(:activerecord_parameter, '' )).to eql('')
|
|
615
|
+
expect(@instance.send(:activerecord_parameter, ' ')).to eql('')
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
it 'returns the parameter if present' do
|
|
619
|
+
expect(@instance.send(:activerecord_parameter, 'BAZ')).to eql('BAZ')
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
it 'removes surrounding quotes if present' do
|
|
623
|
+
expect(@instance.send(:activerecord_parameter, '"BA"Z"')).to eql('BA"Z')
|
|
624
|
+
expect(@instance.send(:activerecord_parameter, '"BA"Z' )).to eql('"BA"Z')
|
|
625
|
+
expect(@instance.send(:activerecord_parameter, 'BA"Z"' )).to eql('BA"Z"')
|
|
626
|
+
end
|
|
627
|
+
end # "context '#parameter' do"
|
|
628
|
+
|
|
629
|
+
# =========================================================================
|
|
630
|
+
# Low level queries
|
|
631
|
+
# =========================================================================
|
|
632
|
+
|
|
633
|
+
context '#apply_scim_filter' do
|
|
634
|
+
|
|
635
|
+
# Use 'let' to define :binary_expectations and :unary_operators, mapping
|
|
636
|
+
# lower case SCIM operators to expected SQL output assuming a base scope
|
|
637
|
+
# of "MockUser.all".
|
|
638
|
+
#
|
|
639
|
+
shared_examples 'generates expected query data' do | is_case_sensitive: |
|
|
640
|
+
it 'with binary operators' do
|
|
641
|
+
|
|
642
|
+
# Self-check: Is test coverage up to date?
|
|
643
|
+
#
|
|
644
|
+
expect(Scimitar::Lists::QueryParser::BINARY_OPERATORS.to_a - binary_expectations().keys).to match_array(['and', 'or'])
|
|
645
|
+
|
|
646
|
+
binary_expectations().each do | input, expected_output |
|
|
647
|
+
query = @instance.send(
|
|
648
|
+
:apply_scim_filter,
|
|
649
|
+
|
|
650
|
+
base_scope: MockUser.all,
|
|
651
|
+
scim_attribute: 'name.familyName',
|
|
652
|
+
scim_operator: input,
|
|
653
|
+
scim_parameter: '"BAZ"',
|
|
654
|
+
case_sensitive: is_case_sensitive
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Run a count just to prove the result is at least of valid syntax and
|
|
658
|
+
# check the SQL against expectations.
|
|
659
|
+
#
|
|
660
|
+
expect { query.count }.to_not raise_error
|
|
661
|
+
expect(query.to_sql).to eql(expected_output)
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
it 'with unary operators' do
|
|
666
|
+
|
|
667
|
+
# Self-check: Is test coverage up to date?
|
|
668
|
+
#
|
|
669
|
+
expect(Scimitar::Lists::QueryParser::UNARY_OPERATORS.to_a - unary_expectations().keys).to be_empty
|
|
670
|
+
|
|
671
|
+
unary_expectations().each do | input, expected_output |
|
|
672
|
+
query = @instance.send(
|
|
673
|
+
:apply_scim_filter,
|
|
674
|
+
|
|
675
|
+
base_scope: MockUser.all,
|
|
676
|
+
scim_attribute: 'name.familyName',
|
|
677
|
+
scim_operator: input,
|
|
678
|
+
scim_parameter: nil,
|
|
679
|
+
case_sensitive: is_case_sensitive
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Run a count just to prove the result is at least of valid syntax and
|
|
683
|
+
# check the SQL against expectations.
|
|
684
|
+
#
|
|
685
|
+
expect { query.count }.to_not raise_error
|
|
686
|
+
expect(query.to_sql).to eql(expected_output)
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
end # "shared_examples 'generates expected query data' do"
|
|
690
|
+
|
|
691
|
+
context 'case sensitive' do
|
|
692
|
+
let(:binary_expectations) {{
|
|
693
|
+
'eq' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" = 'BAZ'},
|
|
694
|
+
'ne' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" != 'BAZ'},
|
|
695
|
+
'gt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" > 'BAZ'},
|
|
696
|
+
'ge' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" >= 'BAZ'},
|
|
697
|
+
'lt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" < 'BAZ'},
|
|
698
|
+
'le' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" <= 'BAZ'},
|
|
699
|
+
'co' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" LIKE '%BAZ%'},
|
|
700
|
+
'sw' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" LIKE 'BAZ%'},
|
|
701
|
+
'ew' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" LIKE '%BAZ'},
|
|
702
|
+
}}
|
|
703
|
+
|
|
704
|
+
let(:unary_expectations) {{
|
|
705
|
+
'pr' => %q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."last_name" != '' AND "mock_users"."last_name" IS NOT NULL)},
|
|
706
|
+
}}
|
|
707
|
+
|
|
708
|
+
include_examples 'generates expected query data', is_case_sensitive: true
|
|
709
|
+
end # "context 'case sensitive' do"
|
|
710
|
+
|
|
711
|
+
context 'case insensitive' do
|
|
712
|
+
let(:binary_expectations) {{
|
|
713
|
+
'eq' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE 'BAZ'},
|
|
714
|
+
'ne' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" NOT ILIKE 'BAZ'},
|
|
715
|
+
'gt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" > 'BAZ'},
|
|
716
|
+
'ge' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" >= 'BAZ'},
|
|
717
|
+
'lt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" < 'BAZ'},
|
|
718
|
+
'le' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" <= 'BAZ'},
|
|
719
|
+
'co' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE '%BAZ%'},
|
|
720
|
+
'sw' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE 'BAZ%'},
|
|
721
|
+
'ew' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE '%BAZ'},
|
|
722
|
+
}}
|
|
723
|
+
|
|
724
|
+
let(:unary_expectations) {{
|
|
725
|
+
'pr' => %q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."last_name" != '' AND "mock_users"."last_name" IS NOT NULL)},
|
|
726
|
+
}}
|
|
727
|
+
|
|
728
|
+
include_examples 'generates expected query data', is_case_sensitive: false
|
|
729
|
+
end # "context 'case insensitive' do"
|
|
730
|
+
|
|
731
|
+
context 'error handling' do
|
|
732
|
+
it 'raises Scimitar::FilterError for unsupported operators' do
|
|
733
|
+
expect {
|
|
734
|
+
query = @instance.send(
|
|
735
|
+
:apply_scim_filter,
|
|
736
|
+
|
|
737
|
+
base_scope: MockUser.all,
|
|
738
|
+
scim_attribute: 'name.familyName',
|
|
739
|
+
scim_operator: 'zz',
|
|
740
|
+
scim_parameter: '"BAZ"',
|
|
741
|
+
case_sensitive: false
|
|
742
|
+
)
|
|
743
|
+
}.to raise_error(Scimitar::FilterError)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
it 'raises Scimitar::FilterError for unsupported columnsx' do
|
|
747
|
+
expect(@instance).to receive(:activerecord_columns).with('name.familyName').and_return(['non_existant_column_name'])
|
|
748
|
+
expect {
|
|
749
|
+
query = @instance.send(
|
|
750
|
+
:apply_scim_filter,
|
|
751
|
+
|
|
752
|
+
base_scope: MockUser.all,
|
|
753
|
+
scim_attribute: 'name.familyName',
|
|
754
|
+
scim_operator: 'eq',
|
|
755
|
+
scim_parameter: '"BAZ"',
|
|
756
|
+
case_sensitive: false
|
|
757
|
+
)
|
|
758
|
+
}.to raise_error(Scimitar::FilterError)
|
|
759
|
+
end
|
|
760
|
+
end # "context 'error handling' do"
|
|
761
|
+
end # "context '#apply_scim_filter' do
|
|
762
|
+
end # "context 'unit tests for internal methods' do"
|
|
763
|
+
end # "RSpec.describe Scimitar::Lists::QueryParser do"
|