Merge branch '15522-arvmount-ops-encoding'
[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, params: {}, session: 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, params: {}, session: 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, params: {
32         id: readonly_project_uuid
33       }, session: session_for(which_user)
34       buttons = css_select('[data-method=post]').select do |el|
35         el.attributes['data-remote-href'].value.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, params: {
50            id: api_fixture("groups")["asubproject"]["uuid"],
51            uuids: uuid_list,
52            format: "json"},
53          session: 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, params: {
61            id: api_fixture("groups")["aproject"]["uuid"],
62            uuids: [share_uuid],
63            format: "json"},
64          session: 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           params: { id: api_fixture("groups")["project_owns_itself"]["uuid"] },
102           session: 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            params: { id: api_fixture("groups")["asubproject"]["uuid"],
115              item_uuid: coll_uuid,
116              format: "js" },
117            session: 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            params: { id: api_fixture('groups', 'asubproject')['uuid'],
134              item_uuid: specimen_uuid,
135              format: 'js' },
136            session: 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   test 'projects#show tab infinite scroll partial obeys limit' do
147     get_contents_rows(limit: 1, filters: [['uuid','is_a',['arvados#job']]])
148     assert_response :success
149     assert_equal(1, json_response['content'].scan('<tr').count,
150                  "Did not get exactly one row")
151   end
152
153   ['', ' asc', ' desc'].each do |direction|
154     test "projects#show tab partial orders correctly by #{direction}" do
155       _test_tab_content_order direction
156     end
157   end
158
159   def _test_tab_content_order direction
160     get_contents_rows(limit: 100,
161                       order: "created_at#{direction}",
162                       filters: [['uuid','is_a',['arvados#job',
163                                                 'arvados#pipelineInstance']]])
164     assert_response :success
165     not_grouped_by_kind = nil
166     last_timestamp = nil
167     last_kind = nil
168     found_kind = {}
169     json_response['content'].scan /<tr[^>]+>/ do |tr_tag|
170       found_timestamps = 0
171       tr_tag.scan(/\ data-object-created-at=\"(.*?)\"/).each do |t,|
172         if last_timestamp
173           correct_operator = / desc$/ =~ direction ? :>= : :<=
174           assert_operator(last_timestamp, correct_operator, t,
175                           "Rows are not sorted by created_at#{direction}")
176         end
177         last_timestamp = t
178         found_timestamps += 1
179       end
180       assert_equal(1, found_timestamps,
181                    "Content row did not have exactly one timestamp")
182
183       # Confirm that the test for timestamp ordering couldn't have
184       # passed merely because the test fixtures have convenient
185       # timestamps (e.g., there is only one pipeline and one job in
186       # the project being tested, or there are no pipelines at all in
187       # the project being tested):
188       tr_tag.scan /\ data-kind=\"(.*?)\"/ do |kind|
189         if last_kind and last_kind != kind and found_kind[kind]
190           # We saw this kind before, then a different kind, then
191           # this kind again. That means objects are not grouped by
192           # kind.
193           not_grouped_by_kind = true
194         end
195         found_kind[kind] ||= 0
196         found_kind[kind] += 1
197         last_kind = kind
198       end
199     end
200     assert_equal(true, not_grouped_by_kind,
201                  "Could not confirm that results are not grouped by kind")
202   end
203
204   def get_contents_rows params
205     params = {
206       id: api_fixture('users')['active']['uuid'],
207       partial: :contents_rows,
208       format: :json,
209     }.merge(params)
210     encoded_params = Hash[params.map { |k,v|
211                             [k, (v.is_a?(Array) || v.is_a?(Hash)) ? v.to_json : v]
212                           }]
213     get :show, params: encoded_params, session: session_for(:active)
214   end
215
216   test "visit non-public project as anonymous when anonymous browsing is enabled and expect page not found" do
217     Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
218     get(:show, params: {id: api_fixture('groups')['aproject']['uuid']})
219     assert_response 404
220     assert_match(/log ?in/i, @response.body)
221   end
222
223   test "visit home page as anonymous when anonymous browsing is enabled and expect login" do
224     Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
225     get(:index)
226     assert_response :redirect
227     assert_match /\/users\/welcome/, @response.redirect_url
228   end
229
230   [
231     nil,
232     :active,
233   ].each do |user|
234     test "visit public projects page when anon config is enabled, as user #{user}, and expect page" do
235       Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
236
237       if user
238         get :public, params: {}, session: session_for(user)
239       else
240         get :public
241       end
242
243       assert_response :success
244       assert_not_nil assigns(:objects)
245       project_names = assigns(:objects).collect(&:name)
246       assert_includes project_names, 'Unrestricted public data'
247       assert_not_includes project_names, 'A Project'
248       refute_empty css_select('[href="/projects/public"]')
249     end
250   end
251
252   test "visit public projects page when anon config is not enabled as active user and expect 404" do
253     Rails.configuration.Users.AnonymousUserToken = ""
254     Rails.configuration.Workbench.EnablePublicProjectsPage = false
255     get :public, params: {}, session: session_for(:active)
256     assert_response 404
257   end
258
259   test "visit public projects page when anon config is enabled but public projects page is disabled as active user and expect 404" do
260     Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
261     Rails.configuration.Workbench.EnablePublicProjectsPage = false
262     get :public, params: {}, session: session_for(:active)
263     assert_response 404
264   end
265
266   test "visit public projects page when anon config is not enabled as anonymous and expect login page" do
267     Rails.configuration.Users.AnonymousUserToken = ""
268     Rails.configuration.Workbench.EnablePublicProjectsPage = false
269     get :public
270     assert_response :redirect
271     assert_match /\/users\/welcome/, @response.redirect_url
272     assert_empty css_select('[href="/projects/public"]')
273   end
274
275   test "visit public projects page when anon config is enabled and public projects page is disabled and expect login page" do
276     Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
277     Rails.configuration.Workbench.EnablePublicProjectsPage = false
278     get :index
279     assert_response :redirect
280     assert_match /\/users\/welcome/, @response.redirect_url
281     assert_empty css_select('[href="/projects/public"]')
282   end
283
284   test "visit public projects page when anon config is not enabled and public projects page is enabled and expect login page" do
285     Rails.configuration.Workbench.EnablePublicProjectsPage = true
286     get :index
287     assert_response :redirect
288     assert_match /\/users\/welcome/, @response.redirect_url
289     assert_empty css_select('[href="/projects/public"]')
290   end
291
292   test "find a project and edit its description" do
293     project = api_fixture('groups')['aproject']
294     use_token :active
295     found = Group.find(project['uuid'])
296     found.description = 'test description update'
297     found.save!
298     get(:show, params: {id: project['uuid']}, session: session_for(:active))
299     assert_includes @response.body, 'test description update'
300   end
301
302   test "find a project and edit description to textile description" do
303     project = api_fixture('groups')['aproject']
304     use_token :active
305     found = Group.find(project['uuid'])
306     found.description = '*test bold description for textile formatting*'
307     found.save!
308     get(:show, params: {id: project['uuid']}, session: session_for(:active))
309     assert_includes @response.body, '<strong>test bold description for textile formatting</strong>'
310   end
311
312   test "find a project and edit description to html description" do
313     project = api_fixture('groups')['aproject']
314     use_token :active
315     found = Group.find(project['uuid'])
316     found.description = '<b>Textile</b> description with link to home page <a href="/">take me home</a>.'
317     found.save!
318     get(:show, params: {id: project['uuid']}, session: session_for(:active))
319     assert_includes @response.body, '<b>Textile</b> description with link to home page <a href="/">take me home</a>.'
320   end
321
322   test "find a project and edit description to unsafe html description" do
323     project = api_fixture('groups')['aproject']
324     use_token :active
325     found = Group.find(project['uuid'])
326     found.description = 'Textile description with unsafe script tag <script language="javascript">alert("Hello there")</script>.'
327     found.save!
328     get(:show, params: {id: project['uuid']}, session: session_for(:active))
329     assert_includes @response.body, 'Textile description with unsafe script tag alert("Hello there").'
330   end
331
332   # Tests #14519
333   test "textile table on description renders as table html markup" do
334     use_token :active
335     project = api_fixture('groups')['aproject']
336     textile_table = <<EOT
337 table(table table-striped table-condensed).
338 |_. First Header |_. Second Header |
339 |Content Cell |Content Cell |
340 |Content Cell |Content Cell |
341 EOT
342     found = Group.find(project['uuid'])
343     found.description = textile_table
344     found.save!
345     get(:show, params: {id: project['uuid']}, session: session_for(:active))
346     assert_includes @response.body, '<th>First Header'
347     assert_includes @response.body, '<td>Content Cell'
348   end
349
350   test "find a project and edit description to textile description with link to object" do
351     project = api_fixture('groups')['aproject']
352     use_token :active
353     found = Group.find(project['uuid'])
354
355     # uses 'Link to object' as a hyperlink for the object
356     found.description = '"Link to object":' + api_fixture('groups')['asubproject']['uuid']
357     found.save!
358     get(:show, params: {id: project['uuid']}, session: session_for(:active))
359
360     # check that input was converted to textile, not staying as inputted
361     refute_includes  @response.body,'"Link to object"'
362     refute_empty css_select('[href="/groups/zzzzz-j7d0g-axqo7eu9pwvna1x"]')
363   end
364
365   test "project viewer can't see project sharing tab" do
366     project = api_fixture('groups')['aproject']
367     get(:show, params: {id: project['uuid']}, session: session_for(:project_viewer))
368     refute_includes @response.body, '<div id="Sharing"'
369     assert_includes @response.body, '<div id="Data_collections"'
370   end
371
372   [
373     'admin',
374     'active',
375   ].each do |username|
376     test "#{username} can see project sharing tab" do
377      project = api_fixture('groups')['aproject']
378      get(:show, params: {id: project['uuid']}, session: session_for(username))
379      assert_includes @response.body, '<div id="Sharing"'
380      assert_includes @response.body, '<div id="Data_collections"'
381     end
382   end
383
384   [
385     ['admin',true],
386     ['active',true],
387     ['project_viewer',false],
388   ].each do |user, can_move|
389     test "#{user} can move subproject from project #{can_move}" do
390       get(:show, params: {id: api_fixture('groups')['aproject']['uuid']}, session: session_for(user))
391       if can_move
392         assert_includes @response.body, 'Move project...'
393       else
394         refute_includes @response.body, 'Move project...'
395       end
396     end
397   end
398
399   [
400     [:admin, true],
401     [:active, false],
402   ].each do |user, expect_all_nodes|
403     test "in dashboard other index page links as #{user}" do
404       get :index, params: {}, session: session_for(user)
405
406       [["processes", "/all_processes"],
407        ["collections", "/collections"],
408       ].each do |target, path|
409         assert_includes @response.body, "href=\"#{path}\""
410         assert_includes @response.body, "All #{target}"
411       end
412
413       if expect_all_nodes
414         assert_includes @response.body, "href=\"/nodes\""
415         assert_includes @response.body, "All nodes"
416       else
417         assert_not_includes @response.body, "href=\"/nodes\""
418         assert_not_includes @response.body, "All nodes"
419       end
420     end
421   end
422
423   test "dashboard should show the correct status for processes" do
424     get :index, params: {}, session: session_for(:active)
425     assert_select 'div.panel-body.recent-processes' do
426       [
427         {
428           fixture: 'container_requests',
429           state: 'completed',
430           selectors: [['div.progress', false],
431                       ['span.label.label-success', true, 'Complete']]
432         },
433         {
434           fixture: 'container_requests',
435           state: 'uncommitted',
436           selectors: [['div.progress', false],
437                       ['span.label.label-default', true, 'Uncommitted']]
438         },
439         {
440           fixture: 'container_requests',
441           state: 'queued',
442           selectors: [['div.progress', false],
443                       ['span.label.label-default', true, 'Queued']]
444         },
445         {
446           fixture: 'container_requests',
447           state: 'running',
448           selectors: [['.label-info', true, 'Running']]
449         },
450         {
451           fixture: 'pipeline_instances',
452           state: 'new_pipeline',
453           selectors: [['div.progress', false],
454                       ['span.label.label-default', true, 'Not started']]
455         },
456         {
457           fixture: 'pipeline_instances',
458           state: 'pipeline_in_running_state',
459           selectors: [['.label-info', true, 'Running']]
460         },
461       ].each do |c|
462         uuid = api_fixture(c[:fixture])[c[:state]]['uuid']
463         assert_select "div.dashboard-panel-info-row.row-#{uuid}" do
464           if c.include? :selectors
465             c[:selectors].each do |selector, should_show, label|
466               assert_select selector, should_show, "UUID #{uuid} should #{should_show ? '' : 'not'} show '#{selector}'"
467               if should_show and not label.nil?
468                 assert_select selector, label, "UUID #{uuid} state label should show #{label}"
469               end
470             end
471           end
472         end
473       end
474     end
475   end
476
477   test "visit a public project and verify the public projects page link exists" do
478     Rails.configuration.Users.AnonymousUserToken = api_fixture('api_client_authorizations')['anonymous']['api_token']
479     uuid = api_fixture('groups')['anonymously_accessible_project']['uuid']
480     get :show, params: {id: uuid}
481     project = assigns(:object)
482     assert_equal uuid, project['uuid']
483     refute_empty css_select("[href=\"/projects/#{project['uuid']}\"]")
484     assert_includes @response.body, "<a href=\"/projects/public\">Public Projects</a>"
485   end
486
487   test 'all_projects unaffected by params after use by ProjectsController (#6640)' do
488     @controller = ProjectsController.new
489     project_uuid = api_fixture('groups')['aproject']['uuid']
490     get :index, params: {
491       filters: [['uuid', '<', project_uuid]].to_json,
492       limit: 0,
493       offset: 1000,
494     }, session: session_for(:active)
495     assert_select "#projects-menu + ul li.divider ~ li a[href=\"/projects/#{project_uuid}\"]"
496   end
497
498   [
499     ["active", 5, ["aproject", "asubproject"], "anonymously_accessible_project"],
500     ["user1_with_load", 2, ["project_with_10_collections"], "project_with_2_pipelines_and_60_crs"],
501     ["admin", 5, ["anonymously_accessible_project", "subproject_in_anonymous_accessible_project"], "aproject"],
502   ].each do |user, page_size, tree_segment, unexpected|
503     # Note: this test is sensitive to database collation. It passes
504     # with en_US.UTF-8.
505     test "build my projects tree for #{user} user and verify #{unexpected} is omitted" do
506       use_token user
507
508       tree, _, _ = @controller.send(:my_wanted_projects_tree,
509                                     User.current,
510                                     page_size)
511
512       tree_segment_at_depth_1 = api_fixture('groups')[tree_segment[0]]
513       tree_segment_at_depth_2 = api_fixture('groups')[tree_segment[1]] if tree_segment[1]
514
515       node_depth = {}
516       tree.each do |x|
517         node_depth[x[:object]['uuid']] = x[:depth]
518       end
519
520       assert_equal(1, node_depth[tree_segment_at_depth_1['uuid']])
521       assert_equal(2, node_depth[tree_segment_at_depth_2['uuid']]) if tree_segment[1]
522
523       unexpected_project = api_fixture('groups')[unexpected]
524       assert_nil(node_depth[unexpected_project['uuid']], node_depth.inspect)
525     end
526   end
527
528   [
529     ["active", 1],
530     ["project_viewer", 1],
531     ["admin", 0],
532   ].each do |user, size|
533     test "starred projects for #{user}" do
534       use_token user
535       ctrl = ProjectsController.new
536       current_user = User.find(api_fixture('users')[user]['uuid'])
537       my_starred_project = ctrl.send :my_starred_projects, current_user
538       assert_equal(size, my_starred_project.andand.size)
539
540       ctrl2 = ProjectsController.new
541       current_user = User.find(api_fixture('users')[user]['uuid'])
542       my_starred_project = ctrl2.send :my_starred_projects, current_user
543       assert_equal(size, my_starred_project.andand.size)
544     end
545   end
546
547   test "unshare project and verify that it is no longer included in shared user's starred projects" do
548     # remove sharing link
549     use_token :system_user
550     Link.find(api_fixture('links')['share_starred_project_with_project_viewer']['uuid']).destroy
551
552     # verify that project is no longer included in starred projects
553     use_token :project_viewer
554     current_user = User.find(api_fixture('users')['project_viewer']['uuid'])
555     ctrl = ProjectsController.new
556     my_starred_project = ctrl.send :my_starred_projects, current_user
557     assert_equal(0, my_starred_project.andand.size)
558
559     # share it again
560     @controller = LinksController.new
561     post :create, params: {
562       link: {
563         link_class: 'permission',
564         name: 'can_read',
565         head_uuid: api_fixture('groups')['starred_and_shared_active_user_project']['uuid'],
566         tail_uuid: api_fixture('users')['project_viewer']['uuid'],
567       },
568       format: :json
569     }, session: session_for(:system_user)
570
571     # verify that the project is again included in starred projects
572     use_token :project_viewer
573     ctrl = ProjectsController.new
574     my_starred_project = ctrl.send :my_starred_projects, current_user
575     assert_equal(1, my_starred_project.andand.size)
576   end
577 end