rack-cache 0.3.0 → 0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack-cache might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/CHANGES +43 -0
  2. data/README +18 -9
  3. data/Rakefile +1 -14
  4. data/TODO +13 -14
  5. data/doc/configuration.markdown +7 -153
  6. data/doc/faq.markdown +8 -0
  7. data/doc/index.markdown +7 -9
  8. data/example/sinatra/app.rb +25 -0
  9. data/example/sinatra/views/index.erb +44 -0
  10. data/lib/rack/cache.rb +5 -11
  11. data/lib/rack/cache/cachecontrol.rb +193 -0
  12. data/lib/rack/cache/context.rb +190 -52
  13. data/lib/rack/cache/entitystore.rb +10 -4
  14. data/lib/rack/cache/key.rb +52 -0
  15. data/lib/rack/cache/metastore.rb +52 -16
  16. data/lib/rack/cache/options.rb +60 -39
  17. data/lib/rack/cache/request.rb +11 -15
  18. data/lib/rack/cache/response.rb +221 -30
  19. data/lib/rack/cache/storage.rb +1 -2
  20. data/rack-cache.gemspec +9 -15
  21. data/test/cache_test.rb +9 -6
  22. data/test/cachecontrol_test.rb +139 -0
  23. data/test/context_test.rb +251 -169
  24. data/test/entitystore_test.rb +12 -11
  25. data/test/key_test.rb +50 -0
  26. data/test/metastore_test.rb +57 -14
  27. data/test/options_test.rb +11 -0
  28. data/test/request_test.rb +19 -0
  29. data/test/response_test.rb +164 -23
  30. data/test/spec_setup.rb +7 -0
  31. metadata +12 -20
  32. data/doc/events.dot +0 -27
  33. data/lib/rack/cache/config.rb +0 -65
  34. data/lib/rack/cache/config/busters.rb +0 -16
  35. data/lib/rack/cache/config/default.rb +0 -133
  36. data/lib/rack/cache/config/no-cache.rb +0 -13
  37. data/lib/rack/cache/core.rb +0 -299
  38. data/lib/rack/cache/headers.rb +0 -325
  39. data/lib/rack/utils/environment_headers.rb +0 -78
  40. data/test/config_test.rb +0 -66
  41. data/test/core_test.rb +0 -84
  42. data/test/environment_headers_test.rb +0 -69
  43. data/test/headers_test.rb +0 -298
  44. data/test/logging_test.rb +0 -45
@@ -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.read(File.dirname(__FILE__) + '/pony.jpg')
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).each { |line| @store.write(line).first.should.be.sha_like }
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
@@ -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
- define_method :store_simple_entry do
66
- @request = mock_request('/test', {})
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('/test').should.not.be.empty
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('/test').first
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('Age' => '0', 'Content-Length' => '4')
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('/test').length.should.equal 3
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('/test').length.should.equal 2
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
@@ -1,37 +1,178 @@
1
1
  require "#{File.dirname(__FILE__)}/spec_setup"
2
2
 
3
3
  describe 'Rack::Cache::Response' do
4
-
5
- before(:each) {
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
- after(:each) {
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
- it 'responds to cache-related methods' do
16
- @response.should.respond_to :ttl
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
- @response.should.respond_to :to_a
23
- @response.to_a.should.be == [200, {'Date' => @now.httpdate}, '']
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
- it 'retrieves headers with #[]' do
27
- @response.headers['X-Foo'] = 'bar'
28
- @response.should.respond_to :[]
29
- @response['X-Foo'].should.be == 'bar'
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
- it 'sets headers with #[]=' do
33
- @response.should.respond_to :[]=
34
- @response['X-Foo'] = 'bar'
35
- @response.headers['X-Foo'].should.be == 'bar'
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