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