20336: Reject >64 bit operands when filtering on integer columns.
[arvados.git] / services / api / test / functional / arvados / v1 / filters_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'test_helper'
6
7 class Arvados::V1::FiltersTest < ActionController::TestCase
8   test '"not in" filter passes null values' do
9     @controller = Arvados::V1::ContainerRequestsController.new
10     authorize_with :admin
11     get :index, params: {
12       filters: [ ['container_uuid', 'not in', ['zzzzz-dz642-queuedcontainer', 'zzzzz-dz642-runningcontainr']] ],
13       controller: 'container_requests',
14     }
15     assert_response :success
16     found = assigns(:objects)
17     assert_includes(found.collect(&:container_uuid), nil,
18                     "'container_uuid not in [zzzzz-dz642-queuedcontainer, zzzzz-dz642-runningcontainr]' filter should pass null")
19   end
20
21   test 'error message for non-array element in filters array' do
22     @controller = Arvados::V1::CollectionsController.new
23     authorize_with :active
24     get :index, params: {
25       filters: [{bogus: 'filter'}],
26     }
27     assert_response 422
28     assert_match(/Invalid element in filters array/,
29                  json_response['errors'].join(' '))
30   end
31
32   test 'error message for unsupported full text search' do
33     @controller = Arvados::V1::CollectionsController.new
34     authorize_with :active
35     get :index, params: {
36       filters: [['uuid', '@@', 'abcdef']],
37     }
38     assert_response 422
39     assert_match(/no longer supported/, json_response['errors'].join(' '))
40   end
41
42   test 'error message for int64 overflow' do
43     # some versions of ActiveRecord cast >64-bit ints to postgres
44     # numeric type, but this is never useful because database content
45     # is 64 bit.
46     @controller = Arvados::V1::LogsController.new
47     authorize_with :active
48     get :index, params: {
49       filters: [['id', '=', 123412341234123412341234]],
50     }
51     assert_response 422
52     assert_match(/Invalid operand .* integer attribute/, json_response['errors'].join(' '))
53   end
54
55   test 'error message for invalid boolean operand' do
56     @controller = Arvados::V1::GroupsController.new
57     authorize_with :active
58     get :index, params: {
59       filters: [['is_trashed', '=', 'fourty']],
60     }
61     assert_response 422
62     assert_match(/Invalid operand .* boolean attribute/, json_response['errors'].join(' '))
63   end
64
65   test 'api responses provide timestamps with nanoseconds' do
66     @controller = Arvados::V1::CollectionsController.new
67     authorize_with :active
68     get :index
69     assert_response :success
70     assert_not_empty json_response['items']
71     json_response['items'].each do |item|
72       %w(created_at modified_at).each do |attr|
73         # Pass fixtures with null timestamps.
74         next if item[attr].nil?
75         assert_match(/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d.\d{9}Z$/, item[attr])
76       end
77     end
78   end
79
80   %w(< > <= >= =).each do |operator|
81     test "timestamp #{operator} filters work with nanosecond precision" do
82       # Python clients like Node Manager rely on this exact format.
83       # If you must change this format for some reason, make sure you
84       # coordinate the change with them.
85       expect_match = !!operator.index('=')
86       mine = act_as_user users(:active) do
87         Collection.create!(manifest_text: '')
88       end
89       timestamp = mine.modified_at.strftime('%Y-%m-%dT%H:%M:%S.%NZ')
90       @controller = Arvados::V1::CollectionsController.new
91       authorize_with :active
92       get :index, params: {
93         filters: [['modified_at', operator, timestamp],
94                   ['uuid', '=', mine.uuid]],
95       }
96       assert_response :success
97       uuids = json_response['items'].map { |item| item['uuid'] }
98       if expect_match
99         assert_includes uuids, mine.uuid
100       else
101         assert_not_includes uuids, mine.uuid
102       end
103     end
104   end
105
106   [['prop1', '=', 'value1', [:collection_with_prop1_value1], [:collection_with_prop1_value2, :collection_with_prop2_1]],
107    ['prop1', '!=', 'value1', [:collection_with_prop1_value2, :collection_with_prop2_1], [:collection_with_prop1_value1]],
108    ['prop1', 'exists', true, [:collection_with_prop1_value1, :collection_with_prop1_value2, :collection_with_prop1_value3, :collection_with_prop1_other1], [:collection_with_prop2_1]],
109    ['prop1', 'exists', false, [:collection_with_prop2_1], [:collection_with_prop1_value1, :collection_with_prop1_value2, :collection_with_prop1_value3, :collection_with_prop1_other1]],
110    ['prop1', 'in', ['value1', 'value2'], [:collection_with_prop1_value1, :collection_with_prop1_value2], [:collection_with_prop1_value3, :collection_with_prop2_1]],
111    ['prop1', 'in', ['value1', 'valueX'], [:collection_with_prop1_value1], [:collection_with_prop1_value3, :collection_with_prop2_1]],
112    ['prop1', 'not in', ['value1', 'value2'], [:collection_with_prop1_value3, :collection_with_prop1_other1, :collection_with_prop2_1], [:collection_with_prop1_value1, :collection_with_prop1_value2]],
113    ['prop1', 'not in', ['value1', 'valueX'], [:collection_with_prop1_value2, :collection_with_prop1_value3, :collection_with_prop1_other1, :collection_with_prop2_1], [:collection_with_prop1_value1]],
114    ['prop1', '>', 'value2', [:collection_with_prop1_value3], [:collection_with_prop1_other1, :collection_with_prop1_value1]],
115    ['prop1', '<', 'value2', [:collection_with_prop1_other1, :collection_with_prop1_value1], [:collection_with_prop1_value2, :collection_with_prop1_value2]],
116    ['prop1', '<=', 'value2', [:collection_with_prop1_other1, :collection_with_prop1_value1, :collection_with_prop1_value2], [:collection_with_prop1_value3]],
117    ['prop1', '>=', 'value2', [:collection_with_prop1_value2, :collection_with_prop1_value3], [:collection_with_prop1_other1, :collection_with_prop1_value1]],
118    ['prop1', 'like', 'value%', [:collection_with_prop1_value1, :collection_with_prop1_value2, :collection_with_prop1_value3], [:collection_with_prop1_other1]],
119    ['prop1', 'like', '%1', [:collection_with_prop1_value1, :collection_with_prop1_other1], [:collection_with_prop1_value2, :collection_with_prop1_value3]],
120    ['prop1', 'ilike', 'VALUE%', [:collection_with_prop1_value1, :collection_with_prop1_value2, :collection_with_prop1_value3], [:collection_with_prop1_other1]],
121    ['prop2', '>',  1, [:collection_with_prop2_5], [:collection_with_prop2_1]],
122    ['prop2', '<',  5, [:collection_with_prop2_1], [:collection_with_prop2_5]],
123    ['prop2', '<=', 5, [:collection_with_prop2_1, :collection_with_prop2_5], []],
124    ['prop2', '>=', 1, [:collection_with_prop2_1, :collection_with_prop2_5], []],
125    ['<http://schema.org/example>', '=', "value1", [:collection_with_uri_prop], []],
126    ['listprop', 'contains', 'elem1', [:collection_with_list_prop_odd, :collection_with_listprop_elem1], [:collection_with_list_prop_even]],
127    ['listprop', '=', 'elem1', [:collection_with_listprop_elem1], [:collection_with_list_prop_odd]],
128    ['listprop', 'contains', 5, [:collection_with_list_prop_odd], [:collection_with_list_prop_even, :collection_with_listprop_elem1]],
129    ['listprop', 'contains', 'elem2', [:collection_with_list_prop_even], [:collection_with_list_prop_odd, :collection_with_listprop_elem1]],
130    ['listprop', 'contains', 'ELEM2', [], [:collection_with_list_prop_even]],
131    ['listprop', 'contains', 'elem8', [], [:collection_with_list_prop_even]],
132    ['listprop', 'contains', 4, [:collection_with_list_prop_even], [:collection_with_list_prop_odd, :collection_with_listprop_elem1]],
133   ].each do |prop, op, opr, inc, ex|
134     test "jsonb filter properties.#{prop} #{op} #{opr})" do
135       @controller = Arvados::V1::CollectionsController.new
136       authorize_with :admin
137       get :index, params: {
138             filters: SafeJSON.dump([ ["properties.#{prop}", op, opr] ]),
139             limit: 1000
140           }
141       assert_response :success
142       found = assigns(:objects).collect(&:uuid)
143
144       inc.each do |i|
145         assert_includes(found, collections(i).uuid)
146       end
147
148       ex.each do |e|
149         assert_not_includes(found, collections(e).uuid)
150       end
151     end
152   end
153
154   test "jsonb hash 'exists' and '!=' filter" do
155     @controller = Arvados::V1::CollectionsController.new
156     authorize_with :admin
157     get :index, params: {
158       filters: [ ['properties.prop1', 'exists', true], ['properties.prop1', '!=', 'value1'] ]
159     }
160     assert_response :success
161     found = assigns(:objects).collect(&:uuid)
162     assert_equal found.length, 3
163     assert_not_includes(found, collections(:collection_with_prop1_value1).uuid)
164     assert_includes(found, collections(:collection_with_prop1_value2).uuid)
165     assert_includes(found, collections(:collection_with_prop1_value3).uuid)
166     assert_includes(found, collections(:collection_with_prop1_other1).uuid)
167   end
168
169   test "jsonb array 'exists'" do
170     @controller = Arvados::V1::CollectionsController.new
171     authorize_with :admin
172     get :index, params: {
173       filters: [ ['storage_classes_confirmed.default', 'exists', true] ]
174     }
175     assert_response :success
176     found = assigns(:objects).collect(&:uuid)
177     assert_equal 2, found.length
178     assert_not_includes(found,
179       collections(:storage_classes_desired_default_unconfirmed).uuid)
180     assert_includes(found,
181       collections(:storage_classes_desired_default_confirmed_default).uuid)
182     assert_includes(found,
183       collections(:storage_classes_desired_archive_confirmed_default).uuid)
184   end
185
186   test "jsonb hash alternate form 'exists' and '!=' filter" do
187     @controller = Arvados::V1::CollectionsController.new
188     authorize_with :admin
189     get :index, params: {
190       filters: [ ['properties', 'exists', 'prop1'], ['properties.prop1', '!=', 'value1'] ]
191     }
192     assert_response :success
193     found = assigns(:objects).collect(&:uuid)
194     assert_equal found.length, 3
195     assert_not_includes(found, collections(:collection_with_prop1_value1).uuid)
196     assert_includes(found, collections(:collection_with_prop1_value2).uuid)
197     assert_includes(found, collections(:collection_with_prop1_value3).uuid)
198     assert_includes(found, collections(:collection_with_prop1_other1).uuid)
199   end
200
201   test "jsonb array alternate form 'exists' filter" do
202     @controller = Arvados::V1::CollectionsController.new
203     authorize_with :admin
204     get :index, params: {
205       filters: [ ['storage_classes_confirmed', 'exists', 'default'] ]
206     }
207     assert_response :success
208     found = assigns(:objects).collect(&:uuid)
209     assert_equal 2, found.length
210     assert_not_includes(found,
211       collections(:storage_classes_desired_default_unconfirmed).uuid)
212     assert_includes(found,
213       collections(:storage_classes_desired_default_confirmed_default).uuid)
214     assert_includes(found,
215       collections(:storage_classes_desired_archive_confirmed_default).uuid)
216   end
217
218   test "jsonb 'exists' must be boolean" do
219     @controller = Arvados::V1::CollectionsController.new
220     authorize_with :admin
221     get :index, params: {
222       filters: [ ['properties.prop1', 'exists', nil] ]
223     }
224     assert_response 422
225     assert_match(/Invalid operand '' for 'exists' must be true or false/,
226                  json_response['errors'].join(' '))
227   end
228
229   test "jsonb checks column exists" do
230     @controller = Arvados::V1::CollectionsController.new
231     authorize_with :admin
232     get :index, params: {
233       filters: [ ['puppies.prop1', '=', 'value1'] ]
234     }
235     assert_response 422
236     assert_match(/Invalid attribute 'puppies' for subproperty filter/,
237                  json_response['errors'].join(' '))
238   end
239
240   test "jsonb checks column is valid" do
241     @controller = Arvados::V1::CollectionsController.new
242     authorize_with :admin
243     get :index, params: {
244       filters: [ ['name.prop1', '=', 'value1'] ]
245     }
246     assert_response 422
247     assert_match(/Invalid attribute 'name' for subproperty filter/,
248                  json_response['errors'].join(' '))
249   end
250
251   test "jsonb invalid operator" do
252     @controller = Arvados::V1::CollectionsController.new
253     authorize_with :admin
254     get :index, params: {
255       filters: [ ['properties.prop1', '###', 'value1'] ]
256     }
257     assert_response 422
258     assert_match(/Invalid operator for subproperty search '###'/,
259                  json_response['errors'].join(' '))
260   end
261
262   test "groups contents with properties filter succeeds on objects with properties field" do
263     @controller = Arvados::V1::GroupsController.new
264     authorize_with :admin
265     get :contents, params: {
266       filters: [
267         ['properties', 'exists', 'foo'],
268         ['uuid', 'is_a', ["arvados#group","arvados#collection","arvados#containerRequest"]],
269       ]
270     }
271     assert_response 200
272     assert json_response['items'].length == 0
273   end
274
275   # Tests bug #19297
276   test "groups contents with properties filter succeeds on some objects with properties field" do
277     @controller = Arvados::V1::GroupsController.new
278     authorize_with :admin
279     get :contents, params: {
280       filters: [
281         ['properties', 'exists', 'foo'],
282         ['uuid', 'is_a', ["arvados#group","arvados#workflow"]],
283       ]
284     }
285     assert_response 200
286     assert json_response['items'].length == 0
287   end
288
289   # Tests bug #19297
290   test "groups contents with properties filter fails on objects without properties field" do
291     @controller = Arvados::V1::GroupsController.new
292     authorize_with :admin
293     get :contents, params: {
294       filters: [
295         ['properties', 'exists', 'foo'],
296         ['uuid', 'is_a', ["arvados#workflow"]],
297       ]
298     }
299     assert_response 422
300     assert_match(/Invalid attribute 'properties' for operator 'exists'.*on object type Workflow/, json_response['errors'].join(' '))
301   end
302
303   test "groups contents without filters and limit=0, count=none" do
304     @controller = Arvados::V1::GroupsController.new
305     authorize_with :admin
306     get :contents, params: {
307       limit: 0,
308       count: 'none',
309     }
310     assert_response 200
311     assert json_response['items'].length == 0
312   end
313
314   test "replication_desired = 2" do
315     @controller = Arvados::V1::CollectionsController.new
316     authorize_with :admin
317     get :index, params: {
318       filters: SafeJSON.dump([ ['replication_desired', '=', 2] ])
319     }
320     assert_response :success
321     found = assigns(:objects).collect(&:uuid)
322     assert_includes(found, collections(:replication_desired_2_unconfirmed).uuid)
323     assert_includes(found, collections(:replication_desired_2_confirmed_2).uuid)
324   end
325
326   [
327     [1, "foo"],
328     [1, ["foo"]],
329     [1, ["bar"]],
330     [1, ["bar", "foo"]],
331     [0, ["foo", "qux"]],
332     [0, ["qux"]],
333     [nil, []],
334     [nil, [[]]],
335     [nil, [["bogus"]]],
336     [nil, [{"foo" => "bar"}]],
337     [nil, {"foo" => "bar"}],
338   ].each do |results, operand|
339     test "storage_classes_desired contains #{operand.inspect}" do
340       @controller = Arvados::V1::CollectionsController.new
341       authorize_with(:active)
342       c = Collection.create!(
343         manifest_text: "",
344         storage_classes_desired: ["foo", "bar", "baz"])
345       get :index, params: {
346             filters: [["storage_classes_desired", "contains", operand]],
347           }
348       if results.nil?
349         assert_response 422
350         next
351       end
352       assert_response :success
353       assert_equal results, json_response["items"].length
354       if results > 0
355         assert_equal c.uuid, json_response["items"][0]["uuid"]
356       end
357     end
358   end
359
360   test "collections properties contains top level key" do
361     @controller = Arvados::V1::CollectionsController.new
362     authorize_with(:active)
363     get :index, params: {
364           filters: [["properties", "contains", "prop1"]],
365         }
366     assert_response :success
367     assert_not_empty json_response["items"]
368     json_response["items"].each do |c|
369       assert c["properties"].has_key?("prop1")
370     end
371   end
372 end