josh-rack-cache 0.5.1
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.
- data/CHANGES +167 -0
- data/COPYING +18 -0
- data/README +110 -0
- data/Rakefile +137 -0
- data/TODO +27 -0
- data/doc/configuration.markdown +112 -0
- data/doc/faq.markdown +141 -0
- data/doc/index.markdown +121 -0
- data/doc/layout.html.erb +34 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/server.ru +34 -0
- data/doc/storage.markdown +164 -0
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +45 -0
- data/lib/rack/cache/appengine.rb +52 -0
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +253 -0
- data/lib/rack/cache/entitystore.rb +339 -0
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +407 -0
- data/lib/rack/cache/options.rb +150 -0
- data/lib/rack/cache/request.rb +33 -0
- data/lib/rack/cache/response.rb +267 -0
- data/lib/rack/cache/storage.rb +62 -0
- data/rack-cache.gemspec +70 -0
- data/test/cache_test.rb +38 -0
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +774 -0
- data/test/entitystore_test.rb +230 -0
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +302 -0
- data/test/options_test.rb +77 -0
- data/test/pony.jpg +0 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +178 -0
- data/test/spec_setup.rb +237 -0
- data/test/storage_test.rb +94 -0
- metadata +118 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rack/cache/metastore'
|
3
|
+
require 'rack/cache/entitystore'
|
4
|
+
|
5
|
+
module Rack::Cache
|
6
|
+
|
7
|
+
# Maintains a collection of MetaStore and EntityStore instances keyed by
|
8
|
+
# URI. A single instance of this class can be used across a single process
|
9
|
+
# to ensure that only a single instance of a backing store is created per
|
10
|
+
# unique storage URI.
|
11
|
+
class Storage
|
12
|
+
def initialize
|
13
|
+
@metastores = {}
|
14
|
+
@entitystores = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def resolve_metastore_uri(uri)
|
18
|
+
@metastores[uri.to_s] ||= create_store(MetaStore, uri)
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve_entitystore_uri(uri)
|
22
|
+
@entitystores[uri.to_s] ||= create_store(EntityStore, uri)
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear
|
26
|
+
@metastores.clear
|
27
|
+
@entitystores.clear
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def create_store(type, uri)
|
33
|
+
if uri.respond_to?(:scheme) || uri.respond_to?(:to_str)
|
34
|
+
uri = URI.parse(uri) unless uri.respond_to?(:scheme)
|
35
|
+
if type.const_defined?(uri.scheme.upcase)
|
36
|
+
klass = type.const_get(uri.scheme.upcase)
|
37
|
+
klass.resolve(uri)
|
38
|
+
else
|
39
|
+
fail "Unknown storage provider: #{uri.to_s}"
|
40
|
+
end
|
41
|
+
else
|
42
|
+
# hack in support for passing a MemCache or Memcached object
|
43
|
+
# as the storage URI.
|
44
|
+
case
|
45
|
+
when defined?(::MemCache) && uri.kind_of?(::MemCache)
|
46
|
+
type.const_get(:MemCache).resolve(uri)
|
47
|
+
when defined?(::Memcached) && uri.respond_to?(:stats)
|
48
|
+
type.const_get(:MemCached).resolve(uri)
|
49
|
+
else
|
50
|
+
fail "Unknown storage provider: #{uri.to_s}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
public
|
56
|
+
@@singleton_instance = new
|
57
|
+
def self.instance
|
58
|
+
@@singleton_instance
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/rack-cache.gemspec
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
3
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
|
+
|
5
|
+
s.name = 'rack-cache'
|
6
|
+
s.version = '0.5.1'
|
7
|
+
s.date = '2009-06-06'
|
8
|
+
|
9
|
+
s.description = "HTTP Caching for Rack"
|
10
|
+
s.summary = "HTTP Caching for Rack"
|
11
|
+
|
12
|
+
s.authors = ["Ryan Tomayko"]
|
13
|
+
s.email = "r@tomayko.com"
|
14
|
+
|
15
|
+
# = MANIFEST =
|
16
|
+
s.files = %w[
|
17
|
+
CHANGES
|
18
|
+
COPYING
|
19
|
+
README
|
20
|
+
Rakefile
|
21
|
+
TODO
|
22
|
+
doc/configuration.markdown
|
23
|
+
doc/faq.markdown
|
24
|
+
doc/index.markdown
|
25
|
+
doc/layout.html.erb
|
26
|
+
doc/license.markdown
|
27
|
+
doc/rack-cache.css
|
28
|
+
doc/server.ru
|
29
|
+
doc/storage.markdown
|
30
|
+
example/sinatra/app.rb
|
31
|
+
example/sinatra/views/index.erb
|
32
|
+
lib/rack/cache.rb
|
33
|
+
lib/rack/cache/appengine.rb
|
34
|
+
lib/rack/cache/cachecontrol.rb
|
35
|
+
lib/rack/cache/context.rb
|
36
|
+
lib/rack/cache/entitystore.rb
|
37
|
+
lib/rack/cache/key.rb
|
38
|
+
lib/rack/cache/metastore.rb
|
39
|
+
lib/rack/cache/options.rb
|
40
|
+
lib/rack/cache/request.rb
|
41
|
+
lib/rack/cache/response.rb
|
42
|
+
lib/rack/cache/storage.rb
|
43
|
+
rack-cache.gemspec
|
44
|
+
test/cache_test.rb
|
45
|
+
test/cachecontrol_test.rb
|
46
|
+
test/context_test.rb
|
47
|
+
test/entitystore_test.rb
|
48
|
+
test/key_test.rb
|
49
|
+
test/metastore_test.rb
|
50
|
+
test/options_test.rb
|
51
|
+
test/pony.jpg
|
52
|
+
test/request_test.rb
|
53
|
+
test/response_test.rb
|
54
|
+
test/spec_setup.rb
|
55
|
+
test/storage_test.rb
|
56
|
+
]
|
57
|
+
# = MANIFEST =
|
58
|
+
|
59
|
+
s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/}
|
60
|
+
|
61
|
+
s.extra_rdoc_files = %w[README COPYING TODO CHANGES]
|
62
|
+
s.add_dependency 'rack', '>= 0.4'
|
63
|
+
|
64
|
+
s.has_rdoc = true
|
65
|
+
s.homepage = "http://tomayko.com/src/rack-cache/"
|
66
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Rack::Cache", "--main", "Rack::Cache"]
|
67
|
+
s.require_paths = %w[lib]
|
68
|
+
s.rubyforge_project = 'wink'
|
69
|
+
s.rubygems_version = '1.1.1'
|
70
|
+
end
|
data/test/cache_test.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
|
3
|
+
def dumb_app(env)
|
4
|
+
body = block_given? ? [yield] : ['Hi']
|
5
|
+
[ 200, {'Content-Type' => 'text/plain'}, body ]
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'Rack::Cache::new' do
|
9
|
+
before { @app = method(:dumb_app) }
|
10
|
+
|
11
|
+
it 'takes a backend and returns a middleware component' do
|
12
|
+
Rack::Cache.new(@app).
|
13
|
+
should.respond_to :call
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'takes an options Hash' do
|
17
|
+
lambda { Rack::Cache.new(@app, {}) }.
|
18
|
+
should.not.raise(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'sets options provided in the options Hash' do
|
22
|
+
object = Rack::Cache.new(@app, :foo => 'bar', 'foo.bar' => 'bling')
|
23
|
+
object.options['foo.bar'].should.equal 'bling'
|
24
|
+
object.options['rack-cache.foo'].should.equal 'bar'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'takes a block; executes it during initialization' do
|
28
|
+
state, object = 'not invoked', nil
|
29
|
+
instance =
|
30
|
+
Rack::Cache.new @app do |cache|
|
31
|
+
object = cache
|
32
|
+
state = 'invoked'
|
33
|
+
cache.should.respond_to :set
|
34
|
+
end
|
35
|
+
state.should.equal 'invoked'
|
36
|
+
object.should.be instance
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/cachecontrol'
|
3
|
+
|
4
|
+
describe 'Rack::Cache::CacheControl' do
|
5
|
+
it 'takes no args and initializes with an empty set of values' do
|
6
|
+
cache_control = Rack::Cache::CacheControl.new
|
7
|
+
cache_control.should.be.empty
|
8
|
+
cache_control.to_s.should.equal ''
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'takes a String and parses it into a Hash when created' do
|
12
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600, foo')
|
13
|
+
cache_control['max-age'].should.equal '600'
|
14
|
+
cache_control['foo'].should.be true
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'takes a String with a single name=value pair' do
|
18
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600')
|
19
|
+
cache_control['max-age'].should.equal '600'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'takes a String with multiple name=value pairs' do
|
23
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600, max-stale=300, min-fresh=570')
|
24
|
+
cache_control['max-age'].should.equal '600'
|
25
|
+
cache_control['max-stale'].should.equal '300'
|
26
|
+
cache_control['min-fresh'].should.equal '570'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'takes a String with a single flag value' do
|
30
|
+
cache_control = Rack::Cache::CacheControl.new('no-cache')
|
31
|
+
cache_control.should.include 'no-cache'
|
32
|
+
cache_control['no-cache'].should.be true
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'takes a String with a bunch of all kinds of stuff' do
|
36
|
+
cache_control =
|
37
|
+
Rack::Cache::CacheControl.new('max-age=600,must-revalidate,min-fresh=3000,foo=bar,baz')
|
38
|
+
cache_control['max-age'].should.equal '600'
|
39
|
+
cache_control['must-revalidate'].should.be true
|
40
|
+
cache_control['min-fresh'].should.equal '3000'
|
41
|
+
cache_control['foo'].should.equal 'bar'
|
42
|
+
cache_control['baz'].should.be true
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'strips leading and trailing spaces from header value' do
|
46
|
+
cache_control = Rack::Cache::CacheControl.new(' public, max-age = 600 ')
|
47
|
+
cache_control.should.include 'public'
|
48
|
+
cache_control.should.include 'max-age'
|
49
|
+
cache_control['max-age'].should.equal '600'
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'removes all directives with #clear' do
|
53
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600, must-revalidate')
|
54
|
+
cache_control.clear
|
55
|
+
cache_control.should.be.empty
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'converts self into header String with #to_s' do
|
59
|
+
cache_control = Rack::Cache::CacheControl.new
|
60
|
+
cache_control['public'] = true
|
61
|
+
cache_control['max-age'] = '600'
|
62
|
+
cache_control.to_s.split(', ').sort.should.equal ['max-age=600', 'public']
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'sorts alphabetically with boolean directives before value directives' do
|
66
|
+
cache_control = Rack::Cache::CacheControl.new('foo=bar, z, x, y, bling=baz, zoom=zib, b, a')
|
67
|
+
cache_control.to_s.should.equal 'a, b, x, y, z, bling=baz, foo=bar, zoom=zib'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'responds to #max_age with an integer when max-age directive present' do
|
71
|
+
cache_control = Rack::Cache::CacheControl.new('public, max-age=600')
|
72
|
+
cache_control.max_age.should.equal 600
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'responds to #max_age with nil when no max-age directive present' do
|
76
|
+
cache_control = Rack::Cache::CacheControl.new('public')
|
77
|
+
cache_control.max_age.should.be nil
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'responds to #shared_max_age with an integer when s-maxage directive present' do
|
81
|
+
cache_control = Rack::Cache::CacheControl.new('public, s-maxage=600')
|
82
|
+
cache_control.shared_max_age.should.equal 600
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'responds to #shared_max_age with nil when no s-maxage directive present' do
|
86
|
+
cache_control = Rack::Cache::CacheControl.new('public')
|
87
|
+
cache_control.shared_max_age.should.be nil
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'responds to #public? truthfully when public directive present' do
|
91
|
+
cache_control = Rack::Cache::CacheControl.new('public')
|
92
|
+
cache_control.should.be.public
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'responds to #public? non-truthfully when no public directive present' do
|
96
|
+
cache_control = Rack::Cache::CacheControl.new('private')
|
97
|
+
cache_control.should.not.be.public
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'responds to #private? truthfully when private directive present' do
|
101
|
+
cache_control = Rack::Cache::CacheControl.new('private')
|
102
|
+
cache_control.should.be.private
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'responds to #private? non-truthfully when no private directive present' do
|
106
|
+
cache_control = Rack::Cache::CacheControl.new('public')
|
107
|
+
cache_control.should.not.be.private
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'responds to #no_cache? truthfully when no-cache directive present' do
|
111
|
+
cache_control = Rack::Cache::CacheControl.new('no-cache')
|
112
|
+
cache_control.should.be.no_cache
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'responds to #no_cache? non-truthfully when no no-cache directive present' do
|
116
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600')
|
117
|
+
cache_control.should.not.be.no_cache
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'responds to #must_revalidate? truthfully when must-revalidate directive present' do
|
121
|
+
cache_control = Rack::Cache::CacheControl.new('must-revalidate')
|
122
|
+
cache_control.should.be.must_revalidate
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'responds to #must_revalidate? non-truthfully when no must-revalidate directive present' do
|
126
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600')
|
127
|
+
cache_control.should.not.be.no_cache
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'responds to #proxy_revalidate? truthfully when proxy-revalidate directive present' do
|
131
|
+
cache_control = Rack::Cache::CacheControl.new('proxy-revalidate')
|
132
|
+
cache_control.should.be.proxy_revalidate
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'responds to #proxy_revalidate? non-truthfully when no proxy-revalidate directive present' do
|
136
|
+
cache_control = Rack::Cache::CacheControl.new('max-age=600')
|
137
|
+
cache_control.should.not.be.no_cache
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,774 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/context'
|
3
|
+
|
4
|
+
describe 'Rack::Cache::Context' do
|
5
|
+
before(:each) { setup_cache_context }
|
6
|
+
after(:each) { teardown_cache_context }
|
7
|
+
|
8
|
+
it 'passes on non-GET/HEAD requests' do
|
9
|
+
respond_with 200
|
10
|
+
post '/'
|
11
|
+
|
12
|
+
app.should.be.called
|
13
|
+
response.should.be.ok
|
14
|
+
cache.trace.should.include :pass
|
15
|
+
response.headers.should.not.include 'Age'
|
16
|
+
end
|
17
|
+
|
18
|
+
%w[post put delete].each do |request_method|
|
19
|
+
it "invalidates on #{request_method} requests" do
|
20
|
+
respond_with 200
|
21
|
+
request request_method, '/'
|
22
|
+
|
23
|
+
app.should.be.called
|
24
|
+
response.should.be.ok
|
25
|
+
cache.trace.should.include :invalidate
|
26
|
+
cache.trace.should.include :pass
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'does not cache with Authorization request header and non public response' do
|
31
|
+
respond_with 200, 'ETag' => '"FOO"'
|
32
|
+
get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
|
33
|
+
|
34
|
+
app.should.be.called
|
35
|
+
response.should.be.ok
|
36
|
+
response.headers['Cache-Control'].should.equal 'private'
|
37
|
+
cache.trace.should.include :miss
|
38
|
+
cache.trace.should.not.include :store
|
39
|
+
response.headers.should.not.include 'Age'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'does cache with Authorization request header and public response' do
|
43
|
+
respond_with 200, 'Cache-Control' => 'public', 'ETag' => '"FOO"'
|
44
|
+
get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
|
45
|
+
|
46
|
+
app.should.be.called
|
47
|
+
response.should.be.ok
|
48
|
+
cache.trace.should.include :miss
|
49
|
+
cache.trace.should.include :store
|
50
|
+
response.headers.should.include 'Age'
|
51
|
+
response.headers['Cache-Control'].should.equal 'public'
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'does not cache with Cookie header and non public response' do
|
55
|
+
respond_with 200, 'ETag' => '"FOO"'
|
56
|
+
get '/', 'HTTP_COOKIE' => 'foo=bar'
|
57
|
+
|
58
|
+
app.should.be.called
|
59
|
+
response.should.be.ok
|
60
|
+
response.headers['Cache-Control'].should.equal 'private'
|
61
|
+
cache.trace.should.include :miss
|
62
|
+
cache.trace.should.not.include :store
|
63
|
+
response.headers.should.not.include 'Age'
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'does not cache requests with a Cookie header' do
|
67
|
+
respond_with 200
|
68
|
+
get '/', 'HTTP_COOKIE' => 'foo=bar'
|
69
|
+
|
70
|
+
response.should.be.ok
|
71
|
+
app.should.be.called
|
72
|
+
cache.trace.should.include :miss
|
73
|
+
cache.trace.should.not.include :store
|
74
|
+
response.headers.should.not.include 'Age'
|
75
|
+
response.headers['Cache-Control'].should.equal 'private'
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'responds with 304 when If-Modified-Since matches Last-Modified' do
|
79
|
+
timestamp = Time.now.httpdate
|
80
|
+
respond_with do |req,res|
|
81
|
+
res.status = 200
|
82
|
+
res['Last-Modified'] = timestamp
|
83
|
+
res['Content-Type'] = 'text/plain'
|
84
|
+
res.body = ['Hello World']
|
85
|
+
end
|
86
|
+
|
87
|
+
get '/',
|
88
|
+
'HTTP_IF_MODIFIED_SINCE' => timestamp
|
89
|
+
app.should.be.called
|
90
|
+
response.status.should.equal 304
|
91
|
+
response.headers.should.not.include 'Content-Length'
|
92
|
+
response.headers.should.not.include 'Content-Type'
|
93
|
+
response.body.should.empty
|
94
|
+
cache.trace.should.include :miss
|
95
|
+
cache.trace.should.include :store
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'responds with 304 when If-None-Match matches ETag' do
|
99
|
+
respond_with do |req,res|
|
100
|
+
res.status = 200
|
101
|
+
res['ETag'] = '12345'
|
102
|
+
res['Content-Type'] = 'text/plain'
|
103
|
+
res.body = ['Hello World']
|
104
|
+
end
|
105
|
+
|
106
|
+
get '/',
|
107
|
+
'HTTP_IF_NONE_MATCH' => '12345'
|
108
|
+
app.should.be.called
|
109
|
+
response.status.should.equal 304
|
110
|
+
response.headers.should.not.include 'Content-Length'
|
111
|
+
response.headers.should.not.include 'Content-Type'
|
112
|
+
response.headers.should.include 'ETag'
|
113
|
+
response.body.should.empty
|
114
|
+
cache.trace.should.include :miss
|
115
|
+
cache.trace.should.include :store
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'stores responses when no-cache request directive present' do
|
119
|
+
respond_with 200, 'Expires' => (Time.now + 5).httpdate
|
120
|
+
|
121
|
+
get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
|
122
|
+
response.should.be.ok
|
123
|
+
cache.trace.should.include :store
|
124
|
+
response.headers.should.include 'Age'
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'reloads responses when cache hits but no-cache request directive present ' +
|
128
|
+
'when allow_reload is set true' do
|
129
|
+
count = 0
|
130
|
+
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
|
131
|
+
count+= 1
|
132
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
133
|
+
end
|
134
|
+
|
135
|
+
get '/'
|
136
|
+
response.should.be.ok
|
137
|
+
response.body.should.equal 'Hello World'
|
138
|
+
cache.trace.should.include :store
|
139
|
+
|
140
|
+
get '/'
|
141
|
+
response.should.be.ok
|
142
|
+
response.body.should.equal 'Hello World'
|
143
|
+
cache.trace.should.include :fresh
|
144
|
+
|
145
|
+
get '/',
|
146
|
+
'rack-cache.allow_reload' => true,
|
147
|
+
'HTTP_CACHE_CONTROL' => 'no-cache'
|
148
|
+
response.should.be.ok
|
149
|
+
response.body.should.equal 'Goodbye World'
|
150
|
+
cache.trace.should.include :reload
|
151
|
+
cache.trace.should.include :store
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'does not reload responses when allow_reload is set false (default)' do
|
155
|
+
count = 0
|
156
|
+
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
|
157
|
+
count+= 1
|
158
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
159
|
+
end
|
160
|
+
|
161
|
+
get '/'
|
162
|
+
response.should.be.ok
|
163
|
+
response.body.should.equal 'Hello World'
|
164
|
+
cache.trace.should.include :store
|
165
|
+
|
166
|
+
get '/'
|
167
|
+
response.should.be.ok
|
168
|
+
response.body.should.equal 'Hello World'
|
169
|
+
cache.trace.should.include :fresh
|
170
|
+
|
171
|
+
get '/',
|
172
|
+
'rack-cache.allow_reload' => false,
|
173
|
+
'HTTP_CACHE_CONTROL' => 'no-cache'
|
174
|
+
response.should.be.ok
|
175
|
+
response.body.should.equal 'Hello World'
|
176
|
+
cache.trace.should.not.include :reload
|
177
|
+
|
178
|
+
# test again without explicitly setting the allow_reload option to false
|
179
|
+
get '/',
|
180
|
+
'HTTP_CACHE_CONTROL' => 'no-cache'
|
181
|
+
response.should.be.ok
|
182
|
+
response.body.should.equal 'Hello World'
|
183
|
+
cache.trace.should.not.include :reload
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'revalidates fresh cache entry when max-age request directive is exceeded ' +
|
187
|
+
'when allow_revalidate option is set true' do
|
188
|
+
count = 0
|
189
|
+
respond_with do |req,res|
|
190
|
+
count+= 1
|
191
|
+
res['Cache-Control'] = 'max-age=10000'
|
192
|
+
res['ETag'] = count.to_s
|
193
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
194
|
+
end
|
195
|
+
|
196
|
+
get '/'
|
197
|
+
response.should.be.ok
|
198
|
+
response.body.should.equal 'Hello World'
|
199
|
+
cache.trace.should.include :store
|
200
|
+
|
201
|
+
get '/'
|
202
|
+
response.should.be.ok
|
203
|
+
response.body.should.equal 'Hello World'
|
204
|
+
cache.trace.should.include :fresh
|
205
|
+
|
206
|
+
get '/',
|
207
|
+
'rack-cache.allow_revalidate' => true,
|
208
|
+
'HTTP_CACHE_CONTROL' => 'max-age=0'
|
209
|
+
response.should.be.ok
|
210
|
+
response.body.should.equal 'Goodbye World'
|
211
|
+
cache.trace.should.include :stale
|
212
|
+
cache.trace.should.include :invalid
|
213
|
+
cache.trace.should.include :store
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'does not revalidate fresh cache entry when enable_revalidate option is set false (default)' do
|
217
|
+
count = 0
|
218
|
+
respond_with do |req,res|
|
219
|
+
count+= 1
|
220
|
+
res['Cache-Control'] = 'max-age=10000'
|
221
|
+
res['ETag'] = count.to_s
|
222
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
223
|
+
end
|
224
|
+
|
225
|
+
get '/'
|
226
|
+
response.should.be.ok
|
227
|
+
response.body.should.equal 'Hello World'
|
228
|
+
cache.trace.should.include :store
|
229
|
+
|
230
|
+
get '/'
|
231
|
+
response.should.be.ok
|
232
|
+
response.body.should.equal 'Hello World'
|
233
|
+
cache.trace.should.include :fresh
|
234
|
+
|
235
|
+
get '/',
|
236
|
+
'rack-cache.allow_revalidate' => false,
|
237
|
+
'HTTP_CACHE_CONTROL' => 'max-age=0'
|
238
|
+
response.should.be.ok
|
239
|
+
response.body.should.equal 'Hello World'
|
240
|
+
cache.trace.should.not.include :stale
|
241
|
+
cache.trace.should.not.include :invalid
|
242
|
+
cache.trace.should.include :fresh
|
243
|
+
|
244
|
+
# test again without explicitly setting the allow_revalidate option to false
|
245
|
+
get '/',
|
246
|
+
'HTTP_CACHE_CONTROL' => 'max-age=0'
|
247
|
+
response.should.be.ok
|
248
|
+
response.body.should.equal 'Hello World'
|
249
|
+
cache.trace.should.not.include :stale
|
250
|
+
cache.trace.should.not.include :invalid
|
251
|
+
cache.trace.should.include :fresh
|
252
|
+
end
|
253
|
+
it 'fetches response from backend when cache misses' do
|
254
|
+
respond_with 200, 'Expires' => (Time.now + 5).httpdate
|
255
|
+
get '/'
|
256
|
+
|
257
|
+
response.should.be.ok
|
258
|
+
cache.trace.should.include :miss
|
259
|
+
response.headers.should.include 'Age'
|
260
|
+
end
|
261
|
+
|
262
|
+
[(201..202),(204..206),(303..305),(400..403),(405..409),(411..417),(500..505)].each do |range|
|
263
|
+
range.each do |response_code|
|
264
|
+
it "does not cache #{response_code} responses" do
|
265
|
+
respond_with response_code, 'Expires' => (Time.now + 5).httpdate
|
266
|
+
get '/'
|
267
|
+
|
268
|
+
cache.trace.should.not.include :store
|
269
|
+
response.status.should.equal response_code
|
270
|
+
response.headers.should.not.include 'Age'
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
it "does not cache responses with explicit no-store directive" do
|
276
|
+
respond_with 200,
|
277
|
+
'Expires' => (Time.now + 5).httpdate,
|
278
|
+
'Cache-Control' => 'no-store'
|
279
|
+
get '/'
|
280
|
+
|
281
|
+
response.should.be.ok
|
282
|
+
cache.trace.should.not.include :store
|
283
|
+
response.headers.should.not.include 'Age'
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'does not cache responses without freshness information or a validator' do
|
287
|
+
respond_with 200
|
288
|
+
get '/'
|
289
|
+
|
290
|
+
response.should.be.ok
|
291
|
+
cache.trace.should.not.include :store
|
292
|
+
end
|
293
|
+
|
294
|
+
it "caches responses with explicit no-cache directive" do
|
295
|
+
respond_with 200,
|
296
|
+
'Expires' => (Time.now + 5).httpdate,
|
297
|
+
'Cache-Control' => 'no-cache'
|
298
|
+
get '/'
|
299
|
+
|
300
|
+
response.should.be.ok
|
301
|
+
cache.trace.should.include :store
|
302
|
+
response.headers.should.include 'Age'
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'caches responses with an Expiration header' do
|
306
|
+
respond_with 200, 'Expires' => (Time.now + 5).httpdate
|
307
|
+
get '/'
|
308
|
+
|
309
|
+
response.should.be.ok
|
310
|
+
response.body.should.equal 'Hello World'
|
311
|
+
response.headers.should.include 'Date'
|
312
|
+
response['Age'].should.not.be.nil
|
313
|
+
response['X-Content-Digest'].should.not.be.nil
|
314
|
+
cache.trace.should.include :miss
|
315
|
+
cache.trace.should.include :store
|
316
|
+
cache.metastore.to_hash.keys.length.should.equal 1
|
317
|
+
end
|
318
|
+
|
319
|
+
it 'caches responses with a max-age directive' do
|
320
|
+
respond_with 200, 'Cache-Control' => 'max-age=5'
|
321
|
+
get '/'
|
322
|
+
|
323
|
+
response.should.be.ok
|
324
|
+
response.body.should.equal 'Hello World'
|
325
|
+
response.headers.should.include 'Date'
|
326
|
+
response['Age'].should.not.be.nil
|
327
|
+
response['X-Content-Digest'].should.not.be.nil
|
328
|
+
cache.trace.should.include :miss
|
329
|
+
cache.trace.should.include :store
|
330
|
+
cache.metastore.to_hash.keys.length.should.equal 1
|
331
|
+
end
|
332
|
+
|
333
|
+
it 'caches responses with a s-maxage directive' do
|
334
|
+
respond_with 200, 'Cache-Control' => 's-maxage=5'
|
335
|
+
get '/'
|
336
|
+
|
337
|
+
response.should.be.ok
|
338
|
+
response.body.should.equal 'Hello World'
|
339
|
+
response.headers.should.include 'Date'
|
340
|
+
response['Age'].should.not.be.nil
|
341
|
+
response['X-Content-Digest'].should.not.be.nil
|
342
|
+
cache.trace.should.include :miss
|
343
|
+
cache.trace.should.include :store
|
344
|
+
cache.metastore.to_hash.keys.length.should.equal 1
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'caches responses with a Last-Modified validator but no freshness information' do
|
348
|
+
respond_with 200, 'Last-Modified' => Time.now.httpdate
|
349
|
+
get '/'
|
350
|
+
|
351
|
+
response.should.be.ok
|
352
|
+
response.body.should.equal 'Hello World'
|
353
|
+
cache.trace.should.include :miss
|
354
|
+
cache.trace.should.include :store
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'caches responses with an ETag validator but no freshness information' do
|
358
|
+
respond_with 200, 'ETag' => '"123456"'
|
359
|
+
get '/'
|
360
|
+
|
361
|
+
response.should.be.ok
|
362
|
+
response.body.should.equal 'Hello World'
|
363
|
+
cache.trace.should.include :miss
|
364
|
+
cache.trace.should.include :store
|
365
|
+
end
|
366
|
+
|
367
|
+
it 'hits cached response with Expires header' do
|
368
|
+
respond_with 200,
|
369
|
+
'Date' => (Time.now - 5).httpdate,
|
370
|
+
'Expires' => (Time.now + 5).httpdate
|
371
|
+
|
372
|
+
get '/'
|
373
|
+
app.should.be.called
|
374
|
+
response.should.be.ok
|
375
|
+
response.headers.should.include 'Date'
|
376
|
+
cache.trace.should.include :miss
|
377
|
+
cache.trace.should.include :store
|
378
|
+
response.body.should.equal 'Hello World'
|
379
|
+
|
380
|
+
get '/'
|
381
|
+
response.should.be.ok
|
382
|
+
app.should.not.be.called
|
383
|
+
response['Date'].should.equal responses.first['Date']
|
384
|
+
response['Age'].to_i.should.satisfy { |age| age > 0 }
|
385
|
+
response['X-Content-Digest'].should.not.be.nil
|
386
|
+
cache.trace.should.include :fresh
|
387
|
+
cache.trace.should.not.include :store
|
388
|
+
response.body.should.equal 'Hello World'
|
389
|
+
end
|
390
|
+
|
391
|
+
it 'hits cached response with max-age directive' do
|
392
|
+
respond_with 200,
|
393
|
+
'Date' => (Time.now - 5).httpdate,
|
394
|
+
'Cache-Control' => 'max-age=10'
|
395
|
+
|
396
|
+
get '/'
|
397
|
+
app.should.be.called
|
398
|
+
response.should.be.ok
|
399
|
+
response.headers.should.include 'Date'
|
400
|
+
cache.trace.should.include :miss
|
401
|
+
cache.trace.should.include :store
|
402
|
+
response.body.should.equal 'Hello World'
|
403
|
+
|
404
|
+
get '/'
|
405
|
+
response.should.be.ok
|
406
|
+
app.should.not.be.called
|
407
|
+
response['Date'].should.equal responses.first['Date']
|
408
|
+
response['Age'].to_i.should.satisfy { |age| age > 0 }
|
409
|
+
response['X-Content-Digest'].should.not.be.nil
|
410
|
+
cache.trace.should.include :fresh
|
411
|
+
cache.trace.should.not.include :store
|
412
|
+
response.body.should.equal 'Hello World'
|
413
|
+
end
|
414
|
+
|
415
|
+
it 'hits cached response with s-maxage directive' do
|
416
|
+
respond_with 200,
|
417
|
+
'Date' => (Time.now - 5).httpdate,
|
418
|
+
'Cache-Control' => 's-maxage=10, max-age=0'
|
419
|
+
|
420
|
+
get '/'
|
421
|
+
app.should.be.called
|
422
|
+
response.should.be.ok
|
423
|
+
response.headers.should.include 'Date'
|
424
|
+
cache.trace.should.include :miss
|
425
|
+
cache.trace.should.include :store
|
426
|
+
response.body.should.equal 'Hello World'
|
427
|
+
|
428
|
+
get '/'
|
429
|
+
response.should.be.ok
|
430
|
+
app.should.not.be.called
|
431
|
+
response['Date'].should.equal responses.first['Date']
|
432
|
+
response['Age'].to_i.should.satisfy { |age| age > 0 }
|
433
|
+
response['X-Content-Digest'].should.not.be.nil
|
434
|
+
cache.trace.should.include :fresh
|
435
|
+
cache.trace.should.not.include :store
|
436
|
+
response.body.should.equal 'Hello World'
|
437
|
+
end
|
438
|
+
|
439
|
+
it 'assigns default_ttl when response has no freshness information' do
|
440
|
+
respond_with 200
|
441
|
+
|
442
|
+
get '/', 'rack-cache.default_ttl' => 10
|
443
|
+
app.should.be.called
|
444
|
+
response.should.be.ok
|
445
|
+
cache.trace.should.include :miss
|
446
|
+
cache.trace.should.include :store
|
447
|
+
response.body.should.equal 'Hello World'
|
448
|
+
response['Cache-Control'].should.include 's-maxage=10'
|
449
|
+
|
450
|
+
get '/', 'rack-cache.default_ttl' => 10
|
451
|
+
response.should.be.ok
|
452
|
+
app.should.not.be.called
|
453
|
+
cache.trace.should.include :fresh
|
454
|
+
cache.trace.should.not.include :store
|
455
|
+
response.body.should.equal 'Hello World'
|
456
|
+
end
|
457
|
+
|
458
|
+
it 'does not assign default_ttl when response has must-revalidate directive' do
|
459
|
+
respond_with 200,
|
460
|
+
'Cache-Control' => 'must-revalidate'
|
461
|
+
|
462
|
+
get '/', 'rack-cache.default_ttl' => 10
|
463
|
+
app.should.be.called
|
464
|
+
response.should.be.ok
|
465
|
+
cache.trace.should.include :miss
|
466
|
+
cache.trace.should.not.include :store
|
467
|
+
response['Cache-Control'].should.not.include 's-maxage'
|
468
|
+
response.body.should.equal 'Hello World'
|
469
|
+
end
|
470
|
+
|
471
|
+
it 'fetches full response when cache stale and no validators present' do
|
472
|
+
respond_with 200, 'Expires' => (Time.now + 5).httpdate
|
473
|
+
|
474
|
+
# build initial request
|
475
|
+
get '/'
|
476
|
+
app.should.be.called
|
477
|
+
response.should.be.ok
|
478
|
+
response.headers.should.include 'Date'
|
479
|
+
response.headers.should.include 'X-Content-Digest'
|
480
|
+
response.headers.should.include 'Age'
|
481
|
+
cache.trace.should.include :miss
|
482
|
+
cache.trace.should.include :store
|
483
|
+
response.body.should.equal 'Hello World'
|
484
|
+
|
485
|
+
# go in and play around with the cached metadata directly ...
|
486
|
+
cache.metastore.to_hash.values.length.should.equal 1
|
487
|
+
cache.metastore.to_hash.values.first.first[1]['Expires'] = Time.now.httpdate
|
488
|
+
|
489
|
+
# build subsequent request; should be found but miss due to freshness
|
490
|
+
get '/'
|
491
|
+
app.should.be.called
|
492
|
+
response.should.be.ok
|
493
|
+
response['Age'].to_i.should.equal 0
|
494
|
+
response.headers.should.include 'X-Content-Digest'
|
495
|
+
cache.trace.should.include :stale
|
496
|
+
cache.trace.should.not.include :fresh
|
497
|
+
cache.trace.should.not.include :miss
|
498
|
+
cache.trace.should.include :store
|
499
|
+
response.body.should.equal 'Hello World'
|
500
|
+
end
|
501
|
+
|
502
|
+
it 'validates cached responses with Last-Modified and no freshness information' do
|
503
|
+
timestamp = Time.now.httpdate
|
504
|
+
respond_with do |req,res|
|
505
|
+
res['Last-Modified'] = timestamp
|
506
|
+
if req.env['HTTP_IF_MODIFIED_SINCE'] == timestamp
|
507
|
+
res.status = 304
|
508
|
+
res.body = []
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# build initial request
|
513
|
+
get '/'
|
514
|
+
app.should.be.called
|
515
|
+
response.should.be.ok
|
516
|
+
response.headers.should.include 'Last-Modified'
|
517
|
+
response.headers.should.include 'X-Content-Digest'
|
518
|
+
response.body.should.equal 'Hello World'
|
519
|
+
cache.trace.should.include :miss
|
520
|
+
cache.trace.should.include :store
|
521
|
+
cache.trace.should.not.include :stale
|
522
|
+
|
523
|
+
# build subsequent request; should be found but miss due to freshness
|
524
|
+
get '/'
|
525
|
+
app.should.be.called
|
526
|
+
response.should.be.ok
|
527
|
+
response.headers.should.include 'Last-Modified'
|
528
|
+
response.headers.should.include 'X-Content-Digest'
|
529
|
+
response['Age'].to_i.should.equal 0
|
530
|
+
response.body.should.equal 'Hello World'
|
531
|
+
cache.trace.should.include :stale
|
532
|
+
cache.trace.should.include :valid
|
533
|
+
cache.trace.should.include :store
|
534
|
+
cache.trace.should.not.include :miss
|
535
|
+
end
|
536
|
+
|
537
|
+
it 'validates cached responses with ETag and no freshness information' do
|
538
|
+
timestamp = Time.now.httpdate
|
539
|
+
respond_with do |req,res|
|
540
|
+
res['ETAG'] = '"12345"'
|
541
|
+
if req.env['HTTP_IF_NONE_MATCH'] == res['Etag']
|
542
|
+
res.status = 304
|
543
|
+
res.body = []
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
# build initial request
|
548
|
+
get '/'
|
549
|
+
app.should.be.called
|
550
|
+
response.should.be.ok
|
551
|
+
response.headers.should.include 'ETag'
|
552
|
+
response.headers.should.include 'X-Content-Digest'
|
553
|
+
response.body.should.equal 'Hello World'
|
554
|
+
cache.trace.should.include :miss
|
555
|
+
cache.trace.should.include :store
|
556
|
+
|
557
|
+
# build subsequent request; should be found but miss due to freshness
|
558
|
+
get '/'
|
559
|
+
app.should.be.called
|
560
|
+
response.should.be.ok
|
561
|
+
response.headers.should.include 'ETag'
|
562
|
+
response.headers.should.include 'X-Content-Digest'
|
563
|
+
response['Age'].to_i.should.equal 0
|
564
|
+
response.body.should.equal 'Hello World'
|
565
|
+
cache.trace.should.include :stale
|
566
|
+
cache.trace.should.include :valid
|
567
|
+
cache.trace.should.include :store
|
568
|
+
cache.trace.should.not.include :miss
|
569
|
+
end
|
570
|
+
|
571
|
+
it 'replaces cached responses when validation results in non-304 response' do
|
572
|
+
timestamp = Time.now.httpdate
|
573
|
+
count = 0
|
574
|
+
respond_with do |req,res|
|
575
|
+
res['Last-Modified'] = timestamp
|
576
|
+
case (count+=1)
|
577
|
+
when 1 ; res.body = ['first response']
|
578
|
+
when 2 ; res.body = ['second response']
|
579
|
+
when 3
|
580
|
+
res.body = []
|
581
|
+
res.status = 304
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
# first request should fetch from backend and store in cache
|
586
|
+
get '/'
|
587
|
+
response.status.should.equal 200
|
588
|
+
response.body.should.equal 'first response'
|
589
|
+
|
590
|
+
# second request is validated, is invalid, and replaces cached entry
|
591
|
+
get '/'
|
592
|
+
response.status.should.equal 200
|
593
|
+
response.body.should.equal 'second response'
|
594
|
+
|
595
|
+
# third respone is validated, valid, and returns cached entry
|
596
|
+
get '/'
|
597
|
+
response.status.should.equal 200
|
598
|
+
response.body.should.equal 'second response'
|
599
|
+
|
600
|
+
count.should.equal 3
|
601
|
+
end
|
602
|
+
|
603
|
+
it 'passes HEAD requests through directly on pass' do
|
604
|
+
respond_with do |req,res|
|
605
|
+
res.status = 200
|
606
|
+
res.body = []
|
607
|
+
req.request_method.should.equal 'HEAD'
|
608
|
+
end
|
609
|
+
|
610
|
+
head '/', 'HTTP_EXPECT' => 'something ...'
|
611
|
+
app.should.be.called
|
612
|
+
response.body.should.equal ''
|
613
|
+
end
|
614
|
+
|
615
|
+
it 'uses cache to respond to HEAD requests when fresh' do
|
616
|
+
respond_with do |req,res|
|
617
|
+
res['Cache-Control'] = 'max-age=10'
|
618
|
+
res.body = ['Hello World']
|
619
|
+
req.request_method.should.not.equal 'HEAD'
|
620
|
+
end
|
621
|
+
|
622
|
+
get '/'
|
623
|
+
app.should.be.called
|
624
|
+
response.status.should.equal 200
|
625
|
+
response.body.should.equal 'Hello World'
|
626
|
+
|
627
|
+
head '/'
|
628
|
+
app.should.not.be.called
|
629
|
+
response.status.should.equal 200
|
630
|
+
response.body.should.equal ''
|
631
|
+
response['Content-Length'].should.equal 'Hello World'.length.to_s
|
632
|
+
end
|
633
|
+
|
634
|
+
it 'invalidates cached responses on POST' do
|
635
|
+
respond_with do |req,res|
|
636
|
+
if req.request_method == 'GET'
|
637
|
+
res.status = 200
|
638
|
+
res['Cache-Control'] = 'public, max-age=500'
|
639
|
+
res.body = ['Hello World']
|
640
|
+
elsif req.request_method == 'POST'
|
641
|
+
res.status = 303
|
642
|
+
res['Location'] = '/'
|
643
|
+
res.headers.delete('Cache-Control')
|
644
|
+
res.body = []
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
# build initial request to enter into the cache
|
649
|
+
get '/'
|
650
|
+
app.should.be.called
|
651
|
+
response.should.be.ok
|
652
|
+
response.body.should.equal 'Hello World'
|
653
|
+
cache.trace.should.include :miss
|
654
|
+
cache.trace.should.include :store
|
655
|
+
|
656
|
+
# make sure it is valid
|
657
|
+
get '/'
|
658
|
+
app.should.not.called
|
659
|
+
response.should.be.ok
|
660
|
+
response.body.should.equal 'Hello World'
|
661
|
+
cache.trace.should.include :fresh
|
662
|
+
|
663
|
+
# now POST to same URL
|
664
|
+
post '/'
|
665
|
+
app.should.be.called
|
666
|
+
response.should.be.redirect
|
667
|
+
response['Location'].should.equal '/'
|
668
|
+
cache.trace.should.include :invalidate
|
669
|
+
cache.trace.should.include :pass
|
670
|
+
response.body.should.equal ''
|
671
|
+
|
672
|
+
# now make sure it was actually invalidated
|
673
|
+
get '/'
|
674
|
+
app.should.be.called
|
675
|
+
response.should.be.ok
|
676
|
+
response.body.should.equal 'Hello World'
|
677
|
+
cache.trace.should.include :stale
|
678
|
+
cache.trace.should.include :invalid
|
679
|
+
cache.trace.should.include :store
|
680
|
+
end
|
681
|
+
|
682
|
+
describe 'with responses that include a Vary header' do
|
683
|
+
before(:each) do
|
684
|
+
count = 0
|
685
|
+
respond_with 200 do |req,res|
|
686
|
+
res['Vary'] = 'Accept User-Agent Foo'
|
687
|
+
res['Cache-Control'] = 'max-age=10'
|
688
|
+
res['X-Response-Count'] = (count+=1).to_s
|
689
|
+
res.body = [req.env['HTTP_USER_AGENT']]
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
it 'serves from cache when headers match' do
|
694
|
+
get '/',
|
695
|
+
'HTTP_ACCEPT' => 'text/html',
|
696
|
+
'HTTP_USER_AGENT' => 'Bob/1.0'
|
697
|
+
response.should.be.ok
|
698
|
+
response.body.should.equal 'Bob/1.0'
|
699
|
+
cache.trace.should.include :miss
|
700
|
+
cache.trace.should.include :store
|
701
|
+
|
702
|
+
get '/',
|
703
|
+
'HTTP_ACCEPT' => 'text/html',
|
704
|
+
'HTTP_USER_AGENT' => 'Bob/1.0'
|
705
|
+
response.should.be.ok
|
706
|
+
response.body.should.equal 'Bob/1.0'
|
707
|
+
cache.trace.should.include :fresh
|
708
|
+
cache.trace.should.not.include :store
|
709
|
+
response.headers.should.include 'X-Content-Digest'
|
710
|
+
end
|
711
|
+
|
712
|
+
it 'stores multiple responses when headers differ' do
|
713
|
+
get '/',
|
714
|
+
'HTTP_ACCEPT' => 'text/html',
|
715
|
+
'HTTP_USER_AGENT' => 'Bob/1.0'
|
716
|
+
response.should.be.ok
|
717
|
+
response.body.should.equal 'Bob/1.0'
|
718
|
+
response['X-Response-Count'].should.equal '1'
|
719
|
+
|
720
|
+
get '/',
|
721
|
+
'HTTP_ACCEPT' => 'text/html',
|
722
|
+
'HTTP_USER_AGENT' => 'Bob/2.0'
|
723
|
+
cache.trace.should.include :miss
|
724
|
+
cache.trace.should.include :store
|
725
|
+
response.body.should.equal 'Bob/2.0'
|
726
|
+
response['X-Response-Count'].should.equal '2'
|
727
|
+
|
728
|
+
get '/',
|
729
|
+
'HTTP_ACCEPT' => 'text/html',
|
730
|
+
'HTTP_USER_AGENT' => 'Bob/1.0'
|
731
|
+
cache.trace.should.include :fresh
|
732
|
+
response.body.should.equal 'Bob/1.0'
|
733
|
+
response['X-Response-Count'].should.equal '1'
|
734
|
+
|
735
|
+
get '/',
|
736
|
+
'HTTP_ACCEPT' => 'text/html',
|
737
|
+
'HTTP_USER_AGENT' => 'Bob/2.0'
|
738
|
+
cache.trace.should.include :fresh
|
739
|
+
response.body.should.equal 'Bob/2.0'
|
740
|
+
response['X-Response-Count'].should.equal '2'
|
741
|
+
|
742
|
+
get '/',
|
743
|
+
'HTTP_USER_AGENT' => 'Bob/2.0'
|
744
|
+
cache.trace.should.include :miss
|
745
|
+
response.body.should.equal 'Bob/2.0'
|
746
|
+
response['X-Response-Count'].should.equal '3'
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
it 'passes if there was a metastore exception' do
|
751
|
+
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
|
752
|
+
res.body = ['Hello World']
|
753
|
+
end
|
754
|
+
|
755
|
+
get '/'
|
756
|
+
response.should.be.ok
|
757
|
+
response.body.should.equal 'Hello World'
|
758
|
+
cache.trace.should.include :store
|
759
|
+
|
760
|
+
get '/' do |cache|
|
761
|
+
cache.meta_def(:metastore) { raise Timeout::Error }
|
762
|
+
end
|
763
|
+
response.should.be.ok
|
764
|
+
response.body.should.equal 'Hello World'
|
765
|
+
cache.trace.should.include :pass
|
766
|
+
|
767
|
+
post '/' do |cache|
|
768
|
+
cache.meta_def(:metastore) { raise Timeout::Error }
|
769
|
+
end
|
770
|
+
response.should.be.ok
|
771
|
+
response.body.should.equal 'Hello World'
|
772
|
+
cache.trace.should.include :pass
|
773
|
+
end
|
774
|
+
end
|