Merge branch '8784-dir-listings'
[arvados.git] / apps / workbench / test / controllers / projects_controller_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 require 'helpers/share_object_helper'
7
8 class ProjectsControllerTest < ActionController::TestCase
9   include ShareObjectHelper
10
11   test "invited user is asked to sign user agreements on front page" do
12     get :index, {}, session_for(:inactive)
13     assert_response :redirect
14     assert_match(/^#{Regexp.escape(user_agreements_url)}\b/,
15                  @response.redirect_url,
16                  "Inactive user was not redirected to user_agreements page")
17   end
18
19   test "uninvited user is asked to wait for activation" do
20     get :index, {}, session_for(:inactive_uninvited)
21     assert_response :redirect
22     assert_match(/^#{Regexp.escape(inactive_users_url)}\b/,
23                  @response.redirect_url,
24                  "Uninvited user was not redirected to inactive user page")
25   end
26
27   [[:active, true],
28    [:project_viewer, false]].each do |which_user, should_show|
29     test "create subproject button #{'not ' unless should_show} shown to #{which_user}" do
30       readonly_project_uuid = api_fixture('groups')['aproject']['uuid']
31       get :show, {
32         id: readonly_project_uuid
33       }, session_for(which_user)
34       buttons = css_select('[data-method=post]').select do |el|
35         el.attributes['data-remote-href'].match /project.*owner_uuid.*#{readonly_project_uuid}/
36       end
37       if should_show
38         assert_not_empty(buttons, "did not offer to create a subproject")
39       else
40         assert_empty(buttons.collect(&:to_s),
41                      "offered to create a subproject in a non-writable project")
42       end
43     end
44   end
45
46   test "sharing a project with a user and group" do
47     uuid_list = [api_fixture("groups")["future_project_viewing_group"]["uuid"],
48                  api_fixture("users")["future_project_user"]["uuid"]]
49     post(:share_with, {
50            id: api_fixture("groups")["asubproject"]["uuid"],
51            uuids: uuid_list,
52            format: "json"},
53          session_for(:active))
54     assert_response :success
55     assert_equal(uuid_list, json_response["success"])
56   end
57
58   test "user with project read permission can't add permissions" do
59     share_uuid = api_fixture("users")["spectator"]["uuid"]
60     post(:share_with, {
61            id: api_fixture("groups")["aproject"]["uuid"],
62            uuids: [share_uuid],
63            format: "json"},
64          session_for(:project_viewer))
65     assert_response 422
66     assert(json_response["errors"].andand.
67              any? { |msg| msg.start_with?("#{share_uuid}: ") },
68            "JSON response missing properly formatted sharing error")
69   end
70
71   test "admin can_manage aproject" do
72     assert user_can_manage(:admin, api_fixture("groups")["aproject"])
73   end
74
75   test "owner can_manage aproject" do
76     assert user_can_manage(:active, api_fixture("groups")["aproject"])
77   end
78
79   test "owner can_manage asubproject" do
80     assert user_can_manage(:active, api_fixture("groups")["asubproject"])
81   end
82
83   test "viewer can't manage aproject" do
84     refute user_can_manage(:project_viewer, api_fixture("groups")["aproject"])
85   end
86
87   test "viewer can't manage asubproject" do
88     refute user_can_manage(:project_viewer, api_fixture("groups")["asubproject"])
89   end
90
91   test "subproject_admin can_manage asubproject" do
92     assert user_can_manage(:subproject_admin, api_fixture("groups")["asubproject"])
93   end
94
95   test "detect ownership loop in project breadcrumbs" do
96     # This test has an arbitrary time limit -- otherwise we'd just sit
97     # here forever instead of reporting that the loop was not
98     # detected. The test passes quickly, but fails slowly.
99     Timeout::timeout 10 do
100       get(:show,
101           { id: api_fixture("groups")["project_owns_itself"]["uuid"] },
102           session_for(:admin))
103     end
104     assert_response :success
105   end
106
107   test "project admin can remove collections from the project" do
108     # Deleting an object that supports 'trash_at' should make it
109     # completely inaccessible to API queries, not simply moved out of
110     # the project.
111     coll_key = "collection_to_remove_from_subproject"
112     coll_uuid = api_fixture("collections")[coll_key]["uuid"]
113     delete(:remove_item,
114            { id: api_fixture("groups")["asubproject"]["uuid"],
115              item_uuid: coll_uuid,
116              format: "js" },
117            session_for(:subproject_admin))
118     assert_response :success
119     assert_match(/\b#{coll_uuid}\b/, @response.body,
120                  "removed object not named in response")
121
122     use_token :subproject_admin
123     assert_raise ArvadosApiClient::NotFoundException do
124       Collection.find(coll_uuid, cache: false)
125     end
126   end
127
128   test "project admin can remove items from project other than collections" do
129     # An object which does not have an trash_at field (e.g. Specimen)
130     # should be implicitly moved to the user's Home project when removed.
131     specimen_uuid = api_fixture('specimens', 'in_asubproject')['uuid']
132     delete(:remove_item,
133            { id: api_fixture('groups', 'asubproject')['uuid'],
134              item_uuid: specimen_uuid,
135              format: 'js' },
136            session_for(:subproject_admin))
137     assert_response :success
138     assert_match(/\b#{specimen_uuid}\b/, @response.body,
139                  "removed object not named in response")
140
141     use_token :subproject_admin
142     new_specimen = Specimen.find(specimen_uuid)
143     assert_equal api_fixture('users', 'subproject_admin')['uuid'], new_specimen.owner_uuid
144   end
145
146   # An object which does not offer an expired_at field but has a xx_owner_uuid_name_unique constraint
147   # will be renamed when removed and another object with the same name exists in user's home project.
148   [
149     ['groups', 'subproject_in_asubproject_with_same_name_as_one_in_active_user_home'],
150     ['pipeline_templates', 'template_in_asubproject_with_same_name_as_one_in_active_user_home'],
151   ].each do |dm, fixture|
152     test "removing #{dm} from a subproject results in renaming it when there is another such object with same name in home project" do
153       object = api_fixture(dm, fixture)
154       delete(:remove_item,
155              { id: api_fixture('groups', 'asubproject')['uuid'],
156                item_uuid: object['uuid'],
157                format: 'js' },
158              session_for(:active))
159       assert_response :success
160       assert_match(/\b#{object['uuid']}\b/, @response.body,
161                    "removed object not named in response")
162       use_token :active
163       if dm.eql?('groups')
164         found = Group.find(object['uuid'])
165       else
166         found = PipelineTemplate.find(object['uuid'])
167       end
168       assert_equal api_fixture('users', 'active')['uuid'], found.owner_uuid
169       assert_equal true, found.name.include?(object['name'] + ' removed from ')
170     end
171   end
172
173   test 'projects#show tab infinite scroll partial obeys limit' do
174     get_contents_rows(limit: 1, filters: [['uuid','is_a',['arvados#job']]])
175     assert_response :success
176     assert_equal(1, json_response['content'].scan('<tr').count,
177                  "Did not get exactly one row")
178   end
179
180   ['', ' asc', ' desc'].each do |direction|
181     test "projects#show tab partial orders correctly by #{direction}" do
182       _test_tab_content_order direction
183     end
184   end
185
186   def _test_tab_content_order direction
187     get_contents_rows(limit: 100,
188                       order: "created_at#{direction}",
189                       filters: [['uuid','is_a',['arvados#job',
190                                                 'arvados#pipelineInstance']]])
191     assert_response :success
192     not_grouped_by_kind = nil
193     last_timestamp = nil
194     last_kind = nil
195     found_kind = {}
196     json_response['content'].scan /<tr[^>]+>/ do |tr_tag|
197       found_timestamps = 0
198       tr_tag.scan(/\ data-object-created-at=\"(.*?)\"/).each do |t,|
199         if last_timestamp
200           correct_operator = / desc$/ =~ direction ? :>= : :<=
201           assert_operator(last_timestamp, correct_operator, t,
202                           "Rows are not sorted by created_at#{direction}")
203         end
204         last_timestamp = t
205         found_timestamps += 1
206       end
207       assert_equal(1, found_timestamps,
208                    "Content row did not have exactly one timestamp")
209
210       # Confirm that the test for timestamp ordering couldn't have
211       # passed merely because the test fixtures have convenient
212       # timestamps (e.g., there is only one pipeline and one job in
213       # the project being tested, or there are no pipelines at all in
214       # the project being tested):
215       tr_tag.scan /\ data-kind=\"(.*?)\"/ do |kind|
216         if last_kind and last_kind != kind and found_kind[kind]
217           # We saw this kind before, then a different kind, then
218           # this kind again. That means objects are not grouped by
219           # kind.
220           not_grouped_by_kind = true
221         end
222         found_kind[kind] ||= 0
223         found_kind[kind] += 1
224         last_kind = kind
225       end
226     end
227     assert_equal(true, not_grouped_by_kind,
228                  "Could not confirm that results are not grouped by kind")
229   end
230
231   def get_contents_rows params
232     params = {
233       id: api_fixture('users')['active']['uuid'],
234       partial: :contents_rows,
235       format: :json,
236     }.merge(params)
237     encoded_params = Hash[params.map { |k,v|
238                             [k, (v.is_a?(Array) || v.is_a?(Hash)) ? v.to_json : v]
239                           }]
240     get :show, encoded_params, session_for(:active)
241   end
242
243   test "visit non-public project as anonymous when anonymous browsing is enabled and expect page not found" do
244     Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
245     get(:show, {id: api_fixture('groups')['aproject']['uuid']})
246     assert_response 404
247     assert_match(/log ?in/i, @response.body)
248   end
249
250   test "visit home page as anonymous when anonymous browsing is enabled and expect login" do
251     Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
252     get(:index)
253     assert_response :redirect
254     assert_match /\/users\/welcome/, @response.redirect_url
255   end
256
257   [
258     nil,
259     :active,
260   ].each do |user|
261     test "visit public projects page when anon config is enabled, as user #{user}, and expect page" do
262       Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
263
264       if user
265         get :public, {}, session_for(user)
266       else
267         get :public
268       end
269
270       assert_response :success
271       assert_not_nil assigns(:objects)
272       project_names = assigns(:objects).collect(&:name)
273       assert_includes project_names, 'Unrestricted public data'
274       assert_not_includes project_names, 'A Project'
275       refute_empty css_select('[href="/projects/public"]')
276     end
277   end
278
279   test "visit public projects page when anon config is not enabled as active user and expect 404" do
280     get :public, {}, session_for(:active)
281     assert_response 404
282   end
283
284   test "visit public projects page when anon config is enabled but public projects page is disabled as active user and expect 404" do
285     Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
286     Rails.configuration.enable_public_projects_page = false
287     get :public, {}, session_for(:active)
288     assert_response 404
289   end
290
291   test "visit public projects page when anon config is not enabled as anonymous and expect login page" do
292     get :public
293     assert_response :redirect
294     assert_match /\/users\/welcome/, @response.redirect_url
295     assert_empty css_select('[href="/projects/public"]')
296   end
297
298   test "visit public projects page when anon config is enabled and public projects page is disabled and expect login page" do
299     Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
300     Rails.configuration.enable_public_projects_page = false
301     get :index
302     assert_response :redirect
303     assert_match /\/users\/welcome/, @response.redirect_url
304     assert_empty css_select('[href="/projects/public"]')
305   end
306
307   test "visit public projects page when anon config is not enabled and public projects page is enabled and expect login page" do
308     Rails.configuration.enable_public_projects_page = true
309     get :index
310     assert_response :redirect
311     assert_match /\/users\/welcome/, @response.redirect_url
312     assert_empty css_select('[href="/projects/public"]')
313   end
314
315   test "find a project and edit its description" do
316     project = api_fixture('groups')['aproject']
317     use_token :active
318     found = Group.find(project['uuid'])
319     found.description = 'test description update'
320     found.save!
321     get(:show, {id: project['uuid']}, session_for(:active))
322     assert_includes @response.body, 'test description update'
323   end
324
325   test "find a project and edit description to textile description" do
326     project = api_fixture('groups')['aproject']
327     use_token :active
328     found = Group.find(project['uuid'])
329     found.description = '*test bold description for textile formatting*'
330     found.save!
331     get(:show, {id: project['uuid']}, session_for(:active))
332     assert_includes @response.body, '<strong>test bold description for textile formatting</strong>'
333   end
334
335   test "find a project and edit description to html description" do
336     project = api_fixture('groups')['aproject']
337     use_token :active
338     found = Group.find(project['uuid'])
339     found.description = 'Textile description with link to home page <a href="/">take me home</a>.'
340     found.save!
341     get(:show, {id: project['uuid']}, session_for(:active))
342     assert_includes @response.body, 'Textile description with link to home page <a href="/">take me home</a>.'
343   end
344
345   test "find a project and edit description to textile description with link to object" do
346     project = api_fixture('groups')['aproject']
347     use_token :active
348     found = Group.find(project['uuid'])
349
350     # uses 'Link to object' as a hyperlink for the object
351     found.description = '"Link to object":' + api_fixture('groups')['asubproject']['uuid']
352     found.save!
353     get(:show, {id: project['uuid']}, session_for(:active))
354
355     # check that input was converted to textile, not staying as inputted
356     refute_includes  @response.body,'"Link to object"'
357     refute_empty css_select('[href="/groups/zzzzz-j7d0g-axqo7eu9pwvna1x"]')
358   end
359
360   test "project viewer can't see project sharing tab" do
361     project = api_fixture('groups')['aproject']
362     get(:show, {id: project['uuid']}, session_for(:project_viewer))
363     refute_includes @response.body, '<div id="Sharing"'
364     assert_includes @response.body, '<div id="Data_collections"'
365   end
366
367   [
368     'admin',
369     'active',
370   ].each do |username|
371     test "#{username} can see project sharing tab" do
372      project = api_fixture('groups')['aproject']
373      get(:show, {id: project['uuid']}, session_for(username))
374      assert_includes @response.body, '<div id="Sharing"'
375      assert_includes @response.body, '<div id="Data_collections"'
376     end
377   end
378
379   [
380     ['admin',true],
381     ['active',true],
382     ['project_viewer',false],
383   ].each do |user, can_move|
384     test "#{user} can move subproject from project #{can_move}" do
385       get(:show, {id: api_fixture('groups')['aproject']['uuid']}, session_for(user))
386       if can_move
387         assert_includes @response.body, 'Move project...'
388       else
389         refute_includes @response.body, 'Move project...'
390       end
391     end
392   end
393
394   [
395     [:admin, true],
396     [:active, false],
397   ].each do |user, expect_all_nodes|
398     test "in dashboard other index page links as #{user}" do
399       get :index, {}, session_for(user)
400
401       [["processes", "/all_processes"],
402        ["collections", "/collections"],
403       ].each do |target, path|
404         assert_includes @response.body, "href=\"#{path}\""
405         assert_includes @response.body, "All #{target}"
406       end
407
408       if expect_all_nodes
409         assert_includes @response.body, "href=\"/nodes\""
410         assert_includes @response.body, "All nodes"
411       else
412         assert_not_includes @response.body, "href=\"/nodes\""
413         assert_not_includes @response.body, "All nodes"
414       end
415     end
416   end
417
418   test "dashboard should show the correct status for processes" do
419     get :index, {}, session_for(:active)
420     assert_select 'div.panel-body.recent-processes' do
421       [
422         {
423           fixture: 'container_requests',
424           state: 'completed',
425           selectors: [['div.progress', false],
426                       ['span.label.label-success', true, 'Complete']]
427         },
428         {
429           fixture: 'container_requests',
430           state: 'uncommitted',
431           selectors: [['div.progress', false],
432                       ['span.label.label-default', true, 'Uncommitted']]
433         },
434         {
435           fixture: 'container_requests',
436           state: 'queued',
437           selectors: [['div.progress', false],
438                       ['span.label.label-default', true, 'Queued']]
439         },
440         {
441           fixture: 'container_requests',
442           state: 'running',
443           selectors: [['div.progress', true]]
444         },
445         {
446           fixture: 'pipeline_instances',
447           state: 'new_pipeline',
448           selectors: [['div.progress', false],
449                       ['span.label.label-default', true, 'Not started']]
450         },
451         {
452           fixture: 'pipeline_instances',
453           state: 'pipeline_in_running_state',
454           selectors: [['div.progress', true]]
455         },
456       ].each do |c|
457         uuid = api_fixture(c[:fixture])[c[:state]]['uuid']
458         assert_select "div.dashboard-panel-info-row.row-#{uuid}" do
459           if c.include? :selectors
460             c[:selectors].each do |selector, should_show, label|
461               assert_select selector, should_show, "UUID #{uuid} should #{should_show ? '' : 'not'} show '#{selector}'"
462               if should_show and not label.nil?
463                 assert_select selector, label, "UUID #{uuid} state label should show #{label}"
464               end
465             end
466           end
467         end
468       end
469     end
470   end
471
472   test "visit a public project and verify the public projects page link exists" do
473     Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
474     uuid = api_fixture('groups')['anonymously_accessible_project']['uuid']
475     get :show, {id: uuid}
476     project = assigns(:object)
477     assert_equal uuid, project['uuid']
478     refute_empty css_select("[href=\"/projects/#{project['uuid']}\"]")
479     assert_includes @response.body, "<a href=\"/projects/public\">Public Projects</a>"
480   end
481
482   test 'all_projects unaffected by params after use by ProjectsController (#6640)' do
483     @controller = ProjectsController.new
484     project_uuid = api_fixture('groups')['aproject']['uuid']
485     get :index, {
486       filters: [['uuid', '<', project_uuid]].to_json,
487       limit: 0,
488       offset: 1000,
489     }, session_for(:active)
490     assert_select "#projects-menu + ul li.divider ~ li a[href=/projects/#{project_uuid}]"
491   end
492
493   [
494     ["active", 5, ["aproject", "asubproject"], "anonymously_accessible_project"],
495     ["user1_with_load", 2, ["project_with_10_collections"], "project_with_2_pipelines_and_60_crs"],
496     ["admin", 5, ["anonymously_accessible_project", "subproject_in_anonymous_accessible_project"], "aproject"],
497   ].each do |user, page_size, tree_segment, unexpected|
498     # Note: this test is sensitive to database collation. It passes
499     # with en_US.UTF-8.
500     test "build my projects tree for #{user} user and verify #{unexpected} is omitted" do
501       use_token user
502
503       tree, _, _ = @controller.send(:my_wanted_projects_tree,
504                                     User.current,
505                                     page_size)
506
507       tree_segment_at_depth_1 = api_fixture('groups')[tree_segment[0]]
508       tree_segment_at_depth_2 = api_fixture('groups')[tree_segment[1]] if tree_segment[1]
509
510       node_depth = {}
511       tree.each do |x|
512         node_depth[x[:object]['uuid']] = x[:depth]
513       end
514
515       assert_equal(1, node_depth[tree_segment_at_depth_1['uuid']])
516       assert_equal(2, node_depth[tree_segment_at_depth_2['uuid']]) if tree_segment[1]
517
518       unexpected_project = api_fixture('groups')[unexpected]
519       assert_nil(node_depth[unexpected_project['uuid']], node_depth.inspect)
520     end
521   end
522
523   [
524     ["active", 1],
525     ["project_viewer", 1],
526     ["admin", 0],
527   ].each do |user, size|
528     test "starred projects for #{user}" do
529       use_token user
530       ctrl = ProjectsController.new
531       current_user = User.find(api_fixture('users')[user]['uuid'])
532       my_starred_project = ctrl.send :my_starred_projects, current_user
533       assert_equal(size, my_starred_project.andand.size)
534
535       ctrl2 = ProjectsController.new
536       current_user = User.find(api_fixture('users')[user]['uuid'])
537       my_starred_project = ctrl2.send :my_starred_projects, current_user
538       assert_equal(size, my_starred_project.andand.size)
539     end
540   end
541
542   test "unshare project and verify that it is no longer included in shared user's starred projects" do
543     # remove sharing link
544     use_token :system_user
545     Link.find(api_fixture('links')['share_starred_project_with_project_viewer']['uuid']).destroy
546
547     # verify that project is no longer included in starred projects
548     use_token :project_viewer
549     current_user = User.find(api_fixture('users')['project_viewer']['uuid'])
550     ctrl = ProjectsController.new
551     my_starred_project = ctrl.send :my_starred_projects, current_user
552     assert_equal(0, my_starred_project.andand.size)
553
554     # share it again
555     @controller = LinksController.new
556     post :create, {
557       link: {
558         link_class: 'permission',
559         name: 'can_read',
560         head_uuid: api_fixture('groups')['starred_and_shared_active_user_project']['uuid'],
561         tail_uuid: api_fixture('users')['project_viewer']['uuid'],
562       },
563       format: :json
564     }, session_for(:system_user)
565
566     # verify that the project is again included in starred projects
567     use_token :project_viewer
568     ctrl = ProjectsController.new
569     my_starred_project = ctrl.send :my_starred_projects, current_user
570     assert_equal(1, my_starred_project.andand.size)
571   end
572 end