@@ -1,15 +0,0 @@
|
||||
require_relative "../spec_helper"
|
||||
|
||||
describe "App" do
|
||||
include Rack::Test::Methods
|
||||
|
||||
def app
|
||||
LiquorCabinet
|
||||
end
|
||||
|
||||
it "returns 404 on non-existing routes" do
|
||||
get "/virginmargarita"
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,640 +0,0 @@
|
||||
require_relative "../spec_helper"
|
||||
|
||||
describe "Directories" do
|
||||
include Rack::Test::Methods
|
||||
|
||||
before do
|
||||
purge_all_buckets
|
||||
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = [":r", "documents:r", "tasks:rw"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
describe "HEAD listing" do
|
||||
before do
|
||||
put "/jimmy/tasks/foo", "do the laundry"
|
||||
put "/jimmy/tasks/http%3A%2F%2F5apps.com", "prettify design"
|
||||
|
||||
head "/jimmy/tasks/"
|
||||
end
|
||||
|
||||
it "has an empty body" do
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal ""
|
||||
end
|
||||
|
||||
it "has an ETag header set" do
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
|
||||
# check that ETag stays the same
|
||||
etag = last_response.headers["ETag"]
|
||||
get "/jimmy/tasks/"
|
||||
last_response.headers["ETag"].must_equal etag
|
||||
end
|
||||
|
||||
it "has CORS headers set" do
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["Access-Control-Allow-Origin"].must_equal "*"
|
||||
last_response.headers["Access-Control-Allow-Methods"].must_equal "GET, PUT, DELETE"
|
||||
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin, If-Match, If-None-Match"
|
||||
last_response.headers["Access-Control-Expose-Headers"].must_equal "ETag, Content-Length"
|
||||
end
|
||||
|
||||
context "for an empty or absent directory" do
|
||||
it "responds with 404" do
|
||||
head "/jimmy/documents/"
|
||||
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET listing" do
|
||||
before do
|
||||
put "/jimmy/tasks/foo", "do the laundry"
|
||||
put "/jimmy/tasks/http%3A%2F%2F5apps.com", "prettify design"
|
||||
put "/jimmy/tasks/%3A/foo%3Abar%40foo.org", "hello world"
|
||||
end
|
||||
|
||||
it "lists the objects with version, length and content-type" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.content_type.must_equal "application/json"
|
||||
|
||||
foo = data_bucket.get("jimmy:tasks:foo")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["http://5apps.com"].wont_be_nil
|
||||
content["items"][":/"].wont_be_nil
|
||||
content["items"]["foo"].wont_be_nil
|
||||
content["items"]["foo"]["ETag"].must_equal foo.etag.gsub(/"/, "")
|
||||
content["items"]["foo"]["Content-Type"].must_equal "text/plain"
|
||||
content["items"]["foo"]["Content-Length"].must_equal 14
|
||||
end
|
||||
|
||||
it "has an ETag header set" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
|
||||
# check that ETag stays the same
|
||||
etag = last_response.headers["ETag"]
|
||||
get "/jimmy/tasks/"
|
||||
last_response.headers["ETag"].must_equal etag
|
||||
end
|
||||
|
||||
it "has CORS headers set" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["Access-Control-Allow-Origin"].must_equal "*"
|
||||
last_response.headers["Access-Control-Allow-Methods"].must_equal "GET, PUT, DELETE"
|
||||
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin, If-Match, If-None-Match"
|
||||
last_response.headers["Access-Control-Expose-Headers"].must_equal "ETag, Content-Length"
|
||||
end
|
||||
|
||||
it "has caching headers set" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["Expires"].must_equal "0"
|
||||
end
|
||||
|
||||
it "doesn't choke on colons in the directory name" do
|
||||
get "/jimmy/tasks/%3A/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.content_type.must_equal "application/json"
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["foo:bar@foo.org"].wont_be_nil
|
||||
end
|
||||
|
||||
context "when If-None-Match header is set" do
|
||||
before do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
@etag = last_response.headers["ETag"]
|
||||
end
|
||||
|
||||
it "responds with 'not modified' when it matches the current ETag" do
|
||||
header "If-None-Match", @etag
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 304
|
||||
last_response.body.must_be_empty
|
||||
last_response.headers["ETag"].must_equal @etag
|
||||
end
|
||||
|
||||
it "responds normally when it does not match the current ETag" do
|
||||
header "If-None-Match", "FOO"
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.wont_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "when If-None-Match header is set with multiple revisions" do
|
||||
before do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
@etag = last_response.headers["ETag"]
|
||||
end
|
||||
|
||||
it "responds with 'not modified' when it contains the current ETag" do
|
||||
header "If-None-Match", "DEADBEEF,#{@etag} ,F00BA4"
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 304
|
||||
last_response.body.must_be_empty
|
||||
last_response.headers["ETag"].must_equal @etag
|
||||
end
|
||||
|
||||
it "responds normally when it does not contain the current ETag" do
|
||||
header "If-None-Match", "FOO,BAR"
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.wont_be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "with sub-directories" do
|
||||
before do
|
||||
get "/jimmy/tasks/"
|
||||
@old_etag = last_response.headers["ETag"]
|
||||
|
||||
put "/jimmy/tasks/home/laundry", "do the laundry"
|
||||
end
|
||||
|
||||
it "lists the containing objects as well as the direct sub-directories" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
home = directory_bucket.get("jimmy:tasks/home")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["foo"].wont_be_nil
|
||||
content["items"]["http://5apps.com"].wont_be_nil
|
||||
content["items"]["home/"].wont_be_nil
|
||||
content["items"]["home/"]["ETag"].must_equal home.etag.gsub(/"/, "")
|
||||
end
|
||||
|
||||
it "updates the ETag of the parent directory" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].wont_equal @old_etag
|
||||
end
|
||||
|
||||
context "for a different user" do
|
||||
before do
|
||||
auth = auth_bucket.new("alice:321")
|
||||
auth.data = [":r", "documents:r", "tasks:rw"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 321"
|
||||
|
||||
put "/alice/tasks/homework", "write an essay"
|
||||
end
|
||||
|
||||
it "does not list the directories of jimmy" do
|
||||
get "/alice/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["/"].must_be_nil
|
||||
content["items"]["tasks/"].must_be_nil
|
||||
content["items"]["home/"].must_be_nil
|
||||
content["items"]["homework"].wont_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "sub-directories without objects" do
|
||||
it "lists the direct sub-directories" do
|
||||
put "/jimmy/tasks/private/projects/world-domination/start", "write a manifesto"
|
||||
get "/jimmy/tasks/private/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
projects = directory_bucket.get("jimmy:tasks/private/projects")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["projects/"]["ETag"].must_equal projects.etag.gsub(/"/, "")
|
||||
end
|
||||
|
||||
it "updates the timestamps of the existing directory objects" do
|
||||
directory = directory_bucket.new("jimmy:tasks")
|
||||
directory.content_type = "text/plain"
|
||||
directory.data = (2.seconds.ago.to_f * 1000).to_i
|
||||
directory.store
|
||||
|
||||
put "/jimmy/tasks/private/projects/world-domination/start", "write a manifesto"
|
||||
|
||||
object = data_bucket.get("jimmy:tasks/private/projects/world-domination:start")
|
||||
directory = directory_bucket.get("jimmy:tasks")
|
||||
|
||||
directory.data.to_i.must_equal object.meta['timestamp'][0].to_i
|
||||
end
|
||||
end
|
||||
|
||||
context "with binary data" do
|
||||
context "charset given in content-type header" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg; charset=binary"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/tasks/jaypeg.jpg", @image
|
||||
end
|
||||
|
||||
it "lists the binary files" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
jaypeg = data_bucket.get("jimmy:tasks:jaypeg.jpg")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["jaypeg.jpg"]["ETag"].must_equal jaypeg.etag.gsub(/"/, "")
|
||||
content["items"]["jaypeg.jpg"]["Content-Type"].must_equal "image/jpeg"
|
||||
content["items"]["jaypeg.jpg"]["Content-Length"].must_equal 16044
|
||||
end
|
||||
end
|
||||
|
||||
context "no charset in content-type header" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/tasks/jaypeg.jpg", @image
|
||||
end
|
||||
|
||||
it "lists the binary files" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
jaypeg = data_bucket.get("jimmy:tasks:jaypeg.jpg")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["jaypeg.jpg"]["ETag"].must_equal jaypeg.etag.gsub(/"/, "")
|
||||
content["items"]["jaypeg.jpg"]["Content-Type"].must_equal "image/jpeg"
|
||||
content["items"]["jaypeg.jpg"]["Content-Length"].must_equal 16044
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for a sub-directory" do
|
||||
before do
|
||||
put "/jimmy/tasks/home/laundry", "do the laundry"
|
||||
end
|
||||
|
||||
it "lists the objects with timestamp" do
|
||||
get "/jimmy/tasks/home/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
laundry = data_bucket.get("jimmy:tasks/home:laundry")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["laundry"]["ETag"].must_equal laundry.etag.gsub(/"/, "")
|
||||
end
|
||||
end
|
||||
|
||||
context "for an empty or absent directory" do
|
||||
it "returns an empty listing" do
|
||||
get "/jimmy/documents/notfound/"
|
||||
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
end
|
||||
|
||||
context "special characters in directory name" do
|
||||
before do
|
||||
put "/jimmy/tasks/foo~bar/task1", "some task"
|
||||
end
|
||||
|
||||
it "lists the directory in the parent directory" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["foo~bar/"].wont_be_nil
|
||||
end
|
||||
|
||||
it "lists the containing objects" do
|
||||
get "/jimmy/tasks/foo~bar/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["task1"].wont_be_nil
|
||||
end
|
||||
|
||||
it "returns the requested object" do
|
||||
get "/jimmy/tasks/foo~bar/task1"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
last_response.body.must_equal "some task"
|
||||
end
|
||||
end
|
||||
|
||||
context "special characters in object name" do
|
||||
before do
|
||||
put "/jimmy/tasks/bla~blub", "some task"
|
||||
end
|
||||
|
||||
it "lists the containing object" do
|
||||
get "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["bla~blub"].wont_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "for the root directory" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = [":rw"]
|
||||
auth.store
|
||||
|
||||
put "/jimmy/root-1", "Put my root down"
|
||||
put "/jimmy/root-2", "Back to the roots"
|
||||
end
|
||||
|
||||
it "lists the containing objects and direct sub-directories" do
|
||||
get "/jimmy/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
tasks = directory_bucket.get("jimmy:tasks")
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["root-1"].wont_be_nil
|
||||
content["items"]["root-2"].wont_be_nil
|
||||
content["items"]["tasks/"].wont_be_nil
|
||||
content["items"]["tasks/"]["ETag"].must_equal tasks.etag.gsub(/"/, "")
|
||||
end
|
||||
|
||||
it "has an ETag header set" do
|
||||
get "/jimmy/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "for the public directory" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents:r", "bookmarks:rw"]
|
||||
auth.store
|
||||
|
||||
put "/jimmy/public/bookmarks/5apps", "http://5apps.com"
|
||||
end
|
||||
|
||||
context "when authorized for the category" do
|
||||
it "lists the files" do
|
||||
get "/jimmy/public/bookmarks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["5apps"].wont_be_nil
|
||||
end
|
||||
|
||||
it "has an ETag header set" do
|
||||
get "/jimmy/public/bookmarks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when directly authorized for the public directory" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents:r", "public/bookmarks:rw"]
|
||||
auth.store
|
||||
end
|
||||
|
||||
it "lists the files" do
|
||||
get "/jimmy/public/bookmarks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["5apps"].wont_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when not authorized" do
|
||||
before do
|
||||
auth_bucket.delete("jimmy:123")
|
||||
end
|
||||
|
||||
it "does not allow a directory listing of the public root" do
|
||||
get "/jimmy/public/"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
|
||||
it "does not allow a directory listing of a sub-directory" do
|
||||
get "/jimmy/public/bookmarks/"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "directory object" do
|
||||
describe "PUT file" do
|
||||
context "no existing directory object" do
|
||||
before do
|
||||
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||
end
|
||||
|
||||
it "creates a new directory object" do
|
||||
object = data_bucket.get("jimmy:tasks/home:trash")
|
||||
directory = directory_bucket.get("jimmy:tasks/home")
|
||||
|
||||
directory.data.wont_be_nil
|
||||
directory.data.to_i.must_equal object.meta['timestamp'][0].to_i
|
||||
end
|
||||
|
||||
it "sets the correct index for the directory object" do
|
||||
object = directory_bucket.get("jimmy:tasks/home")
|
||||
object.indexes["directory_bin"].must_include "tasks"
|
||||
end
|
||||
|
||||
it "creates directory objects for the parent directories" do
|
||||
object = directory_bucket.get("jimmy:tasks")
|
||||
object.indexes["directory_bin"].must_include "/"
|
||||
object.data.wont_be_nil
|
||||
|
||||
object = directory_bucket.get("jimmy:")
|
||||
object.indexes["directory_bin"].must_be_empty
|
||||
object.data.wont_be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "existing directory object" do
|
||||
before do
|
||||
put "/jimmy/tasks/home/trash", "collect some trash"
|
||||
end
|
||||
|
||||
it "updates the timestamp of the directory" do
|
||||
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
object = data_bucket.get("jimmy:tasks/home:trash")
|
||||
directory = directory_bucket.get("jimmy:tasks/home")
|
||||
|
||||
directory.data.to_i.must_equal object.meta['timestamp'][0].to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "OPTIONS listing" do
|
||||
it "has CORS headers set" do
|
||||
options "/jimmy/tasks/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
last_response.headers["Access-Control-Allow-Origin"].must_equal "*"
|
||||
last_response.headers["Access-Control-Allow-Methods"].must_equal "GET, PUT, DELETE"
|
||||
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin, If-Match, If-None-Match"
|
||||
last_response.headers["Access-Control-Expose-Headers"].must_equal "ETag, Content-Length"
|
||||
end
|
||||
|
||||
context "sub-directories" do
|
||||
it "has CORS headers set" do
|
||||
options "/jimmy/tasks/foo/bar/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
last_response.headers["Access-Control-Allow-Origin"].must_equal "*"
|
||||
last_response.headers["Access-Control-Allow-Methods"].must_equal "GET, PUT, DELETE"
|
||||
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin, If-Match, If-None-Match"
|
||||
last_response.headers["Access-Control-Expose-Headers"].must_equal "ETag, Content-Length"
|
||||
end
|
||||
end
|
||||
|
||||
context "root directory" do
|
||||
it "has CORS headers set" do
|
||||
options "/jimmy/"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
last_response.headers["Access-Control-Allow-Origin"].must_equal "*"
|
||||
last_response.headers["Access-Control-Allow-Methods"].must_equal "GET, PUT, DELETE"
|
||||
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin, If-Match, If-None-Match"
|
||||
last_response.headers["Access-Control-Expose-Headers"].must_equal "ETag, Content-Length"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE file" do
|
||||
context "last file in directory" do
|
||||
before do
|
||||
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||
end
|
||||
|
||||
it "deletes the directory objects for all empty parent directories" do
|
||||
delete "/jimmy/tasks/home/trash"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
|
||||
lambda {
|
||||
directory_bucket.get("jimmy:tasks/home")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
|
||||
lambda {
|
||||
directory_bucket.get("jimmy:tasks")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
|
||||
lambda {
|
||||
directory_bucket.get("jimmy:")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
end
|
||||
|
||||
context "with additional files in directory" do
|
||||
before do
|
||||
put "/jimmy/tasks/home/trash", "take out the trash"
|
||||
put "/jimmy/tasks/home/laundry/washing", "wash the clothes"
|
||||
end
|
||||
|
||||
it "does not delete the directory objects for the parent directories" do
|
||||
delete "/jimmy/tasks/home/trash"
|
||||
|
||||
directory_bucket.get("jimmy:tasks/home").wont_be_nil
|
||||
directory_bucket.get("jimmy:tasks").wont_be_nil
|
||||
directory_bucket.get("jimmy:").wont_be_nil
|
||||
end
|
||||
|
||||
it "updates the ETag headers of all parent directories" do
|
||||
get "/jimmy/tasks/home/"
|
||||
home_etag = last_response.headers["ETag"]
|
||||
|
||||
get "/jimmy/tasks/"
|
||||
tasks_etag = last_response.headers["ETag"]
|
||||
|
||||
get "/jimmy/"
|
||||
root_etag = last_response.headers["ETag"]
|
||||
|
||||
delete "/jimmy/tasks/home/trash"
|
||||
|
||||
get "/jimmy/tasks/home/"
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].wont_equal home_etag
|
||||
|
||||
get "/jimmy/tasks/"
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].wont_equal tasks_etag
|
||||
|
||||
get "/jimmy/"
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].wont_equal root_etag
|
||||
end
|
||||
|
||||
describe "timestamps" do
|
||||
before do
|
||||
@old_timestamp = (2.seconds.ago.to_f * 1000).to_i
|
||||
|
||||
["tasks/home", "tasks", ""].each do |dir|
|
||||
directory = directory_bucket.get("jimmy:#{dir}")
|
||||
directory.data = @old_timestamp.to_s
|
||||
directory.store
|
||||
end
|
||||
end
|
||||
|
||||
it "updates the timestamp for the parent directories" do
|
||||
delete "/jimmy/tasks/home/trash"
|
||||
|
||||
directory_bucket.get("jimmy:tasks/home").data.to_i.must_be :>, @old_timestamp
|
||||
directory_bucket.get("jimmy:tasks").data.to_i.must_be :>, @old_timestamp
|
||||
directory_bucket.get("jimmy:").data.to_i.must_be :>, @old_timestamp
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,424 +0,0 @@
|
||||
require_relative "../spec_helper"
|
||||
|
||||
describe "Permissions" do
|
||||
include Rack::Test::Methods
|
||||
|
||||
before do
|
||||
purge_all_buckets
|
||||
end
|
||||
|
||||
describe "GET" do
|
||||
context "public data" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some text data"
|
||||
object.store
|
||||
|
||||
object = data_bucket.new("jimmy:public/documents:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some text data"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "returns the value on all get requests" do
|
||||
get "/jimmy/public/foo"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some text data"
|
||||
end
|
||||
|
||||
it "returns the value from a sub-directory" do
|
||||
get "/jimmy/public/documents/foo"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some text data"
|
||||
end
|
||||
end
|
||||
|
||||
context "private data" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:documents:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some private, authorized text data"
|
||||
object.store
|
||||
|
||||
object = data_bucket.new("jimmy:documents/very/interesting:text")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some very interesting writing"
|
||||
object.store
|
||||
|
||||
object = data_bucket.new("jimmy:confidential:bar")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some private, non-authorized text data"
|
||||
object.store
|
||||
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents:r", "tasks:rw"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
context "when authorized" do
|
||||
it "returns the value for a key in a top-level directory" do
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some private, authorized text data"
|
||||
end
|
||||
|
||||
it "returns the value for a key in a sub-directory" do
|
||||
get "/jimmy/documents/very/interesting/text"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some very interesting writing"
|
||||
end
|
||||
end
|
||||
|
||||
context "when not authorized" do
|
||||
it "returns a 401 for a key in a top-level directory" do
|
||||
get "/jimmy/confidential/bar"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents:r", "contacts:rw", "tasks:r", "tasks/home:rw"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
context "to a top-level directory" do
|
||||
it "saves the value when there are write permissions" do
|
||||
put "/jimmy/contacts/1", "John Doe"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:contacts:1").data.must_equal "John Doe"
|
||||
end
|
||||
|
||||
it "returns a 401 when there are read permissions only" do
|
||||
put "/jimmy/documents/foo", "some text"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
|
||||
context "to a sub-directory" do
|
||||
it "saves the value when there are direct write permissions" do
|
||||
put "/jimmy/tasks/home/1", "take out the trash"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:tasks/home:1").data.must_equal "take out the trash"
|
||||
end
|
||||
|
||||
it "saves the value when there are write permissions for a parent directory" do
|
||||
put "/jimmy/contacts/family/1", "Bobby Brother"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:contacts/family:1").data.must_equal "Bobby Brother"
|
||||
end
|
||||
|
||||
it "returns a 401 when there are read permissions only" do
|
||||
put "/jimmy/documents/business/1", "some text"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
|
||||
context "to the public directory" do
|
||||
context "when authorized for the corresponding category" do
|
||||
it "saves the value" do
|
||||
put "/jimmy/public/contacts/foo", "Foo Bar"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:public/contacts:foo").data.must_equal "Foo Bar"
|
||||
end
|
||||
|
||||
it "saves the value to a sub-directory" do
|
||||
put "/jimmy/public/contacts/family/foo", "Foo Bar"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:public/contacts/family:foo").data.must_equal "Foo Bar"
|
||||
end
|
||||
end
|
||||
|
||||
context "when not authorized for the corresponding category" do
|
||||
it "returns a 401" do
|
||||
put "/jimmy/public/documents/foo", "Foo Bar"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents:r", "tasks:rw"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
context "when authorized" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:tasks:1")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "do the laundry"
|
||||
object.store
|
||||
|
||||
object = data_bucket.new("jimmy:tasks/home:1")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "take out the trash"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "removes the key from a top-level directory" do
|
||||
delete "/jimmy/tasks/1"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:tasks:1")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
|
||||
it "removes the key from a top-level directory" do
|
||||
delete "/jimmy/tasks/home/1"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:tasks/home:1")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
|
||||
context "public directory" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public/tasks:open")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "hello world"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "removes the key" do
|
||||
delete "/jimmy/public/tasks/open"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:public/tasks:open")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when not authorized" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:documents:private")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some private, authorized text data"
|
||||
object.store
|
||||
|
||||
object = data_bucket.new("jimmy:documents/business:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some private, authorized text data"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "returns a 401 for a key in a top-level directory" do
|
||||
delete "/jimmy/documents/private"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
|
||||
it "returns a 401 for a key in a sub-directory" do
|
||||
delete "/jimmy/documents/business/foo"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
|
||||
context "public directory" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public/documents:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some private, authorized text data"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "returns a 401" do
|
||||
delete "/jimmy/public/documents/foo"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "global permissions" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:documents/very/interesting:text")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some very interesting writing"
|
||||
object.store
|
||||
end
|
||||
|
||||
context "write all" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = [":rw", "documents:r"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
it "allows GET requests" do
|
||||
get "/jimmy/documents/very/interesting/text"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some very interesting writing"
|
||||
end
|
||||
|
||||
it "allows PUT requests" do
|
||||
put "/jimmy/contacts/1", "John Doe"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:contacts:1").data.must_equal "John Doe"
|
||||
end
|
||||
|
||||
it "allows DELETE requests" do
|
||||
delete "/jimmy/documents/very/interesting/text"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:documents/very/interesting:text")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
|
||||
context "root directory" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy::root")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "Back to the roots"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "allows GET requests" do
|
||||
get "/jimmy/root"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "Back to the roots"
|
||||
end
|
||||
|
||||
it "allows PUT requests" do
|
||||
put "/jimmy/1", "Gonna kick it root down"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy::1").data.must_equal "Gonna kick it root down"
|
||||
end
|
||||
|
||||
it "allows DELETE requests" do
|
||||
delete "/jimmy/root"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy::root")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
end
|
||||
|
||||
context "public directory" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public/tasks:hello")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "Hello World"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "allows GET requests" do
|
||||
get "/jimmy/public/tasks/"
|
||||
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
|
||||
it "allows PUT requests" do
|
||||
put "/jimmy/public/1", "Hello World"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:public:1").data.must_equal "Hello World"
|
||||
end
|
||||
|
||||
it "allows DELETE requests" do
|
||||
delete "/jimmy/public/tasks/hello"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:public/tasks:hello")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "read all" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = [":r", "contacts:rw"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
it "allows GET requests" do
|
||||
get "/jimmy/documents/very/interesting/text"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some very interesting writing"
|
||||
end
|
||||
|
||||
it "disallows PUT requests" do
|
||||
put "/jimmy/documents/foo", "some text"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
|
||||
it "disallows DELETE requests" do
|
||||
delete "/jimmy/documents/very/interesting/text"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
|
||||
context "public directory" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public/tasks:hello")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "Hello World"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "allows GET requests" do
|
||||
get "/jimmy/public/tasks/"
|
||||
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
|
||||
it "disallows PUT requests" do
|
||||
put "/jimmy/public/tasks/foo", "some text"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
|
||||
it "disallows DELETE requests" do
|
||||
delete "/jimmy/public/tasks/hello"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,775 +0,0 @@
|
||||
require_relative "../spec_helper"
|
||||
|
||||
describe "App with Riak backend" do
|
||||
include Rack::Test::Methods
|
||||
|
||||
before do
|
||||
purge_all_buckets
|
||||
end
|
||||
|
||||
describe "HEAD public data" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some text data"
|
||||
object.store
|
||||
|
||||
head "/jimmy/public/foo"
|
||||
end
|
||||
|
||||
it "works" do
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal ""
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["Content-Length"].must_equal "14"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET public data" do
|
||||
describe "file with content" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some text data"
|
||||
object.store
|
||||
|
||||
get "/jimmy/public/foo"
|
||||
end
|
||||
|
||||
it "works" do
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some text data"
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["Content-Length"].must_equal "14"
|
||||
last_response.headers["Expires"].must_equal "0"
|
||||
end
|
||||
|
||||
describe "empty file" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public:empty")
|
||||
object.content_type = "text/plain"
|
||||
object.data = ""
|
||||
object.store
|
||||
|
||||
get "/jimmy/public/empty"
|
||||
end
|
||||
|
||||
it "returns an empty body" do
|
||||
last_response.status.must_equal 200
|
||||
# Rack::MockRequest turns the body into a string. We can't use
|
||||
# `last_response.body` to check for nil, because:
|
||||
# >> [nil].join
|
||||
# => ""
|
||||
last_response.body.must_equal ''
|
||||
last_response.headers["Content-Length"].must_equal '0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET data with custom content type" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:public:magic")
|
||||
object.content_type = "text/magic"
|
||||
object.raw_data = "some text data"
|
||||
object.store
|
||||
end
|
||||
|
||||
it "returns the value with the correct content type" do
|
||||
get "/jimmy/public/magic"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.content_type.must_equal "text/magic"
|
||||
last_response.body.must_equal "some text data"
|
||||
end
|
||||
end
|
||||
|
||||
describe "private data" do
|
||||
before do
|
||||
object = data_bucket.new("jimmy:documents:foo")
|
||||
object.content_type = "text/plain"
|
||||
object.data = "some private text data"
|
||||
object.store
|
||||
|
||||
@etag = object.etag
|
||||
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents", "public"]
|
||||
auth.store
|
||||
end
|
||||
|
||||
describe "HEAD" do
|
||||
before do
|
||||
header "Authorization", "Bearer 123"
|
||||
head "/jimmy/documents/foo"
|
||||
end
|
||||
|
||||
it "works" do
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal ""
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["Content-Length"].must_equal "22"
|
||||
end
|
||||
end
|
||||
|
||||
describe "HEAD nonexisting key" do
|
||||
it "returns a 404" do
|
||||
header "Authorization", "Bearer 123"
|
||||
head "/jimmy/documents/somestupidkey"
|
||||
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET" do
|
||||
before do
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
it "returns the value" do
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some private text data"
|
||||
end
|
||||
|
||||
describe "when If-None-Match header is set" do
|
||||
it "responds with 'not modified' when it matches the current ETag" do
|
||||
header "If-None-Match", @etag
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 304
|
||||
last_response.body.must_be_empty
|
||||
last_response.headers["ETag"].must_equal @etag
|
||||
end
|
||||
|
||||
it "responds normally when it does not match the current ETag" do
|
||||
header "If-None-Match", "FOO"
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some private text data"
|
||||
end
|
||||
end
|
||||
|
||||
describe "when If-None-Match header is set with multiple revisions" do
|
||||
it "responds with 'not modified' when it contains the current ETag" do
|
||||
header "If-None-Match", "DEADBEEF,#{@etag},F00BA4"
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 304
|
||||
last_response.body.must_be_empty
|
||||
last_response.headers["ETag"].must_equal @etag
|
||||
end
|
||||
|
||||
it "responds normally when it does not contain the current ETag" do
|
||||
header "If-None-Match", "FOO,BAR"
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some private text data"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET nonexisting key" do
|
||||
it "returns a 404" do
|
||||
header "Authorization", "Bearer 123"
|
||||
get "/jimmy/documents/somestupidkey"
|
||||
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT" do
|
||||
before do
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
describe "with implicit content type" do
|
||||
before do
|
||||
put "/jimmy/documents/bar", "another text"
|
||||
end
|
||||
|
||||
it "saves the value" do
|
||||
last_response.status.must_equal 201
|
||||
last_response.body.must_equal ""
|
||||
data_bucket.get("jimmy:documents:bar").data.must_equal "another text"
|
||||
end
|
||||
|
||||
it "stores the data as plain text with utf-8 encoding" do
|
||||
data_bucket.get("jimmy:documents:bar").content_type.must_equal "text/plain; charset=utf-8"
|
||||
end
|
||||
|
||||
it "sets the ETag header" do
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
end
|
||||
|
||||
it "indexes the data set" do
|
||||
indexes = data_bucket.get("jimmy:documents:bar").indexes
|
||||
indexes["user_id_bin"].must_be_kind_of Set
|
||||
indexes["user_id_bin"].must_include "jimmy"
|
||||
|
||||
indexes["directory_bin"].must_include "documents"
|
||||
end
|
||||
|
||||
it "logs the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
log_entry = objects.select{|o| o.data["count"] == 1}.first
|
||||
log_entry.data["size"].must_equal 12
|
||||
log_entry.data["category"].must_equal "documents"
|
||||
log_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
end
|
||||
|
||||
describe "with explicit content type" do
|
||||
before do
|
||||
header "Content-Type", "application/json"
|
||||
put "/jimmy/documents/jason", '{"foo": "bar", "unhosted": 1}'
|
||||
end
|
||||
|
||||
it "saves the value (as JSON)" do
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:documents:jason").data.must_be_kind_of Hash
|
||||
data_bucket.get("jimmy:documents:jason").data.must_equal({"foo" => "bar", "unhosted" => 1})
|
||||
end
|
||||
|
||||
it "uses the requested content type" do
|
||||
data_bucket.get("jimmy:documents:jason").content_type.must_equal "application/json"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
header "Authorization", "Bearer 123"
|
||||
get "/jimmy/documents/jason"
|
||||
|
||||
last_response.body.must_equal '{"foo":"bar","unhosted":1}'
|
||||
last_response.content_type.must_equal "application/json"
|
||||
end
|
||||
end
|
||||
|
||||
describe "with arbitrary content type" do
|
||||
before do
|
||||
header "Content-Type", "text/magic"
|
||||
put "/jimmy/documents/magic", "pure magic"
|
||||
end
|
||||
|
||||
it "saves the value" do
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:documents:magic").raw_data.must_equal "pure magic"
|
||||
end
|
||||
|
||||
it "uses the requested content type" do
|
||||
data_bucket.get("jimmy:documents:magic").content_type.must_equal "text/magic"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
header "Authorization", "Bearer 123"
|
||||
get "/jimmy/documents/magic"
|
||||
|
||||
last_response.body.must_equal "pure magic"
|
||||
last_response.content_type.must_equal "text/magic"
|
||||
end
|
||||
end
|
||||
|
||||
describe "with content type containing the encoding" do
|
||||
before do
|
||||
header "Content-Type", "application/json; charset=UTF-8"
|
||||
put "/jimmy/documents/jason", '{"foo": "bar", "unhosted": 1}'
|
||||
end
|
||||
|
||||
it "saves the value (as JSON)" do
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:documents:jason").data.must_be_kind_of Hash
|
||||
data_bucket.get("jimmy:documents:jason").data.must_equal({"foo" => "bar", "unhosted" => 1})
|
||||
end
|
||||
|
||||
it "uses the requested content type" do
|
||||
data_bucket.get("jimmy:documents:jason").content_type.must_equal "application/json; charset=UTF-8"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
get "/jimmy/documents/jason"
|
||||
|
||||
last_response.body.must_equal '{"foo":"bar","unhosted":1}'
|
||||
last_response.content_type.must_equal "application/json; charset=UTF-8"
|
||||
end
|
||||
end
|
||||
|
||||
describe "naming collisions between documents and directories" do
|
||||
before do
|
||||
put "/jimmy/documents/archive/document", "lorem ipsum"
|
||||
end
|
||||
|
||||
it "responds with 409 when directory with same name already exists" do
|
||||
put "/jimmy/documents/archive", "some awesome content"
|
||||
|
||||
last_response.status.must_equal 409
|
||||
|
||||
lambda {
|
||||
data_bucket.get("jimmy:documents/archive")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
|
||||
it "responds with 409 when there is an existing document with same name as one of the directories" do
|
||||
put "/jimmy/documents/archive/document/subdir/doc", "some awesome content"
|
||||
|
||||
last_response.status.must_equal 409
|
||||
|
||||
lambda {
|
||||
data_bucket.get("jimmy:documents/archive/document/subdir/doc")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
end
|
||||
|
||||
describe "with existing content" do
|
||||
before do
|
||||
put "/jimmy/documents/archive/foo", "lorem ipsum"
|
||||
end
|
||||
|
||||
it "saves the value" do
|
||||
put "/jimmy/documents/archive/foo", "some awesome content"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
data_bucket.get("jimmy:documents/archive:foo").data.must_equal "some awesome content"
|
||||
end
|
||||
|
||||
it "logs the operations" do
|
||||
put "/jimmy/documents/archive/foo", "some awesome content"
|
||||
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
create_entry = objects.select{|o| o.data["count"] == 1}.first
|
||||
create_entry.data["size"].must_equal 11
|
||||
create_entry.data["category"].must_equal "documents"
|
||||
create_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
|
||||
update_entry = objects.select{|o| o.data["count"] == 0}.first
|
||||
update_entry.data["size"].must_equal 9
|
||||
update_entry.data["category"].must_equal "documents"
|
||||
update_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
|
||||
it "changes the ETag header" do
|
||||
old_etag = last_response.headers["ETag"]
|
||||
put "/jimmy/documents/archive/foo", "some awesome content"
|
||||
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].wont_equal old_etag
|
||||
end
|
||||
|
||||
describe "when If-Match header is set" do
|
||||
it "allows the request if the header matches the current ETag" do
|
||||
old_etag = last_response.headers["ETag"]
|
||||
header "If-Match", old_etag
|
||||
|
||||
put "/jimmy/documents/archive/foo", "some awesome content"
|
||||
last_response.status.must_equal 200
|
||||
|
||||
get "/jimmy/documents/archive/foo"
|
||||
last_response.body.must_equal "some awesome content"
|
||||
end
|
||||
|
||||
it "fails the request if the header does not match the current ETag" do
|
||||
header "If-Match", "WONTMATCH"
|
||||
|
||||
put "/jimmy/documents/archive/foo", "some awesome content"
|
||||
last_response.status.must_equal 412
|
||||
|
||||
get "/jimmy/documents/archive/foo"
|
||||
last_response.body.must_equal "lorem ipsum"
|
||||
end
|
||||
end
|
||||
|
||||
describe "when If-None-Match header is set" do
|
||||
before do
|
||||
header "If-None-Match", "*"
|
||||
end
|
||||
|
||||
it "fails when the document already exists" do
|
||||
put "/jimmy/documents/archive/foo", "some awesome content"
|
||||
|
||||
last_response.status.must_equal 412
|
||||
|
||||
get "/jimmy/documents/archive/foo"
|
||||
last_response.body.must_equal "lorem ipsum"
|
||||
end
|
||||
|
||||
it "succeeds when the document does not exist" do
|
||||
put "/jimmy/documents/archive/bar", "my little content"
|
||||
|
||||
last_response.status.must_equal 201
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "exsting content without serializer registered for the given content-type" do
|
||||
before do
|
||||
header "Content-Type", "text/html; charset=UTF-8"
|
||||
put "/jimmy/documents/html", '<html></html>'
|
||||
put "/jimmy/documents/html", '<html><body></body></html>'
|
||||
end
|
||||
|
||||
it "saves the value" do
|
||||
last_response.status.must_equal 200
|
||||
data_bucket.get("jimmy:documents:html").raw_data.must_equal "<html><body></body></html>"
|
||||
end
|
||||
|
||||
it "uses the requested content type" do
|
||||
data_bucket.get("jimmy:documents:html").content_type.must_equal "text/html; charset=UTF-8"
|
||||
end
|
||||
end
|
||||
|
||||
describe "public data" do
|
||||
before do
|
||||
put "/jimmy/public/documents/notes/foo", "note to self"
|
||||
end
|
||||
|
||||
it "saves the value" do
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:public/documents/notes:foo").data.must_equal "note to self"
|
||||
end
|
||||
|
||||
it "logs the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
log_entry = objects.select{|o| o.data["count"] == 1}.first
|
||||
log_entry.data["size"].must_equal 12
|
||||
log_entry.data["category"].must_equal "public/documents"
|
||||
log_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
end
|
||||
|
||||
context "with binary data" do
|
||||
context "binary charset in content-type header" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg; charset=binary"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/documents/jaypeg", @image
|
||||
end
|
||||
|
||||
it "uses the requested content type" do
|
||||
get "/jimmy/documents/jaypeg"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.content_type.must_equal "image/jpeg; charset=binary"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
get "/jimmy/documents/jaypeg"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal @image
|
||||
end
|
||||
|
||||
it "responds with an ETag header" do
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
etag = last_response.headers["ETag"]
|
||||
|
||||
get "/jimmy/documents/jaypeg"
|
||||
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].must_equal etag
|
||||
end
|
||||
|
||||
it "responds with a Content-Length header" do
|
||||
get "/jimmy/documents/jaypeg"
|
||||
|
||||
last_response.headers["Content-Length"].must_equal "16044"
|
||||
end
|
||||
|
||||
it "changes the ETag when updating the file" do
|
||||
old_etag = last_response.headers["ETag"]
|
||||
put "/jimmy/documents/jaypeg", @image
|
||||
|
||||
last_response.headers["ETag"].wont_be_nil
|
||||
last_response.headers["ETag"].wont_equal old_etag
|
||||
end
|
||||
|
||||
it "logs the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
log_entry = objects.select{|o| o.data["count"] == 1}.first
|
||||
log_entry.data["size"].must_equal 16044
|
||||
log_entry.data["category"].must_equal "documents"
|
||||
log_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
|
||||
context "overwriting existing file with same file" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg; charset=binary"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/documents/jaypeg", @image
|
||||
end
|
||||
|
||||
it "doesn't log the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
objects.size.must_equal 1
|
||||
end
|
||||
end
|
||||
|
||||
context "overwriting existing file with different file" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg; charset=binary"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/documents/jaypeg", @image+"foo"
|
||||
end
|
||||
|
||||
it "logs the operation changing only the size" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
objects.size.must_equal 2
|
||||
|
||||
log_entry = objects.select{|o| o.data["count"] == 0}.first
|
||||
log_entry.data["size"].must_equal 3
|
||||
log_entry.data["category"].must_equal "documents"
|
||||
log_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "no binary charset in content-type header" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/documents/jaypeg", @image
|
||||
end
|
||||
|
||||
it "uses the requested content type" do
|
||||
get "/jimmy/documents/jaypeg"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.content_type.must_equal "image/jpeg"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
get "/jimmy/documents/jaypeg"
|
||||
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal @image
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with escaped key" do
|
||||
before do
|
||||
put "/jimmy/documents/bar%3Abaz/http%3A%2F%2F5apps.com", "super website"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
header "Authorization", "Bearer 123"
|
||||
get "/jimmy/documents/bar%3Abaz/http%3A%2F%2F5apps.com"
|
||||
|
||||
last_response.body.must_equal 'super website'
|
||||
end
|
||||
end
|
||||
|
||||
context "with unescaped key" do
|
||||
before do
|
||||
put "/jimmy/documents/bar:baz/john@doe.com", "John Doe"
|
||||
end
|
||||
|
||||
it "lists the document in the directory" do
|
||||
get "/jimmy/documents/bar:baz/"
|
||||
|
||||
content = JSON.parse(last_response.body)
|
||||
content["items"]["john@doe.com"].wont_be_nil
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
get "/jimmy/documents/bar:baz/john@doe.com"
|
||||
|
||||
last_response.body.must_equal "John Doe"
|
||||
end
|
||||
end
|
||||
|
||||
context "escaped square brackets in key" do
|
||||
before do
|
||||
put "/jimmy/documents/gracehopper%5B1%5D.jpg", "super image"
|
||||
end
|
||||
|
||||
it "delivers the data correctly" do
|
||||
header "Authorization", "Bearer 123"
|
||||
get "/jimmy/documents/gracehopper%5B1%5D.jpg"
|
||||
|
||||
last_response.body.must_equal "super image"
|
||||
end
|
||||
end
|
||||
|
||||
context "invalid JSON" do
|
||||
context "empty body" do
|
||||
before do
|
||||
header "Content-Type", "application/json"
|
||||
put "/jimmy/documents/jason", ""
|
||||
end
|
||||
|
||||
it "saves an empty JSON object" do
|
||||
last_response.status.must_equal 201
|
||||
data_bucket.get("jimmy:documents:jason").data.must_be_kind_of Hash
|
||||
data_bucket.get("jimmy:documents:jason").data.must_equal({})
|
||||
end
|
||||
end
|
||||
|
||||
context "unparsable JSON" do
|
||||
before do
|
||||
header "Content-Type", "application/json"
|
||||
put "/jimmy/documents/jason", "foo"
|
||||
end
|
||||
|
||||
it "returns a 422" do
|
||||
last_response.status.must_equal 422
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE" do
|
||||
before do
|
||||
header "Authorization", "Bearer 123"
|
||||
end
|
||||
|
||||
describe "basics" do
|
||||
before do
|
||||
delete "/jimmy/documents/foo"
|
||||
end
|
||||
|
||||
it "removes the key" do
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:documents:foo")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
|
||||
it "logs the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
log_entry = objects.select{|o| o.data["count"] == -1}.first
|
||||
log_entry.data["size"].must_equal(-22)
|
||||
log_entry.data["category"].must_equal "documents"
|
||||
log_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
end
|
||||
|
||||
context "non-existing object" do
|
||||
before do
|
||||
delete "/jimmy/documents/foozius"
|
||||
end
|
||||
|
||||
it "responds with 404" do
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
|
||||
it "doesn't log the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
objects.select{|o| o.data["count"] == -1}.size.must_equal 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when an If-Match header is given" do
|
||||
it "allows the request if it matches the current ETag" do
|
||||
get "/jimmy/documents/foo"
|
||||
old_etag = last_response.headers["ETag"]
|
||||
header "If-Match", old_etag
|
||||
|
||||
delete "/jimmy/documents/foo"
|
||||
last_response.status.must_equal 200
|
||||
|
||||
get "/jimmy/documents/foo"
|
||||
last_response.status.must_equal 404
|
||||
end
|
||||
|
||||
it "fails the request if it does not match the current ETag" do
|
||||
header "If-Match", "WONTMATCH"
|
||||
|
||||
delete "/jimmy/documents/foo"
|
||||
last_response.status.must_equal 412
|
||||
|
||||
get "/jimmy/documents/foo"
|
||||
last_response.status.must_equal 200
|
||||
last_response.body.must_equal "some private text data"
|
||||
end
|
||||
end
|
||||
|
||||
context "binary data" do
|
||||
before do
|
||||
header "Content-Type", "image/jpeg; charset=binary"
|
||||
filename = File.join(File.expand_path(File.dirname(__FILE__)), "..", "fixtures", "rockrule.jpeg")
|
||||
@image = File.open(filename, "r").read
|
||||
put "/jimmy/documents/jaypeg", @image
|
||||
|
||||
delete "/jimmy/documents/jaypeg"
|
||||
end
|
||||
|
||||
it "removes the main object" do
|
||||
last_response.status.must_equal 200
|
||||
lambda {
|
||||
data_bucket.get("jimmy:documents:jaypeg")
|
||||
}.must_raise Riak::HTTPFailedRequest
|
||||
end
|
||||
|
||||
it "removes the binary object" do
|
||||
last_response.status.must_equal 200
|
||||
|
||||
binary = cs_binary_bucket.files.get("jimmy:documents:jaypeg")
|
||||
binary.must_be_nil
|
||||
end
|
||||
|
||||
it "logs the operation" do
|
||||
objects = []
|
||||
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
|
||||
|
||||
log_entry = objects.select{|o| o.data["count"] == -1 && o.data["size"] == -16044}.first
|
||||
log_entry.data["category"].must_equal "documents"
|
||||
log_entry.indexes["user_id_bin"].must_include "jimmy"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "unauthorized access" do
|
||||
before do
|
||||
auth = auth_bucket.new("jimmy:123")
|
||||
auth.data = ["documents", "public"]
|
||||
auth.store
|
||||
|
||||
header "Authorization", "Bearer 321"
|
||||
end
|
||||
|
||||
describe "GET" do
|
||||
it "returns a 401" do
|
||||
get "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT" do
|
||||
it "returns a 401" do
|
||||
put "/jimmy/documents/foo", "some text"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE" do
|
||||
it "returns a 401" do
|
||||
delete "/jimmy/documents/foo"
|
||||
|
||||
last_response.status.must_equal 401
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,6 @@ require_relative '../liquor-cabinet'
|
||||
require 'minitest/autorun'
|
||||
require 'rack/test'
|
||||
require 'purdytest'
|
||||
require 'riak'
|
||||
require "redis"
|
||||
require "rest_client"
|
||||
require "minitest/stub_any_instance"
|
||||
@@ -44,74 +43,3 @@ if app.settings.respond_to? :redis
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if app.settings.respond_to? :riak
|
||||
::Riak.disable_list_keys_warnings = true
|
||||
|
||||
def client
|
||||
@client ||= ::Riak::Client.new(:host => app.settings.riak['host'],
|
||||
:http_port => app.settings.riak['http_port'])
|
||||
end
|
||||
|
||||
def data_bucket
|
||||
@data_bucket ||= begin
|
||||
bucket = client.bucket(app.settings.riak['buckets']['data'])
|
||||
bucket.allow_mult = false
|
||||
bucket
|
||||
end
|
||||
end
|
||||
|
||||
def directory_bucket
|
||||
@directory_bucket ||= begin
|
||||
bucket = client.bucket(app.settings.riak['buckets']['directories'])
|
||||
bucket.allow_mult = false
|
||||
bucket
|
||||
end
|
||||
end
|
||||
|
||||
def auth_bucket
|
||||
@auth_bucket ||= begin
|
||||
bucket = client.bucket(app.settings.riak['buckets']['authorizations'])
|
||||
bucket.allow_mult = false
|
||||
bucket
|
||||
end
|
||||
end
|
||||
|
||||
def opslog_bucket
|
||||
@opslog_bucket ||= begin
|
||||
bucket = client.bucket(app.settings.riak['buckets']['opslog'])
|
||||
bucket.allow_mult = false
|
||||
bucket
|
||||
end
|
||||
end
|
||||
|
||||
def cs_credentials
|
||||
@cs_credentials ||= begin
|
||||
credentials = File.read(app.settings.riak['riak_cs']['credentials_file'])
|
||||
JSON.parse(credentials)
|
||||
end
|
||||
end
|
||||
|
||||
def cs_client
|
||||
@cs_client ||= Fog::Storage.new({
|
||||
:provider => 'AWS',
|
||||
:aws_access_key_id => cs_credentials['key_id'],
|
||||
:aws_secret_access_key => cs_credentials['key_secret'],
|
||||
:endpoint => app.settings.riak['riak_cs']['endpoint']
|
||||
})
|
||||
end
|
||||
|
||||
def cs_binary_bucket
|
||||
@cs_binary_bucket ||= cs_client.directories.create(:key => app.settings.riak['buckets']['cs_binaries'])
|
||||
end
|
||||
|
||||
def purge_all_buckets
|
||||
[data_bucket, directory_bucket, auth_bucket, opslog_bucket].each do |bucket|
|
||||
bucket.keys.each {|key| bucket.delete key}
|
||||
end
|
||||
|
||||
cs_binary_bucket.files.each do |file|
|
||||
file.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user