diff --git a/.travis.yml b/.travis.yml index 3c8de3f..d3d7d89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,15 @@ language: ruby cache: bundler rvm: - 2.2.4 +services: + - redis-server before_install: - sh .travis/install_riakcs.sh - gem install bundler before_script: - cp config.yml.example config.yml -script: bundle exec rake test + - mkdir -p tmp && echo "swifttoken" > tmp/swift_token.txt +script: ruby spec/swift/* branches: only: - master diff --git a/Gemfile b/Gemfile index f5ee5c7..694b773 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ group :test do gem 'rake' gem 'purdytest', :require => false gem 'm' + gem 'minitest-stub_any_instance' end group :staging, :production do diff --git a/Gemfile.lock b/Gemfile.lock index d234198..9d3cc8f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -50,6 +50,7 @@ GEM method_source (0.8.2) mime-types (2.6.1) minitest (5.7.0) + minitest-stub_any_instance (1.0.1) multi_json (1.11.1) multipart-post (2.0.0) net-scp (1.0.4) @@ -109,6 +110,7 @@ DEPENDENCIES fog m mime-types (~> 2.6.1) + minitest-stub_any_instance purdytest rainbows rake diff --git a/config.yml.example b/config.yml.example index f0ed36d..6fb3307 100644 --- a/config.yml.example +++ b/config.yml.example @@ -1,18 +1,18 @@ development: &defaults maintenance: false - riak: &riak_defaults - host: localhost - http_port: 8098 - riak_cs: - credentials_file: "cs_credentials.json" - endpoint: "http://cs.example.com:8080" - buckets: - data: rs_data - directories: rs_directories - binaries: rs_binaries - cs_binaries: rs.binaries - authorizations: rs_authorizations - opslog: rs_opslog + # riak: &riak_defaults + # host: localhost + # http_port: 8098 + # riak_cs: + # credentials_file: "cs_credentials.json" + # endpoint: "http://cs.example.com:8080" + # buckets: + # data: rs_data + # directories: rs_directories + # binaries: rs_binaries + # cs_binaries: rs.binaries + # authorizations: rs_authorizations + # opslog: rs_opslog # # uncomment this section and comment the riak one # swift: &swift_defaults # host: "https://swift.example.com" @@ -23,15 +23,20 @@ development: &defaults test: <<: *defaults - riak: - <<: *riak_defaults - buckets: - data: rs_data_test - directories: rs_directories_test - binaries: rs_binaries_test - cs_binaries: rs.binaries.test - authorizations: rs_authorizations_test - opslog: rs_opslog_test + # riak: + # <<: *riak_defaults + # buckets: + # data: rs_data_test + # directories: rs_directories_test + # binaries: rs_binaries_test + # cs_binaries: rs.binaries.test + # authorizations: rs_authorizations_test + # opslog: rs_opslog_test + swift: + host: "https://swift.example.com" + redis: + host: localhost + port: 6379 staging: <<: *defaults diff --git a/lib/remote_storage/swift.rb b/lib/remote_storage/swift.rb index 5fabbab..a53ee2a 100644 --- a/lib/remote_storage/swift.rb +++ b/lib/remote_storage/swift.rb @@ -28,6 +28,7 @@ module RemoteStorage server.halt 401 unless permission if ["PUT", "DELETE"].include? request_method + server.halt 503 if directory_backend(user).match(/locked/) server.halt 401 unless permission == "rw" end end @@ -75,6 +76,68 @@ module RemoteStorage end def get_directory_listing(user, directory) + if directory_backend(user).match(/new/) + get_directory_listing_from_redis(user, directory) + else + get_directory_listing_from_swift(user, directory) + end + end + + def get_directory_listing_from_redis_via_lua(user, directory) + lua_script = <<-EOF + local user = ARGV[1] + local directory = ARGV[2] + local items = redis.call("smembers", "rs_meta:"..user..":"..directory.."/:items") + local listing = {} + + for index, name in pairs(items) do + local redis_key = "rs_meta:"..user..":" + if directory == "" then + redis_key = redis_key..name + else + redis_key = redis_key..directory.."/"..name + end + + local metadata_values = redis.call("hgetall", redis_key) + local metadata = {} + + -- redis returns hashes as a single list of alternating keys and values + -- this collates it into a table + for idx = 1, #metadata_values, 2 do + metadata[metadata_values[idx]] = metadata_values[idx + 1] + end + + listing[name] = {["ETag"] = metadata["etag"]} + if string.sub(name, -1) ~= "/" then + listing[name]["Content-Type"] = metadata["type"] + listing[name]["Content-Length"] = tonumber(metadata["size"]) + end + end + + return cjson.encode(listing) + EOF + + JSON.parse(redis.eval(lua_script, nil, [user, directory])) + end + + def get_directory_listing_from_redis(user, directory) + etag = redis.hget "rs_meta:#{user}:#{directory}/", "etag" + + none_match = (server.env["HTTP_IF_NONE_MATCH"] || "").split(",").map(&:strip) + server.halt 304 if none_match.include? etag + + server.headers["Content-Type"] = "application/json" + server.headers["ETag"] = %Q("#{etag}") + + listing = { + "@context" => "http://remotestorage.io/spec/folder-description", + "items" => get_directory_listing_from_redis_via_lua(user, directory) + } + + listing.to_json + end + + def get_directory_listing_from_swift(user, directory) is_root_listing = directory.empty? server.headers["Content-Type"] = "application/json" @@ -125,7 +188,16 @@ module RemoteStorage res = do_put_request(url, data, content_type) - if update_dir_objects(user, directory) + # TODO get last modified from response and add to metadata + metadata = { + etag: res.headers[:etag], + size: data.size, + type: content_type + } + + if update_metadata_object(user, directory, key, metadata) && + # TODO provide the last modified to use for the dir objects as well + update_dir_objects(user, directory) server.headers["ETag"] = %Q("#{res.headers[:etag]}") server.halt 200 else @@ -143,6 +215,7 @@ module RemoteStorage end do_delete_request(url) + delete_metadata_objects(user, directory, key) delete_dir_objects(user, directory) server.halt 200 @@ -217,6 +290,58 @@ module RemoteStorage end def has_name_collision?(user, directory, key) + if directory_backend(user).match(/new/) + has_name_collision_via_redis?(user, directory, key) + else + has_name_collision_via_swift?(user, directory, key) + end + end + + def has_name_collision_via_redis?(user, directory, key) + lua_script = <<-EOF + local user = ARGV[1] + local directory = ARGV[2] + local key = ARGV[3] + + -- build table with parent directories from remaining arguments + local parent_dir_count = #ARGV - 3 + local parent_directories = {} + for i = 4, 4 + parent_dir_count do + table.insert(parent_directories, ARGV[i]) + end + + -- check for existing directory with the same name as the document + local redis_key = "rs_meta:"..user..":" + if directory == "" then + redis_key = redis_key..key.."/" + else + redis_key = redis_key..directory.."/"..key.."/" + end + if redis.call("hget", redis_key, "etag") then + return true + end + + for index, dir in pairs(parent_directories) do + if redis.call("hget", "rs_meta:"..user..":"..dir.."/", "etag") then + -- the directory already exists, no need to do further checks + return false + else + -- check for existing document with same name as directory + if redis.call("hget", "rs_meta:"..user..":"..dir, "etag") then + return true + end + end + end + + return false + EOF + + parent_directories = parent_directories_for(directory) + + redis.eval(lua_script, nil, [user, directory, key, *parent_directories]) + end + + def has_name_collision_via_swift?(user, directory, key) # check for existing directory with the same name as the document url = url_for_key(user, directory, key) do_head_request("#{url}/") do |res| @@ -252,39 +377,88 @@ module RemoteStorage directories.pop end + parent_directories << "" # add empty string for the root directory + parent_directories end + def top_directory(directory) + if directory.match(/\//) + directory.split("/").last + elsif directory != "" + return directory + end + end + + def parent_directory_for(directory) + if directory.match(/\//) + return directory[0..directory.rindex("/")] + elsif directory != "" + return "/" + end + end + + def update_metadata_object(user, directory, key, metadata) + redis_key = "rs_meta:#{user}:#{directory}/#{key}" + redis.hmset(redis_key, *metadata) + redis.sadd "rs_meta:#{user}:#{directory}/:items", key + + true + end + def update_dir_objects(user, directory) + # TODO use actual last modified time from the document put request timestamp = (Time.now.to_f * 1000).to_i parent_directories_for(directory).each do |dir| - do_put_request("#{url_for_directory(user, dir)}/", timestamp.to_s, "text/plain") + # TODO check if we can actually do a put request to the root dir + res = do_put_request("#{url_for_directory(user, dir)}/", timestamp.to_s, "text/plain") + key = "rs_meta:#{user}:#{dir}/" + metadata = {etag: res.headers[:etag], modified: timestamp} + redis.hmset(key, *metadata) + redis.sadd "rs_meta:#{user}:#{parent_directory_for(dir)}:items", "#{top_directory(dir)}/" end true rescue parent_directories_for(directory).each do |dir| + # TODO check if we can actually do a delete request to the root dir do_delete_request("#{url_for_directory(user, dir)}/") rescue false end false end + def delete_metadata_objects(user, directory, key) + redis_key = "rs_meta:#{user}:#{directory}/#{key}" + redis.del(redis_key) + redis.srem "rs_meta:#{user}:#{directory}/:items", key + end + def delete_dir_objects(user, directory) parent_directories_for(directory).each do |dir| if dir_empty?(user, dir) + # TODO check if we can actually do a delete request to the root dir do_delete_request("#{url_for_directory(user, dir)}/") + redis.del "rs_meta:#{user}:#{directory}/" + redis.srem "rs_meta:#{user}:#{parent_directory_for(dir)}:items", "#{dir}/" else timestamp = (Time.now.to_f * 1000).to_i - do_put_request("#{url_for_directory(user, dir)}/", timestamp.to_s, "text/plain") + # TODO check if we can actually do a put request to the root dir + res = do_put_request("#{url_for_directory(user, dir)}/", timestamp.to_s, "text/plain") + metadata = {etag: res.headers[:etag], modified: timestamp} + redis.hmset("rs_meta:#{user}:#{dir}/", *metadata) end end end def dir_empty?(user, dir) - do_get_request("#{container_url_for(user)}/?format=plain&limit=1&path=#{escape(dir)}/") do |res| - return res.headers[:content_length] == "0" + if directory_backend(user).match(/new/) + redis.smembers("rs_meta:#{user}:#{dir}/:items").empty? + else + do_get_request("#{container_url_for(user)}/?format=plain&limit=1&path=#{escape(dir)}/") do |res| + return res.headers[:content_length] == "0" + end end end @@ -349,6 +523,10 @@ module RemoteStorage @redis ||= Redis.new(host: settings.redis["host"], port: settings.redis["port"]) end + def directory_backend(user) + @directory_backend ||= redis.get("rs_config:dir_backend:#{user}") || "legacy" + end + def etag_for(body) objects = JSON.parse(body) diff --git a/spec/app_spec.rb b/spec/riak/app_spec.rb similarity index 85% rename from spec/app_spec.rb rename to spec/riak/app_spec.rb index ccdb8a1..411c6c4 100644 --- a/spec/app_spec.rb +++ b/spec/riak/app_spec.rb @@ -1,4 +1,4 @@ -require_relative "spec_helper" +require_relative "../spec_helper" describe "App" do include Rack::Test::Methods @@ -11,4 +11,5 @@ describe "App" do get "/virginmargarita" last_response.status.must_equal 404 end + end diff --git a/spec/directories_spec.rb b/spec/riak/directories_spec.rb similarity index 99% rename from spec/directories_spec.rb rename to spec/riak/directories_spec.rb index ea62080..eea4f82 100644 --- a/spec/directories_spec.rb +++ b/spec/riak/directories_spec.rb @@ -1,4 +1,4 @@ -require_relative "spec_helper" +require_relative "../spec_helper" describe "Directories" do include Rack::Test::Methods @@ -251,7 +251,7 @@ describe "Directories" do context "charset given in content-type header" do before do header "Content-Type", "image/jpeg; charset=binary" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/tasks/jaypeg.jpg", @image end @@ -273,7 +273,7 @@ describe "Directories" do context "no charset in content-type header" do before do header "Content-Type", "image/jpeg" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/tasks/jaypeg.jpg", @image end diff --git a/spec/permissions_spec.rb b/spec/riak/permissions_spec.rb similarity index 99% rename from spec/permissions_spec.rb rename to spec/riak/permissions_spec.rb index 6652c85..6250a2e 100644 --- a/spec/permissions_spec.rb +++ b/spec/riak/permissions_spec.rb @@ -1,4 +1,4 @@ -require_relative "spec_helper" +require_relative "../spec_helper" describe "Permissions" do include Rack::Test::Methods diff --git a/spec/riak_spec.rb b/spec/riak/riak_spec.rb similarity index 98% rename from spec/riak_spec.rb rename to spec/riak/riak_spec.rb index b3116f2..19c2d69 100644 --- a/spec/riak_spec.rb +++ b/spec/riak/riak_spec.rb @@ -1,4 +1,4 @@ -require_relative "spec_helper" +require_relative "../spec_helper" describe "App with Riak backend" do include Rack::Test::Methods @@ -446,7 +446,7 @@ describe "App with Riak backend" do context "binary charset in content-type header" do before do header "Content-Type", "image/jpeg; charset=binary" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/documents/jaypeg", @image end @@ -502,7 +502,7 @@ describe "App with Riak backend" do context "overwriting existing file with same file" do before do header "Content-Type", "image/jpeg; charset=binary" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/documents/jaypeg", @image end @@ -518,7 +518,7 @@ describe "App with Riak backend" do context "overwriting existing file with different file" do before do header "Content-Type", "image/jpeg; charset=binary" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/documents/jaypeg", @image+"foo" end @@ -540,7 +540,7 @@ describe "App with Riak backend" do context "no binary charset in content-type header" do before do header "Content-Type", "image/jpeg" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/documents/jaypeg", @image end @@ -705,7 +705,7 @@ describe "App with Riak backend" do context "binary data" do before do header "Content-Type", "image/jpeg; charset=binary" - filename = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures", "rockrule.jpeg") + filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg") @image = File.open(filename, "r").read put "/jimmy/documents/jaypeg", @image diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ee4daac..4975f52 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,6 +9,10 @@ require 'minitest/autorun' require 'rack/test' require 'purdytest' require 'riak' +require "redis" +require "rest_client" +require "minitest/stub_any_instance" +require "ostruct" def app LiquorCabinet @@ -28,6 +32,19 @@ def write_last_response_to_file(filename = "last_response.html") end alias context describe + +if app.settings.respond_to? :redis + def redis + @redis ||= Redis.new(host: app.settings.redis["host"], port: app.settings.redis["port"]) + end + + def purge_redis + redis.keys("rs_*").each do |key| + redis.del key + end + end +end + if app.settings.respond_to? :riak ::Riak.disable_list_keys_warnings = true diff --git a/spec/swift/app_spec.rb b/spec/swift/app_spec.rb new file mode 100644 index 0000000..dc51ee4 --- /dev/null +++ b/spec/swift/app_spec.rb @@ -0,0 +1,312 @@ +require_relative "../spec_helper" + +describe "App" do + include Rack::Test::Methods + + def app + LiquorCabinet + end + + it "returns 404 on non-existing routes" do + get "/virginmargarita" + last_response.status.must_equal 404 + end + + describe "PUT requests" do + + before do + purge_redis + redis.set "rs_config:dir_backend:phil", "new" + end + + context "authorized" do + before do + redis.sadd "authorizations:phil:amarillo", [":rw"] + header "Authorization", "Bearer amarillo" + end + + it "creates the metadata object in redis" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + end + + metadata = redis.hgetall "rs_meta:phil:food/aguacate" + metadata["size"].must_equal "2" + metadata["type"].must_equal "text/plain; charset=utf-8" + metadata["etag"].must_equal "bla" + metadata["modified"].must_equal nil + end + + it "creates the directory objects metadata in redis" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + put "/phil/food/camaron", "yummi" + end + + metadata = redis.hgetall "rs_meta:phil:/" + metadata["etag"].must_equal "bla" + metadata["modified"].length.must_equal 13 + + metadata = redis.hgetall "rs_meta:phil:food/" + metadata["etag"].must_equal "bla" + metadata["modified"].length.must_equal 13 + + food_items = redis.smembers "rs_meta:phil:food/:items" + food_items.each do |food_item| + ["camaron", "aguacate"].must_include food_item + end + + root_items = redis.smembers "rs_meta:phil:/:items" + root_items.must_equal ["food/"] + end + + describe "name collision checks" do + it "is successful when there is no name collision" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + end + + last_response.status.must_equal 200 + + metadata = redis.hgetall "rs_meta:phil:food/aguacate" + metadata["size"].must_equal "2" + end + + it "conflicts when there is a directory with same name as document" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + put "/phil/food", "wontwork" + end + + last_response.status.must_equal 409 + + metadata = redis.hgetall "rs_meta:phil:food" + metadata.must_be_empty + end + + it "conflicts when there is a document with same name as directory" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + put "/phil/food/aguacate/empanado", "wontwork" + end + + last_response.status.must_equal 409 + + metadata = redis.hgetall "rs_meta:phil:food/aguacate/empanado" + metadata.must_be_empty + end + end + + describe "directory backend configuration" do + context "locked new backed" do + before do + redis.set "rs_config:dir_backend:phil", "new-locked" + end + + it "responds with 503" do + put "/phil/food/aguacate", "si" + + last_response.status.must_equal 503 + + metadata = redis.hgetall "rs_meta:phil:food/aguacate" + metadata.must_be_empty + end + end + + context "locked legacy backend" do + before do + redis.set "rs_config:dir_backend:phil", "legacy-locked" + end + + it "responds with 503" do + put "/phil/food/aguacate", "si" + + last_response.status.must_equal 503 + + metadata = redis.hgetall "rs_meta:phil:food/aguacate" + metadata.must_be_empty + end + end + end + end + end + + describe "DELETE requests" do + + before do + purge_redis + redis.set "rs_config:dir_backend:phil", "new" + end + + context "authorized" do + before do + redis.sadd "authorizations:phil:amarillo", [":rw"] + header "Authorization", "Bearer amarillo" + + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + put "/phil/food/camaron", "yummi" + end + end + + it "deletes the metadata object in redis" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + RestClient.stub :delete, "" do + delete "/phil/food/aguacate" + end + end + + metadata = redis.hgetall "rs_meta:phil:food/aguacate" + metadata.must_be_empty + end + + it "deletes the directory objects metadata in redis" do + old_metadata = redis.hgetall "rs_meta:phil:food/" + + put_stub = OpenStruct.new(headers: {etag: "newetag"}) + RestClient.stub :put, put_stub do + RestClient.stub :delete, "" do + delete "/phil/food/aguacate" + end + end + + metadata = redis.hgetall "rs_meta:phil:food/" + metadata["etag"].must_equal "newetag" + metadata["modified"].length.must_equal 13 + metadata["modified"].wont_equal old_metadata["modified"] + + food_items = redis.smembers "rs_meta:phil:food/:items" + food_items.must_equal ["camaron"] + + root_items = redis.smembers "rs_meta:phil:/:items" + root_items.must_equal ["food/"] + end + + it "deletes the parent directory objects metadata when deleting all items" do + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + RestClient.stub :delete, "" do + delete "/phil/food/aguacate" + delete "/phil/food/camaron" + end + end + + metadata = redis.hgetall "rs_meta:phil:food/" + metadata.must_be_empty + + food_items = redis.smembers "rs_meta:phil:food/:items" + food_items.must_be_empty + + root_items = redis.smembers "rs_meta:phil:/:items" + root_items.must_be_empty + end + end + end + + describe "GET requests" do + + before do + purge_redis + redis.set "rs_config:dir_backend:phil", "new" + end + + context "authorized" do + + before do + redis.sadd "authorizations:phil:amarillo", [":rw"] + header "Authorization", "Bearer amarillo" + + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + put "/phil/food/camaron", "yummi" + put "/phil/food/desunyos/bolon", "wow" + end + end + + describe "directory listings" do + + it "has an ETag in the header" do + get "/phil/food/" + + last_response.status.must_equal 200 + last_response.headers["ETag"].must_equal "\"bla\"" + end + + it "responds with 304 when IF_NONE_MATCH header contains the ETag" do + header "If-None-Match", "bla" + get "/phil/food/" + + last_response.status.must_equal 304 + end + + it "contains all items in the directory" do + get "/phil/food/" + + last_response.status.must_equal 200 + last_response.content_type.must_equal "application/json" + + content = JSON.parse(last_response.body) + content["@context"].must_equal "http://remotestorage.io/spec/folder-description" + content["items"]["aguacate"].wont_be_nil + content["items"]["aguacate"]["Content-Type"].must_equal "text/plain; charset=utf-8" + content["items"]["aguacate"]["Content-Length"].must_equal 2 + content["items"]["aguacate"]["ETag"].must_equal "bla" + content["items"]["camaron"].wont_be_nil + content["items"]["camaron"]["Content-Type"].must_equal "text/plain; charset=utf-8" + content["items"]["camaron"]["Content-Length"].must_equal 5 + content["items"]["camaron"]["ETag"].must_equal "bla" + content["items"]["desunyos/"].wont_be_nil + content["items"]["desunyos/"]["ETag"].must_equal "bla" + end + + it "contains all items in the root directory" do + get "phil/" + + last_response.status.must_equal 200 + last_response.content_type.must_equal "application/json" + + content = JSON.parse(last_response.body) + content["items"]["food/"].wont_be_nil + content["items"]["food/"]["ETag"].must_equal "bla" + end + + end + end + + context "with legacy directory backend" do + + before do + redis.sadd "authorizations:phil:amarillo", [":rw"] + header "Authorization", "Bearer amarillo" + + put_stub = OpenStruct.new(headers: {etag: "bla"}) + RestClient.stub :put, put_stub do + put "/phil/food/aguacate", "si" + put "/phil/food/camaron", "yummi" + end + + redis.set "rs_config:dir_backend:phil", "legacy" + end + + it "serves directory listing from Swift backend" do + RemoteStorage::Swift.stub_any_instance :get_directory_listing_from_swift, "directory listing" do + get "/phil/food/" + end + + last_response.status.must_equal 200 + last_response.body.must_equal "directory listing" + end + + end + + end +end +