rack-cache 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
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
|