Merge branch '18122-select-param'
[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 |unsigned|
378     test "create with duplicate name, ensure_unique_name, unsigned=#{unsigned}" do
379       permit_unsigned_manifests unsigned
380       authorize_with :active
381       manifest_text = ". acbd18db4cc2f85cedef654fccc4a4d8+3 0:0:foo.txt\n"
382       if !unsigned
383         manifest_text = Collection.sign_manifest_only_for_tests manifest_text, api_token(:active)
384       end
385       post :create, params: {
386         collection: {
387           owner_uuid: users(:active).uuid,
388           manifest_text: manifest_text,
389           name: "owned_by_active"
390         },
391         ensure_unique_name: true
392       }
393       assert_response :success
394       assert_match /^owned_by_active \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
395     end
396   end
397
398   test "create with owner_uuid set to group i can_manage" do
399     permit_unsigned_manifests
400     authorize_with :active
401     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
402     post :create, params: {
403       collection: {
404         owner_uuid: groups(:active_user_has_can_manage).uuid,
405         manifest_text: manifest_text,
406         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
407       }
408     }
409     assert_response :success
410     resp = JSON.parse(@response.body)
411     assert_equal groups(:active_user_has_can_manage).uuid, resp['owner_uuid']
412   end
413
414   test "create with owner_uuid fails on group with only can_read permission" do
415     permit_unsigned_manifests
416     authorize_with :active
417     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
418     post :create, params: {
419       collection: {
420         owner_uuid: groups(:all_users).uuid,
421         manifest_text: manifest_text,
422         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
423       }
424     }
425     assert_response 403
426   end
427
428   test "create with owner_uuid fails on group with no permission" do
429     permit_unsigned_manifests
430     authorize_with :active
431     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
432     post :create, params: {
433       collection: {
434         owner_uuid: groups(:public).uuid,
435         manifest_text: manifest_text,
436         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
437       }
438     }
439     assert_response 422
440   end
441
442   test "admin create with owner_uuid set to group with no permission" do
443     permit_unsigned_manifests
444     authorize_with :admin
445     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
446     post :create, params: {
447       collection: {
448         owner_uuid: 'zzzzz-j7d0g-it30l961gq3t0oi',
449         manifest_text: manifest_text,
450         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
451       }
452     }
453     assert_response :success
454   end
455
456   test "should create with collection passed as json" do
457     permit_unsigned_manifests
458     authorize_with :active
459     post :create, params: {
460       collection: <<-EOS
461       {
462         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",\
463         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
464       }
465       EOS
466     }
467     assert_response :success
468   end
469
470   test "should fail to create with checksum mismatch" do
471     permit_unsigned_manifests
472     authorize_with :active
473     post :create, params: {
474       collection: <<-EOS
475       {
476         "manifest_text":". d41d8cd98f00b204e9800998ecf8427e 0:0:bar.txt\n",\
477         "portable_data_hash":"d30fe8ae534397864cb96c544f4cf102+47"\
478       }
479       EOS
480     }
481     assert_response 422
482   end
483
484   test "collection UUID is normalized when created" do
485     permit_unsigned_manifests
486     authorize_with :active
487     post :create, params: {
488       collection: {
489         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
490         portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47+Khint+Xhint+Zhint"
491       }
492     }
493     assert_response :success
494     assert_not_nil assigns(:object)
495     resp = JSON.parse(@response.body)
496     assert_equal "d30fe8ae534397864cb96c544f4cf102+47", resp['portable_data_hash']
497   end
498
499   test "get full provenance for baz file" do
500     authorize_with :active
501     get :provenance, params: {id: 'ea10d51bcf88862dbcc36eb292017dfd+45'}
502     assert_response :success
503     resp = JSON.parse(@response.body)
504     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
505     assert_not_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # bar
506     assert_not_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # foo
507     assert_not_nil resp['zzzzz-8i9sb-cjs4pklxxjykyuq'] # bar->baz
508     assert_not_nil resp['zzzzz-8i9sb-aceg2bnq7jt7kon'] # foo->bar
509   end
510
511   test "get no provenance for foo file" do
512     # spectator user cannot even see baz collection
513     authorize_with :spectator
514     get :provenance, params: {id: '1f4b0bc7583c2a7f9102c395f4ffc5e3+45'}
515     assert_response 404
516   end
517
518   test "get partial provenance for baz file" do
519     # spectator user can see bar->baz job, but not foo->bar job
520     authorize_with :spectator
521     get :provenance, params: {id: 'ea10d51bcf88862dbcc36eb292017dfd+45'}
522     assert_response :success
523     resp = JSON.parse(@response.body)
524     assert_not_nil resp['ea10d51bcf88862dbcc36eb292017dfd+45'] # baz
525     assert_not_nil resp['fa7aeb5140e2848d39b416daeef4ffc5+45'] # bar
526     assert_not_nil resp['zzzzz-8i9sb-cjs4pklxxjykyuq']     # bar->baz
527     assert_nil resp['zzzzz-8i9sb-aceg2bnq7jt7kon']         # foo->bar
528     assert_nil resp['1f4b0bc7583c2a7f9102c395f4ffc5e3+45'] # foo
529   end
530
531   test "search collections with 'any' operator" do
532     expect_pdh = collections(:docker_image).portable_data_hash
533     authorize_with :active
534     get :index, params: {
535       where: { any: ['contains', expect_pdh[5..25]] }
536     }
537     assert_response :success
538     found = assigns(:objects)
539     assert_equal 1, found.count
540     assert_equal expect_pdh, found.first.portable_data_hash
541   end
542
543   [false, true].each do |permit_unsigned|
544     test "create collection with signed manifest, permit_unsigned=#{permit_unsigned}" do
545       permit_unsigned_manifests permit_unsigned
546       authorize_with :active
547       locators = %w(
548       d41d8cd98f00b204e9800998ecf8427e+0
549       acbd18db4cc2f85cedef654fccc4a4d8+3
550       ea10d51bcf88862dbcc36eb292017dfd+45)
551
552       unsigned_manifest = locators.map { |loc|
553         ". " + loc + " 0:0:foo.txt\n"
554       }.join()
555       manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
556         '+' +
557         unsigned_manifest.length.to_s
558
559       # Build a manifest with both signed and unsigned locators.
560       signing_opts = {
561         key: Rails.configuration.Collections.BlobSigningKey,
562         api_token: api_token(:active),
563       }
564       signed_locators = locators.collect do |x|
565         Blob.sign_locator x, signing_opts
566       end
567       if permit_unsigned
568         # Leave a non-empty blob unsigned.
569         signed_locators[1] = locators[1]
570       else
571         # Leave the empty blob unsigned. This should still be allowed.
572         signed_locators[0] = locators[0]
573       end
574       signed_manifest =
575         ". " + signed_locators[0] + " 0:0:foo.txt\n" +
576         ". " + signed_locators[1] + " 0:0:foo.txt\n" +
577         ". " + signed_locators[2] + " 0:0:foo.txt\n"
578
579       post :create, params: {
580         collection: {
581           manifest_text: signed_manifest,
582           portable_data_hash: manifest_uuid,
583         }
584       }
585       assert_response :success
586       assert_not_nil assigns(:object)
587       resp = JSON.parse(@response.body)
588       assert_equal manifest_uuid, resp['portable_data_hash']
589       # All of the signatures in the output must be valid.
590       resp['manifest_text'].lines.each do |entry|
591         m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
592         if m && m[0].index('+A')
593           assert Blob.verify_signature m[0], signing_opts
594         end
595       end
596     end
597   end
598
599   test "create collection with signed manifest and explicit TTL" do
600     authorize_with :active
601     locators = %w(
602       d41d8cd98f00b204e9800998ecf8427e+0
603       acbd18db4cc2f85cedef654fccc4a4d8+3
604       ea10d51bcf88862dbcc36eb292017dfd+45)
605
606     unsigned_manifest = locators.map { |loc|
607       ". " + loc + " 0:0:foo.txt\n"
608     }.join()
609     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
610       '+' +
611       unsigned_manifest.length.to_s
612
613     # build a manifest with both signed and unsigned locators.
614     # TODO(twp): in phase 4, all locators will need to be signed, so
615     # this test should break and will need to be rewritten. Issue #2755.
616     signing_opts = {
617       key: Rails.configuration.Collections.BlobSigningKey,
618       api_token: api_token(:active),
619       ttl: 3600   # 1 hour
620     }
621     signed_manifest =
622       ". " + locators[0] + " 0:0:foo.txt\n" +
623       ". " + Blob.sign_locator(locators[1], signing_opts) + " 0:0:foo.txt\n" +
624       ". " + Blob.sign_locator(locators[2], signing_opts) + " 0:0:foo.txt\n"
625
626     post :create, params: {
627       collection: {
628         manifest_text: signed_manifest,
629         portable_data_hash: manifest_uuid,
630       }
631     }
632     assert_response :success
633     assert_not_nil assigns(:object)
634     resp = JSON.parse(@response.body)
635     assert_equal manifest_uuid, resp['portable_data_hash']
636     # All of the signatures in the output must be valid.
637     resp['manifest_text'].lines.each do |entry|
638       m = /([[:xdigit:]]{32}\+\S+)/.match(entry)
639       if m && m[0].index('+A')
640         assert Blob.verify_signature m[0], signing_opts
641       end
642     end
643   end
644
645   test "create fails with invalid signature" do
646     authorize_with :active
647     signing_opts = {
648       key: Rails.configuration.Collections.BlobSigningKey,
649       api_token: api_token(:active),
650     }
651
652     # Generate a locator with a bad signature.
653     unsigned_locator = "acbd18db4cc2f85cedef654fccc4a4d8+3"
654     bad_locator = unsigned_locator + "+Affffffffffffffffffffffffffffffffffffffff@ffffffff"
655     assert !Blob.verify_signature(bad_locator, signing_opts)
656
657     # Creating a collection with this locator should
658     # produce 403 Permission denied.
659     unsigned_manifest = ". #{unsigned_locator} 0:0:foo.txt\n"
660     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest) +
661       '+' +
662       unsigned_manifest.length.to_s
663
664     bad_manifest = ". #{bad_locator} 0:0:foo.txt\n"
665     post :create, params: {
666       collection: {
667         manifest_text: bad_manifest,
668         portable_data_hash: manifest_uuid
669       }
670     }
671
672     assert_response 403
673   end
674
675   test "create fails with uuid of signed manifest" do
676     authorize_with :active
677     signing_opts = {
678       key: Rails.configuration.Collections.BlobSigningKey,
679       api_token: api_token(:active),
680     }
681
682     unsigned_locator = "d41d8cd98f00b204e9800998ecf8427e+0"
683     signed_locator = Blob.sign_locator(unsigned_locator, signing_opts)
684     signed_manifest = ". #{signed_locator} 0:0:foo.txt\n"
685     manifest_uuid = Digest::MD5.hexdigest(signed_manifest) +
686       '+' +
687       signed_manifest.length.to_s
688
689     post :create, params: {
690       collection: {
691         manifest_text: signed_manifest,
692         portable_data_hash: manifest_uuid
693       }
694     }
695
696     assert_response 422
697   end
698
699   test "reject manifest with unsigned block as stream name" do
700     authorize_with :active
701     post :create, params: {
702       collection: {
703         manifest_text: "00000000000000000000000000000000+1234 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n"
704       }
705     }
706     assert_includes [422, 403], response.code.to_i
707   end
708
709   test "multiple locators per line" do
710     permit_unsigned_manifests
711     authorize_with :active
712     locators = %w(
713       d41d8cd98f00b204e9800998ecf8427e+0
714       acbd18db4cc2f85cedef654fccc4a4d8+3
715       ea10d51bcf88862dbcc36eb292017dfd+45)
716
717     manifest_text = [".", *locators, "0:0:foo.txt\n"].join(" ")
718     manifest_uuid = Digest::MD5.hexdigest(manifest_text) +
719       '+' +
720       manifest_text.length.to_s
721
722     test_collection = {
723       manifest_text: manifest_text,
724       portable_data_hash: manifest_uuid,
725     }
726     post_collection = Marshal.load(Marshal.dump(test_collection))
727     post :create, params: {
728       collection: post_collection
729     }
730     assert_response :success
731     assert_not_nil assigns(:object)
732     resp = JSON.parse(@response.body)
733     assert_equal manifest_uuid, resp['portable_data_hash']
734
735     # The manifest in the response will have had permission hints added.
736     # Remove any permission hints in the response before comparing it to the source.
737     stripped_manifest = resp['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '')
738     assert_equal manifest_text, stripped_manifest
739   end
740
741   test 'Reject manifest with unsigned blob' do
742     permit_unsigned_manifests false
743     authorize_with :active
744     unsigned_manifest = ". 0cc175b9c0f1b6a831c399e269772661+1 0:1:a.txt\n"
745     manifest_uuid = Digest::MD5.hexdigest(unsigned_manifest)
746     post :create, params: {
747       collection: {
748         manifest_text: unsigned_manifest,
749         portable_data_hash: manifest_uuid,
750       }
751     }
752     assert_response 403,
753     "Creating a collection with unsigned blobs should respond 403"
754     assert_empty Collection.where('uuid like ?', manifest_uuid+'%'),
755     "Collection should not exist in database after failed create"
756   end
757
758   test 'List expired collection returns empty list' do
759     authorize_with :active
760     get :index, params: {
761       where: {name: 'expired_collection'},
762     }
763     assert_response :success
764     found = assigns(:objects)
765     assert_equal 0, found.count
766   end
767
768   test 'Show expired collection returns 404' do
769     authorize_with :active
770     get :show, params: {
771       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
772     }
773     assert_response 404
774   end
775
776   test 'Update expired collection returns 404' do
777     authorize_with :active
778     post :update, params: {
779       id: 'zzzzz-4zz18-mto52zx1s7sn3ih',
780       collection: {
781         name: "still expired"
782       }
783     }
784     assert_response 404
785   end
786
787   test 'List collection with future expiration time succeeds' do
788     authorize_with :active
789     get :index, params: {
790       where: {name: 'collection_expires_in_future'},
791     }
792     found = assigns(:objects)
793     assert_equal 1, found.count
794   end
795
796
797   test 'Show collection with future expiration time succeeds' do
798     authorize_with :active
799     get :show, params: {
800       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
801     }
802     assert_response :success
803   end
804
805   test 'Update collection with future expiration time succeeds' do
806     authorize_with :active
807     post :update, params: {
808       id: 'zzzzz-4zz18-padkqo7yb8d9i3j',
809       collection: {
810         name: "still not expired"
811       }
812     }
813     assert_response :success
814   end
815
816   test "get collection and verify that file_names is not included" do
817     authorize_with :active
818     get :show, params: {id: collections(:foo_file).uuid}
819     assert_response :success
820     assert_equal collections(:foo_file).uuid, json_response['uuid']
821     assert_nil json_response['file_names']
822     assert json_response['manifest_text']
823   end
824
825   [
826     [2**8, :success],
827     [2**18, 422],
828   ].each do |description_size, expected_response|
829     # Descriptions are not part of search indexes. Skip until
830     # full-text search is implemented, at which point replace with a
831     # search in description.
832     skip "create collection with description size #{description_size}
833           and expect response #{expected_response}" do
834       authorize_with :active
835
836       description = 'here is a collection with a very large description'
837       while description.length < description_size
838         description = description + description
839       end
840
841       post :create, params: {
842         collection: {
843           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n",
844           description: description,
845         }
846       }
847
848       assert_response expected_response
849     end
850   end
851
852   [1, 5, nil].each do |ask|
853     test "Set replication_desired=#{ask.inspect}" do
854       Rails.configuration.Collections.DefaultReplication = 2
855       authorize_with :active
856       put :update, params: {
857         id: collections(:replication_undesired_unconfirmed).uuid,
858         collection: {
859           replication_desired: ask,
860         },
861       }
862       assert_response :success
863       assert_equal ask, json_response['replication_desired']
864     end
865   end
866
867   test "get collection with properties" do
868     authorize_with :active
869     get :show, params: {id: collections(:collection_with_one_property).uuid}
870     assert_response :success
871     assert_not_nil json_response['uuid']
872     assert_equal 'value1', json_response['properties']['property1']
873   end
874
875   [
876     {'property_1' => 'value_1'},
877     "{\"property_1\":\"value_1\"}",
878   ].each do |p|
879     test "create collection with valid properties param #{p.inspect}" do
880       authorize_with :active
881       manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
882       post :create, params: {
883         collection: {
884           manifest_text: manifest_text,
885           portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47",
886           properties: p
887         }
888       }
889       assert_response :success
890       assert_not_nil json_response['uuid']
891       assert_equal Hash, json_response['properties'].class, 'Collection properties attribute should be of type hash'
892       assert_equal 'value_1', json_response['properties']['property_1']
893     end
894   end
895
896   [
897     false,
898     [],
899     42,
900     'some string',
901     '["json", "encoded", "array"]',
902   ].each do |p|
903     test "create collection with non-valid properties param #{p.inspect}" do
904       authorize_with :active
905       post :create, params: {
906         collection: {
907           name: "test collection with non-valid properties param '#{p.inspect}'",
908           manifest_text: '',
909           properties: p
910         }
911       }
912       assert_response 422
913       response_errors = json_response['errors']
914       assert_not_nil response_errors, 'Expected error in response'
915     end
916   end
917
918   [
919     [". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n", 1, 34],
920     [". 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],
921     [". d41d8cd98f00b204e9800998ecf8427e 0:0:.\n", 0, 0]
922   ].each do |manifest, count, size|
923     test "create collection with valid manifest #{manifest} and expect file stats" do
924       authorize_with :active
925       post :create, params: {
926         collection: {
927           manifest_text: manifest
928         }
929       }
930       assert_response 200
931       assert_equal count, json_response['file_count']
932       assert_equal size, json_response['file_size_total']
933     end
934   end
935
936   test "update collection manifest and expect new file stats" do
937     authorize_with :active
938     post :update, params: {
939       id: collections(:collection_owned_by_active_with_file_stats).uuid,
940       collection: {
941         manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n"
942       }
943     }
944     assert_response 200
945     assert_equal 1, json_response['file_count']
946     assert_equal 34, json_response['file_size_total']
947   end
948
949   [
950     ['file_count', 1],
951     ['file_size_total', 34]
952   ].each do |attribute, val|
953     test "create collection with #{attribute} and expect overwrite" do
954       authorize_with :active
955       post :create, params: {
956         collection: {
957           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
958           "#{attribute}": 10
959         }
960       }
961       assert_response 200
962       assert_equal val, json_response[attribute]
963     end
964   end
965
966   [
967     ['file_count', 1],
968     ['file_size_total', 3]
969   ].each do |attribute, val|
970     test "update collection with #{attribute} and expect ignore" do
971       authorize_with :active
972       post :update, params: {
973         id: collections(:collection_owned_by_active_with_file_stats).uuid,
974         collection: {
975           "#{attribute}": 10
976         }
977       }
978       assert_response 200
979       assert_equal val, json_response[attribute]
980     end
981   end
982
983   [
984     ['file_count', 1],
985     ['file_size_total', 34]
986   ].each do |attribute, val|
987     test "update collection with #{attribute} and manifest and expect manifest values" do
988       authorize_with :active
989       post :update, params: {
990         id: collections(:collection_owned_by_active_with_file_stats).uuid,
991         collection: {
992           manifest_text: ". d41d8cd98f00b204e9800998ecf8427e 0:34:foo.txt\n",
993           "#{attribute}": 10
994         }
995       }
996       assert_response 200
997       assert_equal val, json_response[attribute]
998     end
999   end
1000
1001   [
1002     ". 0:0:foo.txt",
1003     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1004     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1005     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1006   ].each do |manifest_text|
1007     test "create collection with invalid manifest #{manifest_text} and expect error" do
1008       authorize_with :active
1009       post :create, params: {
1010         collection: {
1011           manifest_text: manifest_text,
1012           portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0"
1013         }
1014       }
1015       assert_response 422
1016       response_errors = json_response['errors']
1017       assert_not_nil response_errors, 'Expected error in response'
1018       assert(response_errors.first.include?('Invalid manifest'),
1019              "Expected 'Invalid manifest' error in #{response_errors.first}")
1020     end
1021   end
1022
1023   [
1024     [nil, "d41d8cd98f00b204e9800998ecf8427e+0"],
1025     ["", "d41d8cd98f00b204e9800998ecf8427e+0"],
1026     [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"],
1027   ].each do |manifest_text, pdh|
1028     test "create collection with valid manifest #{manifest_text.inspect} and expect success" do
1029       authorize_with :active
1030       post :create, params: {
1031         collection: {
1032           manifest_text: manifest_text,
1033           portable_data_hash: pdh
1034         }
1035       }
1036       assert_response 200
1037     end
1038   end
1039
1040   [
1041     ". 0:0:foo.txt",
1042     ". d41d8cd98f00b204e9800998ecf8427e foo.txt",
1043     "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1044     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt",
1045   ].each do |manifest_text|
1046     test "update collection with invalid manifest #{manifest_text} and expect error" do
1047       authorize_with :active
1048       post :update, params: {
1049         id: 'zzzzz-4zz18-bv31uwvy3neko21',
1050         collection: {
1051           manifest_text: manifest_text,
1052         }
1053       }
1054       assert_response 422
1055       response_errors = json_response['errors']
1056       assert_not_nil response_errors, 'Expected error in response'
1057       assert(response_errors.first.include?('Invalid manifest'),
1058              "Expected 'Invalid manifest' error in #{response_errors.first}")
1059     end
1060   end
1061
1062   [
1063     nil,
1064     "",
1065     ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n",
1066   ].each do |manifest_text|
1067     test "update collection with valid manifest #{manifest_text.inspect} and expect success" do
1068       authorize_with :active
1069       post :update, params: {
1070         id: 'zzzzz-4zz18-bv31uwvy3neko21',
1071         collection: {
1072           manifest_text: manifest_text,
1073         }
1074       }
1075       assert_response 200
1076     end
1077   end
1078
1079   [true, false].each do |include_trash|
1080     test "get trashed collection with include_trash=#{include_trash}" do
1081       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1082       authorize_with :active
1083       get :show, params: {
1084         id: uuid,
1085         include_trash: include_trash,
1086       }
1087       if include_trash
1088         assert_response 200
1089       else
1090         assert_response 404
1091       end
1092     end
1093   end
1094
1095   [:admin, :active].each do |user|
1096     test "get trashed collection via filters and #{user} user without including its past versions" do
1097       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1098       authorize_with user
1099       get :index, params: {
1100         filters: [["current_version_uuid", "=", uuid]],
1101         include_trash: true,
1102       }
1103       assert_response 200
1104       # Only the current version is returned
1105       assert_equal 1, json_response["items"].size
1106     end
1107   end
1108
1109   [:admin, :active].each do |user|
1110     test "get trashed collection via filters and #{user} user, including its past versions" do
1111       uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1112       authorize_with :admin
1113       get :index, params: {
1114         filters: [["current_version_uuid", "=", uuid]],
1115         include_trash: true,
1116         include_old_versions: true,
1117       }
1118       assert_response 200
1119       # Both current & past version are returned
1120       assert_equal 2, json_response["items"].size
1121     end
1122   end
1123
1124   test "trash collection also trash its past versions" do
1125     uuid = collections(:collection_owned_by_active).uuid
1126     authorize_with :active
1127     versions = Collection.where(current_version_uuid: uuid)
1128     assert_equal 2, versions.size
1129     versions.each do |col|
1130       refute col.is_trashed
1131     end
1132     post :trash, params: {
1133       id: uuid,
1134     }
1135     assert_response 200
1136     versions = Collection.where(current_version_uuid: uuid)
1137     assert_equal 2, versions.size
1138     versions.each do |col|
1139       assert col.is_trashed
1140     end
1141   end
1142
1143   test 'get trashed collection without include_trash' do
1144     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1145     authorize_with :active
1146     get :show, params: {
1147       id: uuid,
1148     }
1149     assert_response 404
1150   end
1151
1152   test 'trash collection using http DELETE verb' do
1153     uuid = collections(:collection_owned_by_active).uuid
1154     authorize_with :active
1155     delete :destroy, params: {
1156       id: uuid,
1157     }
1158     assert_response 200
1159     c = Collection.find_by_uuid(uuid)
1160     assert_operator c.trash_at, :<, db_current_time
1161     assert_equal c.delete_at, c.trash_at + Rails.configuration.Collections.BlobSigningTTL
1162   end
1163
1164   test 'delete long-trashed collection immediately using http DELETE verb' do
1165     uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection
1166     authorize_with :active
1167     delete :destroy, params: {
1168       id: uuid,
1169     }
1170     assert_response 200
1171     c = Collection.find_by_uuid(uuid)
1172     assert_operator c.trash_at, :<, db_current_time
1173     assert_operator c.delete_at, :<, db_current_time
1174   end
1175
1176   ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection
1177    :empty_collection_name_in_active_user_home_project,
1178   ].each do |fixture|
1179     test "trash collection #{fixture} via trash action with grace period" do
1180       if fixture.is_a? String
1181         uuid = fixture
1182       else
1183         uuid = collections(fixture).uuid
1184       end
1185       authorize_with :active
1186       time_before_trashing = db_current_time
1187       post :trash, params: {
1188         id: uuid,
1189       }
1190       assert_response 200
1191       c = Collection.find_by_uuid(uuid)
1192       assert_operator c.trash_at, :<, db_current_time
1193       assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.Collections.DefaultTrashLifetime
1194     end
1195   end
1196
1197   test 'untrash a trashed collection' do
1198     authorize_with :active
1199     post :untrash, params: {
1200       id: collections(:expired_collection).uuid,
1201     }
1202     assert_response 200
1203     assert_equal false, json_response['is_trashed']
1204     assert_nil json_response['trash_at']
1205   end
1206
1207   test 'untrash error on not trashed collection' do
1208     authorize_with :active
1209     post :untrash, params: {
1210       id: collections(:collection_owned_by_active).uuid,
1211     }
1212     assert_response 422
1213   end
1214
1215   [:active, :admin].each do |user|
1216     test "get trashed collections as #{user}" do
1217       authorize_with user
1218       get :index, params: {
1219         filters: [["is_trashed", "=", true]],
1220         include_trash: true,
1221       }
1222       assert_response :success
1223
1224       items = []
1225       json_response["items"].each do |coll|
1226         items << coll['uuid']
1227       end
1228
1229       assert_includes(items, collections('unique_expired_collection')['uuid'])
1230       if user == :admin
1231         assert_includes(items, collections('unique_expired_collection2')['uuid'])
1232       else
1233         assert_not_includes(items, collections('unique_expired_collection2')['uuid'])
1234       end
1235     end
1236   end
1237
1238   test 'untrash collection with same name as another with no ensure unique name' do
1239     authorize_with :active
1240     post :untrash, params: {
1241       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1242     }
1243     assert_response 422
1244   end
1245
1246   test 'untrash collection with same name as another with ensure unique name' do
1247     authorize_with :active
1248     post :untrash, params: {
1249       id: collections(:trashed_collection_to_test_name_conflict_on_untrash).uuid,
1250       ensure_unique_name: true
1251     }
1252     assert_response 200
1253     assert_equal false, json_response['is_trashed']
1254     assert_nil json_response['trash_at']
1255     assert_nil json_response['delete_at']
1256     assert_match /^same name for trashed and persisted collections \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name']
1257   end
1258
1259   test 'cannot show collection in trashed subproject' do
1260     authorize_with :active
1261     get :show, params: {
1262       id: collections(:collection_in_trashed_subproject).uuid,
1263       format: :json
1264     }
1265     assert_response 404
1266   end
1267
1268   test 'can show collection in untrashed subproject' do
1269     authorize_with :active
1270     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1271     get :show, params: {
1272       id: collections(:collection_in_trashed_subproject).uuid,
1273       format: :json,
1274     }
1275     assert_response :success
1276   end
1277
1278   test 'cannot index collection in trashed subproject' do
1279     authorize_with :active
1280     get :index, params: { limit: 1000 }
1281     assert_response :success
1282     item_uuids = json_response['items'].map do |item|
1283       item['uuid']
1284     end
1285     assert_not_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1286   end
1287
1288   test 'can index collection in untrashed subproject' do
1289     authorize_with :active
1290     Group.find_by_uuid(groups(:trashed_project).uuid).update! is_trashed: false
1291     get :index, params: { limit: 1000 }
1292     assert_response :success
1293     item_uuids = json_response['items'].map do |item|
1294       item['uuid']
1295     end
1296     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1297   end
1298
1299   test 'can index trashed subproject collection with include_trash' do
1300     authorize_with :active
1301     get :index, params: {
1302           include_trash: true,
1303           limit: 1000
1304         }
1305     assert_response :success
1306     item_uuids = json_response['items'].map do |item|
1307       item['uuid']
1308     end
1309     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
1310   end
1311
1312   test 'can get collection with past versions' do
1313     authorize_with :active
1314     get :index, params: {
1315       filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]],
1316       include_old_versions: true
1317     }
1318     assert_response :success
1319     assert_equal 2, assigns(:objects).length
1320     assert_equal 2, json_response['items_available']
1321     assert_equal 2, json_response['items'].count
1322     json_response['items'].each do |c|
1323       assert_equal collections(:collection_owned_by_active).uuid,
1324                    c['current_version_uuid'],
1325                    'response includes a version from a different collection'
1326     end
1327   end
1328
1329   test 'can get old version collection by uuid' do
1330     authorize_with :active
1331     get :show, params: {
1332       id: collections(:collection_owned_by_active_past_version_1).uuid,
1333     }
1334     assert_response :success
1335     assert_equal collections(:collection_owned_by_active_past_version_1).name,
1336                   json_response['name']
1337   end
1338
1339   test 'can get old version collection by PDH' do
1340     authorize_with :active
1341     get :show, params: {
1342       id: collections(:collection_owned_by_active_past_version_1).portable_data_hash,
1343     }
1344     assert_response :success
1345     assert_equal collections(:collection_owned_by_active_past_version_1).portable_data_hash,
1346                   json_response['portable_data_hash']
1347   end
1348
1349   test 'version and current_version_uuid are ignored at creation time' do
1350     permit_unsigned_manifests
1351     authorize_with :active
1352     manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
1353     post :create, params: {
1354       collection: {
1355         name: 'Test collection',
1356         version: 42,
1357         current_version_uuid: collections(:collection_owned_by_active).uuid,
1358         manifest_text: manifest_text,
1359       }
1360     }
1361     assert_response :success
1362     resp = JSON.parse(@response.body)
1363     assert_equal 1, resp['version']
1364     assert_equal resp['uuid'], resp['current_version_uuid']
1365   end
1366
1367   test "update collection with versioning enabled" do
1368     Rails.configuration.Collections.CollectionVersioning = true
1369     Rails.configuration.Collections.PreserveVersionIfIdle = 1 # 1 second
1370
1371     col = collections(:collection_owned_by_active)
1372     assert_equal 2, col.version
1373     assert col.modified_at < Time.now - 1.second
1374
1375     token = api_client_authorizations(:active).v2token
1376     signed = Blob.sign_locator(
1377       'acbd18db4cc2f85cedef654fccc4a4d8+3',
1378       key: Rails.configuration.Collections.BlobSigningKey,
1379       api_token: token)
1380     authorize_with_token token
1381     put :update, params: {
1382           id: col.uuid,
1383           collection: {
1384             manifest_text: ". #{signed} 0:3:foo.txt\n",
1385           },
1386         }
1387     assert_response :success
1388     assert_equal 3, json_response['version']
1389   end
1390
1391   test "delete collection with versioning enabled" do
1392     Rails.configuration.Collections.CollectionVersioning = true
1393     Rails.configuration.Collections.PreserveVersionIfIdle = 1 # 1 second
1394
1395     col = collections(:collection_owned_by_active)
1396     assert_equal 2, col.version
1397     assert col.modified_at < Time.now - 1.second
1398
1399     authorize_with(:active)
1400     post :trash, params: {
1401       id: col.uuid,
1402     }
1403     assert_response :success
1404     assert_equal col.version, json_response['version'], 'Trashing a collection should not create a new version'
1405   end
1406
1407   [['<', :<],
1408    ['<=', :<=],
1409    ['>', :>],
1410    ['>=', :>=],
1411    ['=', :==]].each do |op, rubyop|
1412     test "filter collections by replication_desired #{op} replication_confirmed" do
1413       authorize_with(:active)
1414       get :index, params: {
1415             filters: [["(replication_desired #{op} replication_confirmed)", "=", true]],
1416           }
1417       assert_response :success
1418       json_response["items"].each do |c|
1419         assert_operator(c["replication_desired"], rubyop, c["replication_confirmed"])
1420       end
1421     end
1422   end
1423
1424   ["(replication_desired < bogus)",
1425    "replication_desired < replication_confirmed",
1426    "(replication_desired < replication_confirmed",
1427    "(replication_desired ! replication_confirmed)",
1428    "(replication_desired <)",
1429    "(replication_desired < manifest_text)",
1430    "(manifest_text < manifest_text)", # currently only numeric attrs are supported
1431    "(replication_desired < 2)", # currently only attrs are supported, not literals
1432    "(1 < 2)",
1433   ].each do |expr|
1434     test "invalid filter expression #{expr}" do
1435       authorize_with(:active)
1436       get :index, params: {
1437             filters: [[expr, "=", true]],
1438           }
1439       assert_response 422
1440     end
1441   end
1442
1443   test "invalid op/arg with filter expression" do
1444     authorize_with(:active)
1445     get :index, params: {
1446           filters: [["replication_desired < replication_confirmed", "!=", false]],
1447         }
1448     assert_response 422
1449   end
1450
1451   ["storage_classes_desired", "storage_classes_confirmed"].each do |attr|
1452     test "filter collections by #{attr}" do
1453       authorize_with(:active)
1454       get :index, params: {
1455             filters: [[attr, "=", '["default"]']]
1456           }
1457       assert_response :success
1458       assert_not_equal 0, json_response["items"].length
1459       json_response["items"].each do |c|
1460         assert_equal ["default"], c[attr]
1461       end
1462     end
1463   end
1464
1465   test "select param is respected in 'show' response" do
1466     authorize_with :active
1467     get :show, params: {
1468           id: collections(:collection_owned_by_active).uuid,
1469           select: ["name"],
1470         }
1471     assert_response :success
1472     assert_raises ActiveModel::MissingAttributeError do
1473       assigns(:object).manifest_text
1474     end
1475     assert_nil json_response["manifest_text"]
1476     assert_nil json_response["properties"]
1477     assert_equal collections(:collection_owned_by_active).name, json_response["name"]
1478   end
1479
1480   test "select param is respected in 'update' response" do
1481     authorize_with :active
1482     post :update, params: {
1483           id: collections(:collection_owned_by_active).uuid,
1484           collection: {
1485             manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:foobar.txt\n",
1486           },
1487           select: ["name"],
1488         }
1489     assert_response :success
1490     assert_nil json_response["manifest_text"]
1491     assert_nil json_response["properties"]
1492     assert_equal collections(:collection_owned_by_active).name, json_response["name"]
1493   end
1494
1495   [nil,
1496    [],
1497    ["is_trashed", "trash_at"],
1498    ["is_trashed", "trash_at", "portable_data_hash"],
1499    ["portable_data_hash"],
1500    ["portable_data_hash", "manifest_text"],
1501   ].each do |select|
1502     test "select=#{select.inspect} param is respected in 'get by pdh' response" do
1503       authorize_with :active
1504       get :show, params: {
1505             id: collections(:collection_owned_by_active).portable_data_hash,
1506             select: select,
1507           }
1508       assert_response :success
1509       if !select || select.index("manifest_text")
1510         assert_not_nil json_response["manifest_text"]
1511       else
1512         assert_nil json_response["manifest_text"]
1513       end
1514     end
1515   end
1516 end