X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/682dd5b6cc23a455766a7651e3e841257660b31c..aa12c56d26cb72824ddd2399e17e846cd9d9b483:/services/api/test/functional/arvados/v1/collections_controller_test.rb diff --git a/services/api/test/functional/arvados/v1/collections_controller_test.rb b/services/api/test/functional/arvados/v1/collections_controller_test.rb index 3257e494c5..055af9e67c 100644 --- a/services/api/test/functional/arvados/v1/collections_controller_test.rb +++ b/services/api/test/functional/arvados/v1/collections_controller_test.rb @@ -1,6 +1,9 @@ require 'test_helper' class Arvados::V1::CollectionsControllerTest < ActionController::TestCase + include DbCurrentTime + + PERM_TOKEN_RE = /\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/ def permit_unsigned_manifests isok=true # Set security model for the life of a test. @@ -10,11 +13,23 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase def assert_signed_manifest manifest_text, label='' assert_not_nil manifest_text, "#{label} manifest_text was nil" manifest_text.scan(/ [[:xdigit:]]{32}\S*/) do |tok| - assert_match(/\+A[[:xdigit:]]+@[[:xdigit:]]{8}\b/, tok, + assert_match(PERM_TOKEN_RE, tok, "Locator in #{label} manifest_text was not signed") end end + def assert_unsigned_manifest resp, label='' + txt = resp['unsigned_manifest_text'] + assert_not_nil(txt, "#{label} unsigned_manifest_text was nil") + locs = 0 + txt.scan(/ [[:xdigit:]]{32}\S*/) do |tok| + locs += 1 + refute_match(PERM_TOKEN_RE, tok, + "Locator in #{label} unsigned_manifest_text was signed: #{tok}") + end + return locs + end + test "should get index" do authorize_with :active get :index @@ -24,12 +39,13 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase "basic Collections index included manifest_text") end - test "collections.get returns signed locators" do + test "collections.get returns signed locators, and no unsigned_manifest_text" do permit_unsigned_manifests authorize_with :active get :show, {id: collections(:foo_file).uuid} assert_response :success assert_signed_manifest json_response['manifest_text'], 'foo_file' + refute_includes json_response, 'unsigned_manifest_text' end test "index with manifest_text selected returns signed locators" do @@ -40,12 +56,69 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase assert(assigns(:objects).andand.any?, "no Collections returned for index with columns selected") json_response["items"].each do |coll| - assert_equal(columns, columns & coll.keys, + assert_equal(coll.keys - ['kind'], columns, "Collections index did not respect selected columns") assert_signed_manifest coll['manifest_text'], coll['uuid'] end end + test "index with unsigned_manifest_text selected returns only unsigned locators" do + authorize_with :active + get :index, select: ['unsigned_manifest_text'] + assert_response :success + assert_operator json_response["items"].count, :>, 0 + locs = 0 + json_response["items"].each do |coll| + assert_equal(coll.keys - ['kind'], ['unsigned_manifest_text'], + "Collections index did not respect selected columns") + locs += assert_unsigned_manifest coll, coll['uuid'] + end + assert_operator locs, :>, 0, "no locators found in any manifests" + end + + test 'index without select returns everything except manifest' do + authorize_with :active + get :index + assert_response :success + assert json_response['items'].any? + json_response['items'].each do |coll| + assert_includes(coll.keys, 'uuid') + assert_includes(coll.keys, 'name') + assert_includes(coll.keys, 'created_at') + refute_includes(coll.keys, 'manifest_text') + end + end + + ['', nil, false, 'null'].each do |select| + test "index with select=#{select.inspect} returns everything except manifest" do + authorize_with :active + get :index, select: select + assert_response :success + assert json_response['items'].any? + json_response['items'].each do |coll| + assert_includes(coll.keys, 'uuid') + assert_includes(coll.keys, 'name') + assert_includes(coll.keys, 'created_at') + refute_includes(coll.keys, 'manifest_text') + end + end + end + + [["uuid"], + ["uuid", "manifest_text"], + '["uuid"]', + '["uuid", "manifest_text"]'].each do |select| + test "index with select=#{select.inspect} returns no name" do + authorize_with :active + get :index, select: select + assert_response :success + assert json_response['items'].any? + json_response['items'].each do |coll| + refute_includes(coll.keys, 'name') + end + end + end + [0,1,2].each do |limit| test "get index with limit=#{limit}" do authorize_with :active @@ -103,11 +176,36 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase }.merge(params) end + test "index with manifest_text limited by max_index_database_read returns non-empty" do + request_capped_index() { |_| 1 } + assert_response :success + assert_equal(1, json_response["items"].size) + assert_equal(1, json_response["limit"]) + assert_equal(201, json_response["items_available"]) + end + + test "max_index_database_read size check follows same order as real query" do + authorize_with :user1_with_load + txt = '.' + ' d41d8cd98f00b204e9800998ecf8427e+0'*1000 + " 0:0:empty.txt\n" + c = Collection.create! manifest_text: txt, name: '0000000000000000000' + request_capped_index(select: %w(uuid manifest_text name), + order: ['name asc'], + filters: [['name','>=',c.name]]) do |_| + txt.length - 1 + end + assert_response :success + assert_equal(1, json_response["items"].size) + assert_equal(1, json_response["limit"]) + assert_equal(c.uuid, json_response["items"][0]["uuid"]) + # The effectiveness of the test depends on >1 item matching the filters. + assert_operator(1, :<, json_response["items_available"]) + end + test "index with manifest_text limited by max_index_database_read" do request_capped_index() { |size| (size * 3) + 1 } assert_response :success - assert_equal(4, json_response["items"].size) - assert_equal(4, json_response["limit"]) + assert_equal(3, json_response["items"].size) + assert_equal(3, json_response["limit"]) assert_equal(201, json_response["items_available"]) end @@ -120,13 +218,14 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase end test "max_index_database_read does not interfere with order" do - request_capped_index(order: "name DESC") { |size| (size * 15) + 1 } + request_capped_index(select: %w(uuid manifest_text name), + order: "name DESC") { |size| (size * 11) + 1 } assert_response :success - assert_equal(16, json_response["items"].size) + assert_equal(11, json_response["items"].size) assert_empty(json_response["items"].reject do |coll| - coll["name"] !~ /^Collection_9/ + coll["name"] =~ /^Collection_9/ end) - assert_equal(16, json_response["limit"]) + assert_equal(11, json_response["limit"]) assert_equal(201, json_response["items_available"]) end @@ -184,12 +283,12 @@ EOS assert_response :success assert_not_nil assigns(:object) resp = assigns(:object) - assert_equal foo_collection[:portable_data_hash], resp['portable_data_hash'] - assert_signed_manifest resp['manifest_text'] + assert_equal foo_collection[:portable_data_hash], resp[:portable_data_hash] + assert_signed_manifest resp[:manifest_text] # The manifest in the response will have had permission hints added. # Remove any permission hints in the response before comparing it to the source. - stripped_manifest = resp['manifest_text'].gsub(/\+A[A-Za-z0-9@_-]+/, '') + stripped_manifest = resp[:manifest_text].gsub(/\+A[A-Za-z0-9@_-]+/, '') assert_equal foo_collection[:manifest_text], stripped_manifest end end @@ -246,7 +345,7 @@ EOS ensure_unique_name: true } assert_response :success - assert_equal 'owned_by_active (2)', json_response['name'] + assert_match /^owned_by_active \(\d{4}-\d\d-\d\d.*?Z\)$/, json_response['name'] end end @@ -505,8 +604,8 @@ EOS } # Generate a locator with a bad signature. - unsigned_locator = "d41d8cd98f00b204e9800998ecf8427e+0" - bad_locator = unsigned_locator + "+Affffffff@ffffffff" + unsigned_locator = "acbd18db4cc2f85cedef654fccc4a4d8+3" + bad_locator = unsigned_locator + "+Affffffffffffffffffffffffffffffffffffffff@ffffffff" assert !Blob.verify_signature(bad_locator, signing_opts) # Creating a collection with this locator should @@ -551,6 +650,16 @@ EOS assert_response 422 end + test "reject manifest with unsigned block as stream name" do + authorize_with :active + post :create, { + collection: { + manifest_text: "00000000000000000000000000000000+1234 d41d8cd98f00b204e9800998ecf8427e+0 0:0:foo.txt\n" + } + } + assert_includes [422, 403], response.code.to_i + end + test "multiple locators per line" do permit_unsigned_manifests authorize_with :active @@ -715,11 +824,11 @@ EOS [2**8, :success], [2**18, 422], ].each do |description_size, expected_response| - test "create collection with description size #{description_size} + # Descriptions are not part of search indexes. Skip until + # full-text search is implemented, at which point replace with a + # search in description. + skip "create collection with description size #{description_size} and expect response #{expected_response}" do - skip "(Descriptions are not part of search indexes. Skip until full-text search - is implemented, at which point replace with a search in description.)" - authorize_with :active description = 'here is a collection with a very large description' @@ -773,4 +882,187 @@ EOS assert_not_nil json_response['uuid'] assert_equal 'value_1', json_response['properties']['property_1'] end + + [ + ". 0:0:foo.txt", + ". d41d8cd98f00b204e9800998ecf8427e foo.txt", + "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt", + ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt", + ].each do |manifest_text| + test "create collection with invalid manifest #{manifest_text} and expect error" do + authorize_with :active + post :create, { + collection: { + manifest_text: manifest_text, + portable_data_hash: "d41d8cd98f00b204e9800998ecf8427e+0" + } + } + assert_response 422 + response_errors = json_response['errors'] + assert_not_nil response_errors, 'Expected error in response' + assert(response_errors.first.include?('Invalid manifest'), + "Expected 'Invalid manifest' error in #{response_errors.first}") + end + end + + [ + [nil, "d41d8cd98f00b204e9800998ecf8427e+0"], + ["", "d41d8cd98f00b204e9800998ecf8427e+0"], + [". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", "d30fe8ae534397864cb96c544f4cf102+47"], + ].each do |manifest_text, pdh| + test "create collection with valid manifest #{manifest_text.inspect} and expect success" do + authorize_with :active + post :create, { + collection: { + manifest_text: manifest_text, + portable_data_hash: pdh + } + } + assert_response 200 + end + end + + [ + ". 0:0:foo.txt", + ". d41d8cd98f00b204e9800998ecf8427e foo.txt", + "d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt", + ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt", + ].each do |manifest_text| + test "update collection with invalid manifest #{manifest_text} and expect error" do + authorize_with :active + post :update, { + id: 'zzzzz-4zz18-bv31uwvy3neko21', + collection: { + manifest_text: manifest_text, + } + } + assert_response 422 + response_errors = json_response['errors'] + assert_not_nil response_errors, 'Expected error in response' + assert(response_errors.first.include?('Invalid manifest'), + "Expected 'Invalid manifest' error in #{response_errors.first}") + end + end + + [ + nil, + "", + ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", + ].each do |manifest_text| + test "update collection with valid manifest #{manifest_text.inspect} and expect success" do + authorize_with :active + post :update, { + id: 'zzzzz-4zz18-bv31uwvy3neko21', + collection: { + manifest_text: manifest_text, + } + } + assert_response 200 + end + end + + test 'get trashed collection with include_trash' do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :active + get :show, { + id: uuid, + include_trash: true, + } + assert_response 200 + end + + test 'get trashed collection without include_trash' do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :active + get :show, { + id: uuid, + } + assert_response 404 + end + + test 'trash collection using http DELETE verb' do + uuid = collections(:collection_owned_by_active).uuid + authorize_with :active + delete :destroy, { + id: uuid, + } + assert_response 200 + c = Collection.unscoped.find_by_uuid(uuid) + assert_operator c.trash_at, :<, db_current_time + assert_equal c.delete_at, c.trash_at + Rails.configuration.blob_signature_ttl + end + + test 'delete long-trashed collection immediately using http DELETE verb' do + uuid = 'zzzzz-4zz18-mto52zx1s7sn3ih' # expired_collection + authorize_with :active + delete :destroy, { + id: uuid, + } + assert_response 200 + c = Collection.unscoped.find_by_uuid(uuid) + assert_operator c.trash_at, :<, db_current_time + assert_operator c.delete_at, :<, db_current_time + end + + ['zzzzz-4zz18-mto52zx1s7sn3ih', # expired_collection + :empty_collection_name_in_active_user_home_project, + ].each do |fixture| + test "trash collection #{fixture} via trash action with grace period" do + if fixture.is_a? String + uuid = fixture + else + uuid = collections(fixture).uuid + end + authorize_with :active + time_before_trashing = db_current_time + post :trash, { + id: uuid, + } + assert_response 200 + c = Collection.unscoped.find_by_uuid(uuid) + assert_operator c.trash_at, :<, db_current_time + assert_operator c.delete_at, :>=, time_before_trashing + Rails.configuration.default_trash_lifetime + end + end + + test 'untrash a trashed collection' do + authorize_with :active + post :untrash, { + id: collections(:expired_collection).uuid, + } + assert_response 200 + assert_equal false, json_response['is_trashed'] + assert_nil json_response['trash_at'] + end + + test 'untrash error on not trashed collection' do + authorize_with :active + post :untrash, { + id: collections(:collection_owned_by_active).uuid, + } + assert_response 422 + end + + [:active, :admin].each do |user| + test "get trashed collections as #{user}" do + authorize_with :active + get :index, { + filters: [["is_trashed", "=", true]], + include_trash: true, + } + assert_response :success + + items = [] + json_response["items"].each do |coll| + items << coll['uuid'] + end + + assert_includes(items, collections('unique_expired_collection')['uuid']) + if user == :admin + assert_includes(items, collections('unique_expired_collection2')['uuid']) + else + assert_not_includes(items, collections('unique_expired_collection2')['uuid']) + end + end + end end