rack-cache 0.3.0 → 0.4
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 +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/lib/rack/cache/storage.rb
CHANGED
@@ -3,11 +3,11 @@ require 'rack/cache/metastore'
|
|
3
3
|
require 'rack/cache/entitystore'
|
4
4
|
|
5
5
|
module Rack::Cache
|
6
|
+
|
6
7
|
# Maintains a collection of MetaStore and EntityStore instances keyed by
|
7
8
|
# URI. A single instance of this class can be used across a single process
|
8
9
|
# to ensure that only a single instance of a backing store is created per
|
9
10
|
# unique storage URI.
|
10
|
-
|
11
11
|
class Storage
|
12
12
|
def initialize
|
13
13
|
@metastores = {}
|
@@ -22,7 +22,6 @@ module Rack::Cache
|
|
22
22
|
@entitystores[uri.to_s] ||= create_store(EntityStore, uri)
|
23
23
|
end
|
24
24
|
|
25
|
-
# Clear store instances.
|
26
25
|
def clear
|
27
26
|
@metastores.clear
|
28
27
|
@entitystores.clear
|
data/rack-cache.gemspec
CHANGED
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
|
|
3
3
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
4
|
|
5
5
|
s.name = 'rack-cache'
|
6
|
-
s.version = '0.
|
7
|
-
s.date = '
|
6
|
+
s.version = '0.4'
|
7
|
+
s.date = '2009-03-16'
|
8
8
|
|
9
9
|
s.description = "HTTP Caching for Rack"
|
10
10
|
s.summary = "HTTP Caching for Rack"
|
@@ -20,7 +20,6 @@ Gem::Specification.new do |s|
|
|
20
20
|
Rakefile
|
21
21
|
TODO
|
22
22
|
doc/configuration.markdown
|
23
|
-
doc/events.dot
|
24
23
|
doc/faq.markdown
|
25
24
|
doc/index.markdown
|
26
25
|
doc/layout.html.erb
|
@@ -28,33 +27,28 @@ Gem::Specification.new do |s|
|
|
28
27
|
doc/rack-cache.css
|
29
28
|
doc/server.ru
|
30
29
|
doc/storage.markdown
|
30
|
+
example/sinatra/app.rb
|
31
|
+
example/sinatra/views/index.erb
|
31
32
|
lib/rack/cache.rb
|
32
|
-
lib/rack/cache/
|
33
|
-
lib/rack/cache/config/busters.rb
|
34
|
-
lib/rack/cache/config/default.rb
|
35
|
-
lib/rack/cache/config/no-cache.rb
|
33
|
+
lib/rack/cache/cachecontrol.rb
|
36
34
|
lib/rack/cache/context.rb
|
37
|
-
lib/rack/cache/core.rb
|
38
35
|
lib/rack/cache/entitystore.rb
|
39
|
-
lib/rack/cache/
|
36
|
+
lib/rack/cache/key.rb
|
40
37
|
lib/rack/cache/metastore.rb
|
41
38
|
lib/rack/cache/options.rb
|
42
39
|
lib/rack/cache/request.rb
|
43
40
|
lib/rack/cache/response.rb
|
44
41
|
lib/rack/cache/storage.rb
|
45
|
-
lib/rack/utils/environment_headers.rb
|
46
42
|
rack-cache.gemspec
|
47
43
|
test/cache_test.rb
|
48
|
-
test/
|
44
|
+
test/cachecontrol_test.rb
|
49
45
|
test/context_test.rb
|
50
|
-
test/core_test.rb
|
51
46
|
test/entitystore_test.rb
|
52
|
-
test/
|
53
|
-
test/headers_test.rb
|
54
|
-
test/logging_test.rb
|
47
|
+
test/key_test.rb
|
55
48
|
test/metastore_test.rb
|
56
49
|
test/options_test.rb
|
57
50
|
test/pony.jpg
|
51
|
+
test/request_test.rb
|
58
52
|
test/response_test.rb
|
59
53
|
test/spec_setup.rb
|
60
54
|
test/storage_test.rb
|
data/test/cache_test.rb
CHANGED
@@ -12,24 +12,27 @@ describe 'Rack::Cache::new' do
|
|
12
12
|
Rack::Cache.new(@app).
|
13
13
|
should.respond_to :call
|
14
14
|
end
|
15
|
+
|
15
16
|
it 'takes an options Hash' do
|
16
17
|
lambda { Rack::Cache.new(@app, {}) }.
|
17
18
|
should.not.raise(ArgumentError)
|
18
19
|
end
|
20
|
+
|
19
21
|
it 'sets options provided in the options Hash' do
|
20
22
|
object = Rack::Cache.new(@app, :foo => 'bar', 'foo.bar' => 'bling')
|
21
23
|
object.options['foo.bar'].should.equal 'bling'
|
22
24
|
object.options['rack-cache.foo'].should.equal 'bar'
|
23
25
|
end
|
26
|
+
|
24
27
|
it 'takes a block; executes it during initialization' do
|
25
|
-
state,
|
26
|
-
|
27
|
-
Rack::Cache.new @app do
|
28
|
-
|
28
|
+
state, object = 'not invoked', nil
|
29
|
+
instance =
|
30
|
+
Rack::Cache.new @app do |cache|
|
31
|
+
object = cache
|
29
32
|
state = 'invoked'
|
30
|
-
should.respond_to :
|
33
|
+
cache.should.respond_to :set
|
31
34
|
end
|
32
35
|
state.should.equal 'invoked'
|
33
|
-
object.should.be
|
36
|
+
object.should.be instance
|
34
37
|
end
|
35
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
|
data/test/context_test.rb
CHANGED
@@ -11,21 +11,31 @@ describe 'Rack::Cache::Context' do
|
|
11
11
|
|
12
12
|
app.should.be.called
|
13
13
|
response.should.be.ok
|
14
|
-
cache.should.
|
14
|
+
cache.trace.should.include :pass
|
15
15
|
response.headers.should.not.include 'Age'
|
16
16
|
end
|
17
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
|
+
|
18
30
|
it 'does not cache with Authorization request header and non public response' do
|
19
|
-
respond_with 200, '
|
31
|
+
respond_with 200, 'ETag' => '"FOO"'
|
20
32
|
get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
|
21
33
|
|
22
34
|
app.should.be.called
|
23
35
|
response.should.be.ok
|
24
36
|
response.headers['Cache-Control'].should.equal 'private'
|
25
|
-
cache.should.
|
26
|
-
cache.should.
|
27
|
-
cache.should.a.not.performed :store
|
28
|
-
cache.should.a.performed :deliver
|
37
|
+
cache.trace.should.include :miss
|
38
|
+
cache.trace.should.not.include :store
|
29
39
|
response.headers.should.not.include 'Age'
|
30
40
|
end
|
31
41
|
|
@@ -35,24 +45,21 @@ describe 'Rack::Cache::Context' do
|
|
35
45
|
|
36
46
|
app.should.be.called
|
37
47
|
response.should.be.ok
|
38
|
-
cache.should.
|
39
|
-
cache.should.
|
40
|
-
cache.should.a.performed :store
|
48
|
+
cache.trace.should.include :miss
|
49
|
+
cache.trace.should.include :store
|
41
50
|
response.headers.should.include 'Age'
|
42
51
|
response.headers['Cache-Control'].should.equal 'public'
|
43
52
|
end
|
44
53
|
|
45
54
|
it 'does not cache with Cookie header and non public response' do
|
46
|
-
respond_with 200, '
|
55
|
+
respond_with 200, 'ETag' => '"FOO"'
|
47
56
|
get '/', 'HTTP_COOKIE' => 'foo=bar'
|
48
57
|
|
49
58
|
app.should.be.called
|
50
59
|
response.should.be.ok
|
51
60
|
response.headers['Cache-Control'].should.equal 'private'
|
52
|
-
cache.should.
|
53
|
-
cache.should.
|
54
|
-
cache.should.a.not.performed :store
|
55
|
-
cache.should.a.performed :deliver
|
61
|
+
cache.trace.should.include :miss
|
62
|
+
cache.trace.should.not.include :store
|
56
63
|
response.headers.should.not.include 'Age'
|
57
64
|
end
|
58
65
|
|
@@ -62,9 +69,8 @@ describe 'Rack::Cache::Context' do
|
|
62
69
|
|
63
70
|
response.should.be.ok
|
64
71
|
app.should.be.called
|
65
|
-
cache.should.
|
66
|
-
cache.should.
|
67
|
-
cache.should.a.performed :deliver
|
72
|
+
cache.trace.should.include :miss
|
73
|
+
cache.trace.should.not.include :store
|
68
74
|
response.headers.should.not.include 'Age'
|
69
75
|
response.headers['Cache-Control'].should.equal 'private'
|
70
76
|
end
|
@@ -85,14 +91,14 @@ describe 'Rack::Cache::Context' do
|
|
85
91
|
response.headers.should.not.include 'Content-Length'
|
86
92
|
response.headers.should.not.include 'Content-Type'
|
87
93
|
response.body.should.empty
|
88
|
-
cache.should.
|
89
|
-
cache.should.
|
94
|
+
cache.trace.should.include :miss
|
95
|
+
cache.trace.should.include :store
|
90
96
|
end
|
91
97
|
|
92
98
|
it 'responds with 304 when If-None-Match matches ETag' do
|
93
99
|
respond_with do |req,res|
|
94
100
|
res.status = 200
|
95
|
-
res['
|
101
|
+
res['ETag'] = '12345'
|
96
102
|
res['Content-Type'] = 'text/plain'
|
97
103
|
res.body = ['Hello World']
|
98
104
|
end
|
@@ -103,28 +109,131 @@ describe 'Rack::Cache::Context' do
|
|
103
109
|
response.status.should.equal 304
|
104
110
|
response.headers.should.not.include 'Content-Length'
|
105
111
|
response.headers.should.not.include 'Content-Type'
|
106
|
-
response.headers.should.include '
|
112
|
+
response.headers.should.include 'ETag'
|
107
113
|
response.body.should.empty
|
108
|
-
cache.should.
|
109
|
-
cache.should.
|
114
|
+
cache.trace.should.include :miss
|
115
|
+
cache.trace.should.include :store
|
110
116
|
end
|
111
117
|
|
112
|
-
it '
|
118
|
+
it 'stores responses when no-cache request directive present' do
|
113
119
|
respond_with 200, 'Expires' => (Time.now + 5).httpdate
|
114
|
-
get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
|
115
120
|
|
121
|
+
get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
|
116
122
|
response.should.be.ok
|
117
|
-
cache.should.
|
123
|
+
cache.trace.should.include :store
|
118
124
|
response.headers.should.include 'Age'
|
119
125
|
end
|
120
126
|
|
127
|
+
it 'reloads responses when cache hits but no-cache request directive present' do
|
128
|
+
count = 0
|
129
|
+
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
|
130
|
+
count+= 1
|
131
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
132
|
+
end
|
133
|
+
|
134
|
+
get '/'
|
135
|
+
response.should.be.ok
|
136
|
+
response.body.should.equal 'Hello World'
|
137
|
+
cache.trace.should.include :store
|
138
|
+
|
139
|
+
get '/'
|
140
|
+
response.should.be.ok
|
141
|
+
response.body.should.equal 'Hello World'
|
142
|
+
cache.trace.should.include :fresh
|
143
|
+
|
144
|
+
get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
|
145
|
+
response.should.be.ok
|
146
|
+
response.body.should.equal 'Goodbye World'
|
147
|
+
cache.trace.should.include :reload
|
148
|
+
cache.trace.should.include :store
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'does not reload responses when allow_reload is set false' do
|
152
|
+
count = 0
|
153
|
+
respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
|
154
|
+
count+= 1
|
155
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
156
|
+
end
|
157
|
+
|
158
|
+
get '/'
|
159
|
+
response.should.be.ok
|
160
|
+
response.body.should.equal 'Hello World'
|
161
|
+
cache.trace.should.include :store
|
162
|
+
|
163
|
+
get '/'
|
164
|
+
response.should.be.ok
|
165
|
+
response.body.should.equal 'Hello World'
|
166
|
+
cache.trace.should.include :fresh
|
167
|
+
|
168
|
+
get '/',
|
169
|
+
'rack-cache.allow_reload' => false,
|
170
|
+
'HTTP_CACHE_CONTROL' => 'no-cache'
|
171
|
+
response.should.be.ok
|
172
|
+
response.body.should.equal 'Hello World'
|
173
|
+
cache.trace.should.not.include :reload
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'revalidates fresh cache entry when max-age request directive is exceeded' do
|
177
|
+
count = 0
|
178
|
+
respond_with do |req,res|
|
179
|
+
count+= 1
|
180
|
+
res['Cache-Control'] = 'max-age=10000'
|
181
|
+
res['ETag'] = count.to_s
|
182
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
183
|
+
end
|
184
|
+
|
185
|
+
get '/'
|
186
|
+
response.should.be.ok
|
187
|
+
response.body.should.equal 'Hello World'
|
188
|
+
cache.trace.should.include :store
|
189
|
+
|
190
|
+
get '/'
|
191
|
+
response.should.be.ok
|
192
|
+
response.body.should.equal 'Hello World'
|
193
|
+
cache.trace.should.include :fresh
|
194
|
+
|
195
|
+
get '/', 'HTTP_CACHE_CONTROL' => 'max-age=0'
|
196
|
+
response.should.be.ok
|
197
|
+
response.body.should.equal 'Goodbye World'
|
198
|
+
cache.trace.should.include :stale
|
199
|
+
cache.trace.should.include :invalid
|
200
|
+
cache.trace.should.include :store
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'does not revalidate fresh cache entry when enable_revalidate option is set false' do
|
204
|
+
count = 0
|
205
|
+
respond_with do |req,res|
|
206
|
+
count+= 1
|
207
|
+
res['Cache-Control'] = 'max-age=10000'
|
208
|
+
res['ETag'] = count.to_s
|
209
|
+
res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
|
210
|
+
end
|
211
|
+
|
212
|
+
get '/'
|
213
|
+
response.should.be.ok
|
214
|
+
response.body.should.equal 'Hello World'
|
215
|
+
cache.trace.should.include :store
|
216
|
+
|
217
|
+
get '/'
|
218
|
+
response.should.be.ok
|
219
|
+
response.body.should.equal 'Hello World'
|
220
|
+
cache.trace.should.include :fresh
|
221
|
+
|
222
|
+
get '/',
|
223
|
+
'rack-cache.allow_revalidate' => false,
|
224
|
+
'HTTP_CACHE_CONTROL' => 'max-age=0'
|
225
|
+
response.should.be.ok
|
226
|
+
response.body.should.equal 'Hello World'
|
227
|
+
cache.trace.should.not.include :stale
|
228
|
+
cache.trace.should.not.include :invalid
|
229
|
+
cache.trace.should.include :fresh
|
230
|
+
end
|
121
231
|
it 'fetches response from backend when cache misses' do
|
122
232
|
respond_with 200, 'Expires' => (Time.now + 5).httpdate
|
123
233
|
get '/'
|
124
234
|
|
125
235
|
response.should.be.ok
|
126
|
-
cache.should.
|
127
|
-
cache.should.a.performed :fetch
|
236
|
+
cache.trace.should.include :miss
|
128
237
|
response.headers.should.include 'Age'
|
129
238
|
end
|
130
239
|
|
@@ -134,7 +243,7 @@ describe 'Rack::Cache::Context' do
|
|
134
243
|
respond_with response_code, 'Expires' => (Time.now + 5).httpdate
|
135
244
|
get '/'
|
136
245
|
|
137
|
-
cache.should.
|
246
|
+
cache.trace.should.not.include :store
|
138
247
|
response.status.should.equal response_code
|
139
248
|
response.headers.should.not.include 'Age'
|
140
249
|
end
|
@@ -148,7 +257,7 @@ describe 'Rack::Cache::Context' do
|
|
148
257
|
get '/'
|
149
258
|
|
150
259
|
response.should.be.ok
|
151
|
-
cache.should.
|
260
|
+
cache.trace.should.not.include :store
|
152
261
|
response.headers.should.not.include 'Age'
|
153
262
|
end
|
154
263
|
|
@@ -157,7 +266,7 @@ describe 'Rack::Cache::Context' do
|
|
157
266
|
get '/'
|
158
267
|
|
159
268
|
response.should.be.ok
|
160
|
-
cache.should.
|
269
|
+
cache.trace.should.not.include :store
|
161
270
|
end
|
162
271
|
|
163
272
|
it "caches responses with explicit no-cache directive" do
|
@@ -167,7 +276,7 @@ describe 'Rack::Cache::Context' do
|
|
167
276
|
get '/'
|
168
277
|
|
169
278
|
response.should.be.ok
|
170
|
-
cache.should.
|
279
|
+
cache.trace.should.include :store
|
171
280
|
response.headers.should.include 'Age'
|
172
281
|
end
|
173
282
|
|
@@ -180,8 +289,8 @@ describe 'Rack::Cache::Context' do
|
|
180
289
|
response.headers.should.include 'Date'
|
181
290
|
response['Age'].should.not.be.nil
|
182
291
|
response['X-Content-Digest'].should.not.be.nil
|
183
|
-
cache.should.
|
184
|
-
cache.should.
|
292
|
+
cache.trace.should.include :miss
|
293
|
+
cache.trace.should.include :store
|
185
294
|
cache.metastore.to_hash.keys.length.should.equal 1
|
186
295
|
end
|
187
296
|
|
@@ -194,8 +303,8 @@ describe 'Rack::Cache::Context' do
|
|
194
303
|
response.headers.should.include 'Date'
|
195
304
|
response['Age'].should.not.be.nil
|
196
305
|
response['X-Content-Digest'].should.not.be.nil
|
197
|
-
cache.should.
|
198
|
-
cache.should.
|
306
|
+
cache.trace.should.include :miss
|
307
|
+
cache.trace.should.include :store
|
199
308
|
cache.metastore.to_hash.keys.length.should.equal 1
|
200
309
|
end
|
201
310
|
|
@@ -208,8 +317,8 @@ describe 'Rack::Cache::Context' do
|
|
208
317
|
response.headers.should.include 'Date'
|
209
318
|
response['Age'].should.not.be.nil
|
210
319
|
response['X-Content-Digest'].should.not.be.nil
|
211
|
-
cache.should.
|
212
|
-
cache.should.
|
320
|
+
cache.trace.should.include :miss
|
321
|
+
cache.trace.should.include :store
|
213
322
|
cache.metastore.to_hash.keys.length.should.equal 1
|
214
323
|
end
|
215
324
|
|
@@ -219,18 +328,18 @@ describe 'Rack::Cache::Context' do
|
|
219
328
|
|
220
329
|
response.should.be.ok
|
221
330
|
response.body.should.equal 'Hello World'
|
222
|
-
cache.should.
|
223
|
-
cache.should.
|
331
|
+
cache.trace.should.include :miss
|
332
|
+
cache.trace.should.include :store
|
224
333
|
end
|
225
334
|
|
226
335
|
it 'caches responses with an ETag validator but no freshness information' do
|
227
|
-
respond_with 200, '
|
336
|
+
respond_with 200, 'ETag' => '"123456"'
|
228
337
|
get '/'
|
229
338
|
|
230
339
|
response.should.be.ok
|
231
340
|
response.body.should.equal 'Hello World'
|
232
|
-
cache.should.
|
233
|
-
cache.should.
|
341
|
+
cache.trace.should.include :miss
|
342
|
+
cache.trace.should.include :store
|
234
343
|
end
|
235
344
|
|
236
345
|
it 'hits cached response with Expires header' do
|
@@ -242,8 +351,8 @@ describe 'Rack::Cache::Context' do
|
|
242
351
|
app.should.be.called
|
243
352
|
response.should.be.ok
|
244
353
|
response.headers.should.include 'Date'
|
245
|
-
cache.should.
|
246
|
-
cache.should.
|
354
|
+
cache.trace.should.include :miss
|
355
|
+
cache.trace.should.include :store
|
247
356
|
response.body.should.equal 'Hello World'
|
248
357
|
|
249
358
|
get '/'
|
@@ -252,8 +361,8 @@ describe 'Rack::Cache::Context' do
|
|
252
361
|
response['Date'].should.equal responses.first['Date']
|
253
362
|
response['Age'].to_i.should.satisfy { |age| age > 0 }
|
254
363
|
response['X-Content-Digest'].should.not.be.nil
|
255
|
-
cache.should.
|
256
|
-
cache.should.
|
364
|
+
cache.trace.should.include :fresh
|
365
|
+
cache.trace.should.not.include :store
|
257
366
|
response.body.should.equal 'Hello World'
|
258
367
|
end
|
259
368
|
|
@@ -266,8 +375,8 @@ describe 'Rack::Cache::Context' do
|
|
266
375
|
app.should.be.called
|
267
376
|
response.should.be.ok
|
268
377
|
response.headers.should.include 'Date'
|
269
|
-
cache.should.
|
270
|
-
cache.should.
|
378
|
+
cache.trace.should.include :miss
|
379
|
+
cache.trace.should.include :store
|
271
380
|
response.body.should.equal 'Hello World'
|
272
381
|
|
273
382
|
get '/'
|
@@ -276,8 +385,8 @@ describe 'Rack::Cache::Context' do
|
|
276
385
|
response['Date'].should.equal responses.first['Date']
|
277
386
|
response['Age'].to_i.should.satisfy { |age| age > 0 }
|
278
387
|
response['X-Content-Digest'].should.not.be.nil
|
279
|
-
cache.should.
|
280
|
-
cache.should.
|
388
|
+
cache.trace.should.include :fresh
|
389
|
+
cache.trace.should.not.include :store
|
281
390
|
response.body.should.equal 'Hello World'
|
282
391
|
end
|
283
392
|
|
@@ -290,8 +399,8 @@ describe 'Rack::Cache::Context' do
|
|
290
399
|
app.should.be.called
|
291
400
|
response.should.be.ok
|
292
401
|
response.headers.should.include 'Date'
|
293
|
-
cache.should.
|
294
|
-
cache.should.
|
402
|
+
cache.trace.should.include :miss
|
403
|
+
cache.trace.should.include :store
|
295
404
|
response.body.should.equal 'Hello World'
|
296
405
|
|
297
406
|
get '/'
|
@@ -300,8 +409,8 @@ describe 'Rack::Cache::Context' do
|
|
300
409
|
response['Date'].should.equal responses.first['Date']
|
301
410
|
response['Age'].to_i.should.satisfy { |age| age > 0 }
|
302
411
|
response['X-Content-Digest'].should.not.be.nil
|
303
|
-
cache.should.
|
304
|
-
cache.should.
|
412
|
+
cache.trace.should.include :fresh
|
413
|
+
cache.trace.should.not.include :store
|
305
414
|
response.body.should.equal 'Hello World'
|
306
415
|
end
|
307
416
|
|
@@ -311,16 +420,16 @@ describe 'Rack::Cache::Context' do
|
|
311
420
|
get '/', 'rack-cache.default_ttl' => 10
|
312
421
|
app.should.be.called
|
313
422
|
response.should.be.ok
|
314
|
-
cache.should.
|
315
|
-
cache.should.
|
423
|
+
cache.trace.should.include :miss
|
424
|
+
cache.trace.should.include :store
|
316
425
|
response.body.should.equal 'Hello World'
|
317
426
|
response['Cache-Control'].should.include 's-maxage=10'
|
318
427
|
|
319
428
|
get '/', 'rack-cache.default_ttl' => 10
|
320
429
|
response.should.be.ok
|
321
430
|
app.should.not.be.called
|
322
|
-
cache.should.
|
323
|
-
cache.should.
|
431
|
+
cache.trace.should.include :fresh
|
432
|
+
cache.trace.should.not.include :store
|
324
433
|
response.body.should.equal 'Hello World'
|
325
434
|
end
|
326
435
|
|
@@ -331,8 +440,8 @@ describe 'Rack::Cache::Context' do
|
|
331
440
|
get '/', 'rack-cache.default_ttl' => 10
|
332
441
|
app.should.be.called
|
333
442
|
response.should.be.ok
|
334
|
-
cache.should.
|
335
|
-
cache.should.
|
443
|
+
cache.trace.should.include :miss
|
444
|
+
cache.trace.should.not.include :store
|
336
445
|
response['Cache-Control'].should.not.include 's-maxage'
|
337
446
|
response.body.should.equal 'Hello World'
|
338
447
|
end
|
@@ -347,8 +456,8 @@ describe 'Rack::Cache::Context' do
|
|
347
456
|
response.headers.should.include 'Date'
|
348
457
|
response.headers.should.include 'X-Content-Digest'
|
349
458
|
response.headers.should.include 'Age'
|
350
|
-
cache.should.
|
351
|
-
cache.should.
|
459
|
+
cache.trace.should.include :miss
|
460
|
+
cache.trace.should.include :store
|
352
461
|
response.body.should.equal 'Hello World'
|
353
462
|
|
354
463
|
# go in and play around with the cached metadata directly ...
|
@@ -361,10 +470,10 @@ describe 'Rack::Cache::Context' do
|
|
361
470
|
response.should.be.ok
|
362
471
|
response['Age'].to_i.should.equal 0
|
363
472
|
response.headers.should.include 'X-Content-Digest'
|
364
|
-
cache.should.
|
365
|
-
cache.should.
|
366
|
-
cache.should.
|
367
|
-
cache.should.
|
473
|
+
cache.trace.should.include :stale
|
474
|
+
cache.trace.should.not.include :fresh
|
475
|
+
cache.trace.should.not.include :miss
|
476
|
+
cache.trace.should.include :store
|
368
477
|
response.body.should.equal 'Hello World'
|
369
478
|
end
|
370
479
|
|
@@ -385,8 +494,9 @@ describe 'Rack::Cache::Context' do
|
|
385
494
|
response.headers.should.include 'Last-Modified'
|
386
495
|
response.headers.should.include 'X-Content-Digest'
|
387
496
|
response.body.should.equal 'Hello World'
|
388
|
-
cache.should.
|
389
|
-
cache.should.
|
497
|
+
cache.trace.should.include :miss
|
498
|
+
cache.trace.should.include :store
|
499
|
+
cache.trace.should.not.include :stale
|
390
500
|
|
391
501
|
# build subsequent request; should be found but miss due to freshness
|
392
502
|
get '/'
|
@@ -395,11 +505,11 @@ describe 'Rack::Cache::Context' do
|
|
395
505
|
response.headers.should.include 'Last-Modified'
|
396
506
|
response.headers.should.include 'X-Content-Digest'
|
397
507
|
response['Age'].to_i.should.equal 0
|
398
|
-
response['X-Origin-Status'].should.equal '304'
|
399
508
|
response.body.should.equal 'Hello World'
|
400
|
-
cache.should.
|
401
|
-
cache.should.
|
402
|
-
cache.should.
|
509
|
+
cache.trace.should.include :stale
|
510
|
+
cache.trace.should.include :valid
|
511
|
+
cache.trace.should.include :store
|
512
|
+
cache.trace.should.not.include :miss
|
403
513
|
end
|
404
514
|
|
405
515
|
it 'validates cached responses with ETag and no freshness information' do
|
@@ -416,24 +526,24 @@ describe 'Rack::Cache::Context' do
|
|
416
526
|
get '/'
|
417
527
|
app.should.be.called
|
418
528
|
response.should.be.ok
|
419
|
-
response.headers.should.include '
|
529
|
+
response.headers.should.include 'ETag'
|
420
530
|
response.headers.should.include 'X-Content-Digest'
|
421
531
|
response.body.should.equal 'Hello World'
|
422
|
-
cache.should.
|
423
|
-
cache.should.
|
532
|
+
cache.trace.should.include :miss
|
533
|
+
cache.trace.should.include :store
|
424
534
|
|
425
535
|
# build subsequent request; should be found but miss due to freshness
|
426
536
|
get '/'
|
427
537
|
app.should.be.called
|
428
538
|
response.should.be.ok
|
429
|
-
response.headers.should.include '
|
539
|
+
response.headers.should.include 'ETag'
|
430
540
|
response.headers.should.include 'X-Content-Digest'
|
431
541
|
response['Age'].to_i.should.equal 0
|
432
|
-
response['X-Origin-Status'].should.equal '304'
|
433
542
|
response.body.should.equal 'Hello World'
|
434
|
-
cache.should.
|
435
|
-
cache.should.
|
436
|
-
cache.should.
|
543
|
+
cache.trace.should.include :stale
|
544
|
+
cache.trace.should.include :valid
|
545
|
+
cache.trace.should.include :store
|
546
|
+
cache.trace.should.not.include :miss
|
437
547
|
end
|
438
548
|
|
439
549
|
it 'replaces cached responses when validation results in non-304 response' do
|
@@ -442,8 +552,8 @@ describe 'Rack::Cache::Context' do
|
|
442
552
|
respond_with do |req,res|
|
443
553
|
res['Last-Modified'] = timestamp
|
444
554
|
case (count+=1)
|
445
|
-
when 1 ; res.body = 'first response'
|
446
|
-
when 2 ; res.body = 'second response'
|
555
|
+
when 1 ; res.body = ['first response']
|
556
|
+
when 2 ; res.body = ['second response']
|
447
557
|
when 3
|
448
558
|
res.body = []
|
449
559
|
res.status = 304
|
@@ -475,11 +585,7 @@ describe 'Rack::Cache::Context' do
|
|
475
585
|
req.request_method.should.equal 'HEAD'
|
476
586
|
end
|
477
587
|
|
478
|
-
|
479
|
-
on(:receive) { pass! }
|
480
|
-
end
|
481
|
-
|
482
|
-
head '/'
|
588
|
+
head '/', 'HTTP_EXPECT' => 'something ...'
|
483
589
|
app.should.be.called
|
484
590
|
response.body.should.equal ''
|
485
591
|
end
|
@@ -503,6 +609,53 @@ describe 'Rack::Cache::Context' do
|
|
503
609
|
response['Content-Length'].should.equal 'Hello World'.length.to_s
|
504
610
|
end
|
505
611
|
|
612
|
+
it 'invalidates cached responses on POST' do
|
613
|
+
respond_with do |req,res|
|
614
|
+
if req.request_method == 'GET'
|
615
|
+
res.status = 200
|
616
|
+
res['Cache-Control'] = 'public, max-age=500'
|
617
|
+
res.body = ['Hello World']
|
618
|
+
elsif req.request_method == 'POST'
|
619
|
+
res.status = 303
|
620
|
+
res['Location'] = '/'
|
621
|
+
res.headers.delete('Cache-Control')
|
622
|
+
res.body = []
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
# build initial request to enter into the cache
|
627
|
+
get '/'
|
628
|
+
app.should.be.called
|
629
|
+
response.should.be.ok
|
630
|
+
response.body.should.equal 'Hello World'
|
631
|
+
cache.trace.should.include :miss
|
632
|
+
cache.trace.should.include :store
|
633
|
+
|
634
|
+
# make sure it is valid
|
635
|
+
get '/'
|
636
|
+
app.should.not.called
|
637
|
+
response.should.be.ok
|
638
|
+
response.body.should.equal 'Hello World'
|
639
|
+
cache.trace.should.include :fresh
|
640
|
+
|
641
|
+
# now POST to same URL
|
642
|
+
post '/'
|
643
|
+
app.should.be.called
|
644
|
+
response.should.be.redirect
|
645
|
+
response['Location'].should.equal '/'
|
646
|
+
cache.trace.should.include :invalidate
|
647
|
+
cache.trace.should.include :pass
|
648
|
+
response.body.should.equal ''
|
649
|
+
|
650
|
+
# now make sure it was actually invalidated
|
651
|
+
get '/'
|
652
|
+
app.should.be.called
|
653
|
+
response.should.be.ok
|
654
|
+
response.body.should.equal 'Hello World'
|
655
|
+
cache.trace.should.include :stale
|
656
|
+
cache.trace.should.include :invalid
|
657
|
+
cache.trace.should.include :store
|
658
|
+
end
|
506
659
|
|
507
660
|
describe 'with responses that include a Vary header' do
|
508
661
|
before(:each) do
|
@@ -511,7 +664,7 @@ describe 'Rack::Cache::Context' do
|
|
511
664
|
res['Vary'] = 'Accept User-Agent Foo'
|
512
665
|
res['Cache-Control'] = 'max-age=10'
|
513
666
|
res['X-Response-Count'] = (count+=1).to_s
|
514
|
-
res.body = req.env['HTTP_USER_AGENT']
|
667
|
+
res.body = [req.env['HTTP_USER_AGENT']]
|
515
668
|
end
|
516
669
|
end
|
517
670
|
|
@@ -521,16 +674,16 @@ describe 'Rack::Cache::Context' do
|
|
521
674
|
'HTTP_USER_AGENT' => 'Bob/1.0'
|
522
675
|
response.should.be.ok
|
523
676
|
response.body.should.equal 'Bob/1.0'
|
524
|
-
cache.should.
|
525
|
-
cache.should.
|
677
|
+
cache.trace.should.include :miss
|
678
|
+
cache.trace.should.include :store
|
526
679
|
|
527
680
|
get '/',
|
528
681
|
'HTTP_ACCEPT' => 'text/html',
|
529
682
|
'HTTP_USER_AGENT' => 'Bob/1.0'
|
530
683
|
response.should.be.ok
|
531
684
|
response.body.should.equal 'Bob/1.0'
|
532
|
-
cache.should.
|
533
|
-
cache.should.
|
685
|
+
cache.trace.should.include :fresh
|
686
|
+
cache.trace.should.not.include :store
|
534
687
|
response.headers.should.include 'X-Content-Digest'
|
535
688
|
end
|
536
689
|
|
@@ -545,101 +698,30 @@ describe 'Rack::Cache::Context' do
|
|
545
698
|
get '/',
|
546
699
|
'HTTP_ACCEPT' => 'text/html',
|
547
700
|
'HTTP_USER_AGENT' => 'Bob/2.0'
|
548
|
-
cache.should.
|
549
|
-
cache.should.
|
701
|
+
cache.trace.should.include :miss
|
702
|
+
cache.trace.should.include :store
|
550
703
|
response.body.should.equal 'Bob/2.0'
|
551
704
|
response['X-Response-Count'].should.equal '2'
|
552
705
|
|
553
706
|
get '/',
|
554
707
|
'HTTP_ACCEPT' => 'text/html',
|
555
708
|
'HTTP_USER_AGENT' => 'Bob/1.0'
|
556
|
-
cache.should.
|
709
|
+
cache.trace.should.include :fresh
|
557
710
|
response.body.should.equal 'Bob/1.0'
|
558
711
|
response['X-Response-Count'].should.equal '1'
|
559
712
|
|
560
713
|
get '/',
|
561
714
|
'HTTP_ACCEPT' => 'text/html',
|
562
715
|
'HTTP_USER_AGENT' => 'Bob/2.0'
|
563
|
-
cache.should.
|
716
|
+
cache.trace.should.include :fresh
|
564
717
|
response.body.should.equal 'Bob/2.0'
|
565
718
|
response['X-Response-Count'].should.equal '2'
|
566
719
|
|
567
720
|
get '/',
|
568
721
|
'HTTP_USER_AGENT' => 'Bob/2.0'
|
569
|
-
cache.should.
|
722
|
+
cache.trace.should.include :miss
|
570
723
|
response.body.should.equal 'Bob/2.0'
|
571
724
|
response['X-Response-Count'].should.equal '3'
|
572
725
|
end
|
573
726
|
end
|
574
|
-
|
575
|
-
describe 'when transitioning to the error state' do
|
576
|
-
|
577
|
-
setup { respond_with(200) }
|
578
|
-
|
579
|
-
it 'creates a blank slate response object with 500 status with no args' do
|
580
|
-
cache_config do
|
581
|
-
on(:receive) { error! }
|
582
|
-
end
|
583
|
-
get '/'
|
584
|
-
response.status.should.equal 500
|
585
|
-
response.body.should.be.empty
|
586
|
-
cache.should.a.performed :error
|
587
|
-
end
|
588
|
-
|
589
|
-
it 'sets the status code with one arg' do
|
590
|
-
cache_config do
|
591
|
-
on(:receive) { error! 505 }
|
592
|
-
end
|
593
|
-
get '/'
|
594
|
-
response.status.should.equal 505
|
595
|
-
end
|
596
|
-
|
597
|
-
it 'sets the status and headers with args: status, Hash' do
|
598
|
-
cache_config do
|
599
|
-
on(:receive) { error! 504, 'Content-Type' => 'application/x-foo' }
|
600
|
-
end
|
601
|
-
get '/'
|
602
|
-
response.status.should.equal 504
|
603
|
-
response['Content-Type'].should.equal 'application/x-foo'
|
604
|
-
response.body.should.be.empty
|
605
|
-
end
|
606
|
-
|
607
|
-
it 'sets the status and body with args: status, String' do
|
608
|
-
cache_config do
|
609
|
-
on(:receive) { error! 503, 'foo bar baz' }
|
610
|
-
end
|
611
|
-
get '/'
|
612
|
-
response.status.should.equal 503
|
613
|
-
response.body.should.equal 'foo bar baz'
|
614
|
-
end
|
615
|
-
|
616
|
-
it 'sets the status and body with args: status, Array' do
|
617
|
-
cache_config do
|
618
|
-
on(:receive) { error! 503, ['foo bar baz'] }
|
619
|
-
end
|
620
|
-
get '/'
|
621
|
-
response.status.should.equal 503
|
622
|
-
response.body.should.equal 'foo bar baz'
|
623
|
-
end
|
624
|
-
|
625
|
-
it 'fires the error event before finishing' do
|
626
|
-
fired = false
|
627
|
-
cache_config do
|
628
|
-
on(:receive) { error! }
|
629
|
-
on(:error) {
|
630
|
-
fired = true
|
631
|
-
response.status.should.equal 500
|
632
|
-
response['Content-Type'] = 'application/x-foo'
|
633
|
-
response.body = ['overridden response body']
|
634
|
-
}
|
635
|
-
end
|
636
|
-
get '/'
|
637
|
-
fired.should.be true
|
638
|
-
response.status.should.equal 500
|
639
|
-
response.body.should.equal 'overridden response body'
|
640
|
-
response['Content-Type'].should.equal 'application/x-foo'
|
641
|
-
end
|
642
|
-
|
643
|
-
end
|
644
|
-
|
645
727
|
end
|