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