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.

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