Remove all duplication the specs
S3 and Swift now run the same specs. The only difference is the before block that defines the stubbed HTTP requests and the responses from the Swift and S3 servers
This commit is contained in:
parent
21dad2aba7
commit
21f3a9f60f
1
Gemfile
1
Gemfile
@ -15,6 +15,7 @@ group :test do
|
|||||||
gem 'purdytest', :require => false
|
gem 'purdytest', :require => false
|
||||||
gem 'm'
|
gem 'm'
|
||||||
gem 'minitest-stub_any_instance'
|
gem 'minitest-stub_any_instance'
|
||||||
|
gem 'webmock'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :staging, :production do
|
group :staging, :production do
|
||||||
|
12
Gemfile.lock
12
Gemfile.lock
@ -6,12 +6,17 @@ GEM
|
|||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
|
addressable (2.5.2)
|
||||||
|
public_suffix (>= 2.0.2, < 4.0)
|
||||||
backports (3.11.2)
|
backports (3.11.2)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
|
crack (0.4.3)
|
||||||
|
safe_yaml (~> 1.0.0)
|
||||||
domain_name (0.5.20170404)
|
domain_name (0.5.20170404)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
faraday (0.14.0)
|
faraday (0.14.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
|
hashdiff (0.3.7)
|
||||||
http-accept (1.7.0)
|
http-accept (1.7.0)
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
@ -31,6 +36,7 @@ GEM
|
|||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
mustermann (1.0.2)
|
mustermann (1.0.2)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
|
public_suffix (3.0.2)
|
||||||
purdytest (2.0.0)
|
purdytest (2.0.0)
|
||||||
minitest (~> 5.5)
|
minitest (~> 5.5)
|
||||||
rack (2.0.4)
|
rack (2.0.4)
|
||||||
@ -50,6 +56,7 @@ GEM
|
|||||||
http-cookie (>= 1.0.2, < 2.0)
|
http-cookie (>= 1.0.2, < 2.0)
|
||||||
mime-types (>= 1.16, < 4.0)
|
mime-types (>= 1.16, < 4.0)
|
||||||
netrc (~> 0.8)
|
netrc (~> 0.8)
|
||||||
|
safe_yaml (1.0.4)
|
||||||
sentry-raven (2.7.2)
|
sentry-raven (2.7.2)
|
||||||
faraday (>= 0.7.6, < 1.0)
|
faraday (>= 0.7.6, < 1.0)
|
||||||
sinatra (2.0.1)
|
sinatra (2.0.1)
|
||||||
@ -74,6 +81,10 @@ GEM
|
|||||||
unicorn (5.4.0)
|
unicorn (5.4.0)
|
||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
raindrops (~> 0.7)
|
raindrops (~> 0.7)
|
||||||
|
webmock (3.3.0)
|
||||||
|
addressable (>= 2.3.6)
|
||||||
|
crack (>= 0.3.2)
|
||||||
|
hashdiff
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -92,6 +103,7 @@ DEPENDENCIES
|
|||||||
sentry-raven
|
sentry-raven
|
||||||
sinatra
|
sinatra
|
||||||
sinatra-contrib
|
sinatra-contrib
|
||||||
|
webmock
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.16.0
|
1.16.0
|
||||||
|
@ -1,882 +1,56 @@
|
|||||||
require_relative "../spec_helper"
|
require_relative "../spec_helper"
|
||||||
|
|
||||||
describe "App" do
|
describe "App" do
|
||||||
include Rack::Test::Methods
|
def container_url_for(user)
|
||||||
|
"#{app.settings.s3["endpoint"]}#{app.settings.s3["bucket"]}/#{user}"
|
||||||
def app
|
|
||||||
LiquorCabinet
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns 404 on non-existing routes" do
|
def storage_class
|
||||||
get "/virginmargarita"
|
RemoteStorage::S3Rest
|
||||||
last_response.status.must_equal 404
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "PUT requests" do
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
purge_redis
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
end
|
to_return(status: 200, headers: { etag: '"0815etag"' })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
context "authorized" do
|
with(body: "si").
|
||||||
before do
|
to_return(status: 200, headers: { etag: '"0815etag"' })
|
||||||
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
header "Authorization", "Bearer amarillo"
|
with(body: "aye").
|
||||||
end
|
to_return(status: 200, headers: { etag: '"0915etag"' })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
it "creates the metadata object in redis" do
|
with(body: "deliciosa").
|
||||||
put_stub = OpenStruct.new(headers: {
|
to_return(status: 200, headers: { etag: '"0815etag"' })
|
||||||
etag: '"bla"'
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
})
|
with(body: "muy deliciosa").
|
||||||
|
to_return(status: 200, headers: { etag: '"0815etag"' })
|
||||||
RestClient.stub :put, put_stub do
|
stub_request(:head, "#{container_url_for("phil")}/food/aguacate").
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
to_return(status: 200, headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
put "/phil/food/aguacate", "si"
|
stub_request(:delete, "#{container_url_for("phil")}/food/aguacate").
|
||||||
end
|
to_return(status: 200, headers: { etag: '"0815etag"' })
|
||||||
end
|
stub_request(:put, "#{container_url_for("phil")}/food/camaron").
|
||||||
|
to_return(status: 200, headers: { etag: '"0816etag"' })
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
stub_request(:head, "#{container_url_for("phil")}/food/camaron").
|
||||||
metadata["s"].must_equal "2"
|
to_return(status: 200, headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
metadata["t"].must_equal "text/plain; charset=utf-8"
|
stub_request(:delete, "#{container_url_for("phil")}/food/camaron").
|
||||||
metadata["e"].must_equal "bla"
|
to_return(status: 200, headers: { etag: '"0816etag"' })
|
||||||
metadata["m"].length.must_equal 13
|
stub_request(:put, "#{container_url_for("phil")}/food/desayunos/bolon").
|
||||||
end
|
to_return(status: 200, headers: { etag: '"0817etag"' })
|
||||||
|
stub_request(:head, "#{container_url_for("phil")}/food/desayunos/bolon").
|
||||||
it "creates the directory objects metadata in redis" do
|
to_return(status: 200, headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
put_stub = OpenStruct.new(headers: {
|
stub_request(:delete, "#{container_url_for("phil")}/food/desayunos/bolon").
|
||||||
etag: '"bla"'
|
to_return(status: 200, headers: { etag: '"0817etag"' })
|
||||||
})
|
stub_request(:get, "#{container_url_for("phil")}/food/aguacate").
|
||||||
get_stub = OpenStruct.new(body: "rootbody")
|
to_return(status: 200, body: "rootbody", headers: { etag: '"0817etag"', content_type: "text/plain; charset=utf-8" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/bamboo.txt").
|
||||||
RestClient.stub :put, put_stub do
|
to_return(status: 200, headers: { etag: '"0818etag"' })
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
stub_request(:head, "#{container_url_for("phil")}/bamboo.txt").
|
||||||
RestClient.stub :get, get_stub do
|
to_return(status: 200, headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
RemoteStorage::S3Rest.stub_any_instance :etag_for, "newetag" do
|
stub_request(:head, "#{container_url_for("phil")}/food/steak").
|
||||||
put "/phil/food/aguacate", "si"
|
to_return(status: 404)
|
||||||
put "/phil/food/camaron", "yummi"
|
stub_request(:get, "#{container_url_for("phil")}/food/steak").
|
||||||
end
|
to_return(status: 404)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
it_behaves_like 'a REST adapter'
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:/"
|
|
||||||
metadata["e"].must_equal "newetag"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/"
|
|
||||||
metadata["e"].must_equal "newetag"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
|
|
||||||
food_items = redis.smembers "rs:m:phil:food/:items"
|
|
||||||
food_items.each do |food_item|
|
|
||||||
["camaron", "aguacate"].must_include food_item
|
|
||||||
end
|
|
||||||
|
|
||||||
root_items = redis.smembers "rs:m:phil:/:items"
|
|
||||||
root_items.must_equal ["food/"]
|
|
||||||
end
|
|
||||||
|
|
||||||
context "response code" do
|
|
||||||
before do
|
|
||||||
@put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"bla"'
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "is 201 for newly created objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "muy deliciosa"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 201
|
|
||||||
end
|
|
||||||
|
|
||||||
it "is 200 for updated objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "deliciosa"
|
|
||||||
put "/phil/food/aguacate", "muy deliciosa"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "logging usage size" do
|
|
||||||
before do
|
|
||||||
@put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"bla"'
|
|
||||||
})
|
|
||||||
@head_stub = OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
|
||||||
end
|
|
||||||
|
|
||||||
it "logs the complete size when creating new objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
RestClient.stub :head, @head_stub do
|
|
||||||
put "/phil/food/aguacate", "1234567890"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
size_log = redis.get "rs:s:phil"
|
|
||||||
size_log.must_equal "10"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "logs the size difference when updating existing objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
RestClient.stub :head, @head_stub do
|
|
||||||
put "/phil/food/camaron", "1234567890"
|
|
||||||
put "/phil/food/aguacate", "1234567890"
|
|
||||||
put "/phil/food/aguacate", "123"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
size_log = redis.get "rs:s:phil"
|
|
||||||
size_log.must_equal "13"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "objects in root dir" do
|
|
||||||
before do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"bla"',
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/bamboo.txt", "shir kan"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "are listed in the directory listing with all metadata" do
|
|
||||||
get "phil/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.content_type.must_equal "application/ld+json"
|
|
||||||
|
|
||||||
content = JSON.parse(last_response.body)
|
|
||||||
content["items"]["bamboo.txt"].wont_be_nil
|
|
||||||
content["items"]["bamboo.txt"]["ETag"].must_equal "bla"
|
|
||||||
content["items"]["bamboo.txt"]["Content-Type"].must_equal "text/plain; charset=utf-8"
|
|
||||||
content["items"]["bamboo.txt"]["Content-Length"].must_equal 8
|
|
||||||
content["items"]["bamboo.txt"]["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "name collision checks" do
|
|
||||||
it "is successful when there is no name collision" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"bla"',
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
get_stub = OpenStruct.new(body: "rootbody")
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
RestClient.stub :get, get_stub do
|
|
||||||
RemoteStorage::S3Rest.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 201
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
|
||||||
metadata["s"].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
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food", "wontwork"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 409
|
|
||||||
last_response.body.must_equal "Conflict"
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m: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
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/aguacate/empanado", "wontwork"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 409
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate/empanado"
|
|
||||||
metadata.must_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns 400 when a Content-Range header is sent" do
|
|
||||||
header "Content-Range", "bytes 0-3/3"
|
|
||||||
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
|
|
||||||
last_response.status.must_equal 400
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "If-Match header" do
|
|
||||||
before do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"oldetag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if the header matches the current ETag" do
|
|
||||||
header "If-Match", "\"oldetag\""
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"newetag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Etag"].must_equal "\"newetag\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if the header contains a weak ETAG matching the current ETag" do
|
|
||||||
header "If-Match", "W/\"oldetag\""
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"newetag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Etag"].must_equal "\"newetag\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if the header contains a weak ETAG with leading quote matching the current ETag" do
|
|
||||||
header "If-Match", "\"W/\"oldetag\""
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"newetag"',
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Etag"].must_equal "\"newetag\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails the request if the header does not match the current ETag" do
|
|
||||||
header "If-Match", "someotheretag"
|
|
||||||
|
|
||||||
head_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"oldetag"',
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT",
|
|
||||||
content_type: "text/plain",
|
|
||||||
content_length: 23
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :head, head_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 412
|
|
||||||
last_response.body.must_equal "Precondition Failed"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if redis metadata became out of sync" do
|
|
||||||
header "If-Match", "\"existingetag\""
|
|
||||||
|
|
||||||
head_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"existingetag"',
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT",
|
|
||||||
content_type: "text/plain",
|
|
||||||
content_length: 23
|
|
||||||
})
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"newetag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :head, head_stub do
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "If-None-Match header set to '*'" do
|
|
||||||
it "succeeds when the document doesn't exist yet" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"someetag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
header "If-None-Match", "*"
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 201
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails the request if the document already exists" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"someetag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
header "If-None-Match", "*"
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 412
|
|
||||||
last_response.body.must_equal "Precondition Failed"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "DELETE requests" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not authorized" do
|
|
||||||
describe "with no token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with empty token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer "
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with wrong token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer wrongtoken"
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
put "/phil/food/desayunos/bolon", "wow"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "decreases the size log by size of deleted object" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::S3Rest.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
size_log = redis.get "rs:s:phil"
|
|
||||||
size_log.must_equal "8"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes the metadata object in redis" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::S3Rest.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
|
||||||
metadata.must_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes the directory objects metadata in redis" do
|
|
||||||
old_metadata = redis.hgetall "rs:m:phil:food/"
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::S3Rest.stub_any_instance :etag_for, "newetag" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/"
|
|
||||||
metadata["e"].must_equal "newetag"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
metadata["m"].wont_equal old_metadata["m"]
|
|
||||||
|
|
||||||
food_items = redis.smembers "rs:m:phil:food/:items"
|
|
||||||
food_items.sort.must_equal ["camaron", "desayunos/"]
|
|
||||||
|
|
||||||
root_items = redis.smembers "rs:m:phil:/:items"
|
|
||||||
root_items.must_equal ["food/"]
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes the parent directory objects metadata when deleting all items" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::S3Rest.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
delete "/phil/food/camaron"
|
|
||||||
delete "/phil/food/desayunos/bolon"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:food/desayunos:items").must_be_empty
|
|
||||||
redis.hgetall("rs:m:phil:food/desayunos/").must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:food/:items").must_be_empty
|
|
||||||
redis.hgetall("rs:m:phil:food/").must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:/:items").must_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with the ETag of the deleted item in the header" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.headers["ETag"].must_equal "\"bla\""
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when item doesn't exist" do
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"bla"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/steak", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
raises_exception = ->(url, headers) { raise RestClient::ResourceNotFound.new }
|
|
||||||
RestClient.stub :head, raises_exception do
|
|
||||||
delete "/phil/food/steak"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a 404" do
|
|
||||||
last_response.status.must_equal 404
|
|
||||||
last_response.body.must_equal "Not Found"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes any metadata that might still exist" do
|
|
||||||
raises_exception = ->(url, headers) { raise RestClient::ResourceNotFound.new }
|
|
||||||
RestClient.stub :head, raises_exception do
|
|
||||||
delete "/phil/food/steak"
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/steak"
|
|
||||||
metadata.must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:food/:items").must_be_empty
|
|
||||||
redis.hgetall("rs:m:phil:food/").must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:/:items").must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "If-Match header" do
|
|
||||||
it "succeeds when the header matches the current ETag" do
|
|
||||||
header "If-Match", "\"bla\""
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
|
|
||||||
it "succeeds when the header contains a weak ETAG matching the current ETag" do
|
|
||||||
header "If-Match", "W/\"bla\""
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails the request if it does not match the current ETag" do
|
|
||||||
header "If-Match", "someotheretag"
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { etag: '"someetag"' }) do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 412
|
|
||||||
last_response.body.must_equal "Precondition Failed"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "GET requests" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not authorized" do
|
|
||||||
|
|
||||||
describe "without token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with wrong token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer wrongtoken"
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
context "authorized" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
|
||||||
header "Authorization", "Bearer amarillo"
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: '"0815etag"'
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
put "/phil/food/desayunos/bolon", "wow"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "documents" do
|
|
||||||
|
|
||||||
it "returns the required response headers" do
|
|
||||||
get_stub = OpenStruct.new(body: "si", headers: {
|
|
||||||
etag: '"0815etag"',
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT",
|
|
||||||
content_type: "text/plain; charset=utf-8",
|
|
||||||
content_length: 2
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :get, get_stub do
|
|
||||||
get "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Cache-Control"].must_equal "no-cache"
|
|
||||||
last_response.headers["Content-Type"].must_equal "text/plain; charset=utf-8"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a 404 when data doesn't exist" do
|
|
||||||
raises_exception = ->(url, headers) { raise RestClient::ResourceNotFound.new }
|
|
||||||
RestClient.stub :get, raises_exception do
|
|
||||||
get "/phil/food/steak"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 404
|
|
||||||
last_response.body.must_equal "Not Found"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
|
||||||
header "If-None-Match", "\"0815etag\""
|
|
||||||
|
|
||||||
get "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the current ETag" do
|
|
||||||
header "If-None-Match", "W/\"0815etag\""
|
|
||||||
|
|
||||||
get "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "directory listings" do
|
|
||||||
|
|
||||||
it "returns the correct ETag header" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["ETag"].must_equal "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a Cache-Control header with value 'no-cache'" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Cache-Control"].must_equal "no-cache"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
|
||||||
header "If-None-Match", "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the ETag" do
|
|
||||||
header "If-None-Match", "W/\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
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/ld+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 "0815etag"
|
|
||||||
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 "0815etag"
|
|
||||||
content["items"]["desayunos/"].wont_be_nil
|
|
||||||
content["items"]["desayunos/"]["ETag"].must_equal "dd36e3cfe52b5f33421150b289a7d48d"
|
|
||||||
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/ld+json"
|
|
||||||
|
|
||||||
content = JSON.parse(last_response.body)
|
|
||||||
content["items"]["food/"].wont_be_nil
|
|
||||||
content["items"]["food/"]["ETag"].must_equal "f9f85fbf5aa1fa378fd79ac8aa0a457d"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with an empty directory liting when directory doesn't exist" do
|
|
||||||
get "phil/some-non-existing-dir/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.content_type.must_equal "application/ld+json"
|
|
||||||
|
|
||||||
content = JSON.parse(last_response.body)
|
|
||||||
content["items"].must_equal({})
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "HEAD requests" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not authorized" do
|
|
||||||
|
|
||||||
describe "without token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
head "/phil/food/camarones"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with wrong token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer wrongtoken"
|
|
||||||
head "/phil/food/camarones"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
context "authorized" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
|
||||||
header "Authorization", "Bearer amarillo"
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "0815etag"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :head, OpenStruct.new(headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" }) do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
put "/phil/food/desayunos/bolon", "wow"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "directory listings" do
|
|
||||||
it "returns the correct header information" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.content_type.must_equal "application/ld+json"
|
|
||||||
last_response.headers["ETag"].must_equal "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "documents" do
|
|
||||||
context "when the document doesn't exist" do
|
|
||||||
it "returns a 404" do
|
|
||||||
head "/phil/food/steak"
|
|
||||||
|
|
||||||
last_response.status.must_equal 404
|
|
||||||
last_response.body.must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when the document exists" do
|
|
||||||
it "returns the required response headers" do
|
|
||||||
head "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Cache-Control"].must_equal "no-cache"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
|
||||||
header "If-None-Match", "\"0815etag\""
|
|
||||||
|
|
||||||
head "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the current ETag" do
|
|
||||||
header "If-None-Match", "W/\"0815etag\""
|
|
||||||
|
|
||||||
head "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
635
spec/shared_examples.rb
Normal file
635
spec/shared_examples.rb
Normal file
@ -0,0 +1,635 @@
|
|||||||
|
require_relative "./spec_helper"
|
||||||
|
|
||||||
|
shared_examples_for 'a REST adapter' do
|
||||||
|
include Rack::Test::Methods
|
||||||
|
|
||||||
|
def container_url_for(user)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def storage_class
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def config_file
|
||||||
|
raise NotImplementedError
|
||||||
|
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
|
||||||
|
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 "/phil/food/aguacate", "si"
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
||||||
|
metadata["s"].must_equal "2"
|
||||||
|
metadata["t"].must_equal "text/plain; charset=utf-8"
|
||||||
|
metadata["e"].must_equal "0815etag"
|
||||||
|
metadata["m"].length.must_equal 13
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates the directory objects metadata in redis" do
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
put "/phil/food/camaron", "yummi"
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:/"
|
||||||
|
metadata["e"].must_equal "fe2976909daaf074660981ab563fe65d"
|
||||||
|
metadata["m"].length.must_equal 13
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/"
|
||||||
|
metadata["e"].must_equal "926f98ff820f2f9764fd3c60a22865ad"
|
||||||
|
metadata["m"].length.must_equal 13
|
||||||
|
|
||||||
|
food_items = redis.smembers "rs:m:phil:food/:items"
|
||||||
|
food_items.each do |food_item|
|
||||||
|
["camaron", "aguacate"].must_include food_item
|
||||||
|
end
|
||||||
|
|
||||||
|
root_items = redis.smembers "rs:m:phil:/:items"
|
||||||
|
root_items.must_equal ["food/"]
|
||||||
|
end
|
||||||
|
|
||||||
|
context "response code" do
|
||||||
|
it "is 201 for newly created objects" do
|
||||||
|
put "/phil/food/aguacate", "ci"
|
||||||
|
|
||||||
|
last_response.status.must_equal 201
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is 200 for updated objects" do
|
||||||
|
put "/phil/food/aguacate", "deliciosa"
|
||||||
|
put "/phil/food/aguacate", "muy deliciosa"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "logging usage size" do
|
||||||
|
it "logs the complete size when creating new objects" do
|
||||||
|
put "/phil/food/aguacate", "1234567890"
|
||||||
|
|
||||||
|
size_log = redis.get "rs:s:phil"
|
||||||
|
size_log.must_equal "10"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "logs the size difference when updating existing objects" do
|
||||||
|
put "/phil/food/camaron", "1234567890"
|
||||||
|
put "/phil/food/aguacate", "1234567890"
|
||||||
|
put "/phil/food/aguacate", "123"
|
||||||
|
|
||||||
|
size_log = redis.get "rs:s:phil"
|
||||||
|
size_log.must_equal "13"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "objects in root dir" do
|
||||||
|
before do
|
||||||
|
put "/phil/bamboo.txt", "shir kan"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "are listed in the directory listing with all metadata" do
|
||||||
|
get "phil/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.content_type.must_equal "application/ld+json"
|
||||||
|
|
||||||
|
content = JSON.parse(last_response.body)
|
||||||
|
content["items"]["bamboo.txt"].wont_be_nil
|
||||||
|
content["items"]["bamboo.txt"]["ETag"].must_equal "0818etag"
|
||||||
|
content["items"]["bamboo.txt"]["Content-Type"].must_equal "text/plain; charset=utf-8"
|
||||||
|
content["items"]["bamboo.txt"]["Content-Length"].must_equal 8
|
||||||
|
content["items"]["bamboo.txt"]["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "name collision checks" do
|
||||||
|
it "is successful when there is no name collision" do
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
|
||||||
|
last_response.status.must_equal 201
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
||||||
|
metadata["s"].must_equal "2"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "conflicts when there is a directory with same name as document" do
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
put "/phil/food", "wontwork"
|
||||||
|
|
||||||
|
last_response.status.must_equal 409
|
||||||
|
last_response.body.must_equal "Conflict"
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food"
|
||||||
|
metadata.must_be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "conflicts when there is a document with same name as directory" do
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
put "/phil/food/aguacate/empanado", "wontwork"
|
||||||
|
|
||||||
|
last_response.status.must_equal 409
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/aguacate/empanado"
|
||||||
|
metadata.must_be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 400 when a Content-Range header is sent" do
|
||||||
|
header "Content-Range", "bytes 0-3/3"
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
|
||||||
|
last_response.status.must_equal 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "If-Match header" do
|
||||||
|
before do
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the request if the header matches the current ETag" do
|
||||||
|
header "If-Match", "\"0815etag\""
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "aye"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["Etag"].must_equal "\"0915etag\""
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the request if the header contains a weak ETAG matching the current ETag" do
|
||||||
|
header "If-Match", "W/\"0815etag\""
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "aye"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["Etag"].must_equal "\"0915etag\""
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the request if the header contains a weak ETAG with leading quote matching the current ETag" do
|
||||||
|
header "If-Match", "\"W/\"0815etag\""
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "aye"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["Etag"].must_equal "\"0915etag\""
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails the request if the header does not match the current ETag" do
|
||||||
|
header "If-Match", "someotheretag"
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "aye"
|
||||||
|
|
||||||
|
last_response.status.must_equal 412
|
||||||
|
last_response.body.must_equal "Precondition Failed"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows the request if redis metadata became out of sync" do
|
||||||
|
header "If-Match", "\"0815etag\""
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "aye"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "If-None-Match header set to '*'" do
|
||||||
|
it "succeeds when the document doesn't exist yet" do
|
||||||
|
header "If-None-Match", "*"
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
|
||||||
|
last_response.status.must_equal 201
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails the request if the document already exists" do
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
|
||||||
|
header "If-None-Match", "*"
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
|
||||||
|
last_response.status.must_equal 412
|
||||||
|
last_response.body.must_equal "Precondition Failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE requests" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
purge_redis
|
||||||
|
end
|
||||||
|
|
||||||
|
context "not authorized" do
|
||||||
|
describe "with no token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_equal "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with empty token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
header "Authorization", "Bearer "
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_equal "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with wrong token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
header "Authorization", "Bearer wrongtoken"
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_equal "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "authorized" do
|
||||||
|
before do
|
||||||
|
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
||||||
|
header "Authorization", "Bearer amarillo"
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
put "/phil/food/camaron", "yummi"
|
||||||
|
put "/phil/food/desayunos/bolon", "wow"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "decreases the size log by size of deleted object" do
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
size_log = redis.get "rs:s:phil"
|
||||||
|
size_log.must_equal "8"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes the metadata object in redis" do
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
||||||
|
metadata.must_be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes the directory objects metadata in redis" do
|
||||||
|
old_metadata = redis.hgetall "rs:m:phil:food/"
|
||||||
|
|
||||||
|
storage_class.stub_any_instance :etag_for, "newetag" do
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
end
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/"
|
||||||
|
metadata["e"].must_equal "newetag"
|
||||||
|
metadata["m"].length.must_equal 13
|
||||||
|
metadata["m"].wont_equal old_metadata["m"]
|
||||||
|
|
||||||
|
food_items = redis.smembers "rs:m:phil:food/:items"
|
||||||
|
food_items.sort.must_equal ["camaron", "desayunos/"]
|
||||||
|
|
||||||
|
root_items = redis.smembers "rs:m:phil:/:items"
|
||||||
|
root_items.must_equal ["food/"]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes the parent directory objects metadata when deleting all items" do
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
delete "/phil/food/camaron"
|
||||||
|
delete "/phil/food/desayunos/bolon"
|
||||||
|
|
||||||
|
redis.smembers("rs:m:phil:food/desayunos:items").must_be_empty
|
||||||
|
redis.hgetall("rs:m:phil:food/desayunos/").must_be_empty
|
||||||
|
|
||||||
|
redis.smembers("rs:m:phil:food/:items").must_be_empty
|
||||||
|
redis.hgetall("rs:m:phil:food/").must_be_empty
|
||||||
|
|
||||||
|
redis.smembers("rs:m:phil:/:items").must_be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with the ETag of the deleted item in the header" do
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.headers["ETag"].must_equal "\"0815etag\""
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when item doesn't exist" do
|
||||||
|
before do
|
||||||
|
purge_redis
|
||||||
|
|
||||||
|
delete "/phil/food/steak"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 404" do
|
||||||
|
last_response.status.must_equal 404
|
||||||
|
last_response.body.must_equal "Not Found"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes any metadata that might still exist" do
|
||||||
|
delete "/phil/food/steak"
|
||||||
|
|
||||||
|
metadata = redis.hgetall "rs:m:phil:food/steak"
|
||||||
|
metadata.must_be_empty
|
||||||
|
|
||||||
|
redis.smembers("rs:m:phil:food/:items").must_be_empty
|
||||||
|
redis.hgetall("rs:m:phil:food/").must_be_empty
|
||||||
|
|
||||||
|
redis.smembers("rs:m:phil:/:items").must_be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "If-Match header" do
|
||||||
|
it "succeeds when the header matches the current ETag" do
|
||||||
|
header "If-Match", "\"0815etag\""
|
||||||
|
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
end
|
||||||
|
|
||||||
|
it "succeeds when the header contains a weak ETAG matching the current ETag" do
|
||||||
|
header "If-Match", "W/\"0815etag\""
|
||||||
|
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails the request if it does not match the current ETag" do
|
||||||
|
header "If-Match", "someotheretag"
|
||||||
|
|
||||||
|
delete "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 412
|
||||||
|
last_response.body.must_equal "Precondition Failed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET requests" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
purge_redis
|
||||||
|
end
|
||||||
|
|
||||||
|
context "not authorized" do
|
||||||
|
|
||||||
|
describe "without token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
get "/phil/food/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_equal "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with wrong token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
header "Authorization", "Bearer wrongtoken"
|
||||||
|
get "/phil/food/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_equal "Unauthorized"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "authorized" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
||||||
|
header "Authorization", "Bearer amarillo"
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
put "/phil/food/camaron", "yummi"
|
||||||
|
put "/phil/food/desayunos/bolon", "wow"
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "documents" do
|
||||||
|
|
||||||
|
it "returns the required response headers" do
|
||||||
|
get "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["ETag"].must_equal "\"0817etag\""
|
||||||
|
last_response.headers["Cache-Control"].must_equal "no-cache"
|
||||||
|
last_response.headers["Content-Type"].must_equal "text/plain; charset=utf-8"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a 404 when data doesn't exist" do
|
||||||
|
get "/phil/food/steak"
|
||||||
|
|
||||||
|
last_response.status.must_equal 404
|
||||||
|
last_response.body.must_equal "Not Found"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
||||||
|
header "If-None-Match", "\"0815etag\""
|
||||||
|
|
||||||
|
get "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 304
|
||||||
|
last_response.headers["ETag"].must_equal "\"0815etag\""
|
||||||
|
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the current ETag" do
|
||||||
|
header "If-None-Match", "W/\"0815etag\""
|
||||||
|
|
||||||
|
get "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 304
|
||||||
|
last_response.headers["ETag"].must_equal "\"0815etag\""
|
||||||
|
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "directory listings" do
|
||||||
|
|
||||||
|
it "returns the correct ETag header" do
|
||||||
|
get "/phil/food/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["ETag"].must_equal "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns a Cache-Control header with value 'no-cache'" do
|
||||||
|
get "/phil/food/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["Cache-Control"].must_equal "no-cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
||||||
|
header "If-None-Match", "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
||||||
|
get "/phil/food/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 304
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the ETag" do
|
||||||
|
header "If-None-Match", "W/\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
||||||
|
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/ld+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 "0815etag"
|
||||||
|
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 "0816etag"
|
||||||
|
content["items"]["desayunos/"].wont_be_nil
|
||||||
|
content["items"]["desayunos/"]["ETag"].must_equal "dd36e3cfe52b5f33421150b289a7d48d"
|
||||||
|
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/ld+json"
|
||||||
|
|
||||||
|
content = JSON.parse(last_response.body)
|
||||||
|
content["items"]["food/"].wont_be_nil
|
||||||
|
content["items"]["food/"]["ETag"].must_equal "f9f85fbf5aa1fa378fd79ac8aa0a457d"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with an empty directory liting when directory doesn't exist" do
|
||||||
|
get "phil/some-non-existing-dir/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.content_type.must_equal "application/ld+json"
|
||||||
|
|
||||||
|
content = JSON.parse(last_response.body)
|
||||||
|
content["items"].must_equal({})
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "HEAD requests" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
purge_redis
|
||||||
|
end
|
||||||
|
|
||||||
|
context "not authorized" do
|
||||||
|
|
||||||
|
describe "without token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
head "/phil/food/camarones"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with wrong token" do
|
||||||
|
it "says it's not authorized" do
|
||||||
|
header "Authorization", "Bearer wrongtoken"
|
||||||
|
head "/phil/food/camarones"
|
||||||
|
|
||||||
|
last_response.status.must_equal 401
|
||||||
|
last_response.body.must_be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
context "authorized" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
||||||
|
header "Authorization", "Bearer amarillo"
|
||||||
|
|
||||||
|
put "/phil/food/aguacate", "si"
|
||||||
|
put "/phil/food/camaron", "yummi"
|
||||||
|
put "/phil/food/desayunos/bolon", "wow"
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "directory listings" do
|
||||||
|
it "returns the correct header information" do
|
||||||
|
get "/phil/food/"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.content_type.must_equal "application/ld+json"
|
||||||
|
last_response.headers["ETag"].must_equal "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "documents" do
|
||||||
|
context "when the document doesn't exist" do
|
||||||
|
it "returns a 404" do
|
||||||
|
head "/phil/food/steak"
|
||||||
|
|
||||||
|
last_response.status.must_equal 404
|
||||||
|
last_response.body.must_be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the document exists" do
|
||||||
|
it "returns the required response headers" do
|
||||||
|
head "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 200
|
||||||
|
last_response.headers["ETag"].must_equal "\"0815etag\""
|
||||||
|
last_response.headers["Cache-Control"].must_equal "no-cache"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
||||||
|
header "If-None-Match", "\"0815etag\""
|
||||||
|
|
||||||
|
head "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 304
|
||||||
|
last_response.headers["ETag"].must_equal "\"0815etag\""
|
||||||
|
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the current ETag" do
|
||||||
|
header "If-None-Match", "W/\"0815etag\""
|
||||||
|
|
||||||
|
head "/phil/food/aguacate"
|
||||||
|
|
||||||
|
last_response.status.must_equal 304
|
||||||
|
last_response.headers["ETag"].must_equal "\"0815etag\""
|
||||||
|
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -6,12 +6,15 @@ Bundler.require
|
|||||||
|
|
||||||
require_relative '../liquor-cabinet'
|
require_relative '../liquor-cabinet'
|
||||||
require 'minitest/autorun'
|
require 'minitest/autorun'
|
||||||
|
require "minitest/stub_any_instance"
|
||||||
require 'rack/test'
|
require 'rack/test'
|
||||||
require 'purdytest'
|
require 'purdytest'
|
||||||
require "redis"
|
require "redis"
|
||||||
require "rest_client"
|
require "rest_client"
|
||||||
require "minitest/stub_any_instance"
|
|
||||||
require "ostruct"
|
require "ostruct"
|
||||||
|
require 'webmock/minitest'
|
||||||
|
|
||||||
|
WebMock.disable_net_connect!
|
||||||
|
|
||||||
def app
|
def app
|
||||||
LiquorCabinet
|
LiquorCabinet
|
||||||
@ -19,17 +22,6 @@ end
|
|||||||
|
|
||||||
app.set :environment, :test
|
app.set :environment, :test
|
||||||
|
|
||||||
def wait_a_second
|
|
||||||
now = Time.now.to_i
|
|
||||||
while Time.now.to_i == now; end
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_last_response_to_file(filename = "last_response.html")
|
|
||||||
File.open(filename, "w") do |f|
|
|
||||||
f.write last_response.body
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
alias context describe
|
alias context describe
|
||||||
|
|
||||||
if app.settings.respond_to? :redis
|
if app.settings.respond_to? :redis
|
||||||
@ -43,3 +35,23 @@ if app.settings.respond_to? :redis
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
MiniTest::Spec.class_eval do
|
||||||
|
def self.shared_examples
|
||||||
|
@shared_examples ||= {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module MiniTest::Spec::SharedExamples
|
||||||
|
def shared_examples_for(desc, &block)
|
||||||
|
MiniTest::Spec.shared_examples[desc] = block
|
||||||
|
end
|
||||||
|
|
||||||
|
def it_behaves_like(desc)
|
||||||
|
self.instance_eval(&MiniTest::Spec.shared_examples[desc])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Object.class_eval { include(MiniTest::Spec::SharedExamples) }
|
||||||
|
|
||||||
|
require_relative 'shared_examples'
|
||||||
|
@ -1,840 +1,56 @@
|
|||||||
require_relative "../spec_helper"
|
require_relative "../spec_helper"
|
||||||
|
|
||||||
describe "App" do
|
describe "App" do
|
||||||
include Rack::Test::Methods
|
def container_url_for(user)
|
||||||
|
"#{app.settings.swift["host"]}/rs:documents:test/#{user}"
|
||||||
def app
|
|
||||||
LiquorCabinet
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns 404 on non-existing routes" do
|
def storage_class
|
||||||
get "/virginmargarita"
|
RemoteStorage::Swift
|
||||||
last_response.status.must_equal 404
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "PUT requests" do
|
def config_file
|
||||||
|
"config.yml.example.swift"
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
purge_redis
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
to_return(status: 200, headers: { etag: "0815etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
with(body: "si").
|
||||||
|
to_return(status: 200, headers: { etag: "0815etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
with(body: "aye").
|
||||||
|
to_return(status: 200, headers: { etag: "0915etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
with(body: "deliciosa").
|
||||||
|
to_return(status: 200, headers: { etag: "0815etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
with(body: "muy deliciosa").
|
||||||
|
to_return(status: 200, headers: { etag: "0815etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:head, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
to_return(status: 200, headers: { last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:delete, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
to_return(status: 200, headers: { etag: "0815etag" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/camaron").
|
||||||
|
to_return(status: 200, headers: { etag: "0816etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:delete, "#{container_url_for("phil")}/food/camaron").
|
||||||
|
to_return(status: 200, headers: { etag: "0816etag" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/food/desayunos/bolon").
|
||||||
|
to_return(status: 200, headers: { etag: "0817etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:delete, "#{container_url_for("phil")}/food/desayunos/bolon").
|
||||||
|
to_return(status: 200, headers: { etag: "0817etag" })
|
||||||
|
stub_request(:get, "#{container_url_for("phil")}/food/aguacate").
|
||||||
|
to_return(status: 200, body: "rootbody", headers: { etag: "0817etag", content_type: "text/plain; charset=utf-8" })
|
||||||
|
stub_request(:put, "#{container_url_for("phil")}/bamboo.txt").
|
||||||
|
to_return(status: 200, headers: { etag: "0818etag", last_modified: "Fri, 04 Mar 2016 12:20:18 GMT" })
|
||||||
|
stub_request(:head, "#{container_url_for("phil")}/food/steak").
|
||||||
|
to_return(status: 404)
|
||||||
|
stub_request(:get, "#{container_url_for("phil")}/food/steak").
|
||||||
|
to_return(status: 404)
|
||||||
|
stub_request(:delete, "#{container_url_for("phil")}/food/steak").
|
||||||
|
to_return(status: 404)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a REST adapter'
|
||||||
end
|
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",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
|
||||||
metadata["s"].must_equal "2"
|
|
||||||
metadata["t"].must_equal "text/plain; charset=utf-8"
|
|
||||||
metadata["e"].must_equal "bla"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
end
|
|
||||||
|
|
||||||
it "creates the directory objects metadata in redis" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
get_stub = OpenStruct.new(body: "rootbody")
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :get, get_stub do
|
|
||||||
RemoteStorage::Swift.stub_any_instance :etag_for, "newetag" do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:/"
|
|
||||||
metadata["e"].must_equal "newetag"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/"
|
|
||||||
metadata["e"].must_equal "newetag"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
|
|
||||||
food_items = redis.smembers "rs:m:phil:food/:items"
|
|
||||||
food_items.each do |food_item|
|
|
||||||
["camaron", "aguacate"].must_include food_item
|
|
||||||
end
|
|
||||||
|
|
||||||
root_items = redis.smembers "rs:m:phil:/:items"
|
|
||||||
root_items.must_equal ["food/"]
|
|
||||||
end
|
|
||||||
|
|
||||||
context "response code" do
|
|
||||||
before do
|
|
||||||
@put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "is 201 for newly created objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
put "/phil/food/aguacate", "muy deliciosa"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 201
|
|
||||||
end
|
|
||||||
|
|
||||||
it "is 200 for updated objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
put "/phil/food/aguacate", "deliciosa"
|
|
||||||
put "/phil/food/aguacate", "muy deliciosa"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "logging usage size" do
|
|
||||||
before do
|
|
||||||
@put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
it "logs the complete size when creating new objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
put "/phil/food/aguacate", "1234567890"
|
|
||||||
end
|
|
||||||
|
|
||||||
size_log = redis.get "rs:s:phil"
|
|
||||||
size_log.must_equal "10"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "logs the size difference when updating existing objects" do
|
|
||||||
RestClient.stub :put, @put_stub do
|
|
||||||
put "/phil/food/camaron", "1234567890"
|
|
||||||
put "/phil/food/aguacate", "1234567890"
|
|
||||||
put "/phil/food/aguacate", "123"
|
|
||||||
end
|
|
||||||
|
|
||||||
size_log = redis.get "rs:s:phil"
|
|
||||||
size_log.must_equal "13"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "objects in root dir" do
|
|
||||||
before do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/bamboo.txt", "shir kan"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "are listed in the directory listing with all metadata" do
|
|
||||||
get "phil/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.content_type.must_equal "application/ld+json"
|
|
||||||
|
|
||||||
content = JSON.parse(last_response.body)
|
|
||||||
content["items"]["bamboo.txt"].wont_be_nil
|
|
||||||
content["items"]["bamboo.txt"]["ETag"].must_equal "bla"
|
|
||||||
content["items"]["bamboo.txt"]["Content-Type"].must_equal "text/plain; charset=utf-8"
|
|
||||||
content["items"]["bamboo.txt"]["Content-Length"].must_equal 8
|
|
||||||
content["items"]["bamboo.txt"]["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "name collision checks" do
|
|
||||||
it "is successful when there is no name collision" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
get_stub = OpenStruct.new(body: "rootbody")
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
RestClient.stub :get, get_stub do
|
|
||||||
RemoteStorage::Swift.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 201
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
|
||||||
metadata["s"].must_equal "2"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "conflicts when there is a directory with same name as document" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food", "wontwork"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 409
|
|
||||||
last_response.body.must_equal "Conflict"
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m: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",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
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:m:phil:food/aguacate/empanado"
|
|
||||||
metadata.must_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns 400 when a Content-Range header is sent" do
|
|
||||||
header "Content-Range", "bytes 0-3/3"
|
|
||||||
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
|
|
||||||
last_response.status.must_equal 400
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "If-Match header" do
|
|
||||||
before do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "oldetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if the header matches the current ETag" do
|
|
||||||
header "If-Match", "\"oldetag\""
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "newetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Etag"].must_equal "\"newetag\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if the header contains a weak ETAG matching the current ETag" do
|
|
||||||
header "If-Match", "W/\"oldetag\""
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "newetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Etag"].must_equal "\"newetag\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if the header contains a weak ETAG with leading quote matching the current ETag" do
|
|
||||||
header "If-Match", "\"W/\"oldetag\""
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "newetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Etag"].must_equal "\"newetag\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails the request if the header does not match the current ETag" do
|
|
||||||
header "If-Match", "someotheretag"
|
|
||||||
|
|
||||||
head_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "oldetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT",
|
|
||||||
content_type: "text/plain",
|
|
||||||
content_length: 23
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :head, head_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 412
|
|
||||||
last_response.body.must_equal "Precondition Failed"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "allows the request if redis metadata became out of sync" do
|
|
||||||
header "If-Match", "\"existingetag\""
|
|
||||||
|
|
||||||
head_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "existingetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT",
|
|
||||||
content_type: "text/plain",
|
|
||||||
content_length: 23
|
|
||||||
})
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "newetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :head, head_stub do
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "aye"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "If-None-Match header set to '*'" do
|
|
||||||
it "succeeds when the document doesn't exist yet" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "someetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
header "If-None-Match", "*"
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 201
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails the request if the document already exists" do
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "someetag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
|
|
||||||
header "If-None-Match", "*"
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 412
|
|
||||||
last_response.body.must_equal "Precondition Failed"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "DELETE requests" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not authorized" do
|
|
||||||
describe "with no token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with empty token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer "
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with wrong token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer wrongtoken"
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
context "authorized" do
|
|
||||||
before do
|
|
||||||
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
|
||||||
header "Authorization", "Bearer amarillo"
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
put "/phil/food/desayunos/bolon", "wow"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "decreases the size log by size of deleted object" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::Swift.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
size_log = redis.get "rs:s:phil"
|
|
||||||
size_log.must_equal "8"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes the metadata object in redis" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::Swift.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/aguacate"
|
|
||||||
metadata.must_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes the directory objects metadata in redis" do
|
|
||||||
old_metadata = redis.hgetall "rs:m:phil:food/"
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::Swift.stub_any_instance :etag_for, "newetag" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/"
|
|
||||||
metadata["e"].must_equal "newetag"
|
|
||||||
metadata["m"].length.must_equal 13
|
|
||||||
metadata["m"].wont_equal old_metadata["m"]
|
|
||||||
|
|
||||||
food_items = redis.smembers "rs:m:phil:food/:items"
|
|
||||||
food_items.sort.must_equal ["camaron", "desayunos/"]
|
|
||||||
|
|
||||||
root_items = redis.smembers "rs:m:phil:/:items"
|
|
||||||
root_items.must_equal ["food/"]
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes the parent directory objects metadata when deleting all items" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
RemoteStorage::Swift.stub_any_instance :etag_for, "rootetag" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
delete "/phil/food/camaron"
|
|
||||||
delete "/phil/food/desayunos/bolon"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:food/desayunos:items").must_be_empty
|
|
||||||
redis.hgetall("rs:m:phil:food/desayunos/").must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:food/:items").must_be_empty
|
|
||||||
redis.hgetall("rs:m:phil:food/").must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:/:items").must_be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with the ETag of the deleted item in the header" do
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.headers["ETag"].must_equal "\"bla\""
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when item doesn't exist" do
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "bla",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/steak", "si"
|
|
||||||
end
|
|
||||||
|
|
||||||
raises_exception = ->(url, headers) { raise RestClient::ResourceNotFound.new }
|
|
||||||
RestClient.stub :delete, raises_exception do
|
|
||||||
delete "/phil/food/steak"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a 404" do
|
|
||||||
last_response.status.must_equal 404
|
|
||||||
last_response.body.must_equal "Not Found"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "deletes any metadata that might still exist" do
|
|
||||||
raises_exception = ->(url, headers) { raise RestClient::ResourceNotFound.new }
|
|
||||||
RestClient.stub :delete, raises_exception do
|
|
||||||
delete "/phil/food/steak"
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata = redis.hgetall "rs:m:phil:food/steak"
|
|
||||||
metadata.must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:food/:items").must_be_empty
|
|
||||||
redis.hgetall("rs:m:phil:food/").must_be_empty
|
|
||||||
|
|
||||||
redis.smembers("rs:m:phil:/:items").must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "If-Match header" do
|
|
||||||
it "succeeds when the header matches the current ETag" do
|
|
||||||
header "If-Match", "\"bla\""
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
|
|
||||||
it "succeeds when the header contains a weak ETAG matching the current ETag" do
|
|
||||||
header "If-Match", "W/\"bla\""
|
|
||||||
|
|
||||||
RestClient.stub :delete, "" do
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails the request if it does not match the current ETag" do
|
|
||||||
header "If-Match", "someotheretag"
|
|
||||||
|
|
||||||
delete "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 412
|
|
||||||
last_response.body.must_equal "Precondition Failed"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "GET requests" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not authorized" do
|
|
||||||
|
|
||||||
describe "without token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with wrong token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer wrongtoken"
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_equal "Unauthorized"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
context "authorized" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
|
||||||
header "Authorization", "Bearer amarillo"
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "0815etag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
put "/phil/food/desayunos/bolon", "wow"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "documents" do
|
|
||||||
|
|
||||||
it "returns the required response headers" do
|
|
||||||
get_stub = OpenStruct.new(body: "si", headers: {
|
|
||||||
etag: "0815etag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT",
|
|
||||||
content_type: "text/plain; charset=utf-8",
|
|
||||||
content_length: 2
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :get, get_stub do
|
|
||||||
get "/phil/food/aguacate"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Cache-Control"].must_equal "no-cache"
|
|
||||||
last_response.headers["Content-Type"].must_equal "text/plain; charset=utf-8"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a 404 when data doesn't exist" do
|
|
||||||
raises_exception = ->(url, headers) { raise RestClient::ResourceNotFound.new }
|
|
||||||
RestClient.stub :get, raises_exception do
|
|
||||||
get "/phil/food/steak"
|
|
||||||
end
|
|
||||||
|
|
||||||
last_response.status.must_equal 404
|
|
||||||
last_response.body.must_equal "Not Found"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
|
||||||
|
|
||||||
header "If-None-Match", "\"0815etag\""
|
|
||||||
|
|
||||||
get "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the current ETag" do
|
|
||||||
header "If-None-Match", "W/\"0815etag\""
|
|
||||||
|
|
||||||
get "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "directory listings" do
|
|
||||||
|
|
||||||
it "returns the correct ETag header" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["ETag"].must_equal "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a Cache-Control header with value 'no-cache'" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["Cache-Control"].must_equal "no-cache"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
|
||||||
header "If-None-Match", "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the ETag" do
|
|
||||||
header "If-None-Match", "W/\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
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/ld+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 "0815etag"
|
|
||||||
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 "0815etag"
|
|
||||||
content["items"]["desayunos/"].wont_be_nil
|
|
||||||
content["items"]["desayunos/"]["ETag"].must_equal "dd36e3cfe52b5f33421150b289a7d48d"
|
|
||||||
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/ld+json"
|
|
||||||
|
|
||||||
content = JSON.parse(last_response.body)
|
|
||||||
content["items"]["food/"].wont_be_nil
|
|
||||||
content["items"]["food/"]["ETag"].must_equal "f9f85fbf5aa1fa378fd79ac8aa0a457d"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with an empty directory liting when directory doesn't exist" do
|
|
||||||
get "phil/some-non-existing-dir/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.content_type.must_equal "application/ld+json"
|
|
||||||
|
|
||||||
content = JSON.parse(last_response.body)
|
|
||||||
content["items"].must_equal({})
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "HEAD requests" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
purge_redis
|
|
||||||
end
|
|
||||||
|
|
||||||
context "not authorized" do
|
|
||||||
|
|
||||||
describe "without token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
head "/phil/food/camarones"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with wrong token" do
|
|
||||||
it "says it's not authorized" do
|
|
||||||
header "Authorization", "Bearer wrongtoken"
|
|
||||||
head "/phil/food/camarones"
|
|
||||||
|
|
||||||
last_response.status.must_equal 401
|
|
||||||
last_response.body.must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
context "authorized" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
redis.sadd "authorizations:phil:amarillo", [":rw"]
|
|
||||||
header "Authorization", "Bearer amarillo"
|
|
||||||
|
|
||||||
put_stub = OpenStruct.new(headers: {
|
|
||||||
etag: "0815etag",
|
|
||||||
last_modified: "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
})
|
|
||||||
|
|
||||||
RestClient.stub :put, put_stub do
|
|
||||||
put "/phil/food/aguacate", "si"
|
|
||||||
put "/phil/food/camaron", "yummi"
|
|
||||||
put "/phil/food/desayunos/bolon", "wow"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "directory listings" do
|
|
||||||
it "returns the correct header information" do
|
|
||||||
get "/phil/food/"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.content_type.must_equal "application/ld+json"
|
|
||||||
last_response.headers["ETag"].must_equal "\"f9f85fbf5aa1fa378fd79ac8aa0a457d\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "documents" do
|
|
||||||
context "when the document doesn't exist" do
|
|
||||||
it "returns a 404" do
|
|
||||||
head "/phil/food/steak"
|
|
||||||
|
|
||||||
last_response.status.must_equal 404
|
|
||||||
last_response.body.must_be_empty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "when the document exists" do
|
|
||||||
it "returns the required response headers" do
|
|
||||||
head "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 200
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Cache-Control"].must_equal "no-cache"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains the ETag" do
|
|
||||||
header "If-None-Match", "\"0815etag\""
|
|
||||||
|
|
||||||
head "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with 304 when IF_NONE_MATCH header contains weak ETAG matching the current ETag" do
|
|
||||||
header "If-None-Match", "W/\"0815etag\""
|
|
||||||
|
|
||||||
head "/phil/food/aguacate"
|
|
||||||
|
|
||||||
last_response.status.must_equal 304
|
|
||||||
last_response.headers["ETag"].must_equal "\"0815etag\""
|
|
||||||
last_response.headers["Last-Modified"].must_equal "Fri, 04 Mar 2016 12:20:18 GMT"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user