Merge remote-tracking branch 'origin/master' into 14484-collection-record-update
[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     [". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n", 1, 34],
942     [". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt 0:30:foo.txt 0:30:foo1.txt 0:30:foo2.txt 0:30:foo3.txt 0:30:foo4.txt\n", 5, 184],
943     [". d41d8cd98f00b204e9800998ecf8427e 0:0:.\n", 0, 0]
944   ].each do |manifest, count, size|
945     test "create collection with valid manifest #{manifest} and expect file stats" do
946       authorize_with :active
947       post :create, {
948         collection: {
949           manifest_text: manifest
950         }
951       }
952       assert_response 200
953       assert_equal count, json_response['file_count']
954       assert_equal size, json_response['file_size_total']
955     end
956   end
957
958   test "update collection manifest and expect new file stats" do
959     authorize_with :active
960     post :update, {
961       id: collections(:collection_owned_by_active_with_file_stats).uuid,
962       collection: {
963         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n"
964       }
965     }
966     assert_response 200
967     assert_equal 1, json_response['file_count']
968     assert_equal 34, json_response['file_size_total']
969   end
970
971   [
972     ['file_count', 1],
973     ['file_size_total', 34]
974   ].each do |attribute, val|
975     test "create collection with #{attribute} and expect overwrite" do
976       authorize_with :active
977       post :create, {
978         collection: {
979           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
980           "#{attribute}": 10
981         }
982       }
983       assert_response 200
984       assert_equal val, json_response[attribute]
985     end
986   end
987
988   [
989     ['file_count', 1],
990     ['file_size_total', 3]
991   ].each do |attribute, val|
992     test "update collection with #{attribute} and expect ignore" do
993       authorize_with :active
994       post :update, {
995         id: collections(:collection_owned_by_active_with_file_stats).uuid,
996         collection: {
997           "#{attribute}": 10
998         }
999       }
1000       assert_response 200
1001       assert_equal val, json_response[attribute]
1002     end
1003   end
1004
1005   [
1006     ['file_count', 1],
1007     ['file_size_total', 34]
1008   ].each do |attribute, val|
1009     test "update collection with #{attribute} and manifest and expect manifest values" do
1010       authorize_with :active
1011       post :update, {
1012         id: collections(:collection_owned_by_active_with_file_stats).uuid,
1013         collection: {
1014           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
1015           "#{attribute}": 10
1016         }
1017       }
1018       assert_response 200
1019       assert_equal val, json_response[attribute]
1020     end
1021   end
1022
1023   [
1024     ". 0:0:foo.txt",
1025     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1026     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1027     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1028   ].each do |manifest_text|
1029     test "create collection with invalid manifest #{manifest_text} and expect error" do
1030       authorize_with :active
1031       post :create, {
1032         collection: {
1033           manifest_text: manifest_text,
1034           portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0"
1035         }
1036       }
1037       assert_response 422
1038       response_errors = json_response['errors']
1039       assert_not_nil response_errors, 'Expected error in response'
1040       assert(response_errors.first.include?('Invalid manifest'),
1041              "Expected 'Invalid manifest' error in #{response_errors.first}")
1042     end
1043   end
1044
1045   [
1046     [nil, "d41d8cd98f00b204e9800998ecf8427e+0"],
1047     ["", "d41d8cd98f00b204e9800998ecf8427e+0"],
1048     [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"],
1049   ].each do |manifest_text, pdh|
1050     test "create collection with valid manifest #{manifest_text.inspect} and expect success" do
1051       authorize_with :active
1052       post :create, {
1053         collection: {
1054           manifest_text: manifest_text,
1055           portable_data_hash: pdh
1056         }
1057       }
1058       assert_response 200
1059     end
1060   end
1061
1062   [
1063     ". 0:0:foo.txt",
1064     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1065     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1066     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1067   ].each do |manifest_text|
1068     test "update collection with invalid manifest #{manifest_text} and expect error" do
1069       authorize_with :active
1070       post :update, {
1071         id: 'zzzzz-4zz18-bv31uwvy3neko21',
1072         collection: {
1073           manifest_text: manifest_text,
1074         }
1075       }
1076       assert_response 422
1077       response_errors = json_response['errors']
1078       assert_not_nil response_errors, 'Expected error in response'
1079       assert(response_errors.first.include?('Invalid manifest'),
1080              "Expected 'Invalid manifest' error in #{response_errors.first}")
1081     end
1082   end
1083
1084   [
1085     nil,
1086     "",
1087     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
1088   ].each do |manifest_text|
1089     test "update collection with valid manifest #{manifest_text.inspect} and expect success" do
1090       authorize_with :active
1091       post :update, {
1092         id: 'zzzzz-4zz18-bv31uwvy3neko21',
1093         collection: {
1094           manifest_text: manifest_text,
1095         }
1096       }
1097       assert_response 200
1098     end
1099   end
1100
1101   test 'get trashed collection with include_trash' do
1102     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1103     authorize_with :active
1104     get :show, {
1105       id: uuid,
1106       include_trash: true,
1107     }
1108     assert_response 200
1109   end
1110
1111   [:admin, :active].each do |user|
1112     test "get trashed collection via filters and #{user} user" do
1113       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1114       authorize_with user
1115       get :index, {
1116         filters: [["current_version_uuid", "=", uuid]],
1117         include_trash: true,
1118       }
1119       assert_response 200
1120       # Only the current version is returned
1121       assert_equal 1, json_response["items"].size
1122     end
1123   end
1124
1125   [:admin, :active].each do |user|
1126     test "get trashed collection via filters and #{user} user, including its past versions" do
1127       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1128       authorize_with :admin
1129       get :index, {
1130         filters: [["current_version_uuid", "=", uuid]],
1131         include_trash: true,
1132         include_old_versions: true,
1133       }
1134       assert_response 200
1135       # Both current & past version are returned
1136       assert_equal 2, json_response["items"].size
1137     end
1138   end
1139
1140   test "trash collection also trash its past versions" do
1141     uuid = collections(:collection_owned_by_active).uuid
1142     authorize_with :active
1143     versions = Collection.where(current_version_uuid: uuid)
1144     assert_equal 2, versions.size
1145     versions.each do |col|
1146       refute col.is_trashed
1147     end
1148     post :trash, {
1149       id: uuid,
1150     }
1151     assert_response 200
1152     versions = Collection.where(current_version_uuid: uuid)
1153     assert_equal 2, versions.size
1154     versions.each do |col|
1155       assert col.is_trashed
1156     end
1157   end
1158
1159   test 'get trashed collection without include_trash' do
1160     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1161     authorize_with :active
1162     get :show, {
1163       id: uuid,
1164     }
1165     assert_response 404
1166   end
1167
1168   test 'trash collection using http DELETE verb' do
1169     uuid = collections(:collection_owned_by_active).uuid
1170     authorize_with :active
1171     delete :destroy, {
1172       id: uuid,
1173     }
1174     assert_response 200
1175     c = Collection.find_by_uuid(uuid)
1176     assert_operator c.trash_at, :<, db_current_time
1177     assert_equal c.delete_at, c.trash_at + Rails.configuration.blob_signature_ttl
1178   end
1179
1180   test 'delete long-trashed collection immediately using http DELETE verb' do
1181     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1182     authorize_with :active
1183     delete :destroy, {
1184       id: uuid,
1185     }
1186     assert_response 200
1187     c = Collection.find_by_uuid(uuid)
1188     assert_operator c.trash_at, :<, db_current_time
1189     assert_operator c.delete_at, :<, db_current_time
1190   end
1191
1192   ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection
1193    :empty_collection_name_in_active_user_home_project,
1194   ].each do |fixture|
1195     test "trash collection #{fixture} via trash action with grace period" do
1196       if fixture.is_a? String
1197         uuid = fixture
1198       else
1199         uuid = collections(fixture).uuid
1200       end
1201       authorize_with :active
1202       time_before_trashing = db_current_time
1203       post :trash, {
1204         id: uuid,
1205       }
1206       assert_response 200
1207       c = Collection.find_by_uuid(uuid)
1208       assert_operator c.trash_at, :<, db_current_time
1209       assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.default_trash_lifetime
1210     end
1211   end
1212
1213   test 'untrash a trashed collection' do
1214     authorize_with :active
1215     post :untrash, {
1216       id: collections(:expired_collection).uuid,
1217     }
1218     assert_response 200
1219     assert_equal false, json_response['is_trashed']
1220     assert_nil json_response['trash_at']
1221   end
1222
1223   test 'untrash error on not trashed collection' do
1224     authorize_with :active
1225     post :untrash, {
1226       id: collections(:collection_owned_by_active).uuid,
1227     }
1228     assert_response 422
1229   end
1230
1231   [:active, :admin].each do |user|
1232     test "get trashed collections as #{user}" do
1233       authorize_with user
1234       get :index, {
1235         filters: [["is_trashed", "=", true]],
1236         include_trash: true,
1237       }
1238       assert_response :success
1239
1240       items = []
1241       json_response["items"].each do |coll|
1242         items << coll['uuid']
1243       end
1244
1245       assert_includes(items, collections('unique_expired_collection')['uuid'])
1246       if user == :admin
1247         assert_includes(items, collections('unique_expired_collection2')['uuid'])
1248       else
1249         assert_not_includes(items, collections('unique_expired_collection2')['uuid'])
1250       end
1251     end
1252   end
1253
1254   test 'untrash collection with same name as another with no ensure unique name' do
1255     authorize_with :active
1256     post :untrash, {
1257       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1258     }
1259     assert_response 422
1260   end
1261
1262   test 'untrash collection with same name as another with ensure unique name' do
1263     authorize_with :active
1264     post :untrash, {
1265       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1266       ensure_unique_name: true
1267     }
1268     assert_response 200
1269     assert_equal false, json_response['is_trashed']
1270     assert_nil json_response['trash_at']
1271     assert_nil json_response['delete_at']
1272     assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
1273   end
1274
1275   test 'cannot show collection in trashed subproject' do
1276     authorize_with :active
1277     get :show, {
1278       id: collections(:collection_in_trashed_subproject).uuid,
1279       format: :json
1280     }
1281     assert_response 404
1282   end
1283
1284   test 'can show collection in untrashed subproject' do
1285     authorize_with :active
1286     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1287     get :show, {
1288       id: collections(:collection_in_trashed_subproject).uuid,
1289       format: :json,
1290     }
1291     assert_response :success
1292   end
1293
1294   test 'cannot index collection in trashed subproject' do
1295     authorize_with :active
1296     get :index, { limit: 1000 }
1297     assert_response :success
1298     item_uuids = json_response['items'].map do |item|
1299       item['uuid']
1300     end
1301     assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1302   end
1303
1304   test 'can index collection in untrashed subproject' do
1305     authorize_with :active
1306     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1307     get :index, { limit: 1000 }
1308     assert_response :success
1309     item_uuids = json_response['items'].map do |item|
1310       item['uuid']
1311     end
1312     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1313   end
1314
1315   test 'can index trashed subproject collection with include_trash' do
1316     authorize_with :active
1317     get :index, {
1318           include_trash: true,
1319           limit: 1000
1320         }
1321     assert_response :success
1322     item_uuids = json_response['items'].map do |item|
1323       item['uuid']
1324     end
1325     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1326   end
1327
1328   test 'can get collection with past versions' do
1329     authorize_with :active
1330     get :index, {
1331       filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]],
1332       include_old_versions: true
1333     }
1334     assert_response :success
1335     assert_equal 2, assigns(:objects).length
1336     assert_equal 2, json_response['items_available']
1337     assert_equal 2, json_response['items'].count
1338     json_response['items'].each do |c|
1339       assert_equal collections(:collection_owned_by_active).uuid,
1340                    c['current_version_uuid'],
1341                    'response includes a version from a different collection'
1342     end
1343   end
1344
1345   test 'can get old version collection by uuid' do
1346     authorize_with :active
1347     get :show, {
1348       id: collections(:collection_owned_by_active_past_version_1).uuid,
1349     }
1350     assert_response :success
1351     assert_equal collections(:collection_owned_by_active_past_version_1).name,
1352                   json_response['name']
1353   end
1354
1355   test 'version and current_version_uuid are ignored at creation time' do
1356     permit_unsigned_manifests
1357     authorize_with :active
1358     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
1359     post :create, {
1360       collection: {
1361         name: 'Test collection',
1362         version: 42,
1363         current_version_uuid: collections(:collection_owned_by_active).uuid,
1364         manifest_text: manifest_text,
1365       }
1366     }
1367     assert_response :success
1368     resp = JSON.parse(@response.body)
1369     assert_equal 1, resp['version']
1370     assert_equal resp['uuid'], resp['current_version_uuid']
1371   end
1372
1373   test "update collection with versioning enabled" do
1374     Rails.configuration.collection_versioning = true
1375     Rails.configuration.preserve_version_if_idle = 1 # 1 second
1376
1377     col = collections(:collection_owned_by_active)
1378     assert_equal 2, col.version
1379     assert col.modified_at < Time.now - 1.second
1380
1381     token = api_client_authorizations(:active).v2token
1382     signed = Blob.sign_locator(
1383       'acbd18db4cc2f85cedef654fccc4a4d8+3',
1384       key: Rails.configuration.blob_signing_key,
1385       api_token: token)
1386     authorize_with_token token
1387     put :update, {
1388           id: col.uuid,
1389           collection: {
1390             manifest_text: ". #{signed} 0:3:foo.txt\n",
1391           },
1392         }
1393     assert_response :success
1394     assert_equal 3, json_response['version']
1395   end
1396 end