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