diff --git a/CacheBar.gemspec b/CacheBar.gemspec deleted file mode 100644 index a8ab89f..0000000 --- a/CacheBar.gemspec +++ /dev/null @@ -1,93 +0,0 @@ -# Generated by jeweler -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' -# -*- encoding: utf-8 -*- - -Gem::Specification.new do |s| - s.name = %q{CacheBar} - s.version = "1.0.2" - - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Brian Landau", "David Eisinger"] - s.date = %q{2011-11-17} - s.description = %q{A simple API caching layer built on top of HTTParty and Redis} - s.email = %q{brian.landau@viget.com} - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.md" - ] - s.files = [ - ".document", - "CacheBar.gemspec", - "Gemfile", - "HISTORY", - "LICENSE.txt", - "README.md", - "Rakefile", - "VERSION", - "lib/cachebar.rb", - "lib/httparty/httpcache.rb", - "test/fixtures/user_timeline.json", - "test/fixtures/vcr_cassettes/bad_response.yml", - "test/fixtures/vcr_cassettes/good_response.yml", - "test/fixtures/vcr_cassettes/status_update_post.yml", - "test/fixtures/vcr_cassettes/unparsable.yml", - "test/helper.rb", - "test/test_cachebar.rb", - "test/twitter_api.rb" - ] - s.homepage = %q{http://github.com/vigetlabs/cachebar} - s.licenses = ["MIT"] - s.require_paths = ["lib"] - s.rubygems_version = %q{1.6.2} - s.summary = %q{A simple API caching layer built on top of HTTParty and Redis} - - if s.respond_to? :specification_version then - s.specification_version = 3 - - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, [">= 0"]) - s.add_runtime_dependency(%q, [">= 0"]) - s.add_runtime_dependency(%q, ["~> 0.7.7"]) - s.add_runtime_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, ["~> 1.0"]) - s.add_development_dependency(%q, ["~> 1.6"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, ["~> 0.8.7"]) - s.add_development_dependency(%q, [">= 0"]) - else - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.7.7"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, ["~> 1.6"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.8.7"]) - s.add_dependency(%q, [">= 0"]) - end - else - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.7.7"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, ["~> 1.6"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 0.8.7"]) - s.add_dependency(%q, [">= 0"]) - end -end - diff --git a/Gemfile b/Gemfile index a6fa76c..343dda3 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,7 @@ gem "redis" gem "redis-namespace" gem 'httparty', '~> 0.8.3' gem 'activesupport' +gem 'dalli' group :development do gem "shoulda" diff --git a/README.md b/README.md index a2d96b4..33cb918 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # CacheBar -CacheBar is a simple API caching layer built on top of Redis and HTTParty. +CacheBar is a simple API caching layer built on top of a caching data store (like Redis) and HTTParty. -When a good request is made to the API through an HTTParty module or class configured to be cached, it caches the response body in Redis. The cache is set to expire in the configured length of time. All following identical requests use the cache in Redis. When the cache expires it will attempt to refill the cache with a new good response. If though the response that comes back is bad (there was timeout, a 404, or some other problem), then CacheBar will fetch a backup response we also store in Redis (in a separate non-expiring hash). When it pulls this backup response out it inserts it into the standard cache and sets it to expire in 5 minutes. This way we won't look for a new good response for another 5 minutes. +When a good request is made to the API through an HTTParty module or class configured to be cached, it caches the response body in a data store (like Redis). The cache is set to expire in the configured length of time. All following identical requests use the cache in the data store. When the cache expires it will attempt to refill the cache with a new good response. If though the response that comes back is bad (there was timeout, a 404, or some other problem), then CacheBar will fetch a backup response we also stored in the data store. When it pulls this backup response out it inserts it into the standard cache and sets it to expire in 5 minutes. This way we won't look for a new good response for another 5 minutes. Using this gem does not mean that all your HTTParty modules and requests will be automatically cached. You will have to configure them on a case by case basis. This means you can have some APIs that are cached and others that aren't. @@ -10,18 +10,13 @@ Using this gem does not mean that all your HTTParty modules and requests will be gem install cachebar -If you're on Ruby 1.8 it is recommended that you also install the SystemTimer gem: - - gem install SystemTimer - ### Inside a Rails/Rack app: Add this to your Gemfile: - gem 'SystemTimer', :platforms => :ruby_18 gem 'cachebar' -Follow the instructions below for configuring CacheBar. +You should also make sure to specify the data store client you will be using. Follow the instructions below for configuring CacheBar. ## Usage @@ -29,9 +24,19 @@ Although you can use CacheBar in any type of application, the examples provided ### 1. Configuring CacheBar -There's a few configuration options to CacheBar, the first is specifying a Redis connection to use. +There's a few configuration options to CacheBar, the first is specifying a data store and defining it's connection. While CacheBar was built with Redis in mind however we also include a memcached adapter if you prefer that. To configure a data store you can either use a symbol for Redis or memcached: -If you have an initializer for defining your Redis connection already like this: + HTTParty::HTTPCache.data_store_class = :redis + +Or you can build your own data store adapter: + + class MyDataStore < CacheBar::DataStore::AbstractDataStore + # ... + end + + HTTParty::HTTPCache.data_store_class = MyDataStore + +After that you need to configure the client and set it on the adapter. If you're using redis and you have an initializer for defining your Redis connection already like this: REDIS_CONFIG = YAML.load_file(Rails.root+'config/redis.yml')[Rails.env].freeze redis = Redis.new(:host => REDIS_CONFIG['host'], :port => REDIS_CONFIG['port'], @@ -40,7 +45,9 @@ If you have an initializer for defining your Redis connection already like this: Then you can just add this: - HTTParty::HTTPCache.redis = $redis + CacheBar::DataStore::Redis.client = $redis + +However every data store adapter will have a class level attribute of `client` that you should use to configure the client. You'll then also want to turn on caching in the appropriate environments. For instance you'll want to add this to `config/environments/production.rb`: @@ -58,7 +65,7 @@ By default when we fallback to using a backup response, we then hold off looking HTTParty::HTTPCache.cache_stale_backup_time = 120 # 2 minutes -If you want to perform an action (say notify an error tracking service) when an exception happens while performing or processing a request you can specify a callback. The only requirement is that it responds to `call` and that `call` accepts 3 parameters. Those 3 in order will be the exception, the redis key name of the API, and the URL endpoint: +If you want to perform an action (say notify an error tracking service) when an exception happens while performing or processing a request you can specify a callback. The only requirement is that it responds to `call` and that `call` accepts 3 parameters. Those 3 in order will be the exception, the key name of the API, and the URL endpoint: HTTParty::HTTPCache.exception_callback = lambda { |exception, api_name, url| Airbrake.notify_or_ignore(exception, { @@ -77,7 +84,7 @@ If you already have HTTParty included then you just need to use the `caches_api_ * This is used internally to decide which requests to try to cache responses for. If you've defined `base_uri` on the class/module that HTTParty is included into then this option is not needed. * `key_name`: - * This is the name used in Redis to create a part of the cache key to easily differentiate it from other API caches. + * This is the name used in the data store to create a part of the cache key to easily differentiate it from other API caches. * `expire_in`: * This determines how long the good API responses are cached for. diff --git a/lib/cachebar.rb b/lib/cachebar.rb index 41bd145..5c08796 100644 --- a/lib/cachebar.rb +++ b/lib/cachebar.rb @@ -4,7 +4,10 @@ require 'active_support' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/module/delegation' if RUBY_VERSION.split('.')[1].to_i < 9 begin require 'system_timer' @@ -15,6 +18,8 @@ require 'httparty/httpcache' module CacheBar + autoload :DataStore, 'cachebar/data_store' + def self.register_api_to_cache(host, options) raise ArgumentError, "You must provide a host that you are caching API responses for." if host.blank? diff --git a/lib/cachebar/data_store.rb b/lib/cachebar/data_store.rb new file mode 100644 index 0000000..72f39a6 --- /dev/null +++ b/lib/cachebar/data_store.rb @@ -0,0 +1,7 @@ +module CacheBar + module DataStore + autoload :AbstractDataStore, 'cachebar/data_store/abstract_data_store' + autoload :Redis, 'cachebar/data_store/redis' + autoload :Memcached, 'cachebar/data_store/memcached' + end +end diff --git a/lib/cachebar/data_store/abstract_data_store.rb b/lib/cachebar/data_store/abstract_data_store.rb new file mode 100644 index 0000000..1bfa754 --- /dev/null +++ b/lib/cachebar/data_store/abstract_data_store.rb @@ -0,0 +1,48 @@ +module CacheBar + module DataStore + class AbstractDataStore + class_attribute :client + + attr_reader :api_name, :uri_hash + + def initialize(api_name, uri_hash) + @api_name = api_name + @uri_hash = uri_hash + end + + def response_body_exists? + raise NotImplementedError, 'Implement response_body_exists? in sub-class' + end + + def get_response_body + raise NotImplementedError, 'Implement get_response_body in sub-class' + end + + def store_response_body(response_body, interval) + raise NotImplementedError, 'Implement store_response_body in sub-class' + end + + def backup_exists? + raise NotImplementedError, 'Implement backup_exists? in sub-class' + end + + def get_backup + raise NotImplementedError, 'Implement get_backup in sub-class' + end + + def store_backup(response_body) + raise NotImplementedError, 'Implement store_backup in sub-class' + end + + private + + def cache_key_name + "api-cache:#{api_name}:#{uri_hash}" + end + + def backup_key_name + "api-cache:#{api_name}" + end + end + end +end \ No newline at end of file diff --git a/lib/cachebar/data_store/memcached.rb b/lib/cachebar/data_store/memcached.rb new file mode 100644 index 0000000..6c706ae --- /dev/null +++ b/lib/cachebar/data_store/memcached.rb @@ -0,0 +1,34 @@ +module CacheBar + module DataStore + class Memcached < AbstractDataStore + + def backup_key_name + "api-cache:backup:#{api_name}:#{uri_hash}" + end + + def response_body_exists? + !client.get(cache_key_name).nil? + end + + def get_response_body + client.get(cache_key_name) + end + + def store_response_body(response_body, interval) + client.set(cache_key_name, response_body, interval) + end + + def backup_exists? + !client.get(backup_key_name).nil? + end + + def get_backup + client.get(backup_key_name) + end + + def store_backup(response_body) + client.set(backup_key_name, response_body) + end + end + end +end \ No newline at end of file diff --git a/lib/cachebar/data_store/redis.rb b/lib/cachebar/data_store/redis.rb new file mode 100644 index 0000000..50878b5 --- /dev/null +++ b/lib/cachebar/data_store/redis.rb @@ -0,0 +1,30 @@ +module CacheBar + module DataStore + class Redis < AbstractDataStore + def response_body_exists? + client.exists(cache_key_name) + end + + def get_response_body + client.get(cache_key_name) + end + + def store_response_body(response_body, interval) + client.set(cache_key_name, response_body) + client.expire(cache_key_name, interval) + end + + def backup_exists? + client.exists(backup_key_name) && client.hexists(backup_key_name, uri_hash) + end + + def get_backup + client.hget(backup_key_name, uri_hash) + end + + def store_backup(response_body) + client.hset(backup_key_name, uri_hash, response_body) + end + end + end +end \ No newline at end of file diff --git a/lib/httparty/httpcache.rb b/lib/httparty/httpcache.rb index 2a8890b..5165bd9 100644 --- a/lib/httparty/httpcache.rb +++ b/lib/httparty/httpcache.rb @@ -10,22 +10,43 @@ class NoResponseError < StandardError; end :cache_stale_backup_time, :exception_callback + mattr_reader :data_store_class + self.perform_caching = false self.apis = {} self.timeout_length = 5 # 5 seconds self.cache_stale_backup_time = 300 # 5 minutes + delegate :response_body_exists?, + :get_response_body, + :backup_exists?, + :get_backup, + :store_backup, + :to => :data_store + def self.included(base) base.class_eval do alias_method_chain :perform, :caching end end + def self.data_store_class=(data_store_name_or_class) + case data_store_name_or_class + when Symbol + require "cachebar/data_store/#{data_store_name_or_class}" + @@data_store_class = CacheBar::DataStore.const_get(data_store_name_or_class.to_s.camelcase.to_sym) + when Class + @@data_store_class = data_store_name_or_class + else + raise ArgumentError, "data store must be a symbol or a class" + end + end + def perform_with_caching if cacheable? - if response_in_cache? + if response_body_exists? log_message("Retrieving response from cache") - response_from(response_body_from_cache) + response_from(get_response_body) else validate begin @@ -43,7 +64,7 @@ def perform_with_caching end rescue *exceptions => e if exception_callback && exception_callback.respond_to?(:call) - exception_callback.call(e, redis_key_name, normalized_uri) + exception_callback.call(e, api_key_name, normalized_uri) end retrieve_and_store_backup end @@ -68,7 +89,7 @@ def response_from(response_body) def retrieve_and_store_backup(httparty_response = nil) if backup_exists? log_message('using backup') - response_body = backup_response + response_body = get_backup store_in_cache(response_body, cache_stale_backup_time) response_from(response_body) elsif httparty_response @@ -92,44 +113,19 @@ def sort_query_params(query) query.split('&').sort.join('&') unless query.blank? end - def cache_key_name - @cache_key_name ||= "api-cache:#{redis_key_name}:#{uri_hash}" - end - def uri_hash @uri_hash ||= Digest::MD5.hexdigest(normalized_uri) end - def response_in_cache? - redis.exists(cache_key_name) - end - - def backup_key - "api-cache:#{redis_key_name}" - end - - def backup_response - redis.hget(backup_key, uri_hash) - end - - def backup_exists? - redis.exists(backup_key) && redis.hexists(backup_key, uri_hash) - end - - def response_body_from_cache - redis.get(cache_key_name) - end - def store_in_cache(response_body, expires = nil) - redis.set(cache_key_name, response_body) - redis.expire(cache_key_name, (expires || HTTPCache.apis[uri.host][:expire_in])) + data_store.store_response_body(response_body, (expires || HTTPCache.apis[uri.host][:expire_in])) end - def store_backup(response_body) - redis.hset(backup_key, uri_hash, response_body) + def data_store + @data_store ||= data_store_class.new(api_key_name, uri_hash) end - def redis_key_name + def api_key_name HTTPCache.apis[uri.host][:key_name] end diff --git a/test/helper.rb b/test/helper.rb index 9c309ce..d2af6e6 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -18,6 +18,7 @@ require 'cachebar' require 'twitter_api' require 'redis-namespace' +require 'dalli' VCR.config do |c| c.cassette_library_dir = 'test/fixtures/vcr_cassettes' diff --git a/test/test_cachebar.rb b/test/test_cachebar.rb index 1d9a1f7..3ded0d4 100644 --- a/test/test_cachebar.rb +++ b/test/test_cachebar.rb @@ -3,10 +3,30 @@ class TestCacheBar < Test::Unit::TestCase context 'CacheBar' do + context 'defining the data store' do + should 'be able to use a symbol of an existing pre-packaged data store' do + HTTParty::HTTPCache.data_store_class = :redis + assert_equal CacheBar::DataStore::Redis, HTTParty::HTTPCache.data_store_class + end + + should 'be able to use a class of an existing pre-packaged data store' do + class SampleDataStore; end + HTTParty::HTTPCache.data_store_class = SampleDataStore + assert_equal SampleDataStore, HTTParty::HTTPCache.data_store_class + end + + should 'raise exception if something else is passed in' do + assert_raise ArgumentError do + HTTParty::HTTPCache.data_store_class = 'data_store' + end + end + end + context 'mocking redis' do setup do + HTTParty::HTTPCache.data_store_class = :redis @redis = mock - HTTParty::HTTPCache.redis = @redis + CacheBar::DataStore::Redis.client = @redis end context 'with caching on' do @@ -209,12 +229,13 @@ class TestCacheBar < Test::Unit::TestCase context 'connecting to redis' do setup do + HTTParty::HTTPCache.data_store_class = :redis VCR.insert_cassette('good_response') redis = Redis.new(:host => 'localhost', :port => 6379, :thread_safe => true, :db => '3') @redis = Redis::Namespace.new('httpcache', :redis => redis) - HTTParty::HTTPCache.redis = @redis + CacheBar::DataStore::Redis.client = @redis @redis.keys("api-cache*").each do |key| @redis.del(key) @@ -241,5 +262,34 @@ class TestCacheBar < Test::Unit::TestCase end end + context 'connecting to memcached' do + setup do + HTTParty::HTTPCache.data_store_class = :memcached + VCR.insert_cassette('good_response') + + @memcached = Dalli::Client.new('localhost:11211') + CacheBar::DataStore::Memcached.client = @memcached + + @memcached.flush + end + + should "store its response in the cache" do + assert_nil @memcached.get('api-cache:twitter:007a3a7aa28b11ef362040283e114f55') + TwitterAPI.user_timeline('viget') + assert_not_nil @memcached.get('api-cache:twitter:007a3a7aa28b11ef362040283e114f55') + end + + should "store a backup of its response" do + assert_nil @memcached.get('api-cache:backup:twitter:007a3a7aa28b11ef362040283e114f55') + TwitterAPI.user_timeline('viget') + assert_not_nil @memcached.get('api-cache:backup:twitter:007a3a7aa28b11ef362040283e114f55') + end + + teardown do + VCR.eject_cassette + @memcached.flush + end + end + end end diff --git a/test/test_memcached_data_store.rb b/test/test_memcached_data_store.rb new file mode 100644 index 0000000..eba4a16 --- /dev/null +++ b/test/test_memcached_data_store.rb @@ -0,0 +1,77 @@ +require 'helper' + +class TestMemcachedDataStore < Test::Unit::TestCase + context 'CacheBar::DataStore::Memcached' do + should 'initialize with api_name and uri_hash' do + memcached = CacheBar::DataStore::Memcached.new('api_name', 'uri_hash') + assert_equal 'api_name', memcached.api_name + assert_equal 'uri_hash', memcached.uri_hash + end + + context 'an instance of' do + setup do + @client = mock + CacheBar::DataStore::Memcached.client = @client + @memcached = CacheBar::DataStore::Memcached.new('twitter', 'URIHASH') + end + + context '#response_body_exists?' do + should 'return true if the resource is in memcached' do + @client.expects(:get).with("api-cache:twitter:URIHASH").returns(true) + assert_equal true, @memcached.response_body_exists? + end + + should 'return false if the resource is not in memcached' do + @client.expects(:get).with("api-cache:twitter:URIHASH").returns(nil) + assert_equal false, @memcached.response_body_exists? + end + end + + context '#get_response_body' do + should "retrieve the response from the cache" do + @client.expects(:get).with("api-cache:twitter:URIHASH").returns("RESPONSE_BODY") + assert_equal "RESPONSE_BODY", @memcached.get_response_body + end + end + + context '#store_response_body' do + should "store data in memcached and set an expiration time" do + @client.expects(:set).with("api-cache:twitter:URIHASH", "RESPONSE_BODY", 10).returns(true) + @memcached.store_response_body("RESPONSE_BODY", 10) + end + end + + + context '#backup_exists?' do + should 'return true if the resource is in the memcached backup' do + @client.expects(:get).with("api-cache:backup:twitter:URIHASH").returns(true) + assert @memcached.backup_exists? + end + + should 'return false if the resource is not in the memcached backup' do + @client.expects(:get).with("api-cache:backup:twitter:URIHASH").returns(nil) + assert_equal false, @memcached.backup_exists? + end + + should 'return false if the backup does not exist' do + @client.expects(:get).with("api-cache:backup:twitter:URIHASH").returns(nil) + assert_equal false, @memcached.backup_exists? + end + end + + context '#get_backup' do + should 'retrieve the response body from the backup' do + @client.expects(:get).with("api-cache:backup:twitter:URIHASH").returns("RESPONSE_BODY") + assert_equal "RESPONSE_BODY", @memcached.get_backup + end + end + + context '#store_backup' do + should 'store the response body in the backup' do + @client.expects(:set).with("api-cache:backup:twitter:URIHASH", "RESPONSE_BODY").returns(true) + @memcached.store_backup("RESPONSE_BODY") + end + end + end + end +end \ No newline at end of file diff --git a/test/test_redis_data_store.rb b/test/test_redis_data_store.rb new file mode 100644 index 0000000..e665a7c --- /dev/null +++ b/test/test_redis_data_store.rb @@ -0,0 +1,85 @@ +require 'helper' + +class TestRedisDataStore < Test::Unit::TestCase + context 'CacheBar::DataStore::Redis' do + should 'initialize with api_name and uri_hash' do + redis = CacheBar::DataStore::Redis.new('api_name', 'uri_hash') + assert_equal 'api_name', redis.api_name + assert_equal 'uri_hash', redis.uri_hash + end + + context 'an instance of' do + setup do + @client = mock + CacheBar::DataStore::Redis.client = @client + @redis = CacheBar::DataStore::Redis.new('twitter', 'URIHASH') + end + + context '#response_body_exists?' do + should 'return true if the resource is in redis' do + @client.expects(:exists).with("api-cache:twitter:URIHASH").returns(true) + assert @redis.response_body_exists? + end + + should 'return false if the resource is in redis' do + @client.expects(:exists).with("api-cache:twitter:URIHASH").returns(false) + assert !@redis.response_body_exists? + end + end + + context '#get_response_body' do + should "retrieve the response from the cache" do + @client.expects(:get).with("api-cache:twitter:URIHASH").returns("RESPONSE_BODY") + assert_equal "RESPONSE_BODY", @redis.get_response_body + end + end + + context '#store_response_body' do + should "store data in redis" do + @client.expects(:set).with("api-cache:twitter:URIHASH", "RESPONSE_BODY").returns(true) + @client.stubs(:expire).returns(true) + @redis.store_response_body("RESPONSE_BODY", 10) + end + + should "set expires on cache key" do + @client.stubs(:set).returns(true) + @client.expects(:expire).with("api-cache:twitter:URIHASH", 10).returns(true) + @redis.store_response_body("RESPONSE_BODY", 10) + end + end + + context '#backup_exists?' do + should 'return true if the resource is in the redis backup hash' do + @client.expects(:exists).with("api-cache:twitter").returns(true) + @client.expects(:hexists).with("api-cache:twitter", "URIHASH").returns(true) + assert @redis.backup_exists? + end + + should 'return false if the resource is not in the redis backup hash' do + @client.expects(:exists).with("api-cache:twitter").returns(true) + @client.expects(:hexists).with("api-cache:twitter", "URIHASH").returns(false) + assert !@redis.backup_exists? + end + + should 'return false if the backup hash does not exist' do + @client.expects(:exists).with("api-cache:twitter").returns(false) + assert !@redis.backup_exists? + end + end + + context '#get_backup' do + should 'retrieve the response body from the backup hash' do + @client.expects(:hget).with("api-cache:twitter", "URIHASH").returns("RESPONSE_BODY") + assert_equal "RESPONSE_BODY", @redis.get_backup + end + end + + context '#store_backup' do + should 'store the response body in the backup hash' do + @client.expects(:hset).with("api-cache:twitter", "URIHASH", "RESPONSE_BODY").returns(true) + @redis.store_backup("RESPONSE_BODY") + end + end + end + end +end \ No newline at end of file