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