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