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.
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"