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