faraday-http-cache 0.0.1.dev

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Faraday::HttpCache::CacheControl do
4
+ it 'takes a String with multiple name=value pairs' do
5
+ instance = Faraday::HttpCache::CacheControl.new('max-age=600, max-stale=300, min-fresh=570')
6
+ instance.max_age.should == 600
7
+ end
8
+
9
+ it 'takes a String with a single flag value' do
10
+ instance = Faraday::HttpCache::CacheControl.new('no-cache')
11
+ instance.should be_no_cache
12
+ end
13
+
14
+ it 'takes a String with a bunch of all kinds of stuff' do
15
+ instance =
16
+ Faraday::HttpCache::CacheControl.new('max-age=600,must-revalidate,min-fresh=3000,foo=bar,baz')
17
+ instance.max_age.should == 600
18
+ instance.should be_must_revalidate
19
+ end
20
+
21
+ it 'strips leading and trailing spaces' do
22
+ instance = Faraday::HttpCache::CacheControl.new(' public, max-age = 600 ')
23
+ instance.should be_public
24
+ instance.max_age.should == 600
25
+ end
26
+
27
+ it 'ignores blank segments' do
28
+ instance = Faraday::HttpCache::CacheControl.new('max-age=600,,s-maxage=300')
29
+ instance.max_age.should == 600
30
+ instance.shared_max_age.should == 300
31
+ end
32
+
33
+ it 'sorts alphabetically with boolean directives before value directives' do
34
+ instance = Faraday::HttpCache::CacheControl.new('foo=bar, z, x, y, bling=baz, zoom=zib, b, a')
35
+ instance.to_s.should == 'a, b, x, y, z, bling=baz, foo=bar, zoom=zib'
36
+ end
37
+
38
+ it 'responds to #max_age with an integer when max-age directive present' do
39
+ instance = Faraday::HttpCache::CacheControl.new('public, max-age=600')
40
+ instance.max_age.should == 600
41
+ end
42
+
43
+ it 'responds to #max_age with nil when no max-age directive present' do
44
+ instance = Faraday::HttpCache::CacheControl.new('public')
45
+ instance.max_age.should be_nil
46
+ end
47
+
48
+ it 'responds to #shared_max_age with an integer when s-maxage directive present' do
49
+ instance = Faraday::HttpCache::CacheControl.new('public, s-maxage=600')
50
+ instance.shared_max_age.should == 600
51
+ end
52
+
53
+ it 'responds to #shared_max_age with nil when no s-maxage directive present' do
54
+ instance = Faraday::HttpCache::CacheControl.new('public')
55
+ instance.shared_max_age.should be_nil
56
+ end
57
+
58
+ it 'responds to #public? truthfully when public directive present' do
59
+ instance = Faraday::HttpCache::CacheControl.new('public')
60
+ instance.should be_public
61
+ end
62
+
63
+ it 'responds to #public? non-truthfully when no public directive present' do
64
+ instance = Faraday::HttpCache::CacheControl.new('private')
65
+ instance.should_not be_public
66
+ end
67
+
68
+ it 'responds to #private? truthfully when private directive present' do
69
+ instance = Faraday::HttpCache::CacheControl.new('private')
70
+ instance.should be_private
71
+ end
72
+
73
+ it 'responds to #private? non-truthfully when no private directive present' do
74
+ instance = Faraday::HttpCache::CacheControl.new('public')
75
+ instance.should_not be_private
76
+ end
77
+
78
+ it 'responds to #no_cache? truthfully when no-cache directive present' do
79
+ instance = Faraday::HttpCache::CacheControl.new('no-cache')
80
+ instance.should be_no_cache
81
+ end
82
+
83
+ it 'responds to #no_cache? non-truthfully when no no-cache directive present' do
84
+ instance = Faraday::HttpCache::CacheControl.new('max-age=600')
85
+ instance.should_not be_no_cache
86
+ end
87
+
88
+ it 'responds to #must_revalidate? truthfully when must-revalidate directive present' do
89
+ instance = Faraday::HttpCache::CacheControl.new('must-revalidate')
90
+ instance.should be_must_revalidate
91
+ end
92
+
93
+ it 'responds to #must_revalidate? non-truthfully when no must-revalidate directive present' do
94
+ instance = Faraday::HttpCache::CacheControl.new('max-age=600')
95
+ instance.should_not be_no_cache
96
+ end
97
+
98
+ it 'responds to #proxy_revalidate? truthfully when proxy-revalidate directive present' do
99
+ instance = Faraday::HttpCache::CacheControl.new('proxy-revalidate')
100
+ instance.should be_proxy_revalidate
101
+ end
102
+
103
+ it 'responds to #proxy_revalidate? non-truthfully when no proxy-revalidate directive present' do
104
+ instance = Faraday::HttpCache::CacheControl.new('max-age=600')
105
+ instance.should_not be_no_cache
106
+ end
107
+ end
@@ -0,0 +1,143 @@
1
+ require 'spec_helper'
2
+
3
+ describe Faraday::HttpCache do
4
+ let(:logger) { double('a Logger object', :debug => nil) }
5
+
6
+ let(:client) do
7
+ Faraday.new(:url => ENV['FARADAY_SERVER']) do |stack|
8
+ stack.use Faraday::HttpCache, :logger => logger
9
+ adapter = ENV['FARADAY_ADAPTER']
10
+ stack.headers['X-Faraday-Adapter'] = adapter
11
+ stack.adapter adapter.to_sym
12
+ end
13
+ end
14
+
15
+ before do
16
+ client.get('clear')
17
+ end
18
+
19
+ it "doesn't cache POST requests" do
20
+ client.post('post').body
21
+ client.post('post').body.should == "2"
22
+ end
23
+
24
+ it "logs that a POST request is unacceptable" do
25
+ logger.should_receive(:debug).with('HTTP Cache: [POST /post] unacceptable')
26
+ client.post('post').body
27
+ end
28
+
29
+ it "doesn't cache responses with invalid status code" do
30
+ client.get('broken')
31
+ client.get('broken').body.should == "2"
32
+ end
33
+
34
+ it "logs that a response with a bad status code is invalid" do
35
+ logger.should_receive(:debug).with('HTTP Cache: [GET /broken] miss, invalid')
36
+ client.get('broken')
37
+ end
38
+
39
+ it "doesn't cache requests with a private cache control" do
40
+ client.get('private')
41
+ client.get('private').body.should == "2"
42
+ end
43
+
44
+ it "logs that a private response is invalid" do
45
+ logger.should_receive(:debug).with('HTTP Cache: [GET /private] miss, invalid')
46
+ client.get('private')
47
+ end
48
+
49
+ it "doesn't cache requests with a explicit no-store directive" do
50
+ client.get('dontstore')
51
+ client.get('dontstore').body.should == "2"
52
+ end
53
+
54
+ it "logs that a response with a no-store directive is invalid" do
55
+ logger.should_receive(:debug).with('HTTP Cache: [GET /dontstore] miss, invalid')
56
+ client.get('dontstore')
57
+ end
58
+
59
+ it "caches multiple responses when the headers differ" do
60
+ client.get('get', nil, 'HTTP_ACCEPT' => 'text/html')
61
+ client.get('get', nil, 'HTTP_ACCEPT' => 'text/html').body.should == "1"
62
+
63
+ client.get('get', nil, 'HTTP_ACCEPT' => 'application/json').body.should == "2"
64
+ end
65
+
66
+ it "caches requests with the 'Expires' header" do
67
+ client.get('expires')
68
+ client.get('expires').body.should == "1"
69
+ end
70
+
71
+ it "logs that a request with the 'Expires' is fresh and stored" do
72
+ logger.should_receive(:debug).with('HTTP Cache: [GET /expires] miss, store')
73
+ client.get('expires')
74
+ end
75
+
76
+ it "caches GET responses" do
77
+ client.get('get')
78
+ client.get('get').body.should == "1"
79
+ end
80
+
81
+ it "logs that a GET response is stored" do
82
+ logger.should_receive(:debug).with('HTTP Cache: [GET /get] miss, store')
83
+ client.get('get')
84
+ end
85
+
86
+ it "logs that a stored GET response is fresh" do
87
+ client.get('get')
88
+ logger.should_receive(:debug).with('HTTP Cache: [GET /get] fresh')
89
+ client.get('get')
90
+ end
91
+
92
+ it "sends the 'Last-Modified' header on response validation" do
93
+ client.get('timestamped')
94
+ client.get('timestamped').body.should == "1"
95
+ end
96
+
97
+ it "logs that the request with 'Last-Modified' was revalidated" do
98
+ client.get('timestamped')
99
+ logger.should_receive(:debug).with('HTTP Cache: [GET /timestamped] valid, store')
100
+ client.get('timestamped').body.should == "1"
101
+ end
102
+
103
+ it "sends the 'If-None-Match' header on response validation" do
104
+ client.get('etag')
105
+ client.get('etag').body.should == "1"
106
+ end
107
+
108
+ it "logs that the request with 'ETag' was revalidated" do
109
+ client.get('etag')
110
+ logger.should_receive(:debug).with('HTTP Cache: [GET /etag] valid, store')
111
+ client.get('etag').body.should == "1"
112
+ end
113
+
114
+ it "maintains the 'Date' header for cached responses" do
115
+ date = client.get('get').headers['Date']
116
+ client.get('get').headers['Date'].should == date
117
+ end
118
+
119
+ it "preserves an old 'Date' header if present" do
120
+ date = client.get('yesterday').headers['Date']
121
+ date.should =~ /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/
122
+ end
123
+
124
+ describe 'Configuration options' do
125
+ let(:app) { double("it's an app!") }
126
+
127
+ it 'uses the options to create a Cache Store' do
128
+ ActiveSupport::Cache.should_receive(:lookup_store).with(:file_store, ['tmp'])
129
+ Faraday::HttpCache.new(app, :file_store, 'tmp')
130
+ end
131
+
132
+ it 'accepts a Hash option' do
133
+ ActiveSupport::Cache.should_receive(:lookup_store).with(:memory_store, { :size => 1024 })
134
+ Faraday::HttpCache.new(app, :memory_store, :size => 1024)
135
+ end
136
+
137
+ it "consumes the 'logger' key" do
138
+ logger = double('a logger object')
139
+ ActiveSupport::Cache.should_receive(:lookup_store).with(:memory_store, {})
140
+ Faraday::HttpCache.new(app, :memory_store, :logger => logger)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
2
+
3
+ describe Faraday::HttpCache::Response do
4
+ describe 'cacheable?' do
5
+ it "the response isn't' cacheable if the response is marked as private" do
6
+ headers = { 'Cache-Control' => 'private' }
7
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
8
+
9
+ response.should_not be_cacheable
10
+ end
11
+
12
+ it "the response isn't' cacheable if it shouldn't be stored" do
13
+ headers = { 'Cache-Control' => 'no-store' }
14
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
15
+
16
+ response.should_not be_cacheable
17
+ end
18
+
19
+ it "the response isn't cacheable when the status code isn't acceptable" do
20
+ headers = { 'Cache-Control' => 'max-age=400' }
21
+ response = Faraday::HttpCache::Response.new(:status => 503, :response_headers => headers)
22
+ response.should_not be_cacheable
23
+ end
24
+
25
+ [200, 203, 300, 301, 302, 404, 410].each do |status|
26
+ it "the response is cacheable if the status code is #{status} and the response is fresh" do
27
+ headers = { 'Cache-Control' => 'max-age=400' }
28
+ response = Faraday::HttpCache::Response.new(:status => status, :response_headers => headers)
29
+
30
+ response.should be_cacheable
31
+ end
32
+ end
33
+ end
34
+
35
+ describe 'freshness' do
36
+ it "is fresh if the response still has some time to live" do
37
+ date = 200.seconds.ago.httpdate
38
+ headers = { 'Cache-Control' => 'max-age=400', 'Date' => date }
39
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
40
+
41
+ response.should be_fresh
42
+ end
43
+
44
+ it "isn't fresh when the ttl has expired" do
45
+ date = 500.seconds.ago.httpdate
46
+ headers = { 'Cache-Control' => 'max-age=400', 'Date' => date }
47
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
48
+
49
+ response.should_not be_fresh
50
+ end
51
+ end
52
+
53
+ it "sets the 'Date' header if isn't present" do
54
+ headers = { 'Date' => nil }
55
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
56
+
57
+ response.date.should be_present
58
+ end
59
+
60
+ it "the response is not modified if the status code is 304" do
61
+ response = Faraday::HttpCache::Response.new(:status => 304)
62
+ response.should be_not_modified
63
+ end
64
+
65
+ it "returns the 'Last-Modified' header on the #last_modified method" do
66
+ headers = { 'Last-Modified' => '123'}
67
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
68
+ response.last_modified.should == '123'
69
+ end
70
+
71
+ it "returns the 'ETag' header on the #etag method" do
72
+ headers = { 'ETag' => 'tag'}
73
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
74
+ response.etag.should == 'tag'
75
+ end
76
+
77
+ describe 'max age calculation' do
78
+
79
+ it 'uses the shared max age directive when present' do
80
+ headers = { 'Cache-Control' => 's-maxage=200, max-age=0'}
81
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
82
+ response.max_age.should == 200
83
+ end
84
+
85
+ it 'uses the max age directive when present' do
86
+ headers = { 'Cache-Control' => 'max-age=200'}
87
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
88
+ response.max_age.should == 200
89
+ end
90
+
91
+ it "fallsback to the expiration date leftovers" do
92
+ headers = { 'Expires' => (Time.now + 100).httpdate, 'Date' => Time.now.httpdate }
93
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
94
+ response.max_age.should == 100
95
+ end
96
+
97
+ it "returns nil when there's no information to calculate the max age" do
98
+ response = Faraday::HttpCache::Response.new
99
+ response.max_age.should be_nil
100
+ end
101
+ end
102
+
103
+ describe 'age calculation' do
104
+ it "uses the 'Age' header if it's present" do
105
+ response = Faraday::HttpCache::Response.new(:response_headers => { 'Age' => '3' })
106
+ response.age.should == 3
107
+ end
108
+
109
+ it "calculates the time from the 'Date' header" do
110
+ date = 3.seconds.ago.httpdate
111
+ response = Faraday::HttpCache::Response.new(:response_headers => { 'Date' => date })
112
+ response.age.should == 3
113
+ end
114
+
115
+ it "returns 0 if there's no 'Age' or 'Date' header present" do
116
+ response = Faraday::HttpCache::Response.new(:response_headers => {})
117
+ response.age.should == 0
118
+ end
119
+ end
120
+
121
+ describe 'time to live calculation' do
122
+ it "returns the time to live based on the max age limit" do
123
+ date = 200.seconds.ago.httpdate
124
+ headers = { 'Cache-Control' => 'max-age=400', 'Date' => date }
125
+ response = Faraday::HttpCache::Response.new(:response_headers => headers)
126
+ response.ttl.should == 200
127
+ end
128
+ end
129
+
130
+ describe "response unboxing" do
131
+ subject { described_class.new(:status => 200, :response_headers => {}, :body => 'Hi!') }
132
+ let(:response) { subject.to_response }
133
+
134
+ it 'returns a Faraday::Response' do
135
+ response.should be_a Faraday::Response
136
+ end
137
+
138
+ it 'merges the status code' do
139
+ response.status.should == 200
140
+ end
141
+
142
+ it 'merges the headers' do
143
+ response.headers.should be_a Faraday::Utils::Headers
144
+ end
145
+
146
+ it 'merges the body' do
147
+ response.body.should == "Hi!"
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,27 @@
1
+ require 'uri'
2
+ require 'socket'
3
+
4
+ require 'faraday-http-cache'
5
+ require 'active_support/core_ext/date/calculations'
6
+ require 'active_support/core_ext/numeric/time'
7
+ require 'yajl'
8
+
9
+ require 'support/test_app'
10
+ require 'support/test_server'
11
+
12
+ server = TestServer.new
13
+
14
+ ENV['FARADAY_SERVER'] = server.endpoint
15
+ ENV['FARADAY_ADAPTER'] ||= 'net_http'
16
+
17
+ server.start
18
+
19
+ RSpec.configure do |config|
20
+ config.treat_symbols_as_metadata_keys_with_true_values = true
21
+ config.run_all_when_everything_filtered = true
22
+ config.filter_run :focus
23
+
24
+ config.after(:suite) do
25
+ server.stop
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Faraday::HttpCache::Storage do
4
+ let(:request) do
5
+ { :method => :get, :request_headers => {}, :url => URI.parse("http://foo.bar/") }
6
+ end
7
+
8
+ let(:response) { double(:payload => {}) }
9
+
10
+ let(:cache) { ActiveSupport::Cache.lookup_store }
11
+
12
+ subject { Faraday::HttpCache::Storage.new(cache) }
13
+
14
+ describe 'Cache configuration' do
15
+ it 'lookups a ActiveSupport cache store' do
16
+ ActiveSupport::Cache.should_receive(:lookup_store).with(:file_store, '/tmp')
17
+ Faraday::HttpCache::Storage.new(:file_store, '/tmp')
18
+ end
19
+ end
20
+
21
+ describe 'storing responses' do
22
+ it 'writes the response json to the underlying cache using a digest as the key' do
23
+ json = MultiJson.dump(response.payload)
24
+
25
+ cache.should_receive(:write).with('503ac9f7180ca1cdec49e8eb73a9cc0b47c27325', json)
26
+ subject.write(request, response)
27
+ end
28
+ end
29
+
30
+ describe 'reading responses' do
31
+ it "returns nil if the response isn't cached" do
32
+ subject.read(request).should be_nil
33
+ end
34
+
35
+ it 'decodes a stored response' do
36
+ subject.write(request, response)
37
+
38
+ subject.read(request).should be_a(Faraday::HttpCache::Response)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,72 @@
1
+ require 'sinatra/base'
2
+
3
+ class TestApp < Sinatra::Base
4
+
5
+ set :environment, :test
6
+ set :server, 'webrick'
7
+ disable :protection
8
+
9
+ set :counter, 0
10
+ set :requests, 0
11
+ set :yesterday, 1.day.ago.httpdate
12
+
13
+ get '/ping' do
14
+ "PONG"
15
+ end
16
+
17
+ get '/clear' do
18
+ settings.counter = 0
19
+ settings.requests = 0
20
+ status 204
21
+ end
22
+
23
+ post '/post' do
24
+ [200, { 'Cache-Control' => 'max-age=400' }, "#{settings.requests += 1}"]
25
+ end
26
+
27
+ get '/broken' do
28
+ [500, { 'Cache-Control' => 'max-age=400' }, "#{settings.requests += 1}"]
29
+ end
30
+
31
+ get '/get' do
32
+ [200, { 'Cache-Control' => 'max-age=200' }, "#{settings.requests += 1}"]
33
+ end
34
+
35
+ get '/private' do
36
+ [200, { 'Cache-Control' => 'private' }, "#{settings.requests += 1}"]
37
+ end
38
+
39
+ get '/dontstore' do
40
+ [200, { 'Cache-Control' => 'no-store' }, "#{settings.requests += 1}"]
41
+ end
42
+
43
+ get '/expires' do
44
+ [200, { 'Expires' => (Time.now + 10).httpdate }, "#{settings.requests += 1}"]
45
+ end
46
+
47
+ get '/yesterday' do
48
+ [200, { 'Date' => settings.yesterday, 'Expires' => settings.yesterday }, "#{settings.requests += 1}"]
49
+ end
50
+
51
+ get '/timestamped' do
52
+ settings.counter += 1
53
+ header = settings.counter > 2 ? '1' : '2'
54
+
55
+ if env['HTTP_IF_MODIFIED_SINCE'] == header
56
+ [304, {}, ""]
57
+ else
58
+ [200, { 'Last-Modified' => header }, "#{settings.requests += 1}"]
59
+ end
60
+ end
61
+
62
+ get '/etag' do
63
+ settings.counter += 1
64
+ tag = settings.counter > 2 ? '1' : '2'
65
+
66
+ if env['HTTP_IF_NONE_MATCH'] == tag
67
+ [304, { 'ETag' => tag }, ""]
68
+ else
69
+ [200, { 'ETag' => tag }, "#{settings.requests += 1}"]
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,64 @@
1
+ require 'net/http'
2
+
3
+ class TestServer
4
+ attr_reader :endpoint
5
+
6
+ def initialize
7
+ @host = 'localhost'
8
+ @port = find_port
9
+ @endpoint = "http://#{@host}:#{@port}"
10
+ end
11
+
12
+ def start
13
+ @pid = run!
14
+ wait
15
+ end
16
+
17
+ def stop
18
+ `kill -9 #{@pid}`
19
+ end
20
+
21
+ private
22
+
23
+ def run!
24
+ fork do
25
+ require 'webrick'
26
+ log = File.open('log/test.log', 'w+')
27
+ log.sync = true
28
+ webrick_opts = {
29
+ :Port => @port,
30
+ :Logger => WEBrick::Log::new(log),
31
+ :AccessLog => [[log, "[%{X-Faraday-Adapter}i] %m %U -> %s %b"]]
32
+ }
33
+ Rack::Handler::WEBrick.run(TestApp, webrick_opts)
34
+ end
35
+ end
36
+
37
+ def wait
38
+ conn = Net::HTTP.new @host, @port
39
+ conn.open_timeout = conn.read_timeout = 0.1
40
+
41
+ responsive = lambda { |path|
42
+ begin
43
+ res = conn.start { conn.get(path) }
44
+ res.is_a?(Net::HTTPSuccess)
45
+ rescue Errno::ECONNREFUSED, Errno::EBADF, Timeout::Error, Net::HTTPBadResponse
46
+ false
47
+ end
48
+ }
49
+
50
+ server_pings = 0
51
+ begin
52
+ server_pings += 1
53
+ sleep 0.05
54
+ abort "test server didn't manage to start" if server_pings >= 50
55
+ end until responsive.call('/ping')
56
+ end
57
+
58
+ def find_port
59
+ server = TCPServer.new(@host, 0)
60
+ server.addr[1]
61
+ ensure
62
+ server.close if server
63
+ end
64
+ end