13561: Avoid collections.index to include old versions
[arvados.git] / services / api / test / functional / arvados / v1 / collections_controller_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'test_helper'
6
7 class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
8   include DbCurrentTime
9
10   PERM_TOKEN_RE = /\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/
11
12   def permit_unsigned_manifests isok=true
13     # Set security model for the life of a test.
14     Rails.configuration.permit_create_collection_with_unsigned_manifest = isok
15   end
16
17   def assert_signed_manifest manifest_text, label=''
18     assert_not_nil manifest_text, "#{label} manifest_text was nil"
19     manifest_text.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
20       assert_match(PERM_TOKEN_RE, tok,
21                    "Locator in #{label} manifest_text was not signed")
22     end
23   end
24
25   def assert_unsigned_manifest resp, label=''
26     txt = resp['unsigned_manifest_text']
27     assert_not_nil(txt, "#{label} unsigned_manifest_text was nil")
28     locs = 0
29     txt.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
30       locs += 1
31       refute_match(PERM_TOKEN_RE, tok,
32                    "Locator in #{label} unsigned_manifest_text was signed: #{tok}")
33     end
34     return locs
35   end
36
37   test "should get index" do
38     authorize_with :active
39     get :index
40     assert_response :success
41     assert(assigns(:objects).andand.any?, "no Collections returned in index")
42     refute(json_response["items"].any? { |c| c.has_key?("manifest_text") },
43            "basic Collections index included manifest_text")
44     refute(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
45            "basic Collections index included past version")
46   end
47
48   test "collections.get returns signed locators, and no unsigned_manifest_text" do
49     permit_unsigned_manifests
50     authorize_with :active
51     get :show, {id: collections(:foo_file).uuid}
52     assert_response :success
53     assert_signed_manifest json_response['manifest_text'], 'foo_file'
54     refute_includes json_response, 'unsigned_manifest_text'
55   end
56
57   test "index with manifest_text selected returns signed locators" do
58     columns = %w(uuid owner_uuid manifest_text)
59     authorize_with :active
60     get :index, select: columns
61     assert_response :success
62     assert(assigns(:objects).andand.any?,
63            "no Collections returned for index with columns selected")
64     json_response["items"].each do |coll|
65       assert_equal(coll.keys - ['kind'], columns,
66                    "Collections index did not respect selected columns")
67       assert_signed_manifest coll['manifest_text'], coll['uuid']
68     end
69   end
70
71   test "index with unsigned_manifest_text selected returns only unsigned locators" do
72     authorize_with :active
73     get :index, select: ['unsigned_manifest_text']
74     assert_response :success
75     assert_operator json_response["items"].count, :>, 0
76     locs = 0
77     json_response["items"].each do |coll|
78       assert_equal(coll.keys - ['kind'], ['unsigned_manifest_text'],
79                    "Collections index did not respect selected columns")
80       locs += assert_unsigned_manifest coll, coll['uuid']
81     end
82     assert_operator locs, :>, 0, "no locators found in any manifests"
83   end
84
85   test 'index without select returns everything except manifest' do
86     authorize_with :active
87     get :index
88     assert_response :success
89     assert json_response['items'].any?
90     json_response['items'].each do |coll|
91       assert_includes(coll.keys, 'uuid')
92       assert_includes(coll.keys, 'name')
93       assert_includes(coll.keys, 'created_at')
94       refute_includes(coll.keys, 'manifest_text')
95     end
96   end
97
98   ['', nil, false, 'null'].each do |select|
99     test "index with select=#{select.inspect} returns everything except manifest" do
100       authorize_with :active
101       get :index, select: select
102       assert_response :success
103       assert json_response['items'].any?
104       json_response['items'].each do |coll|
105         assert_includes(coll.keys, 'uuid')
106         assert_includes(coll.keys, 'name')
107         assert_includes(coll.keys, 'created_at')
108         refute_includes(coll.keys, 'manifest_text')
109       end
110     end
111   end
112
113   [["uuid"],
114    ["uuid", "manifest_text"],
115    '["uuid"]',
116    '["uuid", "manifest_text"]'].each do |select|
117     test "index with select=#{select.inspect} returns no name" do
118       authorize_with :active
119       get :index, select: select
120       assert_response :success
121       assert json_response['items'].any?
122       json_response['items'].each do |coll|
123         refute_includes(coll.keys, 'name')
124       end
125     end
126   end
127
128   [0,1,2].each do |limit|
129     test "get index with limit=#{limit}" do
130       authorize_with :active
131       get :index, limit: limit
132       assert_response :success
133       assert_equal limit, assigns(:objects).count
134       resp = JSON.parse(@response.body)
135       assert_equal limit, resp['limit']
136     end
137   end
138
139   test "items.count == items_available" do
140     authorize_with :active
141     get :index, limit: 100000
142     assert_response :success
143     resp = JSON.parse(@response.body)
144     assert_equal resp['items_available'], assigns(:objects).length
145     assert_equal resp['items_available'], resp['items'].count
146     unique_uuids = resp['items'].collect { |i| i['uuid'] }.compact.uniq
147     assert_equal unique_uuids.count, resp['items'].count
148   end
149
150   test "items.count == items_available with filters" do
151     authorize_with :active
152     get :index, {
153       limit: 100,
154       filters: [['uuid','=',collections(:foo_file).uuid]]
155     }
156     assert_response :success
157     assert_equal 1, assigns(:objects).length
158     assert_equal 1, json_response['items_available']
159     assert_equal 1, json_response['items'].count
160   end
161
162   test "get index with limit=2 offset=99999" do
163     # Assume there are not that many test fixtures.
164     authorize_with :active
165     get :index, limit: 2, offset: 99999
166     assert_response :success
167     assert_equal 0, assigns(:objects).count
168     resp = JSON.parse(@response.body)
169     assert_equal 2, resp['limit']
170     assert_equal 99999, resp['offset']
171   end
172
173   def request_capped_index(params={})
174     authorize_with :user1_with_load
175     coll1 = collections(:collection_1_of_201)
176     Rails.configuration.max_index_database_read =
177       yield(coll1.manifest_text.size)
178     get :index, {
179       select: %w(uuid manifest_text),
180       filters: [["owner_uuid", "=", coll1.owner_uuid]],
181       limit: 300,
182     }.merge(params)
183   end
184
185   test "index with manifest_text limited by max_index_database_read returns non-empty" do
186     request_capped_index() { |_| 1 }
187     assert_response :success
188     assert_equal(1, json_response["items"].size)
189     assert_equal(1, json_response["limit"])
190     assert_equal(201, json_response["items_available"])
191   end
192
193   test "max_index_database_read size check follows same order as real query" do
194     authorize_with :user1_with_load
195     txt = '.' + ' d41d8cd98f00b204e9800998ecf8427e+0'*1000 + " 0:0:empty.txt\n"
196     c = Collection.create! manifest_text: txt, name: '0000000000000000000'
197     request_capped_index(select: %w(uuid manifest_text name),
198                          order: ['name asc'],
199                          filters: [['name','>=',c.name]]) do |_|
200       txt.length - 1
201     end
202     assert_response :success
203     assert_equal(1, json_response["items"].size)
204     assert_equal(1, json_response["limit"])
205     assert_equal(c.uuid, json_response["items"][0]["uuid"])
206     # The effectiveness of the test depends on >1 item matching the filters.
207     assert_operator(1, :<, json_response["items_available"])
208   end
209
210   test "index with manifest_text limited by max_index_database_read" do
211     request_capped_index() { |size| (size * 3) + 1 }
212     assert_response :success
213     assert_equal(3, json_response["items"].size)
214     assert_equal(3, json_response["limit"])
215     assert_equal(201, json_response["items_available"])
216   end
217
218   test "max_index_database_read does not interfere with limit" do
219     request_capped_index(limit: 5) { |size| size * 20 }
220     assert_response :success
221     assert_equal(5, json_response["items"].size)
222     assert_equal(5, json_response["limit"])
223     assert_equal(201, json_response["items_available"])
224   end
225
226   test "max_index_database_read does not interfere with order" do
227     request_capped_index(select: %w(uuid manifest_text name),
228                          order: "name DESC") { |size| (size * 11) + 1 }
229     assert_response :success
230     assert_equal(11, json_response["items"].size)
231     assert_empty(json_response["items"].reject do |coll|
232                    coll["name"] =~ /^Collection_9/
233                  end)
234     assert_equal(11, json_response["limit"])
235     assert_equal(201, json_response["items_available"])
236   end
237
238   test "admin can create collection with unsigned manifest" do
239     authorize_with :admin
240     test_collection = {
241       manifest_text: <<-EOS
242 . d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt
243 . acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
244 . acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
245 ./baz acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
246 EOS
247     }
248     test_collection[:portable_data_hash] =
249       Digest::MD5.hexdigest(test_collection[:manifest_text]) +
250       '+' +
251       test_collection[:manifest_text].length.to_s
252
253     # post :create will modify test_collection in place, so we save a copy first.
254     # Hash.deep_dup is not sufficient as it preserves references of strings (??!?)
255     post_collection = Marshal.load(Marshal.dump(test_collection))
256     post :create, {
257       collection: post_collection
258     }
259
260     assert_response :success
261     assert_nil assigns(:objects)
262
263     response_collection = assigns(:object)
264
265     stored_collection = Collection.select([:uuid, :portable_data_hash, :manifest_text]).
266       where(portable_data_hash: response_collection['portable_data_hash']).first
267
268     assert_equal test_collection[:portable_data_hash], stored_collection['portable_data_hash']
269
270     # The manifest in the response will have had permission hints added.
271     # Remove any permission hints in the response before comparing it to the source.
272     stripped_manifest = stored_collection['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
273     assert_equal test_collection[:manifest_text], stripped_manifest
274
275     # TBD: create action should add permission signatures to manifest_text in the response,
276     # and we need to check those permission signatures here.
277   end
278
279   [:admin, :active].each do |user|
280     test "#{user} can get collection using portable data hash" do
281       authorize_with user
282
283       foo_collection = collections(:foo_file)
284
285       # Get foo_file using its portable data hash
286       get :show, {
287         id: foo_collection[:portable_data_hash]
288       }
289       assert_response :success
290       assert_not_nil assigns(:object)
291       resp = assigns(:object)
292       assert_equal foo_collection[:portable_data_hash], resp[:portable_data_hash]
293       assert_signed_manifest resp[:manifest_text]
294
295       # The manifest in the response will have had permission hints added.
296       # Remove any permission hints in the response before comparing it to the source.
297       stripped_manifest = resp[:manifest_text].gsub(/\+A[A-Za-z0-9@_-]+/, '')
298       assert_equal foo_collection[:manifest_text], stripped_manifest
299     end
300   end
301
302   test "create with owner_uuid set to owned group" do
303     permit_unsigned_manifests
304     authorize_with :active
305     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
306     post :create, {
307       collection: {
308         owner_uuid: 'zzzzz-j7d0g-rew6elm53kancon',
309         manifest_text: manifest_text,
310         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
311       }
312     }
313     assert_response :success
314     resp = JSON.parse(@response.body)
315     assert_equal 'zzzzz-j7d0g-rew6elm53kancon', resp['owner_uuid']
316   end
317
318   test "create fails with duplicate name" do
319     permit_unsigned_manifests
320     authorize_with :admin
321     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
322     post :create, {
323       collection: {
324         owner_uuid: 'zzzzz-tpzed-000000000000000',
325         manifest_text: manifest_text,
326         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
327         name: "foo_file"
328       }
329     }
330     assert_response 422
331     response_errors = json_response['errors']
332     assert_not_nil response_errors, 'Expected error in response'
333     assert(response_errors.first.include?('duplicate key'),
334            "Expected 'duplicate key' error in #{response_errors.first}")
335   end
336
337   [false, true].each do |unsigned|
338     test "create with duplicate name, ensure_unique_name, unsigned=#{unsigned}" do
339       permit_unsigned_manifests unsigned
340       authorize_with :active
341       manifest_text = ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:foo.txt\n"
342       if !unsigned
343         manifest_text = Collection.sign_manifest manifest_text, api_token(:active)
344       end
345       post :create, {
346         collection: {
347           owner_uuid: users(:active).uuid,
348           manifest_text: manifest_text,
349           name: "owned_by_active"
350         },
351         ensure_unique_name: true
352       }
353       assert_response :success
354       assert_match /^owned_by_active \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
355     end
356   end
357
358   test "create with owner_uuid set to group i can_manage" do
359     permit_unsigned_manifests
360     authorize_with :active
361     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
362     post :create, {
363       collection: {
364         owner_uuid: groups(:active_user_has_can_manage).uuid,
365         manifest_text: manifest_text,
366         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
367       }
368     }
369     assert_response :success
370     resp = JSON.parse(@response.body)
371     assert_equal groups(:active_user_has_can_manage).uuid, resp['owner_uuid']
372   end
373
374   test "create with owner_uuid fails on group with only can_read permission" do
375     permit_unsigned_manifests
376     authorize_with :active
377     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
378     post :create, {
379       collection: {
380         owner_uuid: groups(:all_users).uuid,
381         manifest_text: manifest_text,
382         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
383       }
384     }
385     assert_response 403
386   end
387
388   test "create with owner_uuid fails on group with no permission" do
389     permit_unsigned_manifests
390     authorize_with :active
391     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
392     post :create, {
393       collection: {
394         owner_uuid: groups(:public).uuid,
395         manifest_text: manifest_text,
396         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
397       }
398     }
399     assert_response 422
400   end
401
402   test "admin create with owner_uuid set to group with no permission" do
403     permit_unsigned_manifests
404     authorize_with :admin
405     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
406     post :create, {
407       collection: {
408         owner_uuid: 'zzzzz-j7d0g-it30l961gq3t0oi',
409         manifest_text: manifest_text,
410         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
411       }
412     }
413     assert_response :success
414   end
415
416   test "should create with collection passed as json" do
417     permit_unsigned_manifests
418     authorize_with :active
419     post :create, {
420       collection: <<-EOS
421       {
422         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",\
423         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
424       }
425       EOS
426     }
427     assert_response :success
428   end
429
430   test "should fail to create with checksum mismatch" do
431     permit_unsigned_manifests
432     authorize_with :active
433     post :create, {
434       collection: <<-EOS
435       {
436         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:bar.txt\n",\
437         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
438       }
439       EOS
440     }
441     assert_response 422
442   end
443
444   test "collection UUID is normalized when created" do
445     permit_unsigned_manifests
446     authorize_with :active
447     post :create, {
448       collection: {
449         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
450         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47+Khint+Xhint+Zhint"
451       }
452     }
453     assert_response :success
454     assert_not_nil assigns(:object)
455     resp = JSON.parse(@response.body)
456     assert_equal "d30fe8ae534397864cb96c544f4cf102+47", resp['portable_data_hash']
457   end
458
459   test "get full provenance for baz file" do
460     authorize_with :active
461     get :provenance, id: 'ea10d51bcf88862dbcc36eb292017dfd+45'
462     assert_response :success
463     resp = JSON.parse(@response.body)
464     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
465     assert_not_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # bar
466     assert_not_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # foo
467     assert_not_nil resp['zzzzz-8i9sb-cjs4pklxxjykyuq'] # bar->baz
468     assert_not_nil resp['zzzzz-8i9sb-aceg2bnq7jt7kon'] # foo->bar
469   end
470
471   test "get no provenance for foo file" do
472     # spectator user cannot even see baz collection
473     authorize_with :spectator
474     get :provenance, id: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'
475     assert_response 404
476   end
477
478   test "get partial provenance for baz file" do
479     # spectator user can see bar->baz job, but not foo->bar job
480     authorize_with :spectator
481     get :provenance, id: 'ea10d51bcf88862dbcc36eb292017dfd+45'
482     assert_response :success
483     resp = JSON.parse(@response.body)
484     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
485     assert_not_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # bar
486     assert_not_nil resp['zzzzz-8i9sb-cjs4pklxxjykyuq']     # bar->baz
487     assert_nil resp['zzzzz-8i9sb-aceg2bnq7jt7kon']         # foo->bar
488     assert_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # foo
489   end
490
491   test "search collections with 'any' operator" do
492     expect_pdh = collections(:docker_image).portable_data_hash
493     authorize_with :active
494     get :index, {
495       where: { any: ['contains', expect_pdh[5..25]] }
496     }
497     assert_response :success
498     found = assigns(:objects)
499     assert_equal 1, found.count
500     assert_equal expect_pdh, found.first.portable_data_hash
501   end
502
503   [false, true].each do |permit_unsigned|
504     test "create collection with signed manifest, permit_unsigned=#{permit_unsigned}" do
505       permit_unsigned_manifests permit_unsigned
506       authorize_with :active
507       locators = %w(
508       d41d8cd98f00b204e9800998ecf8427e+0
509       acbd18db4cc2f85cedef654fccc4a4d8+3
510       ea10d51bcf88862dbcc36eb292017dfd+45)
511
512       unsigned_manifest = locators.map { |loc|
513         ". " + loc + " 0:0:foo.txt\n"
514       }.join()
515       manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
516         '+' +
517         unsigned_manifest.length.to_s
518
519       # Build a manifest with both signed and unsigned locators.
520       signing_opts = {
521         key: Rails.configuration.blob_signing_key,
522         api_token: api_token(:active),
523       }
524       signed_locators = locators.collect do |x|
525         Blob.sign_locator x, signing_opts
526       end
527       if permit_unsigned
528         # Leave a non-empty blob unsigned.
529         signed_locators[1] = locators[1]
530       else
531         # Leave the empty blob unsigned. This should still be allowed.
532         signed_locators[0] = locators[0]
533       end
534       signed_manifest =
535         ". " + signed_locators[0] + " 0:0:foo.txt\n" +
536         ". " + signed_locators[1] + " 0:0:foo.txt\n" +
537         ". " + signed_locators[2] + " 0:0:foo.txt\n"
538
539       post :create, {
540         collection: {
541           manifest_text: signed_manifest,
542           portable_data_hash: manifest_uuid,
543         }
544       }
545       assert_response :success
546       assert_not_nil assigns(:object)
547       resp = JSON.parse(@response.body)
548       assert_equal manifest_uuid, resp['portable_data_hash']
549       # All of the locators in the output must be signed.
550       resp['manifest_text'].lines.each do |entry|
551         m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
552         if m
553           assert Blob.verify_signature m[0], signing_opts
554         end
555       end
556     end
557   end
558
559   test "create collection with signed manifest and explicit TTL" do
560     authorize_with :active
561     locators = %w(
562       d41d8cd98f00b204e9800998ecf8427e+0
563       acbd18db4cc2f85cedef654fccc4a4d8+3
564       ea10d51bcf88862dbcc36eb292017dfd+45)
565
566     unsigned_manifest = locators.map { |loc|
567       ". " + loc + " 0:0:foo.txt\n"
568     }.join()
569     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
570       '+' +
571       unsigned_manifest.length.to_s
572
573     # build a manifest with both signed and unsigned locators.
574     # TODO(twp): in phase 4, all locators will need to be signed, so
575     # this test should break and will need to be rewritten. Issue #2755.
576     signing_opts = {
577       key: Rails.configuration.blob_signing_key,
578       api_token: api_token(:active),
579       ttl: 3600   # 1 hour
580     }
581     signed_manifest =
582       ". " + locators[0] + " 0:0:foo.txt\n" +
583       ". " + Blob.sign_locator(locators[1], signing_opts) + " 0:0:foo.txt\n" +
584       ". " + Blob.sign_locator(locators[2], signing_opts) + " 0:0:foo.txt\n"
585
586     post :create, {
587       collection: {
588         manifest_text: signed_manifest,
589         portable_data_hash: manifest_uuid,
590       }
591     }
592     assert_response :success
593     assert_not_nil assigns(:object)
594     resp = JSON.parse(@response.body)
595     assert_equal manifest_uuid, resp['portable_data_hash']
596     # All of the locators in the output must be signed.
597     resp['manifest_text'].lines.each do |entry|
598       m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
599       if m
600         assert Blob.verify_signature m[0], signing_opts
601       end
602     end
603   end
604
605   test "create fails with invalid signature" do
606     authorize_with :active
607     signing_opts = {
608       key: Rails.configuration.blob_signing_key,
609       api_token: api_token(:active),
610     }
611
612     # Generate a locator with a bad signature.
613     unsigned_locator = "acbd18db4cc2f85cedef654fccc4a4d8+3"
614     bad_locator = unsigned_locator + "+Affffffffffffffffffffffffffffffffffffffff@ffffffff"
615     assert !Blob.verify_signature(bad_locator, signing_opts)
616
617     # Creating a collection with this locator should
618     # produce 403 Permission denied.
619     unsigned_manifest = ". #{unsigned_locator} 0:0:foo.txt\n"
620     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
621       '+' +
622       unsigned_manifest.length.to_s
623
624     bad_manifest = ". #{bad_locator} 0:0:foo.txt\n"
625     post :create, {
626       collection: {
627         manifest_text: bad_manifest,
628         portable_data_hash: manifest_uuid
629       }
630     }
631
632     assert_response 403
633   end
634
635   test "create fails with uuid of signed manifest" do
636     authorize_with :active
637     signing_opts = {
638       key: Rails.configuration.blob_signing_key,
639       api_token: api_token(:active),
640     }
641
642     unsigned_locator = "d41d8cd98f00b204e9800998ecf8427e+0"
643     signed_locator = Blob.sign_locator(unsigned_locator, signing_opts)
644     signed_manifest = ". #{signed_locator} 0:0:foo.txt\n"
645     manifest_uuid = Digest::MD5.hexdigest(signed_manifest) +
646       '+' +
647       signed_manifest.length.to_s
648
649     post :create, {
650       collection: {
651         manifest_text: signed_manifest,
652         portable_data_hash: manifest_uuid
653       }
654     }
655
656     assert_response 422
657   end
658
659   test "reject manifest with unsigned block as stream name" do
660     authorize_with :active
661     post :create, {
662       collection: {
663         manifest_text: "00000000000000000000000000000000+1234 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n"
664       }
665     }
666     assert_includes [422, 403], response.code.to_i
667   end
668
669   test "multiple locators per line" do
670     permit_unsigned_manifests
671     authorize_with :active
672     locators = %w(
673       d41d8cd98f00b204e9800998ecf8427e+0
674       acbd18db4cc2f85cedef654fccc4a4d8+3
675       ea10d51bcf88862dbcc36eb292017dfd+45)
676
677     manifest_text = [".", *locators, "0:0:foo.txt\n"].join(" ")
678     manifest_uuid = Digest::MD5.hexdigest(manifest_text) +
679       '+' +
680       manifest_text.length.to_s
681
682     test_collection = {
683       manifest_text: manifest_text,
684       portable_data_hash: manifest_uuid,
685     }
686     post_collection = Marshal.load(Marshal.dump(test_collection))
687     post :create, {
688       collection: post_collection
689     }
690     assert_response :success
691     assert_not_nil assigns(:object)
692     resp = JSON.parse(@response.body)
693     assert_equal manifest_uuid, resp['portable_data_hash']
694
695     # The manifest in the response will have had permission hints added.
696     # Remove any permission hints in the response before comparing it to the source.
697     stripped_manifest = resp['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
698     assert_equal manifest_text, stripped_manifest
699   end
700
701   test "multiple signed locators per line" do
702     permit_unsigned_manifests
703     authorize_with :active
704     locators = %w(
705       d41d8cd98f00b204e9800998ecf8427e+0
706       acbd18db4cc2f85cedef654fccc4a4d8+3
707       ea10d51bcf88862dbcc36eb292017dfd+45)
708
709     signing_opts = {
710       key: Rails.configuration.blob_signing_key,
711       api_token: api_token(:active),
712     }
713
714     unsigned_manifest = [".", *locators, "0:0:foo.txt\n"].join(" ")
715     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
716       '+' +
717       unsigned_manifest.length.to_s
718
719     signed_locators = locators.map { |loc| Blob.sign_locator loc, signing_opts }
720     signed_manifest = [".", *signed_locators, "0:0:foo.txt\n"].join(" ")
721
722     post :create, {
723       collection: {
724         manifest_text: signed_manifest,
725         portable_data_hash: manifest_uuid,
726       }
727     }
728     assert_response :success
729     assert_not_nil assigns(:object)
730     resp = JSON.parse(@response.body)
731     assert_equal manifest_uuid, resp['portable_data_hash']
732     # All of the locators in the output must be signed.
733     # Each line is of the form "path locator locator ... 0:0:file.txt"
734     # entry.split[1..-2] will yield just the tokens in the middle of the line
735     returned_locator_count = 0
736     resp['manifest_text'].lines.each do |entry|
737       entry.split[1..-2].each do |tok|
738         returned_locator_count += 1
739         assert Blob.verify_signature tok, signing_opts
740       end
741     end
742     assert_equal locators.count, returned_locator_count
743   end
744
745   test 'Reject manifest with unsigned blob' do
746     permit_unsigned_manifests false
747     authorize_with :active
748     unsigned_manifest = ". 0cc175b9c0f1b6a831c399e269772661+1 0:1:a.txt\n"
749     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest)
750     post :create, {
751       collection: {
752         manifest_text: unsigned_manifest,
753         portable_data_hash: manifest_uuid,
754       }
755     }
756     assert_response 403,
757     "Creating a collection with unsigned blobs should respond 403"
758     assert_empty Collection.where('uuid like ?', manifest_uuid+'%'),
759     "Collection should not exist in database after failed create"
760   end
761
762   test 'List expired collection returns empty list' do
763     authorize_with :active
764     get :index, {
765       where: {name: 'expired_collection'},
766     }
767     assert_response :success
768     found = assigns(:objects)
769     assert_equal 0, found.count
770   end
771
772   test 'Show expired collection returns 404' do
773     authorize_with :active
774     get :show, {
775       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
776     }
777     assert_response 404
778   end
779
780   test 'Update expired collection returns 404' do
781     authorize_with :active
782     post :update, {
783       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
784       collection: {
785         name: "still expired"
786       }
787     }
788     assert_response 404
789   end
790
791   test 'List collection with future expiration time succeeds' do
792     authorize_with :active
793     get :index, {
794       where: {name: 'collection_expires_in_future'},
795     }
796     found = assigns(:objects)
797     assert_equal 1, found.count
798   end
799
800
801   test 'Show collection with future expiration time succeeds' do
802     authorize_with :active
803     get :show, {
804       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
805     }
806     assert_response :success
807   end
808
809   test 'Update collection with future expiration time succeeds' do
810     authorize_with :active
811     post :update, {
812       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
813       collection: {
814         name: "still not expired"
815       }
816     }
817     assert_response :success
818   end
819
820   test "get collection and verify that file_names is not included" do
821     authorize_with :active
822     get :show, {id: collections(:foo_file).uuid}
823     assert_response :success
824     assert_equal collections(:foo_file).uuid, json_response['uuid']
825     assert_nil json_response['file_names']
826     assert json_response['manifest_text']
827   end
828
829   [
830     [2**8, :success],
831     [2**18, 422],
832   ].each do |description_size, expected_response|
833     # Descriptions are not part of search indexes. Skip until
834     # full-text search is implemented, at which point replace with a
835     # search in description.
836     skip "create collection with description size #{description_size}
837           and expect response #{expected_response}" do
838       authorize_with :active
839
840       description = 'here is a collection with a very large description'
841       while description.length < description_size
842         description = description + description
843       end
844
845       post :create, collection: {
846         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n",
847         description: description,
848       }
849
850       assert_response expected_response
851     end
852   end
853
854   [1, 5, nil].each do |ask|
855     test "Set replication_desired=#{ask.inspect}" do
856       Rails.configuration.default_collection_replication = 2
857       authorize_with :active
858       put :update, {
859         id: collections(:replication_undesired_unconfirmed).uuid,
860         collection: {
861           replication_desired: ask,
862         },
863       }
864       assert_response :success
865       assert_equal ask, json_response['replication_desired']
866     end
867   end
868
869   test "get collection with properties" do
870     authorize_with :active
871     get :show, {id: collections(:collection_with_one_property).uuid}
872     assert_response :success
873     assert_not_nil json_response['uuid']
874     assert_equal 'value1', json_response['properties']['property1']
875   end
876
877   test "create collection with properties" do
878     authorize_with :active
879     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
880     post :create, {
881       collection: {
882         manifest_text: manifest_text,
883         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
884         properties: {'property_1' => 'value_1'}
885       }
886     }
887     assert_response :success
888     assert_not_nil json_response['uuid']
889     assert_equal 'value_1', json_response['properties']['property_1']
890   end
891
892   [
893     ". 0:0:foo.txt",
894     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
895     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
896     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
897   ].each do |manifest_text|
898     test "create collection with invalid manifest #{manifest_text} and expect error" do
899       authorize_with :active
900       post :create, {
901         collection: {
902           manifest_text: manifest_text,
903           portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0"
904         }
905       }
906       assert_response 422
907       response_errors = json_response['errors']
908       assert_not_nil response_errors, 'Expected error in response'
909       assert(response_errors.first.include?('Invalid manifest'),
910              "Expected 'Invalid manifest' error in #{response_errors.first}")
911     end
912   end
913
914   [
915     [nil, "d41d8cd98f00b204e9800998ecf8427e+0"],
916     ["", "d41d8cd98f00b204e9800998ecf8427e+0"],
917     [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"],
918   ].each do |manifest_text, pdh|
919     test "create collection with valid manifest #{manifest_text.inspect} and expect success" do
920       authorize_with :active
921       post :create, {
922         collection: {
923           manifest_text: manifest_text,
924           portable_data_hash: pdh
925         }
926       }
927       assert_response 200
928     end
929   end
930
931   [
932     ". 0:0:foo.txt",
933     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
934     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
935     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
936   ].each do |manifest_text|
937     test "update collection with invalid manifest #{manifest_text} and expect error" do
938       authorize_with :active
939       post :update, {
940         id: 'zzzzz-4zz18-bv31uwvy3neko21',
941         collection: {
942           manifest_text: manifest_text,
943         }
944       }
945       assert_response 422
946       response_errors = json_response['errors']
947       assert_not_nil response_errors, 'Expected error in response'
948       assert(response_errors.first.include?('Invalid manifest'),
949              "Expected 'Invalid manifest' error in #{response_errors.first}")
950     end
951   end
952
953   [
954     nil,
955     "",
956     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
957   ].each do |manifest_text|
958     test "update collection with valid manifest #{manifest_text.inspect} and expect success" do
959       authorize_with :active
960       post :update, {
961         id: 'zzzzz-4zz18-bv31uwvy3neko21',
962         collection: {
963           manifest_text: manifest_text,
964         }
965       }
966       assert_response 200
967     end
968   end
969
970   test 'get trashed collection with include_trash' do
971     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
972     authorize_with :active
973     get :show, {
974       id: uuid,
975       include_trash: true,
976     }
977     assert_response 200
978   end
979
980   test 'get trashed collection without include_trash' do
981     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
982     authorize_with :active
983     get :show, {
984       id: uuid,
985     }
986     assert_response 404
987   end
988
989   test 'trash collection using http DELETE verb' do
990     uuid = collections(:collection_owned_by_active).uuid
991     authorize_with :active
992     delete :destroy, {
993       id: uuid,
994     }
995     assert_response 200
996     c = Collection.find_by_uuid(uuid)
997     assert_operator c.trash_at, :<, db_current_time
998     assert_equal c.delete_at, c.trash_at + Rails.configuration.blob_signature_ttl
999   end
1000
1001   test 'delete long-trashed collection immediately using http DELETE verb' do
1002     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1003     authorize_with :active
1004     delete :destroy, {
1005       id: uuid,
1006     }
1007     assert_response 200
1008     c = Collection.find_by_uuid(uuid)
1009     assert_operator c.trash_at, :<, db_current_time
1010     assert_operator c.delete_at, :<, db_current_time
1011   end
1012
1013   ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection
1014    :empty_collection_name_in_active_user_home_project,
1015   ].each do |fixture|
1016     test "trash collection #{fixture} via trash action with grace period" do
1017       if fixture.is_a? String
1018         uuid = fixture
1019       else
1020         uuid = collections(fixture).uuid
1021       end
1022       authorize_with :active
1023       time_before_trashing = db_current_time
1024       post :trash, {
1025         id: uuid,
1026       }
1027       assert_response 200
1028       c = Collection.find_by_uuid(uuid)
1029       assert_operator c.trash_at, :<, db_current_time
1030       assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.default_trash_lifetime
1031     end
1032   end
1033
1034   test 'untrash a trashed collection' do
1035     authorize_with :active
1036     post :untrash, {
1037       id: collections(:expired_collection).uuid,
1038     }
1039     assert_response 200
1040     assert_equal false, json_response['is_trashed']
1041     assert_nil json_response['trash_at']
1042   end
1043
1044   test 'untrash error on not trashed collection' do
1045     authorize_with :active
1046     post :untrash, {
1047       id: collections(:collection_owned_by_active).uuid,
1048     }
1049     assert_response 422
1050   end
1051
1052   [:active, :admin].each do |user|
1053     test "get trashed collections as #{user}" do
1054       authorize_with user
1055       get :index, {
1056         filters: [["is_trashed", "=", true]],
1057         include_trash: true,
1058       }
1059       assert_response :success
1060
1061       items = []
1062       json_response["items"].each do |coll|
1063         items << coll['uuid']
1064       end
1065
1066       assert_includes(items, collections('unique_expired_collection')['uuid'])
1067       if user == :admin
1068         assert_includes(items, collections('unique_expired_collection2')['uuid'])
1069       else
1070         assert_not_includes(items, collections('unique_expired_collection2')['uuid'])
1071       end
1072     end
1073   end
1074
1075   test 'untrash collection with same name as another with no ensure unique name' do
1076     authorize_with :active
1077     post :untrash, {
1078       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1079     }
1080     assert_response 422
1081   end
1082
1083   test 'untrash collection with same name as another with ensure unique name' do
1084     authorize_with :active
1085     post :untrash, {
1086       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1087       ensure_unique_name: true
1088     }
1089     assert_response 200
1090     assert_equal false, json_response['is_trashed']
1091     assert_nil json_response['trash_at']
1092     assert_nil json_response['delete_at']
1093     assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
1094   end
1095
1096   test 'cannot show collection in trashed subproject' do
1097     authorize_with :active
1098     get :show, {
1099       id: collections(:collection_in_trashed_subproject).uuid,
1100       format: :json
1101     }
1102     assert_response 404
1103   end
1104
1105   test 'can show collection in untrashed subproject' do
1106     authorize_with :active
1107     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1108     get :show, {
1109       id: collections(:collection_in_trashed_subproject).uuid,
1110       format: :json,
1111     }
1112     assert_response :success
1113   end
1114
1115   test 'cannot index collection in trashed subproject' do
1116     authorize_with :active
1117     get :index, { limit: 1000 }
1118     assert_response :success
1119     item_uuids = json_response['items'].map do |item|
1120       item['uuid']
1121     end
1122     assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1123   end
1124
1125   test 'can index collection in untrashed subproject' do
1126     authorize_with :active
1127     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1128     get :index, { limit: 1000 }
1129     assert_response :success
1130     item_uuids = json_response['items'].map do |item|
1131       item['uuid']
1132     end
1133     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1134   end
1135
1136   test 'can index trashed subproject collection with include_trash' do
1137     authorize_with :active
1138     get :index, {
1139           include_trash: true,
1140           limit: 1000
1141         }
1142     assert_response :success
1143     item_uuids = json_response['items'].map do |item|
1144       item['uuid']
1145     end
1146     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1147   end
1148 end