Merge branch '8016-crunchrun-crunchstat'
[arvados.git] / services / api / test / functional / arvados / v1 / groups_controller_test.rb
1 require 'test_helper'
2
3 class Arvados::V1::GroupsControllerTest < ActionController::TestCase
4
5   test "attempt to delete group without read or write access" do
6     authorize_with :active
7     post :destroy, id: groups(:empty_lonely_group).uuid
8     assert_response 404
9   end
10
11   test "attempt to delete group without write access" do
12     authorize_with :active
13     post :destroy, id: groups(:all_users).uuid
14     assert_response 403
15   end
16
17   test "get list of projects" do
18     authorize_with :active
19     get :index, filters: [['group_class', '=', 'project']], format: :json
20     assert_response :success
21     group_uuids = []
22     json_response['items'].each do |group|
23       assert_equal 'project', group['group_class']
24       group_uuids << group['uuid']
25     end
26     assert_includes group_uuids, groups(:aproject).uuid
27     assert_includes group_uuids, groups(:asubproject).uuid
28     assert_not_includes group_uuids, groups(:system_group).uuid
29     assert_not_includes group_uuids, groups(:private).uuid
30   end
31
32   test "get list of groups that are not projects" do
33     authorize_with :active
34     get :index, filters: [['group_class', '!=', 'project']], format: :json
35     assert_response :success
36     group_uuids = []
37     json_response['items'].each do |group|
38       assert_not_equal 'project', group['group_class']
39       group_uuids << group['uuid']
40     end
41     assert_not_includes group_uuids, groups(:aproject).uuid
42     assert_not_includes group_uuids, groups(:asubproject).uuid
43     assert_includes group_uuids, groups(:private).uuid
44     assert_includes group_uuids, groups(:group_with_no_class).uuid
45   end
46
47   test "get list of groups with bogus group_class" do
48     authorize_with :active
49     get :index, {
50       filters: [['group_class', '=', 'nogrouphasthislittleclass']],
51       format: :json,
52     }
53     assert_response :success
54     assert_equal [], json_response['items']
55     assert_equal 0, json_response['items_available']
56   end
57
58   def check_project_contents_response
59     assert_response :success
60     assert_operator 2, :<=, json_response['items_available']
61     assert_operator 2, :<=, json_response['items'].count
62     kinds = json_response['items'].collect { |i| i['kind'] }.uniq
63     expect_kinds = %w'arvados#group arvados#specimen arvados#pipelineTemplate arvados#job'
64     assert_equal expect_kinds, (expect_kinds & kinds)
65
66     json_response['items'].each do |i|
67       if i['kind'] == 'arvados#group'
68         assert(i['group_class'] == 'project',
69                "group#contents returned a non-project group")
70       end
71     end
72   end
73
74   test 'get group-owned objects' do
75     authorize_with :active
76     get :contents, {
77       id: groups(:aproject).uuid,
78       format: :json,
79     }
80     check_project_contents_response
81   end
82
83   test "user with project read permission can see project objects" do
84     authorize_with :project_viewer
85     get :contents, {
86       id: groups(:aproject).uuid,
87       format: :json,
88     }
89     check_project_contents_response
90   end
91
92   test "list objects across projects" do
93     authorize_with :project_viewer
94     get :contents, {
95       format: :json,
96       filters: [['uuid', 'is_a', 'arvados#specimen']]
97     }
98     assert_response :success
99     found_uuids = json_response['items'].collect { |i| i['uuid'] }
100     [[:in_aproject, true],
101      [:in_asubproject, true],
102      [:owned_by_private_group, false]].each do |specimen_fixture, should_find|
103       if should_find
104         assert_includes found_uuids, specimens(specimen_fixture).uuid, "did not find specimen fixture '#{specimen_fixture}'"
105       else
106         refute_includes found_uuids, specimens(specimen_fixture).uuid, "found specimen fixture '#{specimen_fixture}'"
107       end
108     end
109   end
110
111   test "list objects in home project" do
112     authorize_with :active
113     get :contents, {
114       format: :json,
115       id: users(:active).uuid
116     }
117     assert_response :success
118     found_uuids = json_response['items'].collect { |i| i['uuid'] }
119     assert_includes found_uuids, specimens(:owned_by_active_user).uuid, "specimen did not appear in home project"
120     refute_includes found_uuids, specimens(:in_asubproject).uuid, "specimen appeared unexpectedly in home project"
121   end
122
123   test "user with project read permission can see project collections" do
124     authorize_with :project_viewer
125     get :contents, {
126       id: groups(:asubproject).uuid,
127       format: :json,
128     }
129     ids = json_response['items'].map { |item| item["uuid"] }
130     assert_includes ids, collections(:baz_file_in_asubproject).uuid
131   end
132
133   [['asc', :<=],
134    ['desc', :>=]].each do |order, operator|
135     test "user with project read permission can sort project collections #{order}" do
136       authorize_with :project_viewer
137       get :contents, {
138         id: groups(:asubproject).uuid,
139         format: :json,
140         filters: [['uuid', 'is_a', "arvados#collection"]],
141         order: "collections.name #{order}"
142       }
143       sorted_names = json_response['items'].collect { |item| item["name"] }
144       # Here we avoid assuming too much about the database
145       # collation. Both "alice"<"Bob" and "alice">"Bob" can be
146       # correct. Hopefully it _is_ safe to assume that if "a" comes
147       # before "b" in the ascii alphabet, "aX">"bY" is never true for
148       # any strings X and Y.
149       reliably_sortable_names = sorted_names.select do |name|
150         name[0] >= 'a' and name[0] <= 'z'
151       end.uniq do |name|
152         name[0]
153       end
154       # Preserve order of sorted_names. But do not use &=. If
155       # sorted_names has out-of-order duplicates, we want to preserve
156       # them here, so we can detect them and fail the test below.
157       sorted_names.select! do |name|
158         reliably_sortable_names.include? name
159       end
160       actually_checked_anything = false
161       previous = nil
162       sorted_names.each do |entry|
163         if previous
164           assert_operator(previous, operator, entry,
165                           "Entries sorted incorrectly.")
166           actually_checked_anything = true
167         end
168         previous = entry
169       end
170       assert actually_checked_anything, "Didn't even find two names to compare."
171     end
172   end
173
174   test 'list objects across multiple projects' do
175     authorize_with :project_viewer
176     get :contents, {
177       format: :json,
178       filters: [['uuid', 'is_a', 'arvados#specimen']]
179     }
180     assert_response :success
181     found_uuids = json_response['items'].collect { |i| i['uuid'] }
182     [[:in_aproject, true],
183      [:in_asubproject, true],
184      [:owned_by_private_group, false]].each do |specimen_fixture, should_find|
185       if should_find
186         assert_includes found_uuids, specimens(specimen_fixture).uuid, "did not find specimen fixture '#{specimen_fixture}'"
187       else
188         refute_includes found_uuids, specimens(specimen_fixture).uuid, "found specimen fixture '#{specimen_fixture}'"
189       end
190     end
191   end
192
193   # Even though the project_viewer tests go through other controllers,
194   # I'm putting them here so they're easy to find alongside the other
195   # project tests.
196   def check_new_project_link_fails(link_attrs)
197     @controller = Arvados::V1::LinksController.new
198     post :create, link: {
199       link_class: "permission",
200       name: "can_read",
201       head_uuid: groups(:aproject).uuid,
202     }.merge(link_attrs)
203     assert_includes(403..422, response.status)
204   end
205
206   test "user with project read permission can't add users to it" do
207     authorize_with :project_viewer
208     check_new_project_link_fails(tail_uuid: users(:spectator).uuid)
209   end
210
211   test "user with project read permission can't add items to it" do
212     authorize_with :project_viewer
213     check_new_project_link_fails(tail_uuid: collections(:baz_file).uuid)
214   end
215
216   test "user with project read permission can't rename items in it" do
217     authorize_with :project_viewer
218     @controller = Arvados::V1::LinksController.new
219     post :update, {
220       id: jobs(:running).uuid,
221       name: "Denied test name",
222     }
223     assert_includes(403..404, response.status)
224   end
225
226   test "user with project read permission can't remove items from it" do
227     @controller = Arvados::V1::PipelineTemplatesController.new
228     authorize_with :project_viewer
229     post :update, {
230       id: pipeline_templates(:two_part).uuid,
231       pipeline_template: {
232         owner_uuid: users(:project_viewer).uuid,
233       }
234     }
235     assert_response 403
236   end
237
238   test "user with project read permission can't delete it" do
239     authorize_with :project_viewer
240     post :destroy, {id: groups(:aproject).uuid}
241     assert_response 403
242   end
243
244   test 'get group-owned objects with limit' do
245     authorize_with :active
246     get :contents, {
247       id: groups(:aproject).uuid,
248       limit: 1,
249       format: :json,
250     }
251     assert_response :success
252     assert_operator 1, :<, json_response['items_available']
253     assert_equal 1, json_response['items'].count
254   end
255
256   test 'get group-owned objects with limit and offset' do
257     authorize_with :active
258     get :contents, {
259       id: groups(:aproject).uuid,
260       limit: 1,
261       offset: 12345,
262       format: :json,
263     }
264     assert_response :success
265     assert_operator 1, :<, json_response['items_available']
266     assert_equal 0, json_response['items'].count
267   end
268
269   test 'get group-owned objects with additional filter matching nothing' do
270     authorize_with :active
271     get :contents, {
272       id: groups(:aproject).uuid,
273       filters: [['uuid', 'in', ['foo_not_a_uuid','bar_not_a_uuid']]],
274       format: :json,
275     }
276     assert_response :success
277     assert_equal [], json_response['items']
278     assert_equal 0, json_response['items_available']
279   end
280
281   %w(offset limit).each do |arg|
282     ['foo', '', '1234five', '0x10', '-8'].each do |val|
283       test "Raise error on bogus #{arg} parameter #{val.inspect}" do
284         authorize_with :active
285         get :contents, {
286           :id => groups(:aproject).uuid,
287           :format => :json,
288           arg => val,
289         }
290         assert_response 422
291       end
292     end
293   end
294
295   test "Collection contents don't include manifest_text" do
296     authorize_with :active
297     get :contents, {
298       id: groups(:aproject).uuid,
299       filters: [["uuid", "is_a", "arvados#collection"]],
300       format: :json,
301     }
302     assert_response :success
303     refute(json_response["items"].any? { |c| not c["portable_data_hash"] },
304            "response included an item without a portable data hash")
305     refute(json_response["items"].any? { |c| c.include?("manifest_text") },
306            "response included an item with a manifest text")
307   end
308
309   test 'get writable_by list for owned group' do
310     authorize_with :active
311     get :show, {
312       id: groups(:aproject).uuid,
313       format: :json
314     }
315     assert_response :success
316     assert_not_nil(json_response['writable_by'],
317                    "Should receive uuid list in 'writable_by' field")
318     assert_includes(json_response['writable_by'], users(:active).uuid,
319                     "owner should be included in writable_by list")
320   end
321
322   test 'no writable_by list for group with read-only access' do
323     authorize_with :rominiadmin
324     get :show, {
325       id: groups(:testusergroup_admins).uuid,
326       format: :json
327     }
328     assert_response :success
329     assert_equal([json_response['owner_uuid']],
330                  json_response['writable_by'],
331                  "Should only see owner_uuid in 'writable_by' field")
332   end
333
334   test 'get writable_by list by admin user' do
335     authorize_with :admin
336     get :show, {
337       id: groups(:testusergroup_admins).uuid,
338       format: :json
339     }
340     assert_response :success
341     assert_not_nil(json_response['writable_by'],
342                    "Should receive uuid list in 'writable_by' field")
343     assert_includes(json_response['writable_by'],
344                     users(:admin).uuid,
345                     "Current user should be included in 'writable_by' field")
346   end
347
348   test 'creating subproject with duplicate name fails' do
349     authorize_with :active
350     post :create, {
351       group: {
352         name: 'A Project',
353         owner_uuid: users(:active).uuid,
354         group_class: 'project',
355       },
356     }
357     assert_response 422
358     response_errors = json_response['errors']
359     assert_not_nil response_errors, 'Expected error in response'
360     assert(response_errors.first.include?('duplicate key'),
361            "Expected 'duplicate key' error in #{response_errors.first}")
362   end
363
364   test 'creating duplicate named subproject succeeds with ensure_unique_name' do
365     authorize_with :active
366     post :create, {
367       group: {
368         name: 'A Project',
369         owner_uuid: users(:active).uuid,
370         group_class: 'project',
371       },
372       ensure_unique_name: true
373     }
374     assert_response :success
375     new_project = json_response
376     assert_not_equal(new_project['uuid'],
377                      groups(:aproject).uuid,
378                      "create returned same uuid as existing project")
379     assert_equal(new_project['name'],
380                  'A Project (2)',
381                  "new project name '#{new_project['name']}' was expected to be 'A Project (2)'")
382   end
383
384   test "unsharing a project results in hiding it from previously shared user" do
385     # remove sharing link for project
386     @controller = Arvados::V1::LinksController.new
387     authorize_with :admin
388     post :destroy, id: links(:share_starred_project_with_project_viewer).uuid
389     assert_response :success
390
391     # verify that the user can no longer see the project
392     @test_counter = 0  # Reset executed action counter
393     @controller = Arvados::V1::GroupsController.new
394     authorize_with :project_viewer
395     get :index, filters: [['group_class', '=', 'project']], format: :json
396     assert_response :success
397     found_projects = {}
398     json_response['items'].each do |g|
399       found_projects[g['uuid']] = g
400     end
401     assert_equal false, found_projects.include?(groups(:starred_and_shared_active_user_project).uuid)
402
403     # share the project
404     @test_counter = 0
405     @controller = Arvados::V1::LinksController.new
406     authorize_with :system_user
407     post :create, link: {
408       link_class: "permission",
409       name: "can_read",
410       head_uuid: groups(:starred_and_shared_active_user_project).uuid,
411       tail_uuid: users(:project_viewer).uuid,
412     }
413
414     # verify that project_viewer user can now see shared project again
415     @test_counter = 0
416     @controller = Arvados::V1::GroupsController.new
417     authorize_with :project_viewer
418     get :index, filters: [['group_class', '=', 'project']], format: :json
419     assert_response :success
420     found_projects = {}
421     json_response['items'].each do |g|
422       found_projects[g['uuid']] = g
423     end
424     assert_equal true, found_projects.include?(groups(:starred_and_shared_active_user_project).uuid)
425   end
426
427   [
428     [['owner_uuid', '!=', 'zzzzz-tpzed-xurymjxw79nv3jz'], 200,
429         'zzzzz-d1hrv-subprojpipeline', 'zzzzz-d1hrv-1xfj6xkicf2muk2'],
430     [["pipeline_instances.state", "not in", ["Complete", "Failed"]], 200,
431         'zzzzz-d1hrv-1xfj6xkicf2muk2', 'zzzzz-d1hrv-i3e77t9z5y8j9cc'],
432     [['container_requests.requesting_container_uuid', '=', nil], 200,
433         'zzzzz-xvhdp-cr4queuedcontnr', 'zzzzz-xvhdp-cr4requestercn2'],
434     [['container_requests.no_such_column', '=', nil], 422],
435     [['container_requests.', '=', nil], 422],
436     [['.requesting_container_uuid', '=', nil], 422],
437     [['no_such_table.uuid', '!=', 'zzzzz-tpzed-xurymjxw79nv3jz'], 422],
438   ].each do |filter, expect_code, expect_uuid, not_expect_uuid|
439     test "get contents with '#{filter}' filter" do
440       authorize_with :active
441       get :contents, filters: [filter], format: :json
442       assert_response expect_code
443       if expect_code == 200
444         assert_not_empty json_response['items']
445         item_uuids = json_response['items'].collect {|item| item['uuid']}
446         assert_includes(item_uuids, expect_uuid)
447         assert_not_includes(item_uuids, not_expect_uuid)
448       end
449     end
450   end
451 end