17755: Merge branch 'main' into 17755-add-singularity-to-compute-image
[arvados.git] / apps / workbench / test / integration / collections_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'integration_helper'
6 require_relative 'integration_test_utils'
7
8 class CollectionsTest < ActionDispatch::IntegrationTest
9   setup do
10     need_javascript
11   end
12
13   test "Can copy a collection to a project" do
14     collection_uuid = api_fixture('collections')['foo_file']['uuid']
15     collection_name = api_fixture('collections')['foo_file']['name']
16     project_uuid = api_fixture('groups')['aproject']['uuid']
17     project_name = api_fixture('groups')['aproject']['name']
18     visit page_with_token('active', "/collections/#{collection_uuid}")
19     click_link 'Copy to project...'
20     find('.selectable', text: project_name).click
21     find('.modal-footer a,button', text: 'Copy').click
22     # Should navigate to the Data collections tab of the project after copying
23     assert_text project_name
24     assert_text "Copy of #{collection_name}"
25   end
26
27   def check_sharing(want_state, link_regexp)
28     # We specifically want to click buttons.  See #4291.
29     if want_state == :off
30       click_button "Unshare"
31       text_assertion = :assert_no_text
32       link_assertion = :assert_empty
33     else
34       click_button "Create sharing link"
35       text_assertion = :assert_text
36       link_assertion = :refute_empty
37     end
38     using_wait_time(Capybara.default_max_wait_time * 3) do
39       send(text_assertion, "Shared at:")
40     end
41     send(link_assertion, all("a").select { |a| a[:href] =~ link_regexp })
42   end
43
44   test "creating and uncreating a sharing link" do
45     coll_uuid = api_fixture("collections", "collection_owned_by_active", "uuid")
46     download_link_re =
47       Regexp.new(Regexp.escape("/c=#{coll_uuid}/"))
48     visit page_with_token("active_trustedclient", "/collections/#{coll_uuid}")
49     within "#sharing-button" do
50       check_sharing(:on, download_link_re)
51       check_sharing(:off, download_link_re)
52     end
53   end
54
55   test "can download an entire collection with a reader token" do
56     need_selenium "phantomjs does not follow redirects reliably, maybe https://github.com/ariya/phantomjs/issues/10389"
57
58     token = api_token('active')
59     data = "foo\nfile\n"
60     datablock = `echo -n #{data.shellescape} | ARVADOS_API_TOKEN=#{token.shellescape} arv-put --no-progress --raw -`.strip
61     assert $?.success?, $?
62
63     col = nil
64     use_token 'active' do
65       mtxt = ". #{datablock} 0:#{data.length}:foo\n"
66       col = Collection.create(manifest_text: mtxt)
67     end
68
69     uuid = col.uuid
70     token = api_fixture('api_client_authorizations')['active_all_collections']['api_token']
71     url_head = "/collections/download/#{uuid}/#{token}/"
72     visit url_head
73     assert_text "You can download individual files listed below"
74     # It seems that Capybara can't inspect tags outside the body, so this is
75     # a very blunt approach.
76     assert_no_match(/<\s*meta[^>]+\bnofollow\b/i, page.html,
77                     "wget prohibited from recursing the collection page")
78     # Look at all the links that wget would recurse through using our
79     # recommended options, and check that it's exactly the file list.
80     hrefs = []
81     page.html.scan(/href="(.*?)"/) { |m| hrefs << m[0] }
82     assert_equal(['./foo'], hrefs, "download page did provide strictly file links")
83     click_link "foo"
84     assert_text "foo\nfile\n"
85   end
86
87   test "combine selected collections into new collection" do
88     foo_collection = api_fixture('collections')['foo_file']
89     bar_collection = api_fixture('collections')['bar_file']
90
91     visit page_with_token('active', "/collections")
92
93     assert(page.has_text?(foo_collection['uuid']), "Collection page did not include foo file")
94     assert(page.has_text?(bar_collection['uuid']), "Collection page did not include bar file")
95
96     within "tr[data-object-uuid=\"#{foo_collection['uuid']}\"]" do
97       find('input[type=checkbox]').click
98     end
99
100     within "tr[data-object-uuid=\"#{bar_collection['uuid']}\"]" do
101       find('input[type=checkbox]').click
102     end
103
104     click_button 'Selection...'
105     within('.selection-action-container') do
106       click_link 'Create new collection with selected collections'
107     end
108
109     # now in the newly created collection page
110     assert(page.has_text?('Copy to project'), "Copy to project text not found in new collection page")
111     assert(page.has_no_text?(foo_collection['name']), "Collection page did not include foo file")
112     assert(page.has_text?('foo'), "Collection page did not include foo file")
113     assert(page.has_no_text?(bar_collection['name']), "Collection page did not include foo file")
114     assert(page.has_text?('bar'), "Collection page did not include bar file")
115     assert(page.has_text?('Created new collection in your Home project'),
116                           'Not found flash message that new collection is created in Home project')
117   end
118
119   [
120     ['active', 'foo_file', false],
121     ['active', 'foo_collection_in_aproject', true],
122     ['project_viewer', 'foo_file', false],
123     ['project_viewer', 'foo_collection_in_aproject', false], #aproject not writable
124   ].each do |user, collection, expect_collection_in_aproject|
125     test "combine selected collection files into new collection #{user} #{collection} #{expect_collection_in_aproject}" do
126       my_collection = api_fixture('collections')[collection]
127
128       visit page_with_token(user, "/collections")
129
130       # choose file from foo collection
131       within('tr', text: my_collection['uuid']) do
132         click_link 'Show'
133       end
134
135       # now in collection page
136       find('input[type=checkbox]').click
137
138       click_button 'Selection...'
139       within('.selection-action-container') do
140         click_link 'Create new collection with selected files'
141       end
142
143       # now in the newly created collection page
144       assert(page.has_text?('Copy to project'), "Copy to project text not found in new collection page")
145       assert(page.has_no_text?(my_collection['name']), "Collection page did not include foo file")
146       assert(page.has_text?('foo'), "Collection page did not include foo file")
147       if expect_collection_in_aproject
148         aproject = api_fixture('groups')['aproject']
149         assert page.has_text?("Created new collection in the project #{aproject['name']}"),
150                               'Not found flash message that new collection is created in aproject'
151       else
152         assert page.has_text?("Created new collection in your Home project"),
153                               'Not found flash message that new collection is created in Home project'
154       end
155     end
156   end
157
158   test "combine selected collection files from collection subdirectory" do
159     visit page_with_token('user1_with_load', "/collections/zzzzz-4zz18-filesinsubdir00")
160
161     # now in collection page
162     input_files = page.all('input[type=checkbox]')
163     (0..input_files.count-1).each do |i|
164       input_files[i].click
165     end
166
167     click_button 'Selection...'
168     within('.selection-action-container') do
169       click_link 'Create new collection with selected files'
170     end
171
172     # now in the newly created collection page
173     assert(page.has_text?('file_in_subdir1'), 'file not found - file_in_subdir1')
174     assert(page.has_text?('file1_in_subdir3.txt'), 'file not found - file1_in_subdir3.txt')
175     assert(page.has_text?('file2_in_subdir3.txt'), 'file not found - file2_in_subdir3.txt')
176     assert(page.has_text?('file1_in_subdir4.txt'), 'file not found - file1_in_subdir4.txt')
177     assert(page.has_text?('file2_in_subdir4.txt'), 'file not found - file1_in_subdir4.txt')
178   end
179
180   test "Collection portable data hash with multiple matches with more than one page of results" do
181     pdh = api_fixture('collections')['baz_file']['portable_data_hash']
182     visit page_with_token('admin', "/collections/#{pdh}")
183
184     assert_selector 'a', text: 'Collection_1'
185
186     assert_text 'The following collections have this content:'
187     assert_text 'more results are not shown'
188     assert_no_text 'Activity'
189     assert_no_text 'Sharing and permissions'
190   end
191
192   test "Filtering collection files by regexp" do
193     col = api_fixture('collections', 'multilevel_collection_1')
194     visit page_with_token('active', "/collections/#{col['uuid']}")
195
196     # Filter file list to some but not all files in the collection
197     page.find_field('file_regex').set('file[12]')
198     assert page.has_text?("file1")
199     assert page.has_text?("file2")
200     assert page.has_no_text?("file3")
201
202     # Filter file list with a regex matching all files
203     page.find_field('file_regex').set('.*')
204     assert page.has_text?("file1")
205     assert page.has_text?("file2")
206     assert page.has_text?("file3")
207
208     # Filter file list to a regex matching no files
209     page.find_field('file_regex').set('file9')
210     assert page.has_no_text?("file1")
211     assert page.has_no_text?("file2")
212     assert page.has_no_text?("file3")
213     # make sure that we actually are looking at the collections
214     # page and not e.g. a fiddlesticks
215     assert page.has_text?("multilevel_collection_1")
216     assert page.has_text?(col["name"] || col["uuid"])
217
218     # Set filename filter to a syntactically invalid regex
219     # Page loads, but stops filtering after the last valid regex parse
220     page.find_field('file_regex').set('file[2')
221     assert page.has_text?("multilevel_collection_1")
222     assert page.has_text?(col["name"] || col["uuid"])
223     assert page.has_text?("file1")
224     assert page.has_text?("file2")
225     assert page.has_text?("file3")
226
227     # Test the "Select all" button
228
229     # Note: calling .set('') on a Selenium element is not sufficient
230     # to reset the field for this test, as it does not send any key
231     # events to the browser. To clear the field, we must instead send
232     # a backspace character.
233     # See https://selenium.googlecode.com/svn/trunk/docs/api/rb/Selenium/WebDriver/Element.html#clear-instance_method
234     page.find_field('file_regex').set("\b") # backspace
235     find('button#select-all').click
236     assert_checkboxes_state('input[type=checkbox]', true, '"select all" should check all checkboxes')
237
238     # Test the "Unselect all" button
239     page.find_field('file_regex').set("\b") # backspace
240     find('button#unselect-all').click
241     assert_checkboxes_state('input[type=checkbox]', false, '"unselect all" should clear all checkboxes')
242
243     # Filter files, then "select all", then unfilter
244     page.find_field('file_regex').set("\b") # backspace
245     find('button#unselect-all').click
246     page.find_field('file_regex').set('file[12]')
247     find('button#select-all').click
248     page.find_field('file_regex').set("\b") # backspace
249
250     # all "file1" and "file2" checkboxes must be selected
251     # all "file3" checkboxes must be clear
252     assert_checkboxes_state('[value*="file1"]', true, 'checkboxes for file1 should be selected after filtering')
253     assert_checkboxes_state('[value*="file2"]', true, 'checkboxes for file2 should be selected after filtering')
254     assert_checkboxes_state('[value*="file3"]', false, 'checkboxes for file3 should be clear after filtering')
255
256     # Select all files, then filter, then "unselect all", then unfilter
257     page.find_field('file_regex').set("\b") # backspace
258     find('button#select-all').click
259     page.find_field('file_regex').set('file[12]')
260     find('button#unselect-all').click
261     page.find_field('file_regex').set("\b") # backspace
262
263     # all "file1" and "file2" checkboxes must be clear
264     # all "file3" checkboxes must be selected
265     assert_checkboxes_state('[value*="file1"]', false, 'checkboxes for file1 should be clear after filtering')
266     assert_checkboxes_state('[value*="file2"]', false, 'checkboxes for file2 should be clear after filtering')
267     assert_checkboxes_state('[value*="file3"]', true, 'checkboxes for file3 should be selected after filtering')
268   end
269
270   test "Creating collection from list of filtered files" do
271     col = api_fixture('collections', 'collection_with_files_in_subdir')
272     visit page_with_token('user1_with_load', "/collections/#{col['uuid']}")
273     assert page.has_text?('file_in_subdir1'), 'expected file_in_subdir1 not found'
274     assert page.has_text?('file1_in_subdir3'), 'expected file1_in_subdir3 not found'
275     assert page.has_text?('file2_in_subdir3'), 'expected file2_in_subdir3 not found'
276     assert page.has_text?('file1_in_subdir4'), 'expected file1_in_subdir4 not found'
277     assert page.has_text?('file2_in_subdir4'), 'expected file2_in_subdir4 not found'
278
279     # Select all files but then filter them to files in subdir1, subdir2 or subdir3
280     find('button#select-all').click
281     page.find_field('file_regex').set('_in_subdir[123]')
282     assert page.has_text?('file_in_subdir1'), 'expected file_in_subdir1 not in filtered files'
283     assert page.has_text?('file1_in_subdir3'), 'expected file1_in_subdir3 not in filtered files'
284     assert page.has_text?('file2_in_subdir3'), 'expected file2_in_subdir3 not in filtered files'
285     assert page.has_no_text?('file1_in_subdir4'), 'file1_in_subdir4 found in filtered files'
286     assert page.has_no_text?('file2_in_subdir4'), 'file2_in_subdir4 found in filtered files'
287
288     # Create a new collection
289     click_button 'Selection...'
290     within('.selection-action-container') do
291       click_link 'Create new collection with selected files'
292     end
293
294     # now in the newly created collection page
295     # must have files in subdir1 and subdir3 but not subdir4
296     assert page.has_text?('file_in_subdir1'), 'file_in_subdir1 missing from new collection'
297     assert page.has_text?('file1_in_subdir3'), 'file1_in_subdir3 missing from new collection'
298     assert page.has_text?('file2_in_subdir3'), 'file2_in_subdir3 missing from new collection'
299     assert page.has_no_text?('file1_in_subdir4'), 'file1_in_subdir4 found in new collection'
300     assert page.has_no_text?('file2_in_subdir4'), 'file2_in_subdir4 found in new collection'
301
302     # Make sure we're not still on the old collection page.
303     refute_match(%r{/collections/#{col['uuid']}}, page.current_url)
304   end
305
306   test "remove a file from collection using checkbox and dropdown option" do
307     need_selenium 'to confirm unlock'
308
309     visit page_with_token('active', '/collections/zzzzz-4zz18-a21ux3541sxa8sf')
310     assert(page.has_text?('file1'), 'file not found - file1')
311
312     unlock_collection
313
314     # remove first file
315     input_files = page.all('input[type=checkbox]')
316     input_files[0].click
317
318     click_button 'Selection...'
319     within('.selection-action-container') do
320       click_link 'Remove selected files'
321     end
322
323     assert(page.has_no_text?('file1'), 'file found - file')
324     assert(page.has_text?('file2'), 'file not found - file2')
325   end
326
327   test "remove a file in collection using trash icon" do
328     need_selenium 'to confirm unlock'
329
330     visit page_with_token('active', '/collections/zzzzz-4zz18-a21ux3541sxa8sf')
331     assert(page.has_text?('file1'), 'file not found - file1')
332
333     unlock_collection
334
335     first('.fa-trash-o').click
336     accept_alert
337
338     assert(page.has_no_text?('file1'), 'file found - file')
339     assert(page.has_text?('file2'), 'file not found - file2')
340   end
341
342   test "rename a file in collection" do
343     need_selenium 'to confirm unlock'
344
345     visit page_with_token('active', '/collections/zzzzz-4zz18-a21ux3541sxa8sf')
346
347     unlock_collection
348
349     within('.collection_files') do
350       first('.fa-pencil').click
351       find('.editable-input input').set('file1renamed')
352       find('.editable-submit').click
353     end
354
355     assert(page.has_text?('file1renamed'), 'file not found - file1renamed')
356   end
357
358   test "remove/rename file options not presented if user cannot update a collection" do
359     # visit a publicly accessible collection as 'spectator'
360     visit page_with_token('spectator', '/collections/zzzzz-4zz18-uukreo9rbgwsujr')
361
362     click_button 'Selection'
363     within('.selection-action-container') do
364       assert_selector 'li', text: 'Create new collection with selected files'
365       assert_no_selector 'li', text: 'Remove selected files'
366     end
367
368     within('.collection_files') do
369       assert(page.has_text?('GNU_General_Public_License'), 'file not found - GNU_General_Public_License')
370       assert_nil first('.fa-pencil')
371       assert_nil first('.fa-trash-o')
372     end
373   end
374
375   test "unlock collection to modify files" do
376     need_selenium 'to confirm remove'
377
378     collection = api_fixture('collections')['collection_owned_by_active']
379
380     # On load, collection is locked, and upload tab, rename and remove options are disabled
381     visit page_with_token('active', "/collections/#{collection['uuid']}")
382
383     assert_selector 'a[data-toggle="disabled"]', text: 'Upload'
384
385     within('.collection_files') do
386       file_ctrls = page.all('.btn-collection-file-control')
387       assert_equal 2, file_ctrls.size
388       assert_equal true, file_ctrls[0]['class'].include?('disabled')
389       assert_equal true, file_ctrls[1]['class'].include?('disabled')
390       find('input[type=checkbox]').click
391     end
392
393     click_button 'Selection'
394     within('.selection-action-container') do
395       assert_selector 'li.disabled', text: 'Remove selected files'
396       assert_selector 'li', text: 'Create new collection with selected files'
397     end
398
399     unlock_collection
400
401     assert_no_selector 'a[data-toggle="disabled"]', text: 'Upload'
402     assert_selector 'a', text: 'Upload'
403
404     within('.collection_files') do
405       file_ctrls = page.all('.btn-collection-file-control')
406       assert_equal 2, file_ctrls.size
407       assert_equal false, file_ctrls[0]['class'].include?('disabled')
408       assert_equal false, file_ctrls[1]['class'].include?('disabled')
409
410       # previous checkbox selection won't result in firing a new event;
411       # undo and redo checkbox to fire the selection event again
412       find('input[type=checkbox]').click
413       find('input[type=checkbox]').click
414     end
415
416     click_button 'Selection'
417     within('.selection-action-container') do
418       assert_no_selector 'li.disabled', text: 'Remove selected files'
419       assert_selector 'li', text: 'Remove selected files'
420     end
421   end
422
423   def unlock_collection
424     first('.lock-collection-btn').click
425     accept_alert
426   end
427 end