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