rack-cache 0.2.0
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 +27 -0
- data/COPYING +18 -0
- data/README +96 -0
- data/Rakefile +144 -0
- data/TODO +40 -0
- data/doc/configuration.markdown +224 -0
- data/doc/events.dot +27 -0
- data/doc/faq.markdown +133 -0
- data/doc/index.markdown +113 -0
- data/doc/layout.html.erb +33 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/storage.markdown +162 -0
- data/lib/rack/cache.rb +51 -0
- data/lib/rack/cache/config.rb +65 -0
- data/lib/rack/cache/config/busters.rb +16 -0
- data/lib/rack/cache/config/default.rb +134 -0
- data/lib/rack/cache/config/no-cache.rb +13 -0
- data/lib/rack/cache/context.rb +95 -0
- data/lib/rack/cache/core.rb +271 -0
- data/lib/rack/cache/entitystore.rb +224 -0
- data/lib/rack/cache/headers.rb +237 -0
- data/lib/rack/cache/metastore.rb +309 -0
- data/lib/rack/cache/options.rb +119 -0
- data/lib/rack/cache/request.rb +37 -0
- data/lib/rack/cache/response.rb +76 -0
- data/lib/rack/cache/storage.rb +50 -0
- data/lib/rack/utils/environment_headers.rb +78 -0
- data/rack-cache.gemspec +74 -0
- data/test/cache_test.rb +35 -0
- data/test/config_test.rb +66 -0
- data/test/context_test.rb +465 -0
- data/test/core_test.rb +84 -0
- data/test/entitystore_test.rb +176 -0
- data/test/environment_headers_test.rb +71 -0
- data/test/headers_test.rb +215 -0
- data/test/logging_test.rb +45 -0
- data/test/metastore_test.rb +210 -0
- data/test/options_test.rb +64 -0
- data/test/pony.jpg +0 -0
- data/test/response_test.rb +37 -0
- data/test/spec_setup.rb +189 -0
- data/test/storage_test.rb +94 -0
- metadata +120 -0
data/test/core_test.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/core'
|
3
|
+
|
4
|
+
class MockCore
|
5
|
+
include Rack::Cache::Core
|
6
|
+
alias_method :initialize, :initialize_core
|
7
|
+
public :transition, :trigger, :events
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'Rack::Cache::Core' do
|
11
|
+
before :each do
|
12
|
+
@core = MockCore.new
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'has events after instantiation' do
|
16
|
+
@core.events.should.respond_to :[]
|
17
|
+
end
|
18
|
+
it 'defines and triggers event handlers' do
|
19
|
+
executed = false
|
20
|
+
@core.on(:foo) { executed = true }
|
21
|
+
@core.trigger :foo
|
22
|
+
executed.should.be true
|
23
|
+
end
|
24
|
+
it 'executes multiple handlers in LIFO order' do
|
25
|
+
x = 'nothing executed'
|
26
|
+
@core.on :foo do
|
27
|
+
x.should.be == 'bottom executed'
|
28
|
+
x = 'top executed'
|
29
|
+
end
|
30
|
+
@core.on :foo do
|
31
|
+
x.should.be == 'nothing executed'
|
32
|
+
x = 'bottom executed'
|
33
|
+
end
|
34
|
+
@core.trigger :foo
|
35
|
+
x.should.be == 'top executed'
|
36
|
+
end
|
37
|
+
it 'records event execution history' do
|
38
|
+
@core.on(:foo) {}
|
39
|
+
@core.trigger :foo
|
40
|
+
@core.should.a.performed :foo
|
41
|
+
end
|
42
|
+
it 'raises an exception when asked to perform an unknown event' do
|
43
|
+
assert_raises NameError do
|
44
|
+
@core.trigger :foo
|
45
|
+
end
|
46
|
+
end
|
47
|
+
it 'raises an exception when asked to transition to an unknown event' do
|
48
|
+
@core.on(:bling) {}
|
49
|
+
@core.on(:foo) { throw(:transition, [:bling]) }
|
50
|
+
lambda { @core.transition(from=:foo, to=[:bar, :baz]) }.
|
51
|
+
should.raise Rack::Cache::IllegalTransition
|
52
|
+
end
|
53
|
+
it 'passes transition arguments to handlers' do
|
54
|
+
passed = nil
|
55
|
+
@core.meta_def(:perform_bar) do |*args|
|
56
|
+
passed = args
|
57
|
+
'hi'
|
58
|
+
end
|
59
|
+
@core.on(:bar) {}
|
60
|
+
@core.on(:foo) { throw(:transition, [:bar, 1, 2, 3]) }
|
61
|
+
result = @core.transition(from=:foo, to=[:bar])
|
62
|
+
passed.should.be == [1,2,3]
|
63
|
+
result.should.be == 'hi'
|
64
|
+
end
|
65
|
+
it 'fully transitions out of handlers when the next event is invoked' do
|
66
|
+
x = []
|
67
|
+
@core.on(:foo) {
|
68
|
+
x << 'in foo, before transitioning to bar'
|
69
|
+
throw(:transition, [:bar])
|
70
|
+
x << 'in foo, after transitioning to bar'
|
71
|
+
}
|
72
|
+
@core.on(:bar) { x << 'in bar' }
|
73
|
+
@core.trigger(:foo).should.be == [:bar]
|
74
|
+
@core.trigger(:bar).should.be.nil
|
75
|
+
x.should.be == [
|
76
|
+
'in foo, before transitioning to bar',
|
77
|
+
'in bar'
|
78
|
+
]
|
79
|
+
end
|
80
|
+
it 'returns the transition event name' do
|
81
|
+
@core.on(:foo) { throw(:transition, [:bar]) }
|
82
|
+
@core.trigger(:foo).should.be == [:bar]
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/entitystore'
|
3
|
+
|
4
|
+
class Object
|
5
|
+
def sha_like?
|
6
|
+
length == 40 && self =~ /^[0-9a-z]+$/
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe_shared 'A Rack::Cache::EntityStore Implementation' do
|
11
|
+
|
12
|
+
it 'responds to all required messages' do
|
13
|
+
%w[read open write exist?].each do |message|
|
14
|
+
@store.should.respond_to message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'stores bodies with #write' do
|
19
|
+
key, size = @store.write('My wild love went riding,')
|
20
|
+
key.should.not.be.nil
|
21
|
+
key.should.be.sha_like
|
22
|
+
|
23
|
+
data = @store.read(key)
|
24
|
+
data.should.be == 'My wild love went riding,'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'correctly determines whether cached body exists for key with #exist?' do
|
28
|
+
key, size = @store.write('She rode to the devil,')
|
29
|
+
@store.should.exist key
|
30
|
+
@store.should.not.exist '938jasddj83jasdh4438021ksdfjsdfjsdsf'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can read data written with #write' do
|
34
|
+
key, size = @store.write('And asked him to pay.')
|
35
|
+
data = @store.read(key)
|
36
|
+
data.should.be == 'And asked him to pay.'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'gives a 40 character SHA1 hex digest from #write' do
|
40
|
+
key, size = @store.write('she rode to the sea;')
|
41
|
+
key.should.not.be.nil
|
42
|
+
key.length.should.be == 40
|
43
|
+
key.should.be =~ /^[0-9a-z]+$/
|
44
|
+
key.should.be == '90a4c84d51a277f3dafc34693ca264531b9f51b6'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns the entire body as a String from #read' do
|
48
|
+
key, size = @store.write('She gathered together')
|
49
|
+
@store.read(key).should.be == 'She gathered together'
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns nil from #read when key does not exist' do
|
53
|
+
@store.read('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns a Rack compatible body from #open' do
|
57
|
+
key, size = @store.write('Some shells for her hair.')
|
58
|
+
body = @store.open(key)
|
59
|
+
body.should.respond_to :each
|
60
|
+
buf = ''
|
61
|
+
body.each { |part| buf << part }
|
62
|
+
buf.should.be == 'Some shells for her hair.'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'returns nil from #open when key does not exist' do
|
66
|
+
@store.open('87fe0a1ae82a518592f6b12b0183e950b4541c62').should.be.nil
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'can store largish bodies with binary data' do
|
70
|
+
pony = File.read(File.dirname(__FILE__) + '/pony.jpg')
|
71
|
+
key, size = @store.write(pony)
|
72
|
+
key.should.be == 'd0f30d8659b4d268c5c64385d9790024c2d78deb'
|
73
|
+
data = @store.read(key)
|
74
|
+
data.length.should.be == pony.length
|
75
|
+
data.hash.should.be == pony.hash
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'Rack::Cache::EntityStore' do
|
81
|
+
|
82
|
+
describe 'Heap' do
|
83
|
+
it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
|
84
|
+
before { @store = Rack::Cache::EntityStore::Heap.new }
|
85
|
+
it 'takes a Hash to ::new' do
|
86
|
+
@store = Rack::Cache::EntityStore::Heap.new('foo' => ['bar'])
|
87
|
+
@store.read('foo').should.be == 'bar'
|
88
|
+
end
|
89
|
+
it 'uses its own Hash with no args to ::new' do
|
90
|
+
@store.read('foo').should.be.nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'Disk' do
|
95
|
+
it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
|
96
|
+
before do
|
97
|
+
@temp_dir = create_temp_directory
|
98
|
+
@store = Rack::Cache::EntityStore::Disk.new(@temp_dir)
|
99
|
+
end
|
100
|
+
after do
|
101
|
+
@store = nil
|
102
|
+
remove_entry_secure @temp_dir
|
103
|
+
end
|
104
|
+
it 'takes a path to ::new and creates the directory' do
|
105
|
+
path = @temp_dir + '/foo'
|
106
|
+
@store = Rack::Cache::EntityStore::Disk.new(path)
|
107
|
+
File.should.be.a.directory path
|
108
|
+
end
|
109
|
+
it 'spreads data over a 36² hash radius' do
|
110
|
+
(<<-PROSE).each { |line| @store.write(line).first.should.be.sha_like }
|
111
|
+
My wild love went riding,
|
112
|
+
She rode all the day;
|
113
|
+
She rode to the devil,
|
114
|
+
And asked him to pay.
|
115
|
+
|
116
|
+
The devil was wiser
|
117
|
+
It's time to repent;
|
118
|
+
He asked her to give back
|
119
|
+
The money she spent
|
120
|
+
|
121
|
+
My wild love went riding,
|
122
|
+
She rode to sea;
|
123
|
+
She gathered together
|
124
|
+
Some shells for her hair
|
125
|
+
|
126
|
+
She rode on to Christmas,
|
127
|
+
She rode to the farm;
|
128
|
+
She rode to Japan
|
129
|
+
And re-entered a town
|
130
|
+
|
131
|
+
My wild love is crazy
|
132
|
+
She screams like a bird;
|
133
|
+
She moans like a cat
|
134
|
+
When she wants to be heard
|
135
|
+
|
136
|
+
She rode and she rode on
|
137
|
+
She rode for a while,
|
138
|
+
Then stopped for an evening
|
139
|
+
And laid her head down
|
140
|
+
|
141
|
+
By this time the weather
|
142
|
+
Had changed one degree,
|
143
|
+
She asked for the people
|
144
|
+
To let her go free
|
145
|
+
|
146
|
+
My wild love went riding,
|
147
|
+
She rode for an hour;
|
148
|
+
She rode and she rested,
|
149
|
+
And then she rode on
|
150
|
+
My wild love went riding,
|
151
|
+
PROSE
|
152
|
+
subdirs = Dir["#{@temp_dir}/*"]
|
153
|
+
subdirs.each do |subdir|
|
154
|
+
File.basename(subdir).should.be =~ /^[0-9a-z]{2}$/
|
155
|
+
files = Dir["#{subdir}/*"]
|
156
|
+
files.each do |filename|
|
157
|
+
File.basename(filename).should.be =~ /^[0-9a-z]{38}$/
|
158
|
+
end
|
159
|
+
files.length.should.be > 0
|
160
|
+
end
|
161
|
+
subdirs.length.should.be == 28
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
need_memcached 'entity store tests' do
|
166
|
+
describe 'MemCache' do
|
167
|
+
it_should_behave_like 'A Rack::Cache::EntityStore Implementation'
|
168
|
+
before do
|
169
|
+
@store = Rack::Cache::EntityStore::MemCache.new($memcached)
|
170
|
+
end
|
171
|
+
after do
|
172
|
+
@store = nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/utils/environment_headers'
|
3
|
+
|
4
|
+
describe 'Rack::Utils::EnvironmentHeaders' do
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
@now = Time.now.httpdate
|
8
|
+
@env = {
|
9
|
+
'CONTENT_TYPE' => 'text/plain',
|
10
|
+
'CONTENT_LENGTH' => '0x1A4',
|
11
|
+
'HTTP_X_FOO' => 'BAR',
|
12
|
+
'HTTP_IF_MODIFIED_SINCE' => @now,
|
13
|
+
'rack.run_once' => true
|
14
|
+
}
|
15
|
+
@h = Rack::Utils::EnvironmentHeaders.new(@env)
|
16
|
+
end
|
17
|
+
|
18
|
+
after(:each) {
|
19
|
+
@env, @h = nil, nil
|
20
|
+
}
|
21
|
+
|
22
|
+
it 'retrieves headers with #[]' do
|
23
|
+
@h.should.respond_to :[]
|
24
|
+
@h['X-Foo'].should.be == 'BAR'
|
25
|
+
@h['If-Modified-Since'].should.be == @now
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sets headers with #[]=' do
|
29
|
+
@h.should.respond_to :[]=
|
30
|
+
@h['X-Foo'] = 'BAZZLE'
|
31
|
+
@h['X-Foo'].should.be == 'BAZZLE'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'sets values on the underlying environment hash' do
|
35
|
+
@h['X-Something-Else'] = 'FOO'
|
36
|
+
@env['HTTP_X_SOMETHING_ELSE'].should.be == 'FOO'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'handles Content-Type special case' do
|
40
|
+
@h['Content-Type'].should.be == 'text/plain'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'handles Content-Length special case' do
|
44
|
+
@h['Content-Length'].should.be == '0x1A4'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'implements #include? with RFC 2616 header name' do
|
48
|
+
@h.should.include 'If-Modified-Since'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'deletes underlying env entries' do
|
52
|
+
@h.delete('X-Foo')
|
53
|
+
@env.should.not.include? 'HTTP_X_FOO'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns the underlying environment hash with #to_env' do
|
57
|
+
@h.to_env.should.be @env
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'iterates over all headers with #each' do
|
61
|
+
hash = {}
|
62
|
+
@h.each { |name,value| hash[name] = value }
|
63
|
+
hash.should.be == {
|
64
|
+
'Content-Type' => 'text/plain',
|
65
|
+
'Content-Length' => '0x1A4',
|
66
|
+
'X-Foo' => 'BAR',
|
67
|
+
'If-Modified-Since' => @now
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
|
3
|
+
class MockResponse < Rack::MockResponse
|
4
|
+
include Rack::Cache::Headers
|
5
|
+
include Rack::Cache::ResponseHeaders
|
6
|
+
public :now
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'Rack::Cache::Headers' do
|
10
|
+
before :each do
|
11
|
+
@now = Time.now
|
12
|
+
@res = MockResponse.new(200, {'Date' => @now.httpdate}, '')
|
13
|
+
@one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
|
14
|
+
end
|
15
|
+
after :each do
|
16
|
+
@now, @res, @one_hour_ago = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#cache_control' do
|
20
|
+
it 'handles single name=value pair' do
|
21
|
+
@res.headers['Cache-Control'] = 'max-age=600'
|
22
|
+
@res.cache_control['max-age'].should.be == '600'
|
23
|
+
end
|
24
|
+
it 'handles multiple name=value pairs' do
|
25
|
+
@res.headers['Cache-Control'] = 'max-age=600, max-stale=300, min-fresh=570'
|
26
|
+
@res.cache_control['max-age'].should.be == '600'
|
27
|
+
@res.cache_control['max-stale'].should.be == '300'
|
28
|
+
@res.cache_control['min-fresh'].should.be == '570'
|
29
|
+
end
|
30
|
+
it 'handles a single flag value' do
|
31
|
+
@res.headers['Cache-Control'] = 'no-cache'
|
32
|
+
@res.cache_control.should.include 'no-cache'
|
33
|
+
@res.cache_control['no-cache'].should.be true
|
34
|
+
end
|
35
|
+
it 'handles a bunch of all kinds of stuff' do
|
36
|
+
@res.headers['Cache-Control'] = 'max-age=600,must-revalidate,min-fresh=3000,foo=bar,baz'
|
37
|
+
@res.cache_control['max-age'].should.be == '600'
|
38
|
+
@res.cache_control['must-revalidate'].should.be true
|
39
|
+
@res.cache_control['min-fresh'].should.be == '3000'
|
40
|
+
@res.cache_control['foo'].should.be == 'bar'
|
41
|
+
@res.cache_control['baz'].should.be true
|
42
|
+
end
|
43
|
+
it 'removes the header when given an empty hash' do
|
44
|
+
@res.headers['Cache-Control'] = 'max-age=600, must-revalidate'
|
45
|
+
@res.cache_control['max-age'].should.be == '600'
|
46
|
+
@res.cache_control = {}
|
47
|
+
@res.headers.should.not.include 'Cache-Control'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'Rack::Cache::ResponseHeaders' do
|
53
|
+
before :each do
|
54
|
+
@now = Time.now
|
55
|
+
@one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
|
56
|
+
@one_hour_later = Time.httpdate((Time.now + (60**2)).httpdate)
|
57
|
+
@res = MockResponse.new(200, {'Date' => @now.httpdate}, '')
|
58
|
+
end
|
59
|
+
after :each do
|
60
|
+
@now, @res, @one_hour_ago = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#validateable?' do
|
64
|
+
it 'is true when Last-Modified header present' do
|
65
|
+
@res = MockResponse.new(200, { 'Last-Modified' => @one_hour_ago.httpdate }, '')
|
66
|
+
@res.extend Rack::Cache::ResponseHeaders
|
67
|
+
@res.should.be.validateable
|
68
|
+
end
|
69
|
+
it 'is true when Etag header present' do
|
70
|
+
@res = MockResponse.new(200, { 'Etag' => '"12345"' }, '')
|
71
|
+
@res.extend Rack::Cache::ResponseHeaders
|
72
|
+
@res.should.be.validateable
|
73
|
+
end
|
74
|
+
it 'is false when no validator is present' do
|
75
|
+
@res = MockResponse.new(200, {}, '')
|
76
|
+
@res.extend Rack::Cache::ResponseHeaders
|
77
|
+
@res.should.not.be.validateable
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '#date' do
|
82
|
+
it 'uses the Date header if present' do
|
83
|
+
@res = MockResponse.new(200, { 'Date' => @one_hour_ago.httpdate }, '')
|
84
|
+
@res.extend Rack::Cache::ResponseHeaders
|
85
|
+
@res.date.should.be == @one_hour_ago
|
86
|
+
end
|
87
|
+
it 'uses the current time when no Date header present' do
|
88
|
+
@res = MockResponse.new(200, {}, '')
|
89
|
+
@res.extend Rack::Cache::ResponseHeaders
|
90
|
+
@res.date.should.be.close Time.now, 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#expires_at' do
|
95
|
+
it 'returns #date + #max_age when Cache-Control/max-age is present' do
|
96
|
+
@res.headers['Cache-Control'] = 'max-age=500'
|
97
|
+
@res.expires_at.should.be == @res.date + 500
|
98
|
+
end
|
99
|
+
it 'uses the Expires header when present and no Cache-Control/max-age' do
|
100
|
+
@res.headers['Expires'] = @one_hour_ago.httpdate
|
101
|
+
@res.expires_at.should.be == @one_hour_ago
|
102
|
+
end
|
103
|
+
it 'returns nil when no Expires or Cache-Control provided' do
|
104
|
+
@res.expires_at.should.be nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#max_age' do
|
109
|
+
it 'uses Cache-Control to calculate #max_age when present' do
|
110
|
+
@res.headers['Cache-Control'] = 'max-age=600'
|
111
|
+
@res.max_age.should.be == 600
|
112
|
+
end
|
113
|
+
it 'uses Expires for #max_age if no Cache-Control max-age present' do
|
114
|
+
@res.headers['Cache-Control'] = 'must-revalidate'
|
115
|
+
@res.headers['Expires'] = @one_hour_later.httpdate
|
116
|
+
@res.max_age.should.be == 60 ** 2
|
117
|
+
end
|
118
|
+
it 'gives a #max_age of nil when no freshness information available' do
|
119
|
+
@res.max_age.should.be.nil
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe '#freshness_information?' do
|
124
|
+
it 'is true when Expires header is present' do
|
125
|
+
@res.headers['Expires'] = Time.now.httpdate
|
126
|
+
@res.freshness_information?.should.be true
|
127
|
+
end
|
128
|
+
it 'is true when a Cache-Control max-age directive is present' do
|
129
|
+
@res.headers['Cache-Control'] = 'max-age=500'
|
130
|
+
@res.freshness_information?.should.be true
|
131
|
+
end
|
132
|
+
it 'is not true otherwise' do
|
133
|
+
@res.freshness_information?.should.be false
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '#no_cache?' do
|
138
|
+
it 'is true when a Cache-Control no-cache directive is present' do
|
139
|
+
@res.headers['Cache-Control'] = 'no-cache'
|
140
|
+
@res.no_cache?.should.be true
|
141
|
+
end
|
142
|
+
it 'is false otherwise' do
|
143
|
+
@res.no_cache?.should.be false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '#stale?' do
|
148
|
+
it 'is true when TTL cannot be established' do
|
149
|
+
@res.should.be.stale
|
150
|
+
end
|
151
|
+
it 'is false when the TTL is <= 0' do
|
152
|
+
@res.headers['Expires'] = (@res.now + 10).httpdate
|
153
|
+
@res.should.not.be.stale
|
154
|
+
end
|
155
|
+
it 'is true when the TTL is >= 0' do
|
156
|
+
@res.headers['Expires'] = (@res.now - 10).httpdate
|
157
|
+
@res.should.be.stale
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#ttl' do
|
162
|
+
it 'is nil when no Expires or Cache-Control headers present' do
|
163
|
+
@res.ttl.should.be.nil
|
164
|
+
end
|
165
|
+
it 'uses the Expires header when no max-age is present' do
|
166
|
+
@res.headers['Expires'] = (@res.now + (60**2)).httpdate
|
167
|
+
@res.ttl.should.be.close(60**2, 1)
|
168
|
+
end
|
169
|
+
it 'returns negative values when Expires is in part' do
|
170
|
+
@res.ttl.should.be.nil
|
171
|
+
@res.headers['Expires'] = @one_hour_ago.httpdate
|
172
|
+
@res.ttl.should.be < 0
|
173
|
+
end
|
174
|
+
it 'uses the Cache-Control max-age value when present' do
|
175
|
+
@res.headers['Cache-Control'] = 'max-age=60'
|
176
|
+
@res.ttl.should.be.close(60, 1)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe '#vary' do
|
181
|
+
it 'is nil when no Vary header is present' do
|
182
|
+
@res.vary.should.be.nil
|
183
|
+
end
|
184
|
+
it 'returns the literal value of the Vary header' do
|
185
|
+
@res.headers['Vary'] = 'Foo Bar Baz'
|
186
|
+
@res.vary.should.be == 'Foo Bar Baz'
|
187
|
+
end
|
188
|
+
it 'can be checked for existence using the #vary? method' do
|
189
|
+
@res.should.respond_to :vary?
|
190
|
+
@res.should.not.vary
|
191
|
+
@res.headers['Vary'] = '*'
|
192
|
+
@res.should.vary
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe '#vary_header_names' do
|
197
|
+
it 'returns an empty Array when no Vary header is present' do
|
198
|
+
@res.vary_header_names.should.be.empty
|
199
|
+
end
|
200
|
+
it 'parses a single header name value' do
|
201
|
+
@res.headers['Vary'] = 'Accept-Language'
|
202
|
+
@res.vary_header_names.should.be == ['Accept-Language']
|
203
|
+
end
|
204
|
+
it 'parses multiple header name values separated by spaces' do
|
205
|
+
@res.headers['Vary'] = 'Accept-Language User-Agent X-Foo'
|
206
|
+
@res.vary_header_names.should.be ==
|
207
|
+
['Accept-Language', 'User-Agent', 'X-Foo']
|
208
|
+
end
|
209
|
+
it 'parses multiple header name values separated by commas' do
|
210
|
+
@res.headers['Vary'] = 'Accept-Language,User-Agent, X-Foo'
|
211
|
+
@res.vary_header_names.should.be ==
|
212
|
+
['Accept-Language', 'User-Agent', 'X-Foo']
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|