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