rtomayko-rack-cache 0.3.0 → 0.3.9
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +41 -0
- data/README +0 -1
- data/TODO +14 -10
- data/doc/configuration.markdown +7 -153
- data/doc/index.markdown +1 -3
- 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 +188 -51
- 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 +29 -13
- 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 -14
- data/test/cache_test.rb +4 -1
- data/test/cachecontrol_test.rb +139 -0
- data/test/context_test.rb +198 -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 +6 -0
- metadata +13 -19
- 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
|