powerhome-scimitar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +708 -0
  4. data/Rakefile +16 -0
  5. data/app/controllers/scimitar/active_record_backed_resources_controller.rb +257 -0
  6. data/app/controllers/scimitar/application_controller.rb +157 -0
  7. data/app/controllers/scimitar/resource_types_controller.rb +28 -0
  8. data/app/controllers/scimitar/resources_controller.rb +203 -0
  9. data/app/controllers/scimitar/schemas_controller.rb +21 -0
  10. data/app/controllers/scimitar/service_provider_configurations_controller.rb +8 -0
  11. data/app/models/scimitar/authentication_error.rb +9 -0
  12. data/app/models/scimitar/authentication_scheme.rb +18 -0
  13. data/app/models/scimitar/bulk.rb +8 -0
  14. data/app/models/scimitar/complex_types/address.rb +12 -0
  15. data/app/models/scimitar/complex_types/base.rb +83 -0
  16. data/app/models/scimitar/complex_types/email.rb +12 -0
  17. data/app/models/scimitar/complex_types/entitlement.rb +12 -0
  18. data/app/models/scimitar/complex_types/ims.rb +12 -0
  19. data/app/models/scimitar/complex_types/name.rb +12 -0
  20. data/app/models/scimitar/complex_types/phone_number.rb +12 -0
  21. data/app/models/scimitar/complex_types/photo.rb +12 -0
  22. data/app/models/scimitar/complex_types/reference_group.rb +12 -0
  23. data/app/models/scimitar/complex_types/reference_member.rb +12 -0
  24. data/app/models/scimitar/complex_types/role.rb +12 -0
  25. data/app/models/scimitar/complex_types/x509_certificate.rb +12 -0
  26. data/app/models/scimitar/engine_configuration.rb +32 -0
  27. data/app/models/scimitar/error_response.rb +32 -0
  28. data/app/models/scimitar/errors.rb +14 -0
  29. data/app/models/scimitar/filter.rb +11 -0
  30. data/app/models/scimitar/filter_error.rb +22 -0
  31. data/app/models/scimitar/invalid_syntax_error.rb +9 -0
  32. data/app/models/scimitar/lists/count.rb +64 -0
  33. data/app/models/scimitar/lists/query_parser.rb +745 -0
  34. data/app/models/scimitar/meta.rb +7 -0
  35. data/app/models/scimitar/not_found_error.rb +10 -0
  36. data/app/models/scimitar/resource_invalid_error.rb +9 -0
  37. data/app/models/scimitar/resource_type.rb +29 -0
  38. data/app/models/scimitar/resources/base.rb +190 -0
  39. data/app/models/scimitar/resources/group.rb +13 -0
  40. data/app/models/scimitar/resources/mixin.rb +1524 -0
  41. data/app/models/scimitar/resources/user.rb +13 -0
  42. data/app/models/scimitar/schema/address.rb +25 -0
  43. data/app/models/scimitar/schema/attribute.rb +132 -0
  44. data/app/models/scimitar/schema/base.rb +90 -0
  45. data/app/models/scimitar/schema/derived_attributes.rb +24 -0
  46. data/app/models/scimitar/schema/email.rb +10 -0
  47. data/app/models/scimitar/schema/entitlement.rb +10 -0
  48. data/app/models/scimitar/schema/group.rb +27 -0
  49. data/app/models/scimitar/schema/ims.rb +10 -0
  50. data/app/models/scimitar/schema/name.rb +20 -0
  51. data/app/models/scimitar/schema/phone_number.rb +10 -0
  52. data/app/models/scimitar/schema/photo.rb +10 -0
  53. data/app/models/scimitar/schema/reference_group.rb +23 -0
  54. data/app/models/scimitar/schema/reference_member.rb +21 -0
  55. data/app/models/scimitar/schema/role.rb +10 -0
  56. data/app/models/scimitar/schema/user.rb +52 -0
  57. data/app/models/scimitar/schema/vdtp.rb +18 -0
  58. data/app/models/scimitar/schema/x509_certificate.rb +22 -0
  59. data/app/models/scimitar/service_provider_configuration.rb +60 -0
  60. data/app/models/scimitar/supportable.rb +14 -0
  61. data/app/views/layouts/scimitar/application.html.erb +14 -0
  62. data/config/initializers/scimitar.rb +111 -0
  63. data/config/routes.rb +6 -0
  64. data/lib/scimitar/engine.rb +63 -0
  65. data/lib/scimitar/support/hash_with_indifferent_case_insensitive_access.rb +216 -0
  66. data/lib/scimitar/support/utilities.rb +51 -0
  67. data/lib/scimitar/version.rb +13 -0
  68. data/lib/scimitar.rb +29 -0
  69. data/spec/apps/dummy/app/controllers/custom_create_mock_users_controller.rb +25 -0
  70. data/spec/apps/dummy/app/controllers/custom_destroy_mock_users_controller.rb +24 -0
  71. data/spec/apps/dummy/app/controllers/custom_replace_mock_users_controller.rb +25 -0
  72. data/spec/apps/dummy/app/controllers/custom_request_verifiers_controller.rb +30 -0
  73. data/spec/apps/dummy/app/controllers/custom_save_mock_users_controller.rb +24 -0
  74. data/spec/apps/dummy/app/controllers/custom_update_mock_users_controller.rb +25 -0
  75. data/spec/apps/dummy/app/controllers/mock_groups_controller.rb +13 -0
  76. data/spec/apps/dummy/app/controllers/mock_users_controller.rb +13 -0
  77. data/spec/apps/dummy/app/models/mock_group.rb +83 -0
  78. data/spec/apps/dummy/app/models/mock_user.rb +132 -0
  79. data/spec/apps/dummy/config/application.rb +18 -0
  80. data/spec/apps/dummy/config/boot.rb +2 -0
  81. data/spec/apps/dummy/config/environment.rb +2 -0
  82. data/spec/apps/dummy/config/environments/test.rb +38 -0
  83. data/spec/apps/dummy/config/initializers/cookies_serializer.rb +3 -0
  84. data/spec/apps/dummy/config/initializers/scimitar.rb +61 -0
  85. data/spec/apps/dummy/config/initializers/session_store.rb +3 -0
  86. data/spec/apps/dummy/config/routes.rb +45 -0
  87. data/spec/apps/dummy/db/migrate/20210304014602_create_mock_users.rb +24 -0
  88. data/spec/apps/dummy/db/migrate/20210308020313_create_mock_groups.rb +10 -0
  89. data/spec/apps/dummy/db/migrate/20210308044214_create_join_table_mock_groups_mock_users.rb +13 -0
  90. data/spec/apps/dummy/db/schema.rb +48 -0
  91. data/spec/controllers/scimitar/application_controller_spec.rb +296 -0
  92. data/spec/controllers/scimitar/resource_types_controller_spec.rb +94 -0
  93. data/spec/controllers/scimitar/resources_controller_spec.rb +247 -0
  94. data/spec/controllers/scimitar/schemas_controller_spec.rb +83 -0
  95. data/spec/controllers/scimitar/service_provider_configurations_controller_spec.rb +22 -0
  96. data/spec/models/scimitar/complex_types/address_spec.rb +18 -0
  97. data/spec/models/scimitar/complex_types/email_spec.rb +21 -0
  98. data/spec/models/scimitar/lists/count_spec.rb +147 -0
  99. data/spec/models/scimitar/lists/query_parser_spec.rb +830 -0
  100. data/spec/models/scimitar/resource_type_spec.rb +21 -0
  101. data/spec/models/scimitar/resources/base_spec.rb +485 -0
  102. data/spec/models/scimitar/resources/base_validation_spec.rb +86 -0
  103. data/spec/models/scimitar/resources/mixin_spec.rb +3562 -0
  104. data/spec/models/scimitar/resources/user_spec.rb +68 -0
  105. data/spec/models/scimitar/schema/attribute_spec.rb +99 -0
  106. data/spec/models/scimitar/schema/base_spec.rb +64 -0
  107. data/spec/models/scimitar/schema/group_spec.rb +87 -0
  108. data/spec/models/scimitar/schema/user_spec.rb +720 -0
  109. data/spec/requests/active_record_backed_resources_controller_spec.rb +1354 -0
  110. data/spec/requests/application_controller_spec.rb +61 -0
  111. data/spec/requests/controller_configuration_spec.rb +17 -0
  112. data/spec/requests/engine_spec.rb +45 -0
  113. data/spec/spec_helper.rb +101 -0
  114. data/spec/spec_helper_spec.rb +30 -0
  115. data/spec/support/hash_with_indifferent_case_insensitive_access_spec.rb +169 -0
  116. metadata +321 -0
@@ -0,0 +1,830 @@
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(:primary_key)).to eql([user_1.primary_key])
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(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key])
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(:primary_key)).to eql([user_2.primary_key])
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(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key])
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(:primary_key)).to match_array([user_1.primary_key])
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 'when an arel column is mapped' do
485
+ let(:scope_with_groups) { MockUser.left_joins(:mock_groups) }
486
+
487
+ context 'with binary operators' do
488
+ it 'reads across all using OR' do
489
+ @instance.parse('groups eq "12345"')
490
+ query = @instance.to_activerecord_query(scope_with_groups)
491
+
492
+ expect(query.to_sql).to eql(<<~SQL.squish)
493
+ SELECT "mock_users".*
494
+ FROM "mock_users"
495
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
496
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
497
+ WHERE "mock_groups"."id" ILIKE 12345
498
+ SQL
499
+ end
500
+
501
+ it 'works with other query elements using correct precedence' do
502
+ @instance.parse('groups eq "12345" and emails eq "any@test.com"')
503
+ query = @instance.to_activerecord_query(scope_with_groups)
504
+
505
+ expect(query.to_sql).to eql(<<~SQL.squish)
506
+ SELECT "mock_users".*
507
+ FROM "mock_users"
508
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
509
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
510
+ WHERE "mock_groups"."id" ILIKE 12345 AND ("mock_users"."work_email_address" ILIKE 'any@test.com' OR "mock_users"."home_email_address" ILIKE 'any@test.com')
511
+ SQL
512
+ end
513
+ end # "context 'with binary operators' do"
514
+
515
+ context 'with unary operators' do
516
+ it 'reads across all using OR' do
517
+ @instance.parse('groups pr')
518
+ query = @instance.to_activerecord_query(scope_with_groups)
519
+
520
+ expect(query.to_sql).to eql(<<~SQL.squish)
521
+ SELECT "mock_users".*
522
+ FROM "mock_users"
523
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
524
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
525
+ WHERE ("mock_groups"."id" != NULL AND "mock_groups"."id" IS NOT NULL)
526
+ SQL
527
+ end
528
+
529
+ it 'works with other query elements using correct precedence' do
530
+ @instance.parse('name.familyName eq "John" and groups pr')
531
+ query = @instance.to_activerecord_query(scope_with_groups)
532
+
533
+ expect(query.to_sql).to eql(<<~SQL.squish)
534
+ SELECT "mock_users".*
535
+ FROM "mock_users"
536
+ LEFT OUTER JOIN "mock_groups_users" ON "mock_groups_users"."mock_user_id" = "mock_users"."primary_key"
537
+ LEFT OUTER JOIN "mock_groups" ON "mock_groups"."id" = "mock_groups_users"."mock_group_id"
538
+ WHERE "mock_users"."last_name" ILIKE 'John' AND ("mock_groups"."id" != NULL AND "mock_groups"."id" IS NOT NULL)
539
+ SQL
540
+ end
541
+ end # "context 'with unary operators' do
542
+ end # "context 'when an arel column is mapped' do"
543
+
544
+ context 'with complex cases' do
545
+ context 'using AND' do
546
+ it 'generates expected SQL' do
547
+ @instance.parse('name.givenName pr AND name.familyName ne "Doe"')
548
+ query = @instance.to_activerecord_query(MockUser.all)
549
+
550
+ 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'})
551
+ end
552
+
553
+ it 'finds expected items' do
554
+ user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis')
555
+ user_2 = MockUser.create(username: '2', first_name: 'John', last_name: 'Doe')
556
+ user_3 = MockUser.create(username: '3', last_name: 'Doe')
557
+
558
+ @instance.parse('name.givenName pr AND name.familyName eq "Doe"')
559
+ query = @instance.to_activerecord_query(MockUser.all)
560
+
561
+ expect(query.count).to eql(1)
562
+ expect(query.pluck(:primary_key)).to match_array([user_2.primary_key])
563
+ end
564
+ end # "context 'simple AND' do"
565
+
566
+ context 'using OR' do
567
+ it 'generates expected SQL' do
568
+ @instance.parse('name.givenName pr OR name.familyName eq "Doe"')
569
+ query = @instance.to_activerecord_query(MockUser.all)
570
+
571
+ 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')})
572
+ end
573
+
574
+ it 'finds expected items' do
575
+ user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis')
576
+ user_2 = MockUser.create(username: '2', last_name: 'Doe')
577
+ user_3 = MockUser.create(username: '3', last_name: 'Smith')
578
+
579
+ @instance.parse('name.givenName pr OR name.familyName eq "Doe"')
580
+ query = @instance.to_activerecord_query(MockUser.all)
581
+
582
+ expect(query.count).to eql(2)
583
+ expect(query.pluck(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key])
584
+ end
585
+ end # "context 'simple OR' do"
586
+
587
+ context 'combined AND, OR and parentheses' do
588
+ it 'generates expected SQL' do
589
+ @instance.parse('name.givenName eq "Jane" and (name.familyName co "avi" or name.familyName ew "ith")')
590
+ query = @instance.to_activerecord_query(MockUser.all)
591
+
592
+ 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')})
593
+ end
594
+
595
+ it 'combined parentheses generates expected SQL' do
596
+ @instance.parse('(name.givenName eq "Jane" OR name.givenName eq "Jaden") and (name.familyName co "avi" or name.familyName ew "ith")')
597
+ query = @instance.to_activerecord_query(MockUser.all)
598
+
599
+ expect(query.to_sql).to eql(%q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."first_name" ILIKE 'Jane' OR "mock_users"."first_name" ILIKE 'Jaden') AND ("mock_users"."last_name" ILIKE '%avi%' OR "mock_users"."last_name" ILIKE '%ith')})
600
+ end
601
+
602
+ it 'finds expected items' do
603
+ user_1 = MockUser.create(username: '1', first_name: 'Jane', last_name: 'Davis') # Match
604
+ user_2 = MockUser.create(username: '2', first_name: 'Jane', last_name: 'Smith') # Match
605
+ user_3 = MockUser.create(username: '3', first_name: 'Jane', last_name: 'Moreith') # Match
606
+ user_4 = MockUser.create(username: '4', first_name: 'Jane', last_name: 'Doe') # No last name match
607
+ user_5 = MockUser.create(username: '5', first_name: 'Doe', last_name: 'Smith') # No first name match
608
+ user_6 = MockUser.create(username: '6', first_name: 'Bill', last_name: 'Davis') # No first name match
609
+ user_7 = MockUser.create(username: '7', last_name: 'Davis') # Missing first name
610
+ user_8 = MockUser.create(username: '8', last_name: 'Smith') # Missing first name
611
+
612
+ @instance.parse('name.givenName eq "Jane" and (name.familyName co "avi" or name.familyName ew "ith")')
613
+ query = @instance.to_activerecord_query(MockUser.all)
614
+
615
+ expect(query.count).to eql(3)
616
+ expect(query.pluck(:primary_key)).to match_array([user_1.primary_key, user_2.primary_key, user_3.primary_key])
617
+ end
618
+ end # "context 'combined AND and OR' do"
619
+
620
+ context 'when flattening is needed' do
621
+ it 'generates expected SQL' do
622
+ @instance.parse('name[givenName eq "Jane" and (familyName co "avi" or familyName ew "ith")]')
623
+ query = @instance.to_activerecord_query(MockUser.all)
624
+
625
+ 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')})
626
+ end
627
+ end # "context 'when flattening is needed' do"
628
+ end # "context 'complex cases' do"
629
+ end # "context '#to_activerecord_query' do"
630
+
631
+ # ===========================================================================
632
+ # PRIVATE METHODS
633
+ # ===========================================================================
634
+
635
+ context 'internal method' do
636
+
637
+ # =========================================================================
638
+ # Attributes
639
+ # =========================================================================
640
+
641
+ context '#activerecord_columns' do
642
+ it 'returns a column in an array' do
643
+ expect(@instance.send(:activerecord_columns, 'name.familyName')).to eql([:last_name])
644
+ end
645
+
646
+ it 'returns multiple column in an array' do
647
+ expect(@instance.send(:activerecord_columns, 'emails')).to eql([:work_email_address, :home_email_address])
648
+ end
649
+
650
+ it 'returns empty for "ignore"' do
651
+ expect(@instance.send(:activerecord_columns, 'emails.type')).to be_empty
652
+ end
653
+
654
+ it 'complains if there is no column present' do
655
+ expect { @instance.send(:activerecord_columns, nil) }.to raise_error(Scimitar::FilterError)
656
+ expect { @instance.send(:activerecord_columns, '' ) }.to raise_error(Scimitar::FilterError)
657
+ end
658
+
659
+ it 'complains if there is no column mapping available' do
660
+ expect { @instance.send(:activerecord_columns, 'userName') }.to raise_error(Scimitar::FilterError)
661
+ end
662
+
663
+ it 'complains about malformed declarations' do
664
+ local_instance = described_class.new(
665
+ {
666
+ 'name.givenName' => { wut: true }
667
+ }
668
+ )
669
+
670
+ expect { local_instance.send(:activerecord_columns, 'name.givenName' ) }.to raise_error(RuntimeError)
671
+ end
672
+ end # "context '#activerecord_columns' do"
673
+
674
+ # =========================================================================
675
+ # Parameters
676
+ # =========================================================================
677
+
678
+ context '#activerecord_parameter' do
679
+ it 'returns a blank string if a parameter is missing' do
680
+ expect(@instance.send(:activerecord_parameter, nil )).to eql('')
681
+ expect(@instance.send(:activerecord_parameter, '' )).to eql('')
682
+ expect(@instance.send(:activerecord_parameter, ' ')).to eql('')
683
+ end
684
+
685
+ it 'returns the parameter if present' do
686
+ expect(@instance.send(:activerecord_parameter, 'BAZ')).to eql('BAZ')
687
+ end
688
+
689
+ it 'removes surrounding quotes if present' do
690
+ expect(@instance.send(:activerecord_parameter, '"BA"Z"')).to eql('BA"Z')
691
+ expect(@instance.send(:activerecord_parameter, '"BA"Z' )).to eql('"BA"Z')
692
+ expect(@instance.send(:activerecord_parameter, 'BA"Z"' )).to eql('BA"Z"')
693
+ end
694
+ end # "context '#parameter' do"
695
+
696
+ # =========================================================================
697
+ # Low level queries
698
+ # =========================================================================
699
+
700
+ context '#apply_scim_filter' do
701
+
702
+ # Use 'let' to define :binary_expectations and :unary_operators, mapping
703
+ # lower case SCIM operators to expected SQL output assuming a base scope
704
+ # of "MockUser.all".
705
+ #
706
+ shared_examples 'generates expected query data' do | is_case_sensitive: |
707
+ it 'with binary operators' do
708
+
709
+ # Self-check: Is test coverage up to date?
710
+ #
711
+ expect(Scimitar::Lists::QueryParser::BINARY_OPERATORS.to_a - binary_expectations().keys).to match_array(['and', 'or'])
712
+
713
+ binary_expectations().each do | input, expected_output |
714
+ query = @instance.send(
715
+ :apply_scim_filter,
716
+
717
+ base_scope: MockUser.all,
718
+ scim_attribute: 'name.familyName',
719
+ scim_operator: input,
720
+ scim_parameter: '"BAZ"',
721
+ case_sensitive: is_case_sensitive
722
+ )
723
+
724
+ # Run a count just to prove the result is at least of valid syntax and
725
+ # check the SQL against expectations.
726
+ #
727
+ expect { query.count }.to_not raise_error
728
+ expect(query.to_sql).to eql(expected_output)
729
+ end
730
+ end
731
+
732
+ it 'with unary operators' do
733
+
734
+ # Self-check: Is test coverage up to date?
735
+ #
736
+ expect(Scimitar::Lists::QueryParser::UNARY_OPERATORS.to_a - unary_expectations().keys).to be_empty
737
+
738
+ unary_expectations().each do | input, expected_output |
739
+ query = @instance.send(
740
+ :apply_scim_filter,
741
+
742
+ base_scope: MockUser.all,
743
+ scim_attribute: 'name.familyName',
744
+ scim_operator: input,
745
+ scim_parameter: nil,
746
+ case_sensitive: is_case_sensitive
747
+ )
748
+
749
+ # Run a count just to prove the result is at least of valid syntax and
750
+ # check the SQL against expectations.
751
+ #
752
+ expect { query.count }.to_not raise_error
753
+ expect(query.to_sql).to eql(expected_output)
754
+ end
755
+ end
756
+ end # "shared_examples 'generates expected query data' do"
757
+
758
+ context 'case sensitive' do
759
+ let(:binary_expectations) {{
760
+ 'eq' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" = 'BAZ'},
761
+ 'ne' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" != 'BAZ'},
762
+ 'gt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" > 'BAZ'},
763
+ 'ge' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" >= 'BAZ'},
764
+ 'lt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" < 'BAZ'},
765
+ 'le' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" <= 'BAZ'},
766
+ 'co' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" LIKE '%BAZ%'},
767
+ 'sw' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" LIKE 'BAZ%'},
768
+ 'ew' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" LIKE '%BAZ'},
769
+ }}
770
+
771
+ let(:unary_expectations) {{
772
+ 'pr' => %q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."last_name" != '' AND "mock_users"."last_name" IS NOT NULL)},
773
+ }}
774
+
775
+ include_examples 'generates expected query data', is_case_sensitive: true
776
+ end # "context 'case sensitive' do"
777
+
778
+ context 'case insensitive' do
779
+ let(:binary_expectations) {{
780
+ 'eq' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE 'BAZ'},
781
+ 'ne' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" NOT ILIKE 'BAZ'},
782
+ 'gt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" > 'BAZ'},
783
+ 'ge' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" >= 'BAZ'},
784
+ 'lt' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" < 'BAZ'},
785
+ 'le' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" <= 'BAZ'},
786
+ 'co' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE '%BAZ%'},
787
+ 'sw' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE 'BAZ%'},
788
+ 'ew' => %q{SELECT "mock_users".* FROM "mock_users" WHERE "mock_users"."last_name" ILIKE '%BAZ'},
789
+ }}
790
+
791
+ let(:unary_expectations) {{
792
+ 'pr' => %q{SELECT "mock_users".* FROM "mock_users" WHERE ("mock_users"."last_name" != '' AND "mock_users"."last_name" IS NOT NULL)},
793
+ }}
794
+
795
+ include_examples 'generates expected query data', is_case_sensitive: false
796
+ end # "context 'case insensitive' do"
797
+
798
+ context 'error handling' do
799
+ it 'raises Scimitar::FilterError for unsupported operators' do
800
+ expect {
801
+ query = @instance.send(
802
+ :apply_scim_filter,
803
+
804
+ base_scope: MockUser.all,
805
+ scim_attribute: 'name.familyName',
806
+ scim_operator: 'zz',
807
+ scim_parameter: '"BAZ"',
808
+ case_sensitive: false
809
+ )
810
+ }.to raise_error(Scimitar::FilterError)
811
+ end
812
+
813
+ it 'raises Scimitar::FilterError for unsupported columnsx' do
814
+ expect(@instance).to receive(:activerecord_columns).with('name.familyName').and_return(['non_existant_column_name'])
815
+ expect {
816
+ query = @instance.send(
817
+ :apply_scim_filter,
818
+
819
+ base_scope: MockUser.all,
820
+ scim_attribute: 'name.familyName',
821
+ scim_operator: 'eq',
822
+ scim_parameter: '"BAZ"',
823
+ case_sensitive: false
824
+ )
825
+ }.to raise_error(Scimitar::FilterError)
826
+ end
827
+ end # "context 'error handling' do"
828
+ end # "context '#apply_scim_filter' do
829
+ end # "context 'unit tests for internal methods' do"
830
+ end # "RSpec.describe Scimitar::Lists::QueryParser do"