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