faraday-http-cache 0.0.1.dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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