scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +16 -0
  3. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +180 -0
  4. data/app/controllers/scimitar/application_controller.rb +129 -0
  5. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  6. data/app/controllers/scimitar/resources_controller.rb +203 -0
  7. data/app/controllers/scimitar/schemas_controller.rb +16 -0
  8. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  9. data/app/models/scimitar/authentication_error.rb +9 -0
  10. data/app/models/scimitar/authentication_scheme.rb +18 -0
  11. data/app/models/scimitar/bulk.rb +8 -0
  12. data/app/models/scimitar/complex_types/address.rb +18 -0
  13. data/app/models/scimitar/complex_types/base.rb +41 -0
  14. data/app/models/scimitar/complex_types/email.rb +12 -0
  15. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  16. data/app/models/scimitar/complex_types/ims.rb +12 -0
  17. data/app/models/scimitar/complex_types/name.rb +12 -0
  18. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  19. data/app/models/scimitar/complex_types/photo.rb +12 -0
  20. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  21. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  22. data/app/models/scimitar/complex_types/role.rb +12 -0
  23. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  24. data/app/models/scimitar/engine_configuration.rb +24 -0
  25. data/app/models/scimitar/error_response.rb +20 -0
  26. data/app/models/scimitar/errors.rb +14 -0
  27. data/app/models/scimitar/filter.rb +11 -0
  28. data/app/models/scimitar/filter_error.rb +22 -0
  29. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  30. data/app/models/scimitar/lists/count.rb +64 -0
  31. data/app/models/scimitar/lists/query_parser.rb +730 -0
  32. data/app/models/scimitar/meta.rb +7 -0
  33. data/app/models/scimitar/not_found_error.rb +10 -0
  34. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  35. data/app/models/scimitar/resource_type.rb +29 -0
  36. data/app/models/scimitar/resources/base.rb +159 -0
  37. data/app/models/scimitar/resources/group.rb +13 -0
  38. data/app/models/scimitar/resources/mixin.rb +964 -0
  39. data/app/models/scimitar/resources/user.rb +13 -0
  40. data/app/models/scimitar/schema/address.rb +24 -0
  41. data/app/models/scimitar/schema/attribute.rb +123 -0
  42. data/app/models/scimitar/schema/base.rb +86 -0
  43. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  44. data/app/models/scimitar/schema/email.rb +10 -0
  45. data/app/models/scimitar/schema/entitlement.rb +10 -0
  46. data/app/models/scimitar/schema/group.rb +27 -0
  47. data/app/models/scimitar/schema/ims.rb +10 -0
  48. data/app/models/scimitar/schema/name.rb +20 -0
  49. data/app/models/scimitar/schema/phone_number.rb +10 -0
  50. data/app/models/scimitar/schema/photo.rb +10 -0
  51. data/app/models/scimitar/schema/reference_group.rb +23 -0
  52. data/app/models/scimitar/schema/reference_member.rb +21 -0
  53. data/app/models/scimitar/schema/role.rb +10 -0
  54. data/app/models/scimitar/schema/user.rb +52 -0
  55. data/app/models/scimitar/schema/vdtp.rb +18 -0
  56. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  57. data/app/models/scimitar/service_provider_configuration.rb +49 -0
  58. data/app/models/scimitar/supportable.rb +14 -0
  59. data/app/views/layouts/scimitar/application.html.erb +14 -0
  60. data/config/initializers/scimitar.rb +82 -0
  61. data/config/routes.rb +6 -0
  62. data/lib/scimitar.rb +23 -0
  63. data/lib/scimitar/engine.rb +63 -0
  64. data/lib/scimitar/version.rb +13 -0
  65. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  66. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  67. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  68. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  69. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  70. data/spec/apps/dummy/app/models/mock_user.rb +104 -0
  71. data/spec/apps/dummy/config/application.rb +17 -0
  72. data/spec/apps/dummy/config/boot.rb +2 -0
  73. data/spec/apps/dummy/config/environment.rb +2 -0
  74. data/spec/apps/dummy/config/environments/test.rb +15 -0
  75. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  76. data/spec/apps/dummy/config/initializers/scimitar.rb +14 -0
  77. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  78. data/spec/apps/dummy/config/routes.rb +24 -0
  79. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +15 -0
  80. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  81. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +8 -0
  82. data/spec/apps/dummy/db/schema.rb +42 -0
  83. data/spec/controllers/scimitar/application_controller_spec.rb +173 -0
  84. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  85. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  86. data/spec/controllers/scimitar/schemas_controller_spec.rb +75 -0
  87. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  88. data/spec/models/scimitar/complex_types/address_spec.rb +19 -0
  89. data/spec/models/scimitar/complex_types/email_spec.rb +23 -0
  90. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  91. data/spec/models/scimitar/lists/query_parser_spec.rb +763 -0
  92. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  93. data/spec/models/scimitar/resources/base_spec.rb +289 -0
  94. data/spec/models/scimitar/resources/base_validation_spec.rb +61 -0
  95. data/spec/models/scimitar/resources/mixin_spec.rb +2127 -0
  96. data/spec/models/scimitar/resources/user_spec.rb +55 -0
  97. data/spec/models/scimitar/schema/attribute_spec.rb +80 -0
  98. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  99. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  100. data/spec/models/scimitar/schema/user_spec.rb +710 -0
  101. data/spec/requests/active_record_backed_resources_controller_spec.rb +569 -0
  102. data/spec/requests/application_controller_spec.rb +49 -0
  103. data/spec/requests/controller_configuration_spec.rb +17 -0
  104. data/spec/requests/engine_spec.rb +20 -0
  105. data/spec/spec_helper.rb +66 -0
  106. 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"