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