8286: to facilitate in-place star icon refresh without the whole page refresh, it...
[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     ["jobs", "/jobs"],
391     ["pipelines", "/pipeline_instances"],
392     ["collections", "/collections"],
393   ].each do |target,path|
394     test "test dashboard button all #{target}" do
395       get :index, {}, session_for(:active)
396       assert_includes @response.body, "href=\"#{path}\""
397       assert_includes @response.body, "All #{target}"
398     end
399   end
400
401   test "visit a public project and verify the public projects page link exists" do
402     Rails.configuration.anonymous_user_token = api_fixture('api_client_authorizations')['anonymous']['api_token']
403     uuid = api_fixture('groups')['anonymously_accessible_project']['uuid']
404     get :show, {id: uuid}
405     project = assigns(:object)
406     assert_equal uuid, project['uuid']
407     refute_empty css_select("[href=\"/projects/#{project['uuid']}\"]")
408     assert_includes @response.body, "<a href=\"/projects/public\">Public Projects</a>"
409   end
410
411   test 'all_projects unaffected by params after use by ProjectsController (#6640)' do
412     @controller = ProjectsController.new
413     project_uuid = api_fixture('groups')['aproject']['uuid']
414     get :index, {
415       filters: [['uuid', '<', project_uuid]].to_json,
416       limit: 0,
417       offset: 1000,
418     }, session_for(:active)
419     assert_select "#projects-menu + ul li.divider ~ li a[href=/projects/#{project_uuid}]"
420   end
421
422   [
423     ["active", 5, ["aproject", "asubproject"], "anonymously_accessible_project"],
424     ["user1_with_load", 2, ["project_with_10_collections"], "project_with_2_pipelines_and_60_jobs"],
425     ["admin", 5, ["anonymously_accessible_project", "subproject_in_anonymous_accessible_project"], "aproject"],
426   ].each do |user, page_size, tree_segment, unexpected|
427     test "build my projects tree for #{user} user and verify #{unexpected} is omitted" do
428       use_token user
429       ctrl = ProjectsController.new
430
431       current_user = User.find(api_fixture('users')[user]['uuid'])
432
433       my_tree = ctrl.send :my_wanted_projects_tree, current_user, page_size
434
435       tree_segment_at_depth_1 = api_fixture('groups')[tree_segment[0]]
436       tree_segment_at_depth_2 = api_fixture('groups')[tree_segment[1]] if tree_segment[1]
437
438       tree_nodes = {}
439       my_tree[0].each do |x|
440         tree_nodes[x[:object]['uuid']] = x[:depth]
441       end
442
443       assert_equal(1, tree_nodes[tree_segment_at_depth_1['uuid']])
444       assert_equal(2, tree_nodes[tree_segment_at_depth_2['uuid']]) if tree_segment[1]
445
446       unexpected_project = api_fixture('groups')[unexpected]
447       assert_nil(tree_nodes[unexpected_project['uuid']])
448     end
449   end
450
451   [
452     ["active", 1],
453     ["project_viewer", 1],
454     ["admin", 0],
455   ].each do |user, size|
456     test "starred projects for #{user}" do
457       use_token user
458       ctrl = ProjectsController.new
459       current_user = User.find(api_fixture('users')[user]['uuid'])
460       my_starred_project = ctrl.send :my_starred_projects, current_user
461       assert_equal(size, my_starred_project.andand.size)
462
463       ctrl2 = ProjectsController.new
464       current_user = User.find(api_fixture('users')[user]['uuid'])
465       my_starred_project = ctrl2.send :my_starred_projects, current_user
466       assert_equal(size, my_starred_project.andand.size)
467     end
468   end
469
470   test "unshare project and verify that it is no longer included in shared user's starred projects" do
471     # remove sharing link
472     use_token :system_user
473     Link.find(api_fixture('links')['share_starred_project_with_project_viewer']['uuid']).destroy
474
475     # verify that project is no longer included in starred projects
476     use_token :project_viewer
477     current_user = User.find(api_fixture('users')['project_viewer']['uuid'])
478     ctrl = ProjectsController.new
479     my_starred_project = ctrl.send :my_starred_projects, current_user
480     assert_equal(0, my_starred_project.andand.size)
481
482     # share it again
483     @controller = LinksController.new
484     post :create, {
485       link: {
486         link_class: 'permission',
487         name: 'can_read',
488         head_uuid: api_fixture('groups')['starred_and_shared_active_user_project']['uuid'],
489         tail_uuid: api_fixture('users')['project_viewer']['uuid'],
490       },
491       format: :json
492     }, session_for(:system_user)
493
494     # verify that the project is again included in starred projects
495     use_token :project_viewer
496     ctrl = ProjectsController.new
497     my_starred_project = ctrl.send :my_starred_projects, current_user
498     assert_equal(1, my_starred_project.andand.size)
499   end
500 end