1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
7 class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
10 PERM_TOKEN_RE = /\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/
12 def permit_unsigned_manifests isok=true
13 # Set security model for the life of a test.
14 Rails.configuration.Collections.BlobSigning = !isok
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")
23 bare = tok.gsub(/\+A[^\+]*/, '').sub(/^ /, '')
24 exp = tok[/\+A[[:xdigit:]]+@([[:xdigit:]]+)/, 1].to_i(16)
25 sig = Blob.sign_locator(
27 key: Rails.configuration.Collections.BlobSigningKey,
29 api_token: token)[/\+A[^\+]*/, 0]
30 assert_includes tok, sig
35 def assert_unsigned_manifest resp, label=''
36 txt = resp['unsigned_manifest_text']
37 assert_not_nil(txt, "#{label} unsigned_manifest_text was nil")
39 txt.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
41 refute_match(PERM_TOKEN_RE, tok,
42 "Locator in #{label} unsigned_manifest_text was signed: #{tok}")
47 test "should get index" do
48 authorize_with :active
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")
58 test "get index with include_old_versions" do
59 authorize_with :active
61 include_old_versions: true
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")
69 test "collections.get returns signed locators, and no unsigned_manifest_text" do
70 permit_unsigned_manifests
71 authorize_with :active
72 get :show, params: {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'
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, params: {id: collections(:foo_file).uuid}
83 assert_response :success
84 assert_signed_manifest json_response['manifest_text'], 'foo_file', token: token
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.Collections.BlobSigningKey,
93 authorize_with_token token
94 put :update, params: {
95 id: collections(:collection_owned_by_active).uuid,
97 manifest_text: ". #{signed} 0:3:foo.txt\n",
100 assert_response :success
101 assert_signed_manifest json_response['manifest_text'], 'updated', token: token
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, params: {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']
119 test "index with unsigned_manifest_text selected returns only unsigned locators" do
120 authorize_with :active
121 get :index, params: {select: ['unsigned_manifest_text']}
122 assert_response :success
123 assert_operator json_response["items"].count, :>, 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']
130 assert_operator locs, :>, 0, "no locators found in any manifests"
133 test 'index without select returns everything except manifest' do
134 authorize_with :active
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')
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, params: {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')
162 ["uuid", "manifest_text"],
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, params: {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')
176 [0,1,2].each do |limit|
177 test "get index with limit=#{limit}" do
178 authorize_with :active
179 get :index, params: {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']
187 test "items.count == items_available" do
188 authorize_with :active
189 get :index, params: {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
198 test "items.count == items_available with filters" do
199 authorize_with :active
200 get :index, params: {
202 filters: [['uuid','=',collections(:foo_file).uuid]]
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
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, params: {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']
221 def request_capped_index(params={})
222 authorize_with :user1_with_load
223 coll1 = collections(:collection_1_of_201)
224 Rails.configuration.API.MaxIndexDatabaseRead =
225 yield(coll1.manifest_text.size)
226 get :index, params: {
227 select: %w(uuid manifest_text),
228 filters: [["owner_uuid", "=", coll1.owner_uuid]],
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"])
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),
247 filters: [['name','>=',c.name]]) do |_|
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"])
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"])
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"])
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/
282 assert_equal(11, json_response["limit"])
283 assert_equal(201, json_response["items_available"])
286 test "admin can create collection with unsigned manifest" do
287 authorize_with :admin
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
296 test_collection[:portable_data_hash] =
297 Digest::MD5.hexdigest(test_collection[:manifest_text]) +
299 test_collection[:manifest_text].length.to_s
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, params: {
305 collection: post_collection
308 assert_response :success
309 assert_nil assigns(:objects)
311 response_collection = assigns(:object)
313 stored_collection = Collection.select([:uuid, :portable_data_hash, :manifest_text]).
314 where(portable_data_hash: response_collection['portable_data_hash']).first
316 assert_equal test_collection[:portable_data_hash], stored_collection['portable_data_hash']
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
323 # TBD: create action should add permission signatures to manifest_text in the response,
324 # and we need to check those permission signatures here.
327 [:admin, :active].each do |user|
328 test "#{user} can get collection using portable data hash" do
331 foo_collection = collections(:foo_file)
333 # Get foo_file using its portable data hash
335 id: foo_collection[:portable_data_hash]
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]
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
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, params: {
356 owner_uuid: 'zzzzz-j7d0g-rew6elm53kancon',
357 manifest_text: manifest_text,
358 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
361 assert_response :success
362 resp = JSON.parse(@response.body)
363 assert_equal 'zzzzz-j7d0g-rew6elm53kancon', resp['owner_uuid']
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, params: {
372 owner_uuid: 'zzzzz-tpzed-000000000000000',
373 manifest_text: manifest_text,
374 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
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}")
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"
391 manifest_text = Collection.sign_manifest manifest_text, api_token(:active)
393 post :create, params: {
395 owner_uuid: users(:active).uuid,
396 manifest_text: manifest_text,
397 name: "owned_by_active"
399 ensure_unique_name: true
401 assert_response :success
402 assert_match /^owned_by_active \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
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, params: {
412 owner_uuid: groups(:active_user_has_can_manage).uuid,
413 manifest_text: manifest_text,
414 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
417 assert_response :success
418 resp = JSON.parse(@response.body)
419 assert_equal groups(:active_user_has_can_manage).uuid, resp['owner_uuid']
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, params: {
428 owner_uuid: groups(:all_users).uuid,
429 manifest_text: manifest_text,
430 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
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, params: {
442 owner_uuid: groups(:public).uuid,
443 manifest_text: manifest_text,
444 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
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, params: {
456 owner_uuid: 'zzzzz-j7d0g-it30l961gq3t0oi',
457 manifest_text: manifest_text,
458 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
461 assert_response :success
464 test "should create with collection passed as json" do
465 permit_unsigned_manifests
466 authorize_with :active
467 post :create, params: {
470 "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",\
471 "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
475 assert_response :success
478 test "should fail to create with checksum mismatch" do
479 permit_unsigned_manifests
480 authorize_with :active
481 post :create, params: {
484 "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:bar.txt\n",\
485 "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
492 test "collection UUID is normalized when created" do
493 permit_unsigned_manifests
494 authorize_with :active
495 post :create, params: {
497 manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
498 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47+Khint+Xhint+Zhint"
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']
507 test "get full provenance for baz file" do
508 authorize_with :active
509 get :provenance, params: {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
519 test "get no provenance for foo file" do
520 # spectator user cannot even see baz collection
521 authorize_with :spectator
522 get :provenance, params: {id: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'}
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, params: {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
539 test "search collections with 'any' operator" do
540 expect_pdh = collections(:docker_image).portable_data_hash
541 authorize_with :active
542 get :index, params: {
543 where: { any: ['contains', expect_pdh[5..25]] }
545 assert_response :success
546 found = assigns(:objects)
547 assert_equal 1, found.count
548 assert_equal expect_pdh, found.first.portable_data_hash
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
556 d41d8cd98f00b204e9800998ecf8427e+0
557 acbd18db4cc2f85cedef654fccc4a4d8+3
558 ea10d51bcf88862dbcc36eb292017dfd+45)
560 unsigned_manifest = locators.map { |loc|
561 ". " + loc + " 0:0:foo.txt\n"
563 manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
565 unsigned_manifest.length.to_s
567 # Build a manifest with both signed and unsigned locators.
569 key: Rails.configuration.Collections.BlobSigningKey,
570 api_token: api_token(:active),
572 signed_locators = locators.collect do |x|
573 Blob.sign_locator x, signing_opts
576 # Leave a non-empty blob unsigned.
577 signed_locators[1] = locators[1]
579 # Leave the empty blob unsigned. This should still be allowed.
580 signed_locators[0] = locators[0]
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"
587 post :create, params: {
589 manifest_text: signed_manifest,
590 portable_data_hash: manifest_uuid,
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)
601 assert Blob.verify_signature m[0], signing_opts
607 test "create collection with signed manifest and explicit TTL" do
608 authorize_with :active
610 d41d8cd98f00b204e9800998ecf8427e+0
611 acbd18db4cc2f85cedef654fccc4a4d8+3
612 ea10d51bcf88862dbcc36eb292017dfd+45)
614 unsigned_manifest = locators.map { |loc|
615 ". " + loc + " 0:0:foo.txt\n"
617 manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
619 unsigned_manifest.length.to_s
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.
625 key: Rails.configuration.Collections.BlobSigningKey,
626 api_token: api_token(:active),
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"
634 post :create, params: {
636 manifest_text: signed_manifest,
637 portable_data_hash: manifest_uuid,
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)
648 assert Blob.verify_signature m[0], signing_opts
653 test "create fails with invalid signature" do
654 authorize_with :active
656 key: Rails.configuration.Collections.BlobSigningKey,
657 api_token: api_token(:active),
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)
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) +
670 unsigned_manifest.length.to_s
672 bad_manifest = ". #{bad_locator} 0:0:foo.txt\n"
673 post :create, params: {
675 manifest_text: bad_manifest,
676 portable_data_hash: manifest_uuid
683 test "create fails with uuid of signed manifest" do
684 authorize_with :active
686 key: Rails.configuration.Collections.BlobSigningKey,
687 api_token: api_token(:active),
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) +
695 signed_manifest.length.to_s
697 post :create, params: {
699 manifest_text: signed_manifest,
700 portable_data_hash: manifest_uuid
707 test "reject manifest with unsigned block as stream name" do
708 authorize_with :active
709 post :create, params: {
711 manifest_text: "00000000000000000000000000000000+1234 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n"
714 assert_includes [422, 403], response.code.to_i
717 test "multiple locators per line" do
718 permit_unsigned_manifests
719 authorize_with :active
721 d41d8cd98f00b204e9800998ecf8427e+0
722 acbd18db4cc2f85cedef654fccc4a4d8+3
723 ea10d51bcf88862dbcc36eb292017dfd+45)
725 manifest_text = [".", *locators, "0:0:foo.txt\n"].join(" ")
726 manifest_uuid = Digest::MD5.hexdigest(manifest_text) +
728 manifest_text.length.to_s
731 manifest_text: manifest_text,
732 portable_data_hash: manifest_uuid,
734 post_collection = Marshal.load(Marshal.dump(test_collection))
735 post :create, params: {
736 collection: post_collection
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']
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
749 test "multiple signed locators per line" do
750 permit_unsigned_manifests
751 authorize_with :active
753 d41d8cd98f00b204e9800998ecf8427e+0
754 acbd18db4cc2f85cedef654fccc4a4d8+3
755 ea10d51bcf88862dbcc36eb292017dfd+45)
758 key: Rails.configuration.Collections.BlobSigningKey,
759 api_token: api_token(:active),
762 unsigned_manifest = [".", *locators, "0:0:foo.txt\n"].join(" ")
763 manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
765 unsigned_manifest.length.to_s
767 signed_locators = locators.map { |loc| Blob.sign_locator loc, signing_opts }
768 signed_manifest = [".", *signed_locators, "0:0:foo.txt\n"].join(" ")
770 post :create, params: {
772 manifest_text: signed_manifest,
773 portable_data_hash: manifest_uuid,
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
790 assert_equal locators.count, returned_locator_count
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, params: {
800 manifest_text: unsigned_manifest,
801 portable_data_hash: manifest_uuid,
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"
810 test 'List expired collection returns empty list' do
811 authorize_with :active
812 get :index, params: {
813 where: {name: 'expired_collection'},
815 assert_response :success
816 found = assigns(:objects)
817 assert_equal 0, found.count
820 test 'Show expired collection returns 404' do
821 authorize_with :active
823 id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
828 test 'Update expired collection returns 404' do
829 authorize_with :active
830 post :update, params: {
831 id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
833 name: "still expired"
839 test 'List collection with future expiration time succeeds' do
840 authorize_with :active
841 get :index, params: {
842 where: {name: 'collection_expires_in_future'},
844 found = assigns(:objects)
845 assert_equal 1, found.count
849 test 'Show collection with future expiration time succeeds' do
850 authorize_with :active
852 id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
854 assert_response :success
857 test 'Update collection with future expiration time succeeds' do
858 authorize_with :active
859 post :update, params: {
860 id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
862 name: "still not expired"
865 assert_response :success
868 test "get collection and verify that file_names is not included" do
869 authorize_with :active
870 get :show, params: {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']
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
888 description = 'here is a collection with a very large description'
889 while description.length < description_size
890 description = description + description
893 post :create, params: {
895 manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n",
896 description: description,
900 assert_response expected_response
904 [1, 5, nil].each do |ask|
905 test "Set replication_desired=#{ask.inspect}" do
906 Rails.configuration.Collections.DefaultReplication = 2
907 authorize_with :active
908 put :update, params: {
909 id: collections(:replication_undesired_unconfirmed).uuid,
911 replication_desired: ask,
914 assert_response :success
915 assert_equal ask, json_response['replication_desired']
919 test "get collection with properties" do
920 authorize_with :active
921 get :show, params: {id: collections(:collection_with_one_property).uuid}
922 assert_response :success
923 assert_not_nil json_response['uuid']
924 assert_equal 'value1', json_response['properties']['property1']
928 {'property_1' => 'value_1'},
929 "{\"property_1\":\"value_1\"}",
931 test "create collection with valid properties param #{p.inspect}" do
932 authorize_with :active
933 manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
934 post :create, params: {
936 manifest_text: manifest_text,
937 portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
941 assert_response :success
942 assert_not_nil json_response['uuid']
943 assert_equal Hash, json_response['properties'].class, 'Collection properties attribute should be of type hash'
944 assert_equal 'value_1', json_response['properties']['property_1']
953 '["json", "encoded", "array"]',
955 test "create collection with non-valid properties param #{p.inspect}" do
956 authorize_with :active
957 post :create, params: {
959 name: "test collection with non-valid properties param '#{p.inspect}'",
965 response_errors = json_response['errors']
966 assert_not_nil response_errors, 'Expected error in response'
971 [". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n", 1, 34],
972 [". 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],
973 [". d41d8cd98f00b204e9800998ecf8427e 0:0:.\n", 0, 0]
974 ].each do |manifest, count, size|
975 test "create collection with valid manifest #{manifest} and expect file stats" do
976 authorize_with :active
977 post :create, params: {
979 manifest_text: manifest
983 assert_equal count, json_response['file_count']
984 assert_equal size, json_response['file_size_total']
988 test "update collection manifest and expect new file stats" do
989 authorize_with :active
990 post :update, params: {
991 id: collections(:collection_owned_by_active_with_file_stats).uuid,
993 manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n"
997 assert_equal 1, json_response['file_count']
998 assert_equal 34, json_response['file_size_total']
1003 ['file_size_total', 34]
1004 ].each do |attribute, val|
1005 test "create collection with #{attribute} and expect overwrite" do
1006 authorize_with :active
1007 post :create, params: {
1009 manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
1014 assert_equal val, json_response[attribute]
1020 ['file_size_total', 3]
1021 ].each do |attribute, val|
1022 test "update collection with #{attribute} and expect ignore" do
1023 authorize_with :active
1024 post :update, params: {
1025 id: collections(:collection_owned_by_active_with_file_stats).uuid,
1031 assert_equal val, json_response[attribute]
1037 ['file_size_total', 34]
1038 ].each do |attribute, val|
1039 test "update collection with #{attribute} and manifest and expect manifest values" do
1040 authorize_with :active
1041 post :update, params: {
1042 id: collections(:collection_owned_by_active_with_file_stats).uuid,
1044 manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
1049 assert_equal val, json_response[attribute]
1055 ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1056 "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1057 ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1058 ].each do |manifest_text|
1059 test "create collection with invalid manifest #{manifest_text} and expect error" do
1060 authorize_with :active
1061 post :create, params: {
1063 manifest_text: manifest_text,
1064 portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0"
1068 response_errors = json_response['errors']
1069 assert_not_nil response_errors, 'Expected error in response'
1070 assert(response_errors.first.include?('Invalid manifest'),
1071 "Expected 'Invalid manifest' error in #{response_errors.first}")
1076 [nil, "d41d8cd98f00b204e9800998ecf8427e+0"],
1077 ["", "d41d8cd98f00b204e9800998ecf8427e+0"],
1078 [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"],
1079 ].each do |manifest_text, pdh|
1080 test "create collection with valid manifest #{manifest_text.inspect} and expect success" do
1081 authorize_with :active
1082 post :create, params: {
1084 manifest_text: manifest_text,
1085 portable_data_hash: pdh
1094 ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1095 "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1096 ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1097 ].each do |manifest_text|
1098 test "update collection with invalid manifest #{manifest_text} and expect error" do
1099 authorize_with :active
1100 post :update, params: {
1101 id: 'zzzzz-4zz18-bv31uwvy3neko21',
1103 manifest_text: manifest_text,
1107 response_errors = json_response['errors']
1108 assert_not_nil response_errors, 'Expected error in response'
1109 assert(response_errors.first.include?('Invalid manifest'),
1110 "Expected 'Invalid manifest' error in #{response_errors.first}")
1117 ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
1118 ].each do |manifest_text|
1119 test "update collection with valid manifest #{manifest_text.inspect} and expect success" do
1120 authorize_with :active
1121 post :update, params: {
1122 id: 'zzzzz-4zz18-bv31uwvy3neko21',
1124 manifest_text: manifest_text,
1131 [true, false].each do |include_trash|
1132 test "get trashed collection with include_trash=#{include_trash}" do
1133 uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1134 authorize_with :active
1135 get :show, params: {
1137 include_trash: include_trash,
1147 [:admin, :active].each do |user|
1148 test "get trashed collection via filters and #{user} user" do
1149 uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1151 get :index, params: {
1152 filters: [["current_version_uuid", "=", uuid]],
1153 include_trash: true,
1156 # Only the current version is returned
1157 assert_equal 1, json_response["items"].size
1161 [:admin, :active].each do |user|
1162 test "get trashed collection via filters and #{user} user, including its past versions" do
1163 uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1164 authorize_with :admin
1165 get :index, params: {
1166 filters: [["current_version_uuid", "=", uuid]],
1167 include_trash: true,
1168 include_old_versions: true,
1171 # Both current & past version are returned
1172 assert_equal 2, json_response["items"].size
1176 test "trash collection also trash its past versions" do
1177 uuid = collections(:collection_owned_by_active).uuid
1178 authorize_with :active
1179 versions = Collection.where(current_version_uuid: uuid)
1180 assert_equal 2, versions.size
1181 versions.each do |col|
1182 refute col.is_trashed
1184 post :trash, params: {
1188 versions = Collection.where(current_version_uuid: uuid)
1189 assert_equal 2, versions.size
1190 versions.each do |col|
1191 assert col.is_trashed
1195 test 'get trashed collection without include_trash' do
1196 uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1197 authorize_with :active
1198 get :show, params: {
1204 test 'trash collection using http DELETE verb' do
1205 uuid = collections(:collection_owned_by_active).uuid
1206 authorize_with :active
1207 delete :destroy, params: {
1211 c = Collection.find_by_uuid(uuid)
1212 assert_operator c.trash_at, :<, db_current_time
1213 assert_equal c.delete_at, c.trash_at + Rails.configuration.Collections.BlobSigningTTL
1216 test 'delete long-trashed collection immediately using http DELETE verb' do
1217 uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1218 authorize_with :active
1219 delete :destroy, params: {
1223 c = Collection.find_by_uuid(uuid)
1224 assert_operator c.trash_at, :<, db_current_time
1225 assert_operator c.delete_at, :<, db_current_time
1228 ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection
1229 :empty_collection_name_in_active_user_home_project,
1231 test "trash collection #{fixture} via trash action with grace period" do
1232 if fixture.is_a? String
1235 uuid = collections(fixture).uuid
1237 authorize_with :active
1238 time_before_trashing = db_current_time
1239 post :trash, params: {
1243 c = Collection.find_by_uuid(uuid)
1244 assert_operator c.trash_at, :<, db_current_time
1245 assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.Collections.DefaultTrashLifetime
1249 test 'untrash a trashed collection' do
1250 authorize_with :active
1251 post :untrash, params: {
1252 id: collections(:expired_collection).uuid,
1255 assert_equal false, json_response['is_trashed']
1256 assert_nil json_response['trash_at']
1259 test 'untrash error on not trashed collection' do
1260 authorize_with :active
1261 post :untrash, params: {
1262 id: collections(:collection_owned_by_active).uuid,
1267 [:active, :admin].each do |user|
1268 test "get trashed collections as #{user}" do
1270 get :index, params: {
1271 filters: [["is_trashed", "=", true]],
1272 include_trash: true,
1274 assert_response :success
1277 json_response["items"].each do |coll|
1278 items << coll['uuid']
1281 assert_includes(items, collections('unique_expired_collection')['uuid'])
1283 assert_includes(items, collections('unique_expired_collection2')['uuid'])
1285 assert_not_includes(items, collections('unique_expired_collection2')['uuid'])
1290 test 'untrash collection with same name as another with no ensure unique name' do
1291 authorize_with :active
1292 post :untrash, params: {
1293 id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1298 test 'untrash collection with same name as another with ensure unique name' do
1299 authorize_with :active
1300 post :untrash, params: {
1301 id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1302 ensure_unique_name: true
1305 assert_equal false, json_response['is_trashed']
1306 assert_nil json_response['trash_at']
1307 assert_nil json_response['delete_at']
1308 assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
1311 test 'cannot show collection in trashed subproject' do
1312 authorize_with :active
1313 get :show, params: {
1314 id: collections(:collection_in_trashed_subproject).uuid,
1320 test 'can show collection in untrashed subproject' do
1321 authorize_with :active
1322 Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1323 get :show, params: {
1324 id: collections(:collection_in_trashed_subproject).uuid,
1327 assert_response :success
1330 test 'cannot index collection in trashed subproject' do
1331 authorize_with :active
1332 get :index, params: { limit: 1000 }
1333 assert_response :success
1334 item_uuids = json_response['items'].map do |item|
1337 assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1340 test 'can index collection in untrashed subproject' do
1341 authorize_with :active
1342 Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1343 get :index, params: { limit: 1000 }
1344 assert_response :success
1345 item_uuids = json_response['items'].map do |item|
1348 assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1351 test 'can index trashed subproject collection with include_trash' do
1352 authorize_with :active
1353 get :index, params: {
1354 include_trash: true,
1357 assert_response :success
1358 item_uuids = json_response['items'].map do |item|
1361 assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1364 test 'can get collection with past versions' do
1365 authorize_with :active
1366 get :index, params: {
1367 filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]],
1368 include_old_versions: true
1370 assert_response :success
1371 assert_equal 2, assigns(:objects).length
1372 assert_equal 2, json_response['items_available']
1373 assert_equal 2, json_response['items'].count
1374 json_response['items'].each do |c|
1375 assert_equal collections(:collection_owned_by_active).uuid,
1376 c['current_version_uuid'],
1377 'response includes a version from a different collection'
1381 test 'can get old version collection by uuid' do
1382 authorize_with :active
1383 get :show, params: {
1384 id: collections(:collection_owned_by_active_past_version_1).uuid,
1386 assert_response :success
1387 assert_equal collections(:collection_owned_by_active_past_version_1).name,
1388 json_response['name']
1391 test 'version and current_version_uuid are ignored at creation time' do
1392 permit_unsigned_manifests
1393 authorize_with :active
1394 manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
1395 post :create, params: {
1397 name: 'Test collection',
1399 current_version_uuid: collections(:collection_owned_by_active).uuid,
1400 manifest_text: manifest_text,
1403 assert_response :success
1404 resp = JSON.parse(@response.body)
1405 assert_equal 1, resp['version']
1406 assert_equal resp['uuid'], resp['current_version_uuid']
1409 test "update collection with versioning enabled" do
1410 Rails.configuration.Collections.CollectionVersioning = true
1411 Rails.configuration.Collections.PreserveVersionIfIdle = 1 # 1 second
1413 col = collections(:collection_owned_by_active)
1414 assert_equal 2, col.version
1415 assert col.modified_at < Time.now - 1.second
1417 token = api_client_authorizations(:active).v2token
1418 signed = Blob.sign_locator(
1419 'acbd18db4cc2f85cedef654fccc4a4d8+3',
1420 key: Rails.configuration.Collections.BlobSigningKey,
1422 authorize_with_token token
1423 put :update, params: {
1426 manifest_text: ". #{signed} 0:3:foo.txt\n",
1429 assert_response :success
1430 assert_equal 3, json_response['version']
1433 test "delete collection with versioning enabled" do
1434 Rails.configuration.Collections.CollectionVersioning = true
1435 Rails.configuration.Collections.PreserveVersionIfIdle = 1 # 1 second
1437 col = collections(:collection_owned_by_active)
1438 assert_equal 2, col.version
1439 assert col.modified_at < Time.now - 1.second
1441 authorize_with(:active)
1442 post :trash, params: {
1445 assert_response :success
1446 assert_equal col.version, json_response['version'], 'Trashing a collection should not create a new version'