Merge pull request #31 from 5apps/rs_spec_01

remoteStorage spec 01
This commit is contained in:
2013-10-27 15:32:33 -07:00
5 changed files with 344 additions and 105 deletions

View File

@@ -40,8 +40,11 @@ module RemoteStorage
def get_data(user, directory, key) def get_data(user, directory, key)
object = data_bucket.get("#{user}:#{directory}:#{key}") object = data_bucket.get("#{user}:#{directory}:#{key}")
server.halt 304 if server.env["HTTP_IF_NONE_MATCH"] == object.etag
server.headers["Content-Type"] = object.content_type server.headers["Content-Type"] = object.content_type
server.headers["Last-Modified"] = last_modified_date_for(object) server.headers["Last-Modified"] = last_modified_date_for(object)
server.headers["ETag"] = object.etag
if binary_key = object.meta["binary_key"] if binary_key = object.meta["binary_key"]
object = cs_binary_bucket.files.get(binary_key[0]) object = cs_binary_bucket.files.get(binary_key[0])
@@ -66,25 +69,34 @@ module RemoteStorage
def get_directory_listing(user, directory) def get_directory_listing(user, directory)
directory_object = directory_bucket.get("#{user}:#{directory}") directory_object = directory_bucket.get("#{user}:#{directory}")
server.halt 304 if server.env["HTTP_IF_NONE_MATCH"] == directory_object.etag
timestamp = directory_object.data.to_i timestamp = directory_object.data.to_i
timestamp /= 1000 if timestamp.to_s.length == 13 timestamp /= 1000 if timestamp.to_s.length == 13
server.headers["Content-Type"] = "application/json" server.headers["Content-Type"] = "application/json"
server.headers["Last-Modified"] = Time.at(timestamp).to_s(:rfc822) server.headers["Last-Modified"] = Time.at(timestamp).to_s(:rfc822)
server.headers["ETag"] = directory_object.etag
listing = directory_listing(user, directory) listing = directory_listing(user, directory)
return listing.to_json return listing.to_json
rescue ::Riak::HTTPFailedRequest rescue ::Riak::HTTPFailedRequest
server.headers["Content-Type"] = "application/json" server.halt 404
return "{}"
end end
def put_data(user, directory, key, data, content_type=nil) def put_data(user, directory, key, data, content_type=nil)
object = build_data_object(user, directory, key, data, content_type) object = build_data_object(user, directory, key, data, content_type)
if required_match = server.env["HTTP_IF_MATCH"]
server.halt 412 unless required_match == object.etag
end
object_exists = !object.raw_data.nil? object_exists = !object.raw_data.nil?
existing_object_size = object_size(object) existing_object_size = object_size(object)
server.halt 412 if object_exists && server.env["HTTP_IF_NONE_MATCH"] == "*"
timestamp = (Time.now.to_f * 1000).to_i timestamp = (Time.now.to_f * 1000).to_i
object.meta["timestamp"] = timestamp object.meta["timestamp"] = timestamp
@@ -96,13 +108,14 @@ module RemoteStorage
new_object_size = object.raw_data.size new_object_size = object.raw_data.size
end end
response = object.store object.store
log_count = object_exists ? 0 : 1 log_count = object_exists ? 0 : 1
log_operation(user, directory, log_count, new_object_size, existing_object_size) log_operation(user, directory, log_count, new_object_size, existing_object_size)
update_all_directory_objects(user, directory, timestamp) update_all_directory_objects(user, directory, timestamp)
server.headers["ETag"] = object.etag
server.halt 200 server.halt 200
rescue ::Riak::HTTPFailedRequest rescue ::Riak::HTTPFailedRequest
server.halt 422 server.halt 422
@@ -111,6 +124,11 @@ module RemoteStorage
def delete_data(user, directory, key) def delete_data(user, directory, key)
object = data_bucket.get("#{user}:#{directory}:#{key}") object = data_bucket.get("#{user}:#{directory}:#{key}")
existing_object_size = object_size(object) existing_object_size = object_size(object)
etag = object.etag
if required_match = server.env["HTTP_IF_MATCH"]
server.halt 412 unless required_match == etag
end
if binary_key = object.meta["binary_key"] if binary_key = object.meta["binary_key"]
object = cs_binary_bucket.files.get(binary_key[0]) object = cs_binary_bucket.files.get(binary_key[0])
@@ -126,6 +144,7 @@ module RemoteStorage
timestamp = (Time.now.to_f * 1000).to_i timestamp = (Time.now.to_f * 1000).to_i
delete_or_update_directory_objects(user, directory, timestamp) delete_or_update_directory_objects(user, directory, timestamp)
server.headers["ETag"] = etag
server.halt riak_response[:code] server.halt riak_response[:code]
rescue ::Riak::HTTPFailedRequest rescue ::Riak::HTTPFailedRequest
server.halt 404 server.halt 404
@@ -222,8 +241,9 @@ module RemoteStorage
sub_directories(user, directory).each do |entry| sub_directories(user, directory).each do |entry|
directory_name = entry["name"].split("/").last directory_name = entry["name"].split("/").last
timestamp = entry["timestamp"].to_i timestamp = entry["timestamp"].to_i
etag = entry["etag"]
listing.merge!({ "#{directory_name}/" => timestamp }) listing.merge!({ "#{directory_name}/" => etag })
end end
directory_entries(user, directory).each do |entry| directory_entries(user, directory).each do |entry|
@@ -233,20 +253,16 @@ module RemoteStorage
else else
DateTime.rfc2822(entry["last_modified"]).to_time.to_i DateTime.rfc2822(entry["last_modified"]).to_time.to_i
end end
etag = entry["etag"]
listing.merge!({ entry_name => timestamp }) listing.merge!({ entry_name => etag })
end end
listing listing
end end
def directory_entries(user, directory) def directory_entries(user, directory)
directory = "/" if directory == "" all_keys = user_directory_keys(user, directory, data_bucket)
user_keys = data_bucket.get_index("user_id_bin", user)
directory_keys = data_bucket.get_index("directory_bin", directory)
all_keys = user_keys & directory_keys
return [] if all_keys.empty? return [] if all_keys.empty?
map_query = <<-EOH map_query = <<-EOH
@@ -256,48 +272,53 @@ module RemoteStorage
key_name = keys.join(':'); key_name = keys.join(':');
last_modified_date = v.values[0]['metadata']['X-Riak-Last-Modified']; last_modified_date = v.values[0]['metadata']['X-Riak-Last-Modified'];
timestamp = v.values[0]['metadata']['X-Riak-Meta']['X-Riak-Meta-Timestamp']; timestamp = v.values[0]['metadata']['X-Riak-Meta']['X-Riak-Meta-Timestamp'];
etag = v.values[0]['metadata']['X-Riak-VTag'];
return [{ return [{
name: key_name, name: key_name,
last_modified: last_modified_date, last_modified: last_modified_date,
timestamp: timestamp, timestamp: timestamp,
etag: etag
}]; }];
} }
EOH EOH
map_reduce = ::Riak::MapReduce.new(client) run_map_reduce(data_bucket, all_keys, map_query)
all_keys.each do |key|
map_reduce.add(data_bucket.name, key)
end
map_reduce.
map(map_query, :keep => true).
run
end end
def sub_directories(user, directory) def sub_directories(user, directory)
directory = "/" if directory == "" all_keys = user_directory_keys(user, directory, directory_bucket)
user_keys = directory_bucket.get_index("user_id_bin", user)
directory_keys = directory_bucket.get_index("directory_bin", directory)
all_keys = user_keys & directory_keys
return [] if all_keys.empty? return [] if all_keys.empty?
map_query = <<-EOH map_query = <<-EOH
function(v){ function(v){
keys = v.key.split(':'); keys = v.key.split(':');
key_name = keys[keys.length-1]; key_name = keys[keys.length-1];
timestamp = v.values[0]['data'] timestamp = v.values[0]['data'];
etag = v.values[0]['metadata']['X-Riak-VTag'];
return [{ return [{
name: key_name, name: key_name,
timestamp: timestamp, timestamp: timestamp,
etag: etag
}]; }];
} }
EOH EOH
run_map_reduce(directory_bucket, all_keys, map_query)
end
def user_directory_keys(user, directory, bucket)
directory = "/" if directory == ""
user_keys = bucket.get_index("user_id_bin", user)
directory_keys = bucket.get_index("directory_bin", directory)
user_keys & directory_keys
end
def run_map_reduce(bucket, keys, map_query)
map_reduce = ::Riak::MapReduce.new(client) map_reduce = ::Riak::MapReduce.new(client)
all_keys.each do |key| keys.each do |key|
map_reduce.add(directory_bucket.name, key) map_reduce.add(bucket.name, key)
end end
map_reduce. map_reduce.

View File

@@ -42,7 +42,8 @@ class LiquorCabinet < Sinatra::Base
before path do before path do
headers 'Access-Control-Allow-Origin' => '*', headers 'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET, PUT, DELETE', 'Access-Control-Allow-Methods' => 'GET, PUT, DELETE',
'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin' 'Access-Control-Allow-Headers' => 'Authorization, Content-Type, Origin, If-Match, If-None-Match',
'Access-Control-Expose-Headers' => 'ETag'
headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"] headers['Access-Control-Allow-Origin'] = env["HTTP_ORIGIN"] if env["HTTP_ORIGIN"]
headers['Cache-Control'] = 'no-cache' headers['Cache-Control'] = 'no-cache'

View File

@@ -19,17 +19,18 @@ describe "Directories" do
put "/jimmy/tasks/http%3A%2F%2F5apps.com", "prettify design" put "/jimmy/tasks/http%3A%2F%2F5apps.com", "prettify design"
end end
it "lists the objects with a timestamp of the last modification" do it "lists the objects with their version" do
get "/jimmy/tasks/" get "/jimmy/tasks/"
last_response.status.must_equal 200 last_response.status.must_equal 200
last_response.content_type.must_equal "application/json" last_response.content_type.must_equal "application/json"
foo = data_bucket.get("jimmy:tasks:foo")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "http://5apps.com" content.must_include "http://5apps.com"
content.must_include "foo" content.must_include "foo"
content["foo"].must_be_kind_of Integer content["foo"].must_equal foo.etag.gsub(/"/, "")
content["foo"].to_s.length.must_equal 13
end end
it "has a Last-Modifier header set" do it "has a Last-Modifier header set" do
@@ -44,17 +45,57 @@ describe "Directories" do
last_modified.day.must_equal now.day last_modified.day.must_equal now.day
end 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 it "has CORS headers set" do
get "/jimmy/tasks/" get "/jimmy/tasks/"
last_response.status.must_equal 200 last_response.status.must_equal 200
last_response.headers["Access-Control-Allow-Origin"].must_equal "*" 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-Methods"].must_equal "GET, PUT, DELETE"
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin" 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"
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
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 end
context "with sub-directories" do context "with sub-directories" do
before do before do
get "/jimmy/tasks/"
@old_etag = last_response.headers["ETag"]
put "/jimmy/tasks/home/laundry", "do the laundry" put "/jimmy/tasks/home/laundry", "do the laundry"
end end
@@ -63,12 +104,20 @@ describe "Directories" do
last_response.status.must_equal 200 last_response.status.must_equal 200
home = directory_bucket.get("jimmy:tasks/home")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "foo" content.must_include "foo"
content.must_include "http://5apps.com" content.must_include "http://5apps.com"
content.must_include "home/" content.must_include "home/"
content["home/"].must_be_kind_of Integer content["home/"].must_equal home.etag.gsub(/"/, "")
content["home/"].to_s.length.must_equal 13 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 end
context "for a different user" do context "for a different user" do
@@ -102,10 +151,11 @@ describe "Directories" do
last_response.status.must_equal 200 last_response.status.must_equal 200
projects = directory_bucket.get("jimmy:tasks/private/projects")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "projects/" content.must_include "projects/"
content["projects/"].must_be_kind_of Integer content["projects/"].must_equal projects.etag.gsub(/"/, "")
content["projects/"].to_s.length.must_equal 13
end end
it "updates the timestamps of the existing directory objects" do it "updates the timestamps of the existing directory objects" do
@@ -137,10 +187,11 @@ describe "Directories" do
last_response.status.must_equal 200 last_response.status.must_equal 200
jaypeg = data_bucket.get("jimmy:tasks:jaypeg.jpg")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "jaypeg.jpg" content.must_include "jaypeg.jpg"
content["jaypeg.jpg"].must_be_kind_of Integer content["jaypeg.jpg"].must_equal jaypeg.etag.gsub(/"/, "")
content["jaypeg.jpg"].to_s.length.must_equal 13
end end
end end
@@ -157,10 +208,11 @@ describe "Directories" do
last_response.status.must_equal 200 last_response.status.must_equal 200
jaypeg = data_bucket.get("jimmy:tasks:jaypeg.jpg")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "jaypeg.jpg" content.must_include "jaypeg.jpg"
content["jaypeg.jpg"].must_be_kind_of Integer content["jaypeg.jpg"].must_equal jaypeg.etag.gsub(/"/, "")
content["jaypeg.jpg"].to_s.length.must_equal 13
end end
end end
end end
@@ -176,10 +228,11 @@ describe "Directories" do
last_response.status.must_equal 200 last_response.status.must_equal 200
laundry = data_bucket.get("jimmy:tasks/home:laundry")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "laundry" content.must_include "laundry"
content["laundry"].must_be_kind_of Integer content["laundry"].must_equal laundry.etag.gsub(/"/, "")
content["laundry"].to_s.length.must_equal 13
end end
end end
@@ -187,8 +240,7 @@ describe "Directories" do
it "returns an empty listing" do it "returns an empty listing" do
get "/jimmy/documents/notfound/" get "/jimmy/documents/notfound/"
last_response.status.must_equal 200 last_response.status.must_equal 404
last_response.body.must_equal "{}"
end end
end end
@@ -254,12 +306,20 @@ describe "Directories" do
last_response.status.must_equal 200 last_response.status.must_equal 200
tasks = directory_bucket.get("jimmy:tasks")
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "root-1" content.must_include "root-1"
content.must_include "root-2" content.must_include "root-2"
content.must_include "tasks/" content.must_include "tasks/"
content["tasks/"].must_be_kind_of Integer content["tasks/"].must_equal tasks.etag.gsub(/"/, "")
content["tasks/"].to_s.length.must_equal 13 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
end end
@@ -281,6 +341,13 @@ describe "Directories" do
content = JSON.parse(last_response.body) content = JSON.parse(last_response.body)
content.must_include "5apps" content.must_include "5apps"
end 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 end
context "when directly authorized for the public directory" do context "when directly authorized for the public directory" do
@@ -323,9 +390,11 @@ describe "Directories" do
describe "directory object" do describe "directory object" do
describe "PUT file" do describe "PUT file" do
context "no existing directory object" do context "no existing directory object" do
it "creates a new directory object" do before do
put "/jimmy/tasks/home/trash", "take out the trash" 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") object = data_bucket.get("jimmy:tasks/home:trash")
directory = directory_bucket.get("jimmy:tasks/home") directory = directory_bucket.get("jimmy:tasks/home")
@@ -334,15 +403,11 @@ describe "Directories" do
end end
it "sets the correct index for the directory object" do it "sets the correct index for the directory object" do
put "/jimmy/tasks/home/trash", "take out the trash"
object = directory_bucket.get("jimmy:tasks/home") object = directory_bucket.get("jimmy:tasks/home")
object.indexes["directory_bin"].must_include "tasks" object.indexes["directory_bin"].must_include "tasks"
end end
it "creates directory objects for the parent directories" do it "creates directory objects for the parent directories" do
put "/jimmy/tasks/home/trash", "take out the trash"
object = directory_bucket.get("jimmy:tasks") object = directory_bucket.get("jimmy:tasks")
object.indexes["directory_bin"].must_include "/" object.indexes["directory_bin"].must_include "/"
object.data.wont_be_nil object.data.wont_be_nil
@@ -355,10 +420,7 @@ describe "Directories" do
context "existing directory object" do context "existing directory object" do
before do before do
directory = directory_bucket.new("jimmy:tasks/home") put "/jimmy/tasks/home/trash", "collect some trash"
directory.content_type = "text/plain"
directory.data = (2.seconds.ago.to_f * 1000).to_i
directory.store
end end
it "updates the timestamp of the directory" do it "updates the timestamp of the directory" do
@@ -383,7 +445,8 @@ describe "Directories" do
last_response.headers["Access-Control-Allow-Origin"].must_equal "*" 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-Methods"].must_equal "GET, PUT, DELETE"
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin" 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"
end end
context "sub-directories" do context "sub-directories" do
@@ -394,7 +457,8 @@ describe "Directories" do
last_response.headers["Access-Control-Allow-Origin"].must_equal "*" 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-Methods"].must_equal "GET, PUT, DELETE"
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin" 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"
end end
end end
@@ -406,7 +470,8 @@ describe "Directories" do
last_response.headers["Access-Control-Allow-Origin"].must_equal "*" 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-Methods"].must_equal "GET, PUT, DELETE"
last_response.headers["Access-Control-Allow-Headers"].must_equal "Authorization, Content-Type, Origin" 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"
end end
end end
end end
@@ -450,6 +515,31 @@ describe "Directories" do
directory_bucket.get("jimmy:").wont_be_nil directory_bucket.get("jimmy:").wont_be_nil
end 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 describe "timestamps" do
before do before do
@old_timestamp = (2.seconds.ago.to_f * 1000).to_i @old_timestamp = (2.seconds.ago.to_f * 1000).to_i

View File

@@ -345,7 +345,7 @@ describe "Permissions" do
it "allows GET requests" do it "allows GET requests" do
get "/jimmy/public/tasks/" get "/jimmy/public/tasks/"
last_response.status.must_equal 200 last_response.status.must_equal 404
end end
it "allows PUT requests" do it "allows PUT requests" do
@@ -403,19 +403,19 @@ describe "Permissions" do
end end
it "allows GET requests" do it "allows GET requests" do
get "/jimmy/tasks/" get "/jimmy/public/tasks/"
last_response.status.must_equal 200 last_response.status.must_equal 404
end end
it "disallows PUT requests" do it "disallows PUT requests" do
put "/jimmy/tasks/foo", "some text" put "/jimmy/public/tasks/foo", "some text"
last_response.status.must_equal 403 last_response.status.must_equal 403
end end
it "disallows DELETE requests" do it "disallows DELETE requests" do
delete "/jimmy/tasks/hello" delete "/jimmy/public/tasks/hello"
last_response.status.must_equal 403 last_response.status.must_equal 403
end end

View File

@@ -32,6 +32,11 @@ describe "App with Riak backend" do
last_modified.year.must_equal now.year last_modified.year.must_equal now.year
last_modified.day.must_equal now.day last_modified.day.must_equal now.day
end end
it "has an ETag header set" do
last_response.status.must_equal 200
last_response.headers["ETag"].wont_be_nil
end
end end
describe "GET data with custom content type" do describe "GET data with custom content type" do
@@ -58,19 +63,42 @@ describe "App with Riak backend" do
object.data = "some private text data" object.data = "some private text data"
object.store object.store
@etag = object.etag
auth = auth_bucket.new("jimmy:123") auth = auth_bucket.new("jimmy:123")
auth.data = ["documents", "public"] auth.data = ["documents", "public"]
auth.store auth.store
end end
describe "GET" do describe "GET" do
it "returns the value" do before do
header "Authorization", "Bearer 123" header "Authorization", "Bearer 123"
end
it "returns the value" do
get "/jimmy/documents/foo" get "/jimmy/documents/foo"
last_response.status.must_equal 200 last_response.status.must_equal 200
last_response.body.must_equal "some private text data" last_response.body.must_equal "some private text data"
end 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
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
end end
describe "GET nonexisting key" do describe "GET nonexisting key" do
@@ -102,6 +130,10 @@ describe "App with Riak backend" do
data_bucket.get("jimmy:documents:bar").content_type.must_equal "text/plain; charset=utf-8" data_bucket.get("jimmy:documents:bar").content_type.must_equal "text/plain; charset=utf-8"
end end
it "sets the ETag header" do
last_response.headers["ETag"].wont_be_nil
end
it "indexes the data set" do it "indexes the data set" do
indexes = data_bucket.get("jimmy:documents:bar").indexes indexes = data_bucket.get("jimmy:documents:bar").indexes
indexes["user_id_bin"].must_be_kind_of Set indexes["user_id_bin"].must_be_kind_of Set
@@ -197,15 +229,18 @@ describe "App with Riak backend" do
describe "with existing content" do describe "with existing content" do
before do before do
put "/jimmy/documents/archive/foo", "lorem ipsum" put "/jimmy/documents/archive/foo", "lorem ipsum"
put "/jimmy/documents/archive/foo", "some awesome content"
end end
it "saves the value" do it "saves the value" do
put "/jimmy/documents/archive/foo", "some awesome content"
last_response.status.must_equal 200 last_response.status.must_equal 200
data_bucket.get("jimmy:documents/archive:foo").data.must_equal "some awesome content" data_bucket.get("jimmy:documents/archive:foo").data.must_equal "some awesome content"
end end
it "logs the operations" do it "logs the operations" do
put "/jimmy/documents/archive/foo", "some awesome content"
objects = [] objects = []
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil } opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
@@ -220,22 +255,74 @@ describe "App with Riak backend" do
update_entry.indexes["user_id_bin"].must_include "jimmy" update_entry.indexes["user_id_bin"].must_include "jimmy"
end end
describe "when no serializer is registered for the given content-type" do it "changes the ETag header" do
before do old_etag = last_response.headers["ETag"]
header "Content-Type", "text/html; charset=UTF-8" put "/jimmy/documents/archive/foo", "some awesome content"
put "/jimmy/documents/html", '<html></html>'
put "/jimmy/documents/html", '<html><body></body></html>'
end
it "saves the value" do 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 last_response.status.must_equal 200
data_bucket.get("jimmy:documents:html").raw_data.must_equal "<html><body></body></html>"
get "/jimmy/documents/archive/foo"
last_response.body.must_equal "some awesome content"
end end
it "uses the requested content type" do it "fails the request if the header does not match the current ETag" do
data_bucket.get("jimmy:documents:html").content_type.must_equal "text/html; charset=UTF-8" 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
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 200
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 end
describe "public data" do describe "public data" do
@@ -282,13 +369,23 @@ describe "App with Riak backend" do
last_response.body.must_equal @image last_response.body.must_equal @image
end end
# it "indexes the binary set" do it "responds with an ETag header" do
# indexes = binary_bucket.get("jimmy:documents:jaypeg").indexes last_response.headers["ETag"].wont_be_nil
# indexes["user_id_bin"].must_be_kind_of Set etag = last_response.headers["ETag"]
# indexes["user_id_bin"].must_include "jimmy"
# indexes["directory_bin"].must_include "documents" get "/jimmy/documents/jaypeg"
# end
last_response.headers["ETag"].wont_be_nil
last_response.headers["ETag"].must_equal etag
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 it "logs the operation" do
objects = [] objects = []
@@ -322,14 +419,6 @@ describe "App with Riak backend" do
last_response.status.must_equal 200 last_response.status.must_equal 200
last_response.body.must_equal @image last_response.body.must_equal @image
end end
# it "indexes the binary set" do
# indexes = binary_bucket.get("jimmy:documents:jaypeg").indexes
# indexes["user_id_bin"].must_be_kind_of Set
# indexes["user_id_bin"].must_include "jimmy"
# indexes["directory_bin"].must_include "documents"
# end
end end
end end
@@ -376,24 +465,33 @@ describe "App with Riak backend" do
describe "DELETE" do describe "DELETE" do
before do before do
header "Authorization", "Bearer 123" header "Authorization", "Bearer 123"
delete "/jimmy/documents/foo"
end end
it "removes the key" do describe "basics" do
last_response.status.must_equal 204 before do
lambda { delete "/jimmy/documents/foo"
data_bucket.get("jimmy:documents:foo") end
}.must_raise Riak::HTTPFailedRequest
end
it "logs the operation" do it "removes the key" do
objects = [] last_response.status.must_equal 204
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil } lambda {
data_bucket.get("jimmy:documents:foo")
}.must_raise Riak::HTTPFailedRequest
end
log_entry = objects.select{|o| o.data["count"] == -1}.first it "logs the operation" do
log_entry.data["size"].must_equal(-22) objects = []
log_entry.data["category"].must_equal "documents" opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
log_entry.indexes["user_id_bin"].must_include "jimmy"
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
it "sets the ETag header" do
last_response.headers["ETag"].wont_be_nil
end
end end
context "non-existing object" do context "non-existing object" do
@@ -401,10 +499,39 @@ describe "App with Riak backend" do
delete "/jimmy/documents/foozius" delete "/jimmy/documents/foozius"
end end
it "responds with 404" do
last_response.status.must_equal 404
end
it "doesn't log the operation" do it "doesn't log the operation" do
objects = [] objects = []
opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil } opslog_bucket.keys.each { |k| objects << opslog_bucket.get(k) rescue nil }
objects.select{|o| o.data["count"] == -1}.size.must_equal 1 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 204
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
end end