Merge branch '15397-remove-obsolete-apis'
[arvados.git] / services / api / test / functional / arvados / v1 / collections_controller_test.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require 'test_helper'
6
7 class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
8   include DbCurrentTime
9
10   PERM_TOKEN_RE = /\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/
11
12   def permit_unsigned_manifests isok=true
13     # Set security model for the life of a test.
14     Rails.configuration.Collections.BlobSigning = !isok
15   end
16
17   def assert_signed_manifest manifest_text, label='', token: false
18     assert_not_nil manifest_text, "#{label} manifest_text was nil"
19     manifest_text.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
20       assert_match(PERM_TOKEN_RE, tok,
21                    "Locator in #{label} manifest_text was not signed")
22       if token
23         bare = tok.gsub(/\+A[^\+]*/, '').sub(/^ /, '')
24         exp = tok[/\+A[[:xdigit:]]+@([[:xdigit:]]+)/, 1].to_i(16)
25         sig = Blob.sign_locator(
26           bare,
27           key: Rails.configuration.Collections.BlobSigningKey,
28           expire: exp,
29           api_token: token)[/\+A[^\+]*/, 0]
30         assert_includes tok, sig
31       end
32     end
33   end
34
35   def assert_unsigned_manifest txt, label=''
36     assert_not_nil(txt, "#{label} unsigned_manifest_text was nil")
37     locs = 0
38     txt.scan(/ [[:xdigit:]]{32}\S*/) do |tok|
39       locs += 1
40       refute_match(PERM_TOKEN_RE, tok,
41                    "Locator in #{label} unsigned_manifest_text was signed: #{tok}")
42     end
43     return locs
44   end
45
46   test "should get index" do
47     authorize_with :active
48     get :index
49     assert_response :success
50     assert(assigns(:objects).andand.any?, "no Collections returned in index")
51     refute(json_response["items"].any? { |c| c.has_key?("manifest_text") },
52            "basic Collections index included manifest_text")
53     refute(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
54            "basic Collections index included past version")
55   end
56
57   test "get index with include_old_versions" do
58     authorize_with :active
59     get :index, params: {
60       include_old_versions: true
61     }
62     assert_response :success
63     assert(assigns(:objects).andand.any?, "no Collections returned in index")
64     assert(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
65            "past version not included on index")
66   end
67
68   test "collections.get returns unsigned locators, and no unsigned_manifest_text" do
69     permit_unsigned_manifests
70     authorize_with :active
71     get :show, params: {id: collections(:foo_file).uuid}
72     assert_response :success
73     assert_unsigned_manifest json_response["manifest_text"], 'foo_file'
74     refute_includes json_response, 'unsigned_manifest_text'
75   end
76
77   ['v1token', 'v2token'].each do |token_method|
78     test "signatures with #{token_method} are accepted" do
79       token = api_client_authorizations(:active).send(token_method)
80       signed = Blob.sign_locator(
81         'acbd18db4cc2f85cedef654fccc4a4d8+3',
82         key: Rails.configuration.Collections.BlobSigningKey,
83         api_token: token)
84       authorize_with_token token
85       put :update, params: {
86             id: collections(:collection_owned_by_active).uuid,
87             collection: {
88               manifest_text: ". #{signed} 0:3:foo.txt\n",
89             },
90           }
91       assert_response :success
92       assert_unsigned_manifest json_response['manifest_text'], 'updated'
93     end
94   end
95
96   test "index with manifest_text selected returns unsigned locators" do
97     columns = %w(uuid owner_uuid manifest_text)
98     authorize_with :active
99     get :index, params: {select: columns}
100     assert_response :success
101     assert(assigns(:objects).andand.any?,
102            "no Collections returned for index with columns selected")
103     json_response["items"].each do |coll|
104       assert_equal(coll.keys - ['kind'], columns,
105                    "Collections index did not respect selected columns")
106       assert_unsigned_manifest coll['manifest_text'], coll['uuid']
107     end
108   end
109
110   test "index with unsigned_manifest_text selected returns only unsigned locators" do
111     authorize_with :active
112     get :index, params: {select: ['unsigned_manifest_text']}
113     assert_response :success
114     assert_operator json_response["items"].count, :>, 0
115     locs = 0
116     json_response["items"].each do |coll|
117       assert_equal(coll.keys - ['kind'], ['unsigned_manifest_text'],
118                    "Collections index did not respect selected columns")
119       assert_nil coll['manifest_text']
120       locs += assert_unsigned_manifest coll['unsigned_manifest_text'], coll['uuid']
121     end
122     assert_operator locs, :>, 0, "no locators found in any manifests"
123   end
124
125   test 'index without select returns everything except manifest' do
126     authorize_with :active
127     get :index
128     assert_response :success
129     assert json_response['items'].any?
130     json_response['items'].each do |coll|
131       assert_includes(coll.keys, 'uuid')
132       assert_includes(coll.keys, 'name')
133       assert_includes(coll.keys, 'created_at')
134       refute_includes(coll.keys, 'manifest_text')
135     end
136   end
137
138   ['', nil, false, 'null'].each do |select|
139     test "index with select=#{select.inspect} returns everything except manifest" do
140       authorize_with :active
141       get :index, params: {select: select}
142       assert_response :success
143       assert json_response['items'].any?
144       json_response['items'].each do |coll|
145         assert_includes(coll.keys, 'uuid')
146         assert_includes(coll.keys, 'name')
147         assert_includes(coll.keys, 'created_at')
148         refute_includes(coll.keys, 'manifest_text')
149       end
150     end
151   end
152
153   [["uuid"],
154    ["uuid", "manifest_text"],
155    '["uuid"]',
156    '["uuid", "manifest_text"]'].each do |select|
157     test "index with select=#{select.inspect} returns no name" do
158       authorize_with :active
159       get :index, params: {select: select}
160       assert_response :success
161       assert json_response['items'].any?
162       json_response['items'].each do |coll|
163         refute_includes(coll.keys, 'name')
164       end
165     end
166   end
167
168   [0,1,2].each do |limit|
169     test "get index with limit=#{limit}" do
170       authorize_with :active
171       get :index, params: {limit: limit}
172       assert_response :success
173       assert_equal limit, assigns(:objects).count
174       resp = JSON.parse(@response.body)
175       assert_equal limit, resp['limit']
176     end
177   end
178
179   test "items.count == items_available" do
180     authorize_with :active
181     get :index, params: {limit: 100000}
182     assert_response :success
183     resp = JSON.parse(@response.body)
184     assert_equal resp['items_available'], assigns(:objects).length
185     assert_equal resp['items_available'], resp['items'].count
186     unique_uuids = resp['items'].collect { |i| i['uuid'] }.compact.uniq
187     assert_equal unique_uuids.count, resp['items'].count
188   end
189
190   test "items.count == items_available with filters" do
191     authorize_with :active
192     get :index, params: {
193       limit: 100,
194       filters: [['uuid','=',collections(:foo_file).uuid]]
195     }
196     assert_response :success
197     assert_equal 1, assigns(:objects).length
198     assert_equal 1, json_response['items_available']
199     assert_equal 1, json_response['items'].count
200   end
201
202   test "get index with limit=2 offset=99999" do
203     # Assume there are not that many test fixtures.
204     authorize_with :active
205     get :index, params: {limit: 2, offset: 99999}
206     assert_response :success
207     assert_equal 0, assigns(:objects).count
208     resp = JSON.parse(@response.body)
209     assert_equal 2, resp['limit']
210     assert_equal 99999, resp['offset']
211   end
212
213   def request_capped_index(params={})
214     authorize_with :user1_with_load
215     coll1 = collections(:collection_1_of_201)
216     Rails.configuration.API.MaxIndexDatabaseRead =
217       yield(coll1.manifest_text.size)
218     get :index, params: {
219       select: %w(uuid manifest_text),
220       filters: [["owner_uuid", "=", coll1.owner_uuid]],
221       limit: 300,
222     }.merge(params)
223   end
224
225   test "index with manifest_text limited by max_index_database_read returns non-empty" do
226     request_capped_index() { |_| 1 }
227     assert_response :success
228     assert_equal(1, json_response["items"].size)
229     assert_equal(1, json_response["limit"])
230     assert_equal(201, json_response["items_available"])
231   end
232
233   test "max_index_database_read size check follows same order as real query" do
234     authorize_with :user1_with_load
235     txt = '.' + ' d41d8cd98f00b204e9800998ecf8427e+0'*1000 + " 0:0:empty.txt\n"
236     c = Collection.create! manifest_text: txt, name: '0000000000000000000'
237     request_capped_index(select: %w(uuid manifest_text name),
238                          order: ['name asc'],
239                          filters: [['name','>=',c.name]]) do |_|
240       txt.length - 1
241     end
242     assert_response :success
243     assert_equal(1, json_response["items"].size)
244     assert_equal(1, json_response["limit"])
245     assert_equal(c.uuid, json_response["items"][0]["uuid"])
246     # The effectiveness of the test depends on >1 item matching the filters.
247     assert_operator(1, :<, json_response["items_available"])
248   end
249
250   test "index with manifest_text limited by max_index_database_read" do
251     request_capped_index() { |size| (size * 3) + 1 }
252     assert_response :success
253     assert_equal(3, json_response["items"].size)
254     assert_equal(3, json_response["limit"])
255     assert_equal(201, json_response["items_available"])
256   end
257
258   test "max_index_database_read does not interfere with limit" do
259     request_capped_index(limit: 5) { |size| size * 20 }
260     assert_response :success
261     assert_equal(5, json_response["items"].size)
262     assert_equal(5, json_response["limit"])
263     assert_equal(201, json_response["items_available"])
264   end
265
266   test "max_index_database_read does not interfere with order" do
267     request_capped_index(select: %w(uuid manifest_text name),
268                          order: "name DESC") { |size| (size * 11) + 1 }
269     assert_response :success
270     assert_equal(11, json_response["items"].size)
271     assert_empty(json_response["items"].reject do |coll|
272                    coll["name"] =~ /^Collection_9/
273                  end)
274     assert_equal(11, json_response["limit"])
275     assert_equal(201, json_response["items_available"])
276   end
277
278   test "admin can create collection with unsigned manifest" do
279     authorize_with :admin
280     test_collection = {
281       manifest_text: <<-EOS
282 . d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt
283 . acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
284 . acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
285 ./baz acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:bar.txt
286 EOS
287     }
288     test_collection[:portable_data_hash] =
289       Digest::MD5.hexdigest(test_collection[:manifest_text]) +
290       '+' +
291       test_collection[:manifest_text].length.to_s
292
293     # post :create will modify test_collection in place, so we save a copy first.
294     # Hash.deep_dup is not sufficient as it preserves references of strings (??!?)
295     post_collection = Marshal.load(Marshal.dump(test_collection))
296     post :create, params: {
297       collection: post_collection
298     }
299
300     assert_response :success
301     assert_nil assigns(:objects)
302
303     response_collection = assigns(:object)
304
305     stored_collection = Collection.select([:uuid, :portable_data_hash, :manifest_text]).
306       where(portable_data_hash: response_collection['portable_data_hash']).first
307
308     assert_equal test_collection[:portable_data_hash], stored_collection['portable_data_hash']
309
310     # The manifest in the response will have had permission hints added.
311     # Remove any permission hints in the response before comparing it to the source.
312     stripped_manifest = stored_collection['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
313     assert_equal test_collection[:manifest_text], stripped_manifest
314
315     # TBD: create action should add permission signatures to manifest_text in the response,
316     # and we need to check those permission signatures here.
317   end
318
319   [:admin, :active].each do |user|
320     test "#{user} can get collection using portable data hash" do
321       authorize_with user
322
323       foo_collection = collections(:foo_file)
324
325       # Get foo_file using its portable data hash
326       get :show, params: {
327         id: foo_collection[:portable_data_hash]
328       }
329       assert_response :success
330       assert_not_nil assigns(:object)
331       resp = assigns(:object)
332       assert_equal foo_collection[:portable_data_hash], resp[:portable_data_hash]
333       assert_unsigned_manifest resp[:manifest_text]
334
335       # The manifest in the response will have had permission hints added.
336       # Remove any permission hints in the response before comparing it to the source.
337       stripped_manifest = resp[:manifest_text].gsub(/\+A[A-Za-z0-9@_-]+/, '')
338       assert_equal foo_collection[:manifest_text], stripped_manifest
339     end
340   end
341
342   test "create with owner_uuid set to owned group" do
343     permit_unsigned_manifests
344     authorize_with :active
345     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
346     post :create, params: {
347       collection: {
348         owner_uuid: 'zzzzz-j7d0g-rew6elm53kancon',
349         manifest_text: manifest_text,
350         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
351       }
352     }
353     assert_response :success
354     resp = JSON.parse(@response.body)
355     assert_equal 'zzzzz-j7d0g-rew6elm53kancon', resp['owner_uuid']
356   end
357
358   test "create fails with duplicate name" do
359     permit_unsigned_manifests
360     authorize_with :admin
361     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
362     post :create, params: {
363       collection: {
364         owner_uuid: 'zzzzz-tpzed-000000000000000',
365         manifest_text: manifest_text,
366         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
367         name: "foo_file"
368       }
369     }
370     assert_response 422
371     response_errors = json_response['errors']
372     assert_not_nil response_errors, 'Expected error in response'
373     assert(response_errors.first.include?('duplicate key'),
374            "Expected 'duplicate key' error in #{response_errors.first}")
375   end
376
377   [false, true].each do |ensure_unique_name|
378     test "create failure with duplicate name, ensure_unique_name #{ensure_unique_name}" do
379       authorize_with :active
380       post :create, params: {
381              collection: {
382                owner_uuid: users(:active).uuid,
383                manifest_text: "",
384                name: "this...............................................................................................................................................................................................................................................................name is too long"
385              },
386              ensure_unique_name: ensure_unique_name
387            }
388       assert_response 422
389       # check the real error isn't masked by an
390       # ensure_unique_name-related error (#19698)
391       assert_match /value too long for type/, json_response['errors'][0]
392     end
393   end
394
395   [false, true].each do |unsigned|
396     test "create with duplicate name, ensure_unique_name, unsigned=#{unsigned}" do
397       permit_unsigned_manifests unsigned
398       authorize_with :active
399       manifest_text = ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:foo.txt\n"
400       if !unsigned
401         manifest_text = Collection.sign_manifest_only_for_tests manifest_text, api_token(:active)
402       end
403       post :create, params: {
404         collection: {
405           owner_uuid: users(:active).uuid,
406           manifest_text: manifest_text,
407           name: "owned_by_active"
408         },
409         ensure_unique_name: true
410       }
411       assert_response :success
412       assert_match /^owned_by_active \(#{json_response['uuid'][-15..-1]}\)$/, json_response['name']
413     end
414   end
415
416   test "create with owner_uuid set to group i can_manage" do
417     permit_unsigned_manifests
418     authorize_with :active
419     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
420     post :create, params: {
421       collection: {
422         owner_uuid: groups(:active_user_has_can_manage).uuid,
423         manifest_text: manifest_text,
424         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
425       }
426     }
427     assert_response :success
428     resp = JSON.parse(@response.body)
429     assert_equal groups(:active_user_has_can_manage).uuid, resp['owner_uuid']
430   end
431
432   test "create with owner_uuid fails on group with only can_read permission" do
433     permit_unsigned_manifests
434     authorize_with :active
435     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
436     post :create, params: {
437       collection: {
438         owner_uuid: groups(:all_users).uuid,
439         manifest_text: manifest_text,
440         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
441       }
442     }
443     assert_response 403
444   end
445
446   test "create with owner_uuid fails on group with no permission" do
447     permit_unsigned_manifests
448     authorize_with :active
449     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
450     post :create, params: {
451       collection: {
452         owner_uuid: groups(:public).uuid,
453         manifest_text: manifest_text,
454         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
455       }
456     }
457     assert_response 422
458   end
459
460   test "admin create with owner_uuid set to group with no permission" do
461     permit_unsigned_manifests
462     authorize_with :admin
463     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
464     post :create, params: {
465       collection: {
466         owner_uuid: 'zzzzz-j7d0g-it30l961gq3t0oi',
467         manifest_text: manifest_text,
468         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
469       }
470     }
471     assert_response :success
472   end
473
474   test "should create with collection passed as json" do
475     permit_unsigned_manifests
476     authorize_with :active
477     post :create, params: {
478       collection: <<-EOS
479       {
480         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",\
481         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
482       }
483       EOS
484     }
485     assert_response :success
486   end
487
488   test "should fail to create with checksum mismatch" do
489     permit_unsigned_manifests
490     authorize_with :active
491     post :create, params: {
492       collection: <<-EOS
493       {
494         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:bar.txt\n",\
495         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
496       }
497       EOS
498     }
499     assert_response 422
500   end
501
502   test "collection UUID is normalized when created" do
503     permit_unsigned_manifests
504     authorize_with :active
505     post :create, params: {
506       collection: {
507         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
508         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47+Khint+Xhint+Zhint"
509       }
510     }
511     assert_response :success
512     assert_not_nil assigns(:object)
513     resp = JSON.parse(@response.body)
514     assert_equal "d30fe8ae534397864cb96c544f4cf102+47", resp['portable_data_hash']
515   end
516
517   test "get full provenance for baz file" do
518     authorize_with :active
519     get :provenance, params: {id: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'}
520     assert_response :success
521     resp = JSON.parse(@response.body)
522     assert_not_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # baz collection
523   end
524
525   test "get no provenance for foo file" do
526     # spectator user cannot even see baz collection
527     authorize_with :spectator
528     get :provenance, params: {id: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'}
529     assert_response 404
530   end
531
532   test "get partial provenance for baz file" do
533     # spectator user can see bar->baz job, but not foo->bar job
534     authorize_with :spectator
535     get :provenance, params: {id: 'ea10d51bcf88862dbcc36eb292017dfd+45'}
536     assert_response :success
537     resp = JSON.parse(@response.body)
538     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
539     assert_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # foo->bar
540   end
541
542   test "search collections with 'any' operator" do
543     expect_pdh = collections(:docker_image).portable_data_hash
544     authorize_with :active
545     get :index, params: {
546       where: { any: ['contains', expect_pdh[5..25]] }
547     }
548     assert_response :success
549     found = assigns(:objects)
550     assert_equal 1, found.count
551     assert_equal expect_pdh, found.first.portable_data_hash
552   end
553
554   [false, true].each do |permit_unsigned|
555     test "create collection with signed manifest, permit_unsigned=#{permit_unsigned}" do
556       permit_unsigned_manifests permit_unsigned
557       authorize_with :active
558       locators = %w(
559       d41d8cd98f00b204e9800998ecf8427e+0
560       acbd18db4cc2f85cedef654fccc4a4d8+3
561       ea10d51bcf88862dbcc36eb292017dfd+45)
562
563       unsigned_manifest = locators.map { |loc|
564         ". " + loc + " 0:0:foo.txt\n"
565       }.join()
566       manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
567         '+' +
568         unsigned_manifest.length.to_s
569
570       # Build a manifest with both signed and unsigned locators.
571       signing_opts = {
572         key: Rails.configuration.Collections.BlobSigningKey,
573         api_token: api_token(:active),
574       }
575       signed_locators = locators.collect do |x|
576         Blob.sign_locator x, signing_opts
577       end
578       if permit_unsigned
579         # Leave a non-empty blob unsigned.
580         signed_locators[1] = locators[1]
581       else
582         # Leave the empty blob unsigned. This should still be allowed.
583         signed_locators[0] = locators[0]
584       end
585       signed_manifest =
586         ". " + signed_locators[0] + " 0:0:foo.txt\n" +
587         ". " + signed_locators[1] + " 0:0:foo.txt\n" +
588         ". " + signed_locators[2] + " 0:0:foo.txt\n"
589
590       post :create, params: {
591         collection: {
592           manifest_text: signed_manifest,
593           portable_data_hash: manifest_uuid,
594         }
595       }
596       assert_response :success
597       assert_not_nil assigns(:object)
598       resp = JSON.parse(@response.body)
599       assert_equal manifest_uuid, resp['portable_data_hash']
600       # All of the signatures in the output must be valid.
601       resp['manifest_text'].lines.each do |entry|
602         m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
603         if m && m[0].index('+A')
604           assert Blob.verify_signature m[0], signing_opts
605         end
606       end
607     end
608   end
609
610   test "create collection with signed manifest and explicit TTL" do
611     authorize_with :active
612     locators = %w(
613       d41d8cd98f00b204e9800998ecf8427e+0
614       acbd18db4cc2f85cedef654fccc4a4d8+3
615       ea10d51bcf88862dbcc36eb292017dfd+45)
616
617     unsigned_manifest = locators.map { |loc|
618       ". " + loc + " 0:0:foo.txt\n"
619     }.join()
620     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
621       '+' +
622       unsigned_manifest.length.to_s
623
624     # build a manifest with both signed and unsigned locators.
625     # TODO(twp): in phase 4, all locators will need to be signed, so
626     # this test should break and will need to be rewritten. Issue #2755.
627     signing_opts = {
628       key: Rails.configuration.Collections.BlobSigningKey,
629       api_token: api_token(:active),
630       ttl: 3600   # 1 hour
631     }
632     signed_manifest =
633       ". " + locators[0] + " 0:0:foo.txt\n" +
634       ". " + Blob.sign_locator(locators[1], signing_opts) + " 0:0:foo.txt\n" +
635       ". " + Blob.sign_locator(locators[2], signing_opts) + " 0:0:foo.txt\n"
636
637     post :create, params: {
638       collection: {
639         manifest_text: signed_manifest,
640         portable_data_hash: manifest_uuid,
641       }
642     }
643     assert_response :success
644     assert_not_nil assigns(:object)
645     resp = JSON.parse(@response.body)
646     assert_equal manifest_uuid, resp['portable_data_hash']
647     # All of the signatures in the output must be valid.
648     resp['manifest_text'].lines.each do |entry|
649       m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
650       if m && m[0].index('+A')
651         assert Blob.verify_signature m[0], signing_opts
652       end
653     end
654   end
655
656   test "create fails with invalid signature" do
657     authorize_with :active
658     signing_opts = {
659       key: Rails.configuration.Collections.BlobSigningKey,
660       api_token: api_token(:active),
661     }
662
663     # Generate a locator with a bad signature.
664     unsigned_locator = "acbd18db4cc2f85cedef654fccc4a4d8+3"
665     bad_locator = unsigned_locator + "+Affffffffffffffffffffffffffffffffffffffff@ffffffff"
666     assert !Blob.verify_signature(bad_locator, signing_opts)
667
668     # Creating a collection with this locator should
669     # produce 403 Permission denied.
670     unsigned_manifest = ". #{unsigned_locator} 0:0:foo.txt\n"
671     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
672       '+' +
673       unsigned_manifest.length.to_s
674
675     bad_manifest = ". #{bad_locator} 0:0:foo.txt\n"
676     post :create, params: {
677       collection: {
678         manifest_text: bad_manifest,
679         portable_data_hash: manifest_uuid
680       }
681     }
682
683     assert_response 403
684   end
685
686   test "create fails with uuid of signed manifest" do
687     authorize_with :active
688     signing_opts = {
689       key: Rails.configuration.Collections.BlobSigningKey,
690       api_token: api_token(:active),
691     }
692
693     unsigned_locator = "d41d8cd98f00b204e9800998ecf8427e+0"
694     signed_locator = Blob.sign_locator(unsigned_locator, signing_opts)
695     signed_manifest = ". #{signed_locator} 0:0:foo.txt\n"
696     manifest_uuid = Digest::MD5.hexdigest(signed_manifest) +
697       '+' +
698       signed_manifest.length.to_s
699
700     post :create, params: {
701       collection: {
702         manifest_text: signed_manifest,
703         portable_data_hash: manifest_uuid
704       }
705     }
706
707     assert_response 422
708   end
709
710   test "reject manifest with unsigned block as stream name" do
711     authorize_with :active
712     post :create, params: {
713       collection: {
714         manifest_text: "00000000000000000000000000000000+1234 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n"
715       }
716     }
717     assert_includes [422, 403], response.code.to_i
718   end
719
720   test "multiple locators per line" do
721     permit_unsigned_manifests
722     authorize_with :active
723     locators = %w(
724       d41d8cd98f00b204e9800998ecf8427e+0
725       acbd18db4cc2f85cedef654fccc4a4d8+3
726       ea10d51bcf88862dbcc36eb292017dfd+45)
727
728     manifest_text = [".", *locators, "0:0:foo.txt\n"].join(" ")
729     manifest_uuid = Digest::MD5.hexdigest(manifest_text) +
730       '+' +
731       manifest_text.length.to_s
732
733     test_collection = {
734       manifest_text: manifest_text,
735       portable_data_hash: manifest_uuid,
736     }
737     post_collection = Marshal.load(Marshal.dump(test_collection))
738     post :create, params: {
739       collection: post_collection
740     }
741     assert_response :success
742     assert_not_nil assigns(:object)
743     resp = JSON.parse(@response.body)
744     assert_equal manifest_uuid, resp['portable_data_hash']
745
746     # The manifest in the response will have had permission hints added.
747     # Remove any permission hints in the response before comparing it to the source.
748     stripped_manifest = resp['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
749     assert_equal manifest_text, stripped_manifest
750   end
751
752   test 'Reject manifest with unsigned blob' do
753     permit_unsigned_manifests false
754     authorize_with :active
755     unsigned_manifest = ". 0cc175b9c0f1b6a831c399e269772661+1 0:1:a.txt\n"
756     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest)
757     post :create, params: {
758       collection: {
759         manifest_text: unsigned_manifest,
760         portable_data_hash: manifest_uuid,
761       }
762     }
763     assert_response 403,
764     "Creating a collection with unsigned blobs should respond 403"
765     assert_empty Collection.where('uuid like ?', manifest_uuid+'%'),
766     "Collection should not exist in database after failed create"
767   end
768
769   test 'List expired collection returns empty list' do
770     authorize_with :active
771     get :index, params: {
772       where: {name: 'expired_collection'},
773     }
774     assert_response :success
775     found = assigns(:objects)
776     assert_equal 0, found.count
777   end
778
779   test 'Show expired collection returns 404' do
780     authorize_with :active
781     get :show, params: {
782       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
783     }
784     assert_response 404
785   end
786
787   test 'Update expired collection returns 404' do
788     authorize_with :active
789     post :update, params: {
790       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
791       collection: {
792         name: "still expired"
793       }
794     }
795     assert_response 404
796   end
797
798   test 'List collection with future expiration time succeeds' do
799     authorize_with :active
800     get :index, params: {
801       where: {name: 'collection_expires_in_future'},
802     }
803     found = assigns(:objects)
804     assert_equal 1, found.count
805   end
806
807
808   test 'Show collection with future expiration time succeeds' do
809     authorize_with :active
810     get :show, params: {
811       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
812     }
813     assert_response :success
814   end
815
816   test 'Update collection with future expiration time succeeds' do
817     authorize_with :active
818     post :update, params: {
819       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
820       collection: {
821         name: "still not expired"
822       }
823     }
824     assert_response :success
825   end
826
827   test "get collection and verify that file_names is not included" do
828     authorize_with :active
829     get :show, params: {id: collections(:foo_file).uuid}
830     assert_response :success
831     assert_equal collections(:foo_file).uuid, json_response['uuid']
832     assert_nil json_response['file_names']
833     assert json_response['manifest_text']
834   end
835
836   [
837     [2**8, :success],
838     [2**18, 422],
839   ].each do |description_size, expected_response|
840     # Descriptions are not part of search indexes. Skip until
841     # full-text search is implemented, at which point replace with a
842     # search in description.
843     skip "create collection with description size #{description_size}
844           and expect response #{expected_response}" do
845       authorize_with :active
846
847       description = 'here is a collection with a very large description'
848       while description.length < description_size
849         description = description + description
850       end
851
852       post :create, params: {
853         collection: {
854           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n",
855           description: description,
856         }
857       }
858
859       assert_response expected_response
860     end
861   end
862
863   [1, 5, nil].each do |ask|
864     test "Set replication_desired=#{ask.inspect}" do
865       Rails.configuration.Collections.DefaultReplication = 2
866       authorize_with :active
867       put :update, params: {
868         id: collections(:replication_undesired_unconfirmed).uuid,
869         collection: {
870           replication_desired: ask,
871         },
872       }
873       assert_response :success
874       assert_equal ask, json_response['replication_desired']
875     end
876   end
877
878   test "get collection with properties" do
879     authorize_with :active
880     get :show, params: {id: collections(:collection_with_one_property).uuid}
881     assert_response :success
882     assert_not_nil json_response['uuid']
883     assert_equal 'value1', json_response['properties']['property1']
884   end
885
886   [
887     {'property_1' => 'value_1'},
888     "{\"property_1\":\"value_1\"}",
889   ].each do |p|
890     test "create collection with valid properties param #{p.inspect}" do
891       authorize_with :active
892       manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
893       post :create, params: {
894         collection: {
895           manifest_text: manifest_text,
896           portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
897           properties: p
898         }
899       }
900       assert_response :success
901       assert_not_nil json_response['uuid']
902       assert_equal Hash, json_response['properties'].class, 'Collection properties attribute should be of type hash'
903       assert_equal 'value_1', json_response['properties']['property_1']
904     end
905   end
906
907   [
908     false,
909     [],
910     42,
911     'some string',
912     '["json", "encoded", "array"]',
913   ].each do |p|
914     test "create collection with non-valid properties param #{p.inspect}" do
915       authorize_with :active
916       post :create, params: {
917         collection: {
918           name: "test collection with non-valid properties param '#{p.inspect}'",
919           manifest_text: '',
920           properties: p
921         }
922       }
923       assert_response 422
924       response_errors = json_response['errors']
925       assert_not_nil response_errors, 'Expected error in response'
926     end
927   end
928
929   [
930     [". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n", 1, 34],
931     [". 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],
932     [". d41d8cd98f00b204e9800998ecf8427e 0:0:.\n", 0, 0]
933   ].each do |manifest, count, size|
934     test "create collection with valid manifest #{manifest} and expect file stats" do
935       authorize_with :active
936       post :create, params: {
937         collection: {
938           manifest_text: manifest
939         }
940       }
941       assert_response 200
942       assert_equal count, json_response['file_count']
943       assert_equal size, json_response['file_size_total']
944     end
945   end
946
947   test "update collection manifest and expect new file stats" do
948     authorize_with :active
949     post :update, params: {
950       id: collections(:collection_owned_by_active_with_file_stats).uuid,
951       collection: {
952         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n"
953       }
954     }
955     assert_response 200
956     assert_equal 1, json_response['file_count']
957     assert_equal 34, json_response['file_size_total']
958   end
959
960   [
961     ['file_count', 1],
962     ['file_size_total', 34]
963   ].each do |attribute, val|
964     test "create collection with #{attribute} and expect overwrite" do
965       authorize_with :active
966       post :create, params: {
967         collection: {
968           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
969           "#{attribute}": 10
970         }
971       }
972       assert_response 200
973       assert_equal val, json_response[attribute]
974     end
975   end
976
977   [
978     ['file_count', 1],
979     ['file_size_total', 3]
980   ].each do |attribute, val|
981     test "update collection with #{attribute} and expect ignore" do
982       authorize_with :active
983       post :update, params: {
984         id: collections(:collection_owned_by_active_with_file_stats).uuid,
985         collection: {
986           "#{attribute}": 10
987         }
988       }
989       assert_response 200
990       assert_equal val, json_response[attribute]
991     end
992   end
993
994   [
995     ['file_count', 1],
996     ['file_size_total', 34]
997   ].each do |attribute, val|
998     test "update collection with #{attribute} and manifest and expect manifest values" do
999       authorize_with :active
1000       post :update, params: {
1001         id: collections(:collection_owned_by_active_with_file_stats).uuid,
1002         collection: {
1003           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
1004           "#{attribute}": 10
1005         }
1006       }
1007       assert_response 200
1008       assert_equal val, json_response[attribute]
1009     end
1010   end
1011
1012   [
1013     ". 0:0:foo.txt",
1014     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1015     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1016     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1017   ].each do |manifest_text|
1018     test "create collection with invalid manifest #{manifest_text} and expect error" do
1019       authorize_with :active
1020       post :create, params: {
1021         collection: {
1022           manifest_text: manifest_text,
1023           portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0"
1024         }
1025       }
1026       assert_response 422
1027       response_errors = json_response['errors']
1028       assert_not_nil response_errors, 'Expected error in response'
1029       assert(response_errors.first.include?('Invalid manifest'),
1030              "Expected 'Invalid manifest' error in #{response_errors.first}")
1031     end
1032   end
1033
1034   [
1035     [nil, "d41d8cd98f00b204e9800998ecf8427e+0"],
1036     ["", "d41d8cd98f00b204e9800998ecf8427e+0"],
1037     [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"],
1038   ].each do |manifest_text, pdh|
1039     test "create collection with valid manifest #{manifest_text.inspect} and expect success" do
1040       authorize_with :active
1041       post :create, params: {
1042         collection: {
1043           manifest_text: manifest_text,
1044           portable_data_hash: pdh
1045         }
1046       }
1047       assert_response 200
1048     end
1049   end
1050
1051   [
1052     ". 0:0:foo.txt",
1053     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1054     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1055     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1056   ].each do |manifest_text|
1057     test "update collection with invalid manifest #{manifest_text} and expect error" do
1058       authorize_with :active
1059       post :update, params: {
1060         id: 'zzzzz-4zz18-bv31uwvy3neko21',
1061         collection: {
1062           manifest_text: manifest_text,
1063         }
1064       }
1065       assert_response 422
1066       response_errors = json_response['errors']
1067       assert_not_nil response_errors, 'Expected error in response'
1068       assert(response_errors.first.include?('Invalid manifest'),
1069              "Expected 'Invalid manifest' error in #{response_errors.first}")
1070     end
1071   end
1072
1073   [
1074     nil,
1075     "",
1076     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
1077   ].each do |manifest_text|
1078     test "update collection with valid manifest #{manifest_text.inspect} and expect success" do
1079       authorize_with :active
1080       post :update, params: {
1081         id: 'zzzzz-4zz18-bv31uwvy3neko21',
1082         collection: {
1083           manifest_text: manifest_text,
1084         }
1085       }
1086       assert_response 200
1087     end
1088   end
1089
1090   [true, false].each do |include_trash|
1091     test "get trashed collection with include_trash=#{include_trash}" do
1092       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1093       authorize_with :active
1094       get :show, params: {
1095         id: uuid,
1096         include_trash: include_trash,
1097       }
1098       if include_trash
1099         assert_response 200
1100       else
1101         assert_response 404
1102       end
1103     end
1104   end
1105
1106   [:admin, :active].each do |user|
1107     test "get trashed collection via filters and #{user} user without including its past versions" do
1108       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1109       authorize_with user
1110       get :index, params: {
1111         filters: [["current_version_uuid", "=", uuid]],
1112         include_trash: true,
1113       }
1114       assert_response 200
1115       # Only the current version is returned
1116       assert_equal 1, json_response["items"].size
1117     end
1118   end
1119
1120   [:admin, :active].each do |user|
1121     test "get trashed collection via filters and #{user} user, including its past versions" do
1122       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1123       authorize_with :admin
1124       get :index, params: {
1125         filters: [["current_version_uuid", "=", uuid]],
1126         include_trash: true,
1127         include_old_versions: true,
1128       }
1129       assert_response 200
1130       # Both current & past version are returned
1131       assert_equal 2, json_response["items"].size
1132     end
1133   end
1134
1135   test "trash collection also trash its past versions" do
1136     uuid = collections(:collection_owned_by_active).uuid
1137     authorize_with :active
1138     versions = Collection.where(current_version_uuid: uuid)
1139     assert_equal 2, versions.size
1140     versions.each do |col|
1141       refute col.is_trashed
1142     end
1143     post :trash, params: {
1144       id: uuid,
1145     }
1146     assert_response 200
1147     versions = Collection.where(current_version_uuid: uuid)
1148     assert_equal 2, versions.size
1149     versions.each do |col|
1150       assert col.is_trashed
1151     end
1152   end
1153
1154   test 'get trashed collection without include_trash' do
1155     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1156     authorize_with :active
1157     get :show, params: {
1158       id: uuid,
1159     }
1160     assert_response 404
1161   end
1162
1163   test 'trash collection using http DELETE verb' do
1164     uuid = collections(:collection_owned_by_active).uuid
1165     authorize_with :active
1166     delete :destroy, params: {
1167       id: uuid,
1168     }
1169     assert_response 200
1170     c = Collection.find_by_uuid(uuid)
1171     assert_operator c.trash_at, :<, db_current_time
1172     assert_equal c.delete_at, c.trash_at + Rails.configuration.Collections.BlobSigningTTL
1173   end
1174
1175   test 'delete long-trashed collection immediately using http DELETE verb' do
1176     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1177     authorize_with :active
1178     delete :destroy, params: {
1179       id: uuid,
1180     }
1181     assert_response 200
1182     c = Collection.find_by_uuid(uuid)
1183     assert_operator c.trash_at, :<, db_current_time
1184     assert_operator c.delete_at, :<, db_current_time
1185   end
1186
1187   ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection
1188    :empty_collection_name_in_active_user_home_project,
1189   ].each do |fixture|
1190     test "trash collection #{fixture} via trash action with grace period" do
1191       if fixture.is_a? String
1192         uuid = fixture
1193       else
1194         uuid = collections(fixture).uuid
1195       end
1196       authorize_with :active
1197       time_before_trashing = db_current_time
1198       post :trash, params: {
1199         id: uuid,
1200       }
1201       assert_response 200
1202       c = Collection.find_by_uuid(uuid)
1203       assert_operator c.trash_at, :<, db_current_time
1204       assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.Collections.DefaultTrashLifetime
1205     end
1206   end
1207
1208   test 'untrash a trashed collection' do
1209     authorize_with :active
1210     post :untrash, params: {
1211       id: collections(:expired_collection).uuid,
1212     }
1213     assert_response 200
1214     assert_equal false, json_response['is_trashed']
1215     assert_nil json_response['trash_at']
1216   end
1217
1218   test 'untrash a trashed collection by assigning nil to trash_at' do
1219     authorize_with :active
1220     post :update, params: {
1221            id: collections(:expired_collection).uuid,
1222            collection: {
1223              trash_at: nil,
1224            },
1225            include_trash: true,
1226     }
1227     assert_response 200
1228     assert_equal false, json_response['is_trashed']
1229     assert_nil json_response['trash_at']
1230   end
1231
1232   test 'untrash error on not trashed collection' do
1233     authorize_with :active
1234     post :untrash, params: {
1235       id: collections(:collection_owned_by_active).uuid,
1236     }
1237     assert_response 422
1238   end
1239
1240   [:active, :admin].each do |user|
1241     test "get trashed collections as #{user}" do
1242       authorize_with user
1243       get :index, params: {
1244         filters: [["is_trashed", "=", true]],
1245         include_trash: true,
1246       }
1247       assert_response :success
1248
1249       items = []
1250       json_response["items"].each do |coll|
1251         items << coll['uuid']
1252       end
1253
1254       assert_includes(items, collections('unique_expired_collection')['uuid'])
1255       if user == :admin
1256         assert_includes(items, collections('unique_expired_collection2')['uuid'])
1257       else
1258         assert_not_includes(items, collections('unique_expired_collection2')['uuid'])
1259       end
1260     end
1261   end
1262
1263   test 'untrash collection with same name as another with no ensure unique name' do
1264     authorize_with :active
1265     post :untrash, params: {
1266       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1267     }
1268     assert_response 422
1269   end
1270
1271   test 'untrash collection with same name as another with ensure unique name' do
1272     authorize_with :active
1273     post :untrash, params: {
1274       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1275       ensure_unique_name: true
1276     }
1277     assert_response 200
1278     assert_equal false, json_response['is_trashed']
1279     assert_nil json_response['trash_at']
1280     assert_nil json_response['delete_at']
1281     assert_match /^same name for trashed and persisted collections \(#{json_response['uuid'][-15..-1]}\)$/, json_response['name']
1282   end
1283
1284   test 'cannot show collection in trashed subproject' do
1285     authorize_with :active
1286     get :show, params: {
1287       id: collections(:collection_in_trashed_subproject).uuid,
1288       format: :json
1289     }
1290     assert_response 404
1291   end
1292
1293   test 'can show collection in untrashed subproject' do
1294     authorize_with :active
1295     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1296     get :show, params: {
1297       id: collections(:collection_in_trashed_subproject).uuid,
1298       format: :json,
1299     }
1300     assert_response :success
1301   end
1302
1303   test 'cannot index collection in trashed subproject' do
1304     authorize_with :active
1305     get :index, params: { limit: 1000 }
1306     assert_response :success
1307     item_uuids = json_response['items'].map do |item|
1308       item['uuid']
1309     end
1310     assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1311   end
1312
1313   test 'can index collection in untrashed subproject' do
1314     authorize_with :active
1315     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1316     get :index, params: { limit: 1000 }
1317     assert_response :success
1318     item_uuids = json_response['items'].map do |item|
1319       item['uuid']
1320     end
1321     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1322   end
1323
1324   test 'can index trashed subproject collection with include_trash' do
1325     authorize_with :active
1326     get :index, params: {
1327           include_trash: true,
1328           limit: 1000
1329         }
1330     assert_response :success
1331     item_uuids = json_response['items'].map do |item|
1332       item['uuid']
1333     end
1334     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1335   end
1336
1337   test 'can get collection with past versions' do
1338     authorize_with :active
1339     get :index, params: {
1340       filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]],
1341       include_old_versions: true
1342     }
1343     assert_response :success
1344     assert_equal 2, assigns(:objects).length
1345     assert_equal 2, json_response['items_available']
1346     assert_equal 2, json_response['items'].count
1347     json_response['items'].each do |c|
1348       assert_equal collections(:collection_owned_by_active).uuid,
1349                    c['current_version_uuid'],
1350                    'response includes a version from a different collection'
1351     end
1352   end
1353
1354   test 'can get old version collection by uuid' do
1355     authorize_with :active
1356     get :show, params: {
1357       id: collections(:collection_owned_by_active_past_version_1).uuid,
1358     }
1359     assert_response :success
1360     assert_equal collections(:collection_owned_by_active_past_version_1).name,
1361                   json_response['name']
1362   end
1363
1364   test 'can get old version collection by PDH' do
1365     authorize_with :active
1366     get :show, params: {
1367       id: collections(:collection_owned_by_active_past_version_1).portable_data_hash,
1368     }
1369     assert_response :success
1370     assert_equal collections(:collection_owned_by_active_past_version_1).portable_data_hash,
1371                   json_response['portable_data_hash']
1372   end
1373
1374   test 'version and current_version_uuid are ignored at creation time' do
1375     permit_unsigned_manifests
1376     authorize_with :active
1377     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
1378     post :create, params: {
1379       collection: {
1380         name: 'Test collection',
1381         version: 42,
1382         current_version_uuid: collections(:collection_owned_by_active).uuid,
1383         manifest_text: manifest_text,
1384       }
1385     }
1386     assert_response :success
1387     resp = JSON.parse(@response.body)
1388     assert_equal 1, resp['version']
1389     assert_equal resp['uuid'], resp['current_version_uuid']
1390   end
1391
1392   test "update collection with versioning enabled" do
1393     Rails.configuration.Collections.CollectionVersioning = true
1394     Rails.configuration.Collections.PreserveVersionIfIdle = 1 # 1 second
1395
1396     col = collections(:collection_owned_by_active)
1397     assert_equal 2, col.version
1398     assert col.modified_at < Time.now - 1.second
1399
1400     token = api_client_authorizations(:active).v2token
1401     signed = Blob.sign_locator(
1402       'acbd18db4cc2f85cedef654fccc4a4d8+3',
1403       key: Rails.configuration.Collections.BlobSigningKey,
1404       api_token: token)
1405     authorize_with_token token
1406     put :update, params: {
1407           id: col.uuid,
1408           collection: {
1409             manifest_text: ". #{signed} 0:3:foo.txt\n",
1410           },
1411         }
1412     assert_response :success
1413     assert_equal 3, json_response['version']
1414   end
1415
1416   test "delete collection with versioning enabled" do
1417     Rails.configuration.Collections.CollectionVersioning = true
1418     Rails.configuration.Collections.PreserveVersionIfIdle = 1 # 1 second
1419
1420     col = collections(:collection_owned_by_active)
1421     assert_equal 2, col.version
1422     assert col.modified_at < Time.now - 1.second
1423
1424     authorize_with(:active)
1425     post :trash, params: {
1426       id: col.uuid,
1427     }
1428     assert_response :success
1429     assert_equal col.version, json_response['version'], 'Trashing a collection should not create a new version'
1430   end
1431
1432   [['<', :<],
1433    ['<=', :<=],
1434    ['>', :>],
1435    ['>=', :>=],
1436    ['=', :==]].each do |op, rubyop|
1437     test "filter collections by replication_desired #{op} replication_confirmed" do
1438       authorize_with(:active)
1439       get :index, params: {
1440             filters: [["(replication_desired #{op} replication_confirmed)", "=", true]],
1441           }
1442       assert_response :success
1443       json_response["items"].each do |c|
1444         assert_operator(c["replication_desired"], rubyop, c["replication_confirmed"])
1445       end
1446     end
1447   end
1448
1449   ["(replication_desired < bogus)",
1450    "replication_desired < replication_confirmed",
1451    "(replication_desired < replication_confirmed",
1452    "(replication_desired ! replication_confirmed)",
1453    "(replication_desired <)",
1454    "(replication_desired < manifest_text)",
1455    "(manifest_text < manifest_text)", # currently only numeric attrs are supported
1456    "(replication_desired < 2)", # currently only attrs are supported, not literals
1457    "(1 < 2)",
1458   ].each do |expr|
1459     test "invalid filter expression #{expr}" do
1460       authorize_with(:active)
1461       get :index, params: {
1462             filters: [[expr, "=", true]],
1463           }
1464       assert_response 422
1465     end
1466   end
1467
1468   test "invalid op/arg with filter expression" do
1469     authorize_with(:active)
1470     get :index, params: {
1471           filters: [["replication_desired < replication_confirmed", "!=", false]],
1472         }
1473     assert_response 422
1474   end
1475
1476   ["storage_classes_desired", "storage_classes_confirmed"].each do |attr|
1477     test "filter collections by #{attr}" do
1478       authorize_with(:active)
1479       get :index, params: {
1480             filters: [[attr, "=", '["default"]']]
1481           }
1482       assert_response :success
1483       assert_not_equal 0, json_response["items"].length
1484       json_response["items"].each do |c|
1485         assert_equal ["default"], c[attr]
1486       end
1487     end
1488   end
1489
1490   test "select param is respected in 'show' response" do
1491     authorize_with :active
1492     get :show, params: {
1493           id: collections(:collection_owned_by_active).uuid,
1494           select: ["name"],
1495         }
1496     assert_response :success
1497     assert_raises ActiveModel::MissingAttributeError do
1498       assigns(:object).manifest_text
1499     end
1500     assert_nil json_response["manifest_text"]
1501     assert_nil json_response["properties"]
1502     assert_equal collections(:collection_owned_by_active).name, json_response["name"]
1503   end
1504
1505   test "select param is respected in 'update' response" do
1506     authorize_with :active
1507     post :update, params: {
1508           id: collections(:collection_owned_by_active).uuid,
1509           collection: {
1510             manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foobar.txt\n",
1511           },
1512           select: ["name"],
1513         }
1514     assert_response :success
1515     assert_nil json_response["manifest_text"]
1516     assert_nil json_response["properties"]
1517     assert_equal collections(:collection_owned_by_active).name, json_response["name"]
1518   end
1519
1520   [nil,
1521    [],
1522    ["is_trashed", "trash_at"],
1523    ["is_trashed", "trash_at", "portable_data_hash"],
1524    ["portable_data_hash"],
1525    ["portable_data_hash", "manifest_text"],
1526   ].each do |select|
1527     test "select=#{select.inspect} param is respected in 'get by pdh' response" do
1528       authorize_with :active
1529       get :show, params: {
1530             id: collections(:collection_owned_by_active).portable_data_hash,
1531             select: select,
1532           }
1533       assert_response :success
1534       if !select || select.index("manifest_text")
1535         assert_not_nil json_response["manifest_text"]
1536       else
1537         assert_nil json_response["manifest_text"]
1538       end
1539     end
1540   end
1541 end