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