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