rack-cache 0.3.0 → 0.4
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.
Potentially problematic release.
This version of rack-cache might be problematic. Click here for more details.
- data/CHANGES +43 -0
- data/README +18 -9
- data/Rakefile +1 -14
- data/TODO +13 -14
- data/doc/configuration.markdown +7 -153
- data/doc/faq.markdown +8 -0
- data/doc/index.markdown +7 -9
- data/example/sinatra/app.rb +25 -0
- data/example/sinatra/views/index.erb +44 -0
- data/lib/rack/cache.rb +5 -11
- data/lib/rack/cache/cachecontrol.rb +193 -0
- data/lib/rack/cache/context.rb +190 -52
- data/lib/rack/cache/entitystore.rb +10 -4
- data/lib/rack/cache/key.rb +52 -0
- data/lib/rack/cache/metastore.rb +52 -16
- data/lib/rack/cache/options.rb +60 -39
- data/lib/rack/cache/request.rb +11 -15
- data/lib/rack/cache/response.rb +221 -30
- data/lib/rack/cache/storage.rb +1 -2
- data/rack-cache.gemspec +9 -15
- data/test/cache_test.rb +9 -6
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +251 -169
- data/test/entitystore_test.rb +12 -11
- data/test/key_test.rb +50 -0
- data/test/metastore_test.rb +57 -14
- data/test/options_test.rb +11 -0
- data/test/request_test.rb +19 -0
- data/test/response_test.rb +164 -23
- data/test/spec_setup.rb +7 -0
- metadata +12 -20
- data/doc/events.dot +0 -27
- data/lib/rack/cache/config.rb +0 -65
- data/lib/rack/cache/config/busters.rb +0 -16
- data/lib/rack/cache/config/default.rb +0 -133
- data/lib/rack/cache/config/no-cache.rb +0 -13
- data/lib/rack/cache/core.rb +0 -299
- data/lib/rack/cache/headers.rb +0 -325
- data/lib/rack/utils/environment_headers.rb +0 -78
- data/test/config_test.rb +0 -66
- data/test/core_test.rb +0 -84
- data/test/environment_headers_test.rb +0 -69
- data/test/headers_test.rb +0 -298
- data/test/logging_test.rb +0 -45
data/test/entitystore_test.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
3
|
require 'rack/cache/entitystore'
|
3
4
|
|
@@ -16,7 +17,7 @@ describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
|
16
17
|
end
|
17
18
|
|
18
19
|
it 'stores bodies with #write' do
|
19
|
-
key, size = @store.write('My wild love went riding,')
|
20
|
+
key, size = @store.write(['My wild love went riding,'])
|
20
21
|
key.should.not.be.nil
|
21
22
|
key.should.be.sha_like
|
22
23
|
|
@@ -25,19 +26,19 @@ describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
|
25
26
|
end
|
26
27
|
|
27
28
|
it 'correctly determines whether cached body exists for key with #exist?' do
|
28
|
-
key, size = @store.write('She rode to the devil,')
|
29
|
+
key, size = @store.write(['She rode to the devil,'])
|
29
30
|
@store.should.exist key
|
30
31
|
@store.should.not.exist '938jasddj83jasdh4438021ksdfjsdfjsdsf'
|
31
32
|
end
|
32
33
|
|
33
34
|
it 'can read data written with #write' do
|
34
|
-
key, size = @store.write('And asked him to pay.')
|
35
|
+
key, size = @store.write(['And asked him to pay.'])
|
35
36
|
data = @store.read(key)
|
36
37
|
data.should.equal 'And asked him to pay.'
|
37
38
|
end
|
38
39
|
|
39
40
|
it 'gives a 40 character SHA1 hex digest from #write' do
|
40
|
-
key, size = @store.write('she rode to the sea;')
|
41
|
+
key, size = @store.write(['she rode to the sea;'])
|
41
42
|
key.should.not.be.nil
|
42
43
|
key.length.should.equal 40
|
43
44
|
key.should.be =~ /^[0-9a-z]+$/
|
@@ -45,7 +46,7 @@ describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
|
45
46
|
end
|
46
47
|
|
47
48
|
it 'returns the entire body as a String from #read' do
|
48
|
-
key, size = @store.write('She gathered together')
|
49
|
+
key, size = @store.write(['She gathered together'])
|
49
50
|
@store.read(key).should.equal 'She gathered together'
|
50
51
|
end
|
51
52
|
|
@@ -54,7 +55,7 @@ describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
|
54
55
|
end
|
55
56
|
|
56
57
|
it 'returns a Rack compatible body from #open' do
|
57
|
-
key, size = @store.write('Some shells for her hair.')
|
58
|
+
key, size = @store.write(['Some shells for her hair.'])
|
58
59
|
body = @store.open(key)
|
59
60
|
body.should.respond_to :each
|
60
61
|
buf = ''
|
@@ -67,8 +68,8 @@ describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
|
67
68
|
end
|
68
69
|
|
69
70
|
it 'can store largish bodies with binary data' do
|
70
|
-
pony = File.
|
71
|
-
key, size = @store.write(pony)
|
71
|
+
pony = File.open(File.dirname(__FILE__) + '/pony.jpg', 'rb') { |f| f.read }
|
72
|
+
key, size = @store.write([pony])
|
72
73
|
key.should.equal 'd0f30d8659b4d268c5c64385d9790024c2d78deb'
|
73
74
|
data = @store.read(key)
|
74
75
|
data.length.should.equal pony.length
|
@@ -76,7 +77,7 @@ describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
|
76
77
|
end
|
77
78
|
|
78
79
|
it 'deletes stored entries with #purge' do
|
79
|
-
key, size = @store.write('My wild love went riding,')
|
80
|
+
key, size = @store.write(['My wild love went riding,'])
|
80
81
|
@store.purge(key).should.be.nil
|
81
82
|
@store.read(key).should.be.nil
|
82
83
|
end
|
@@ -112,14 +113,14 @@ describe 'Rack::Cache::EntityStore' do
|
|
112
113
|
File.should.be.a.directory path
|
113
114
|
end
|
114
115
|
it 'produces a body that responds to #to_path' do
|
115
|
-
key, size = @store.write('Some shells for her hair.')
|
116
|
+
key, size = @store.write(['Some shells for her hair.'])
|
116
117
|
body = @store.open(key)
|
117
118
|
body.should.respond_to :to_path
|
118
119
|
path = "#{@temp_dir}/#{key[0..1]}/#{key[2..-1]}"
|
119
120
|
body.to_path.should.equal path
|
120
121
|
end
|
121
122
|
it 'spreads data over a 36² hash radius' do
|
122
|
-
(<<-PROSE).
|
123
|
+
(<<-PROSE).each_line { |line| @store.write([line]).first.should.be.sha_like }
|
123
124
|
My wild love went riding,
|
124
125
|
She rode all the day;
|
125
126
|
She rode to the devil,
|
data/test/key_test.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/key'
|
3
|
+
|
4
|
+
describe 'A Rack::Cache::Key' do
|
5
|
+
it "sorts params" do
|
6
|
+
request = mock_request('/test?z=last&a=first')
|
7
|
+
new_key(request).should.include('a=first&z=last')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "includes the scheme" do
|
11
|
+
request = mock_request(
|
12
|
+
'/test',
|
13
|
+
'rack.url_scheme' => 'https',
|
14
|
+
'HTTP_HOST' => 'www2.example.org'
|
15
|
+
)
|
16
|
+
new_key(request).should.include('https://')
|
17
|
+
end
|
18
|
+
|
19
|
+
it "includes host" do
|
20
|
+
request = mock_request('/test', "HTTP_HOST" => 'www2.example.org')
|
21
|
+
new_key(request).should.include('www2.example.org')
|
22
|
+
end
|
23
|
+
|
24
|
+
it "includes path" do
|
25
|
+
request = mock_request('/test')
|
26
|
+
new_key(request).should.include('/test')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "sorts the query string by key/value after decoding" do
|
30
|
+
request = mock_request('/test?x=q&a=b&%78=c')
|
31
|
+
new_key(request).should.match(/\?a=b&x=c&x=q$/)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "is in order of scheme, host, path, params" do
|
35
|
+
request = mock_request('/test?x=y', "HTTP_HOST" => 'www2.example.org')
|
36
|
+
new_key(request).should.equal "http://www2.example.org/test?x=y"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Helper Methods =============================================================
|
40
|
+
|
41
|
+
define_method :mock_request do |*args|
|
42
|
+
uri, opts = args
|
43
|
+
env = Rack::MockRequest.env_for(uri, opts || {})
|
44
|
+
Rack::Cache::Request.new(env)
|
45
|
+
end
|
46
|
+
|
47
|
+
define_method :new_key do |request|
|
48
|
+
Rack::Cache::Key.call(request)
|
49
|
+
end
|
50
|
+
end
|
data/test/metastore_test.rb
CHANGED
@@ -60,24 +60,43 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
60
60
|
@store.read(key).should.equal [[{},{}]]
|
61
61
|
end
|
62
62
|
|
63
|
+
it "allows custom cache keys from block" do
|
64
|
+
request = mock_request('/test', {})
|
65
|
+
request.env['rack-cache.cache_key'] =
|
66
|
+
lambda { |request| request.path_info.reverse }
|
67
|
+
@store.cache_key(request).should == 'tset/'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "allows custom cache keys from class" do
|
71
|
+
request = mock_request('/test', {})
|
72
|
+
request.env['rack-cache.cache_key'] = Class.new do
|
73
|
+
def self.call(request); request.path_info.reverse end
|
74
|
+
end
|
75
|
+
@store.cache_key(request).should == 'tset/'
|
76
|
+
end
|
77
|
+
|
63
78
|
# Abstract methods ===========================================================
|
64
79
|
|
65
|
-
|
66
|
-
|
80
|
+
# Stores an entry for the given request args, returns a url encoded cache key
|
81
|
+
# for the request.
|
82
|
+
define_method :store_simple_entry do |*request_args|
|
83
|
+
path, headers = request_args
|
84
|
+
@request = mock_request(path || '/test', headers || {})
|
67
85
|
@response = mock_response(200, {'Cache-Control' => 'max-age=420'}, ['test'])
|
68
86
|
body = @response.body
|
69
|
-
@store.store(@request, @response, @entity_store)
|
87
|
+
cache_key = @store.store(@request, @response, @entity_store)
|
70
88
|
@response.body.should.not.be body
|
89
|
+
cache_key
|
71
90
|
end
|
72
91
|
|
73
92
|
it 'stores a cache entry' do
|
74
|
-
store_simple_entry
|
75
|
-
@store.read(
|
93
|
+
cache_key = store_simple_entry
|
94
|
+
@store.read(cache_key).should.not.be.empty
|
76
95
|
end
|
77
96
|
|
78
97
|
it 'sets the X-Content-Digest response header before storing' do
|
79
|
-
store_simple_entry
|
80
|
-
req, res = @store.read(
|
98
|
+
cache_key = store_simple_entry
|
99
|
+
req, res = @store.read(cache_key).first
|
81
100
|
res['X-Content-Digest'].should.equal 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
|
82
101
|
end
|
83
102
|
|
@@ -93,10 +112,20 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
93
112
|
@store.lookup(req, @entity_store).should.be.nil
|
94
113
|
end
|
95
114
|
|
115
|
+
it "canonizes urls for cache keys" do
|
116
|
+
store_simple_entry(path='/test?x=y&p=q')
|
117
|
+
|
118
|
+
hits_req = mock_request(path, {})
|
119
|
+
miss_req = mock_request('/test?p=x', {})
|
120
|
+
|
121
|
+
@store.lookup(hits_req, @entity_store).should.not.be.nil
|
122
|
+
@store.lookup(miss_req, @entity_store).should.be.nil
|
123
|
+
end
|
124
|
+
|
96
125
|
it 'does not find an entry with #lookup when the body does not exist' do
|
97
126
|
store_simple_entry
|
98
|
-
@response['X-Content-Digest'].should.not.be.nil
|
99
|
-
@entity_store.purge(@response['X-Content-Digest'])
|
127
|
+
@response.headers['X-Content-Digest'].should.not.be.nil
|
128
|
+
@entity_store.purge(@response.headers['X-Content-Digest'])
|
100
129
|
@store.lookup(@request, @entity_store).should.be.nil
|
101
130
|
end
|
102
131
|
|
@@ -104,7 +133,7 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
104
133
|
store_simple_entry
|
105
134
|
response = @store.lookup(@request, @entity_store)
|
106
135
|
response.headers.
|
107
|
-
should.equal @response.headers.merge('
|
136
|
+
should.equal @response.headers.merge('Content-Length' => '4')
|
108
137
|
end
|
109
138
|
|
110
139
|
it 'restores response body from entity store with #lookup' do
|
@@ -114,6 +143,20 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
114
143
|
body.should.equal 'test'
|
115
144
|
end
|
116
145
|
|
146
|
+
it 'invalidates meta and entity store entries with #invalidate' do
|
147
|
+
store_simple_entry
|
148
|
+
@store.invalidate(@request, @entity_store)
|
149
|
+
response = @store.lookup(@request, @entity_store)
|
150
|
+
response.should.be.kind_of Rack::Cache::Response
|
151
|
+
response.should.not.be.fresh
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'succeeds quietly when #invalidate called with no matching entries' do
|
155
|
+
req = mock_request('/test', {})
|
156
|
+
@store.invalidate(req, @entity_store)
|
157
|
+
@store.lookup(@request, @entity_store).should.be.nil
|
158
|
+
end
|
159
|
+
|
117
160
|
# Vary =======================================================================
|
118
161
|
|
119
162
|
it 'does not return entries that Vary with #lookup' do
|
@@ -128,7 +171,7 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
128
171
|
it 'stores multiple responses for each Vary combination' do
|
129
172
|
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
|
130
173
|
res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
|
131
|
-
@store.store(req1, res1, @entity_store)
|
174
|
+
key = @store.store(req1, res1, @entity_store)
|
132
175
|
|
133
176
|
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
|
134
177
|
res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
|
@@ -142,13 +185,13 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
142
185
|
slurp(@store.lookup(req1, @entity_store).body).should.equal 'test 1'
|
143
186
|
slurp(@store.lookup(req2, @entity_store).body).should.equal 'test 2'
|
144
187
|
|
145
|
-
@store.read(
|
188
|
+
@store.read(key).length.should.equal 3
|
146
189
|
end
|
147
190
|
|
148
191
|
it 'overwrites non-varying responses with #store' do
|
149
192
|
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
|
150
193
|
res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
|
151
|
-
@store.store(req1, res1, @entity_store)
|
194
|
+
key = @store.store(req1, res1, @entity_store)
|
152
195
|
slurp(@store.lookup(req1, @entity_store).body).should.equal 'test 1'
|
153
196
|
|
154
197
|
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
|
@@ -161,7 +204,7 @@ describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
|
161
204
|
@store.store(req3, res3, @entity_store)
|
162
205
|
slurp(@store.lookup(req1, @entity_store).body).should.equal 'test 3'
|
163
206
|
|
164
|
-
@store.read(
|
207
|
+
@store.read(key).length.should.equal 2
|
165
208
|
end
|
166
209
|
|
167
210
|
# Helper Methods =============================================================
|
data/test/options_test.rb
CHANGED
@@ -45,6 +45,17 @@ describe 'Rack::Cache::Options' do
|
|
45
45
|
@options.options['rack-cache.bar'].should.equal 'baz'
|
46
46
|
end
|
47
47
|
|
48
|
+
it "allows storing the value as a block" do
|
49
|
+
block = Proc.new { "bar block" }
|
50
|
+
@options.set(:foo, &block)
|
51
|
+
@options.options['rack-cache.foo'].should.equal block
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'allows the cache key generator to be configured' do
|
55
|
+
@options.should.respond_to :cache_key
|
56
|
+
@options.should.respond_to :cache_key=
|
57
|
+
end
|
58
|
+
|
48
59
|
it 'allows the meta store to be configured' do
|
49
60
|
@options.should.respond_to :metastore
|
50
61
|
@options.should.respond_to :metastore=
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/request'
|
3
|
+
|
4
|
+
describe 'Rack::Cache::Request' do
|
5
|
+
it 'is marked as no_cache when the Cache-Control header includes the no-cache directive' do
|
6
|
+
request = Rack::Cache::Request.new('HTTP_CACHE_CONTROL' => 'public, no-cache')
|
7
|
+
assert request.no_cache?
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'is marked as no_cache when request should not be loaded from cache' do
|
11
|
+
request = Rack::Cache::Request.new('HTTP_PRAGMA' => 'no-cache')
|
12
|
+
assert request.no_cache?
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'is not marked as no_cache when neither no-cache directive is specified' do
|
16
|
+
request = Rack::Cache::Request.new('HTTP_CACHE_CONTROL' => 'public')
|
17
|
+
assert !request.no_cache?
|
18
|
+
end
|
19
|
+
end
|
data/test/response_test.rb
CHANGED
@@ -1,37 +1,178 @@
|
|
1
1
|
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
2
|
|
3
3
|
describe 'Rack::Cache::Response' do
|
4
|
-
|
5
|
-
|
6
|
-
@now = Time.now
|
7
|
-
@response = Rack::Cache::Response.new(200, {'Date' => @now.httpdate}, '')
|
4
|
+
before :each do
|
5
|
+
@now = Time.httpdate(Time.now.httpdate)
|
8
6
|
@one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@now, @response, @one_hour_ago = nil
|
13
|
-
}
|
7
|
+
@one_hour_later = Time.httpdate((Time.now + (60**2)).httpdate)
|
8
|
+
@res = Rack::Cache::Response.new(200, {'Date' => @now.httpdate}, [])
|
9
|
+
end
|
14
10
|
|
15
|
-
|
16
|
-
@
|
17
|
-
@response.should.respond_to :age
|
18
|
-
@response.should.respond_to :date
|
11
|
+
after :each do
|
12
|
+
@now, @res, @one_hour_ago = nil
|
19
13
|
end
|
20
14
|
|
21
15
|
it 'responds to #to_a with a Rack response tuple' do
|
22
|
-
@
|
23
|
-
@
|
16
|
+
@res.should.respond_to :to_a
|
17
|
+
@res.to_a.should.equal [200, {'Date' => @now.httpdate}, []]
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#cache_control' do
|
21
|
+
it 'handles multiple name=value pairs' do
|
22
|
+
@res.headers['Cache-Control'] = 'max-age=600, max-stale=300, min-fresh=570'
|
23
|
+
@res.cache_control['max-age'].should.equal '600'
|
24
|
+
@res.cache_control['max-stale'].should.equal '300'
|
25
|
+
@res.cache_control['min-fresh'].should.equal '570'
|
26
|
+
end
|
27
|
+
it 'removes the header when given an empty hash' do
|
28
|
+
@res.headers['Cache-Control'] = 'max-age=600, must-revalidate'
|
29
|
+
@res.cache_control['max-age'].should.equal '600'
|
30
|
+
@res.cache_control = {}
|
31
|
+
@res.headers.should.not.include 'Cache-Control'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#validateable?' do
|
36
|
+
it 'is true when Last-Modified header present' do
|
37
|
+
@res = Rack::Cache::Response.new(200, {'Last-Modified' => @one_hour_ago.httpdate}, [])
|
38
|
+
@res.should.be.validateable
|
39
|
+
end
|
40
|
+
it 'is true when ETag header present' do
|
41
|
+
@res = Rack::Cache::Response.new(200, {'ETag' => '"12345"'}, [])
|
42
|
+
@res.should.be.validateable
|
43
|
+
end
|
44
|
+
it 'is false when no validator is present' do
|
45
|
+
@res = Rack::Cache::Response.new(200, {}, [])
|
46
|
+
@res.should.not.be.validateable
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#date' do
|
51
|
+
it 'uses the Date header if present' do
|
52
|
+
@res = Rack::Cache::Response.new(200, {'Date' => @one_hour_ago.httpdate}, [])
|
53
|
+
@res.date.should.equal @one_hour_ago
|
54
|
+
end
|
55
|
+
it 'uses the current time when no Date header present' do
|
56
|
+
@res = Rack::Cache::Response.new(200, {}, [])
|
57
|
+
@res.date.should.be.close Time.now, 1
|
58
|
+
end
|
59
|
+
it 'returns the correct date when the header is modified directly' do
|
60
|
+
@res = Rack::Cache::Response.new(200, { 'Date' => @one_hour_ago.httpdate }, [])
|
61
|
+
@res.date.should.equal @one_hour_ago
|
62
|
+
@res.headers['Date'] = @now.httpdate
|
63
|
+
@res.date.should.equal @now
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#max_age' do
|
68
|
+
it 'uses s-maxage cache control directive when present' do
|
69
|
+
@res.headers['Cache-Control'] = 's-maxage=600, max-age=0'
|
70
|
+
@res.max_age.should.equal 600
|
71
|
+
end
|
72
|
+
it 'falls back to max-age when no s-maxage directive present' do
|
73
|
+
@res.headers['Cache-Control'] = 'max-age=600'
|
74
|
+
@res.max_age.should.equal 600
|
75
|
+
end
|
76
|
+
it 'falls back to Expires when no max-age or s-maxage directive present' do
|
77
|
+
@res.headers['Cache-Control'] = 'must-revalidate'
|
78
|
+
@res.headers['Expires'] = @one_hour_later.httpdate
|
79
|
+
@res.max_age.should.equal 60 ** 2
|
80
|
+
end
|
81
|
+
it 'gives a #max_age of nil when no freshness information available' do
|
82
|
+
@res.max_age.should.be.nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#private=' do
|
87
|
+
it 'adds the private Cache-Control directive when set true' do
|
88
|
+
@res.headers['Cache-Control'] = 'max-age=100'
|
89
|
+
@res.private = true
|
90
|
+
@res.headers['Cache-Control'].split(', ').sort.
|
91
|
+
should.equal ['max-age=100', 'private']
|
92
|
+
end
|
93
|
+
it 'removes the public Cache-Control directive' do
|
94
|
+
@res.headers['Cache-Control'] = 'public, max-age=100'
|
95
|
+
@res.private = true
|
96
|
+
@res.headers['Cache-Control'].split(', ').sort.
|
97
|
+
should.equal ['max-age=100', 'private']
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#expire!' do
|
102
|
+
it 'sets the Age to be equal to the max-age' do
|
103
|
+
@res.headers['Cache-Control'] = 'max-age=100'
|
104
|
+
@res.expire!
|
105
|
+
@res.headers['Age'].should.equal '100'
|
106
|
+
end
|
107
|
+
it 'sets the Age to be equal to the s-maxage when both max-age and s-maxage present' do
|
108
|
+
@res.headers['Cache-Control'] = 'max-age=100, s-maxage=500'
|
109
|
+
@res.expire!
|
110
|
+
@res.headers['Age'].should.equal '500'
|
111
|
+
end
|
112
|
+
it 'does nothing when the response is already stale/expired' do
|
113
|
+
@res.headers['Cache-Control'] = 'max-age=5, s-maxage=500'
|
114
|
+
@res.headers['Age'] = '1000'
|
115
|
+
@res.expire!
|
116
|
+
@res.headers['Age'].should.equal '1000'
|
117
|
+
end
|
118
|
+
it 'does nothing when the response does not include freshness information' do
|
119
|
+
@res.expire!
|
120
|
+
@res.headers.should.not.include 'Age'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#ttl' do
|
125
|
+
it 'is nil when no Expires or Cache-Control headers present' do
|
126
|
+
@res.ttl.should.be.nil
|
127
|
+
end
|
128
|
+
it 'uses the Expires header when no max-age is present' do
|
129
|
+
@res.headers['Expires'] = (@res.now + (60**2)).httpdate
|
130
|
+
@res.ttl.should.be.close(60**2, 1)
|
131
|
+
end
|
132
|
+
it 'returns negative values when Expires is in part' do
|
133
|
+
@res.ttl.should.be.nil
|
134
|
+
@res.headers['Expires'] = @one_hour_ago.httpdate
|
135
|
+
@res.ttl.should.be < 0
|
136
|
+
end
|
137
|
+
it 'uses the Cache-Control max-age value when present' do
|
138
|
+
@res.headers['Cache-Control'] = 'max-age=60'
|
139
|
+
@res.ttl.should.be.close(60, 1)
|
140
|
+
end
|
24
141
|
end
|
25
142
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
143
|
+
describe '#vary' do
|
144
|
+
it 'is nil when no Vary header is present' do
|
145
|
+
@res.vary.should.be.nil
|
146
|
+
end
|
147
|
+
it 'returns the literal value of the Vary header' do
|
148
|
+
@res.headers['Vary'] = 'Foo Bar Baz'
|
149
|
+
@res.vary.should.equal 'Foo Bar Baz'
|
150
|
+
end
|
151
|
+
it 'can be checked for existence using the #vary? method' do
|
152
|
+
@res.should.respond_to :vary?
|
153
|
+
@res.should.not.vary
|
154
|
+
@res.headers['Vary'] = '*'
|
155
|
+
@res.should.vary
|
156
|
+
end
|
30
157
|
end
|
31
158
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
159
|
+
describe '#vary_header_names' do
|
160
|
+
it 'returns an empty Array when no Vary header is present' do
|
161
|
+
@res.vary_header_names.should.be.empty
|
162
|
+
end
|
163
|
+
it 'parses a single header name value' do
|
164
|
+
@res.headers['Vary'] = 'Accept-Language'
|
165
|
+
@res.vary_header_names.should.equal ['Accept-Language']
|
166
|
+
end
|
167
|
+
it 'parses multiple header name values separated by spaces' do
|
168
|
+
@res.headers['Vary'] = 'Accept-Language User-Agent X-Foo'
|
169
|
+
@res.vary_header_names.should.equal \
|
170
|
+
['Accept-Language', 'User-Agent', 'X-Foo']
|
171
|
+
end
|
172
|
+
it 'parses multiple header name values separated by commas' do
|
173
|
+
@res.headers['Vary'] = 'Accept-Language,User-Agent, X-Foo'
|
174
|
+
@res.vary_header_names.should.equal \
|
175
|
+
['Accept-Language', 'User-Agent', 'X-Foo']
|
176
|
+
end
|
36
177
|
end
|
37
178
|
end
|