josh-rack-cache 0.5.1

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.
@@ -0,0 +1,77 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+ require 'rack/cache/options'
3
+
4
+ module Rack::Cache::Options
5
+ option_accessor :foo
6
+ end
7
+
8
+ class MockOptions
9
+ include Rack::Cache::Options
10
+ def initialize
11
+ @env = nil
12
+ initialize_options
13
+ end
14
+ end
15
+
16
+ describe 'Rack::Cache::Options' do
17
+ before { @options = MockOptions.new }
18
+
19
+ describe '#set' do
20
+ it 'sets a Symbol option as rack-cache.symbol' do
21
+ @options.set :bar, 'baz'
22
+ @options.options['rack-cache.bar'].should.equal 'baz'
23
+ end
24
+ it 'sets a String option as string' do
25
+ @options.set 'foo.bar', 'bling'
26
+ @options.options['foo.bar'].should.equal 'bling'
27
+ end
28
+ it 'sets all key/value pairs when given a Hash' do
29
+ @options.set :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling'
30
+ @options.foo.should.equal 'bar'
31
+ @options.options['rack-cache.bar'].should.equal 'baz'
32
+ @options.options['foo.bar'].should.equal 'bling'
33
+ end
34
+ end
35
+
36
+ it 'makes options declared with option_accessor available as attributes' do
37
+ @options.set :foo, 'bar'
38
+ @options.foo.should.equal 'bar'
39
+ end
40
+
41
+ it 'allows setting multiple options via assignment' do
42
+ @options.options = { :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling' }
43
+ @options.foo.should.equal 'bar'
44
+ @options.options['foo.bar'].should.equal 'bling'
45
+ @options.options['rack-cache.bar'].should.equal 'baz'
46
+ end
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
+
59
+ it 'allows the meta store to be configured' do
60
+ @options.should.respond_to :metastore
61
+ @options.should.respond_to :metastore=
62
+ @options.metastore.should.not.be nil
63
+ end
64
+
65
+ it 'allows the entity store to be configured' do
66
+ @options.should.respond_to :entitystore
67
+ @options.should.respond_to :entitystore=
68
+ @options.entitystore.should.not.be nil
69
+ end
70
+
71
+ it 'allows log verbosity to be configured' do
72
+ @options.should.respond_to :verbose
73
+ @options.should.respond_to :verbose=
74
+ @options.should.respond_to :verbose?
75
+ @options.verbose.should.not.be.nil
76
+ end
77
+ end
Binary file
@@ -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
@@ -0,0 +1,178 @@
1
+ require "#{File.dirname(__FILE__)}/spec_setup"
2
+
3
+ describe 'Rack::Cache::Response' do
4
+ before :each do
5
+ @now = Time.httpdate(Time.now.httpdate)
6
+ @one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
7
+ @one_hour_later = Time.httpdate((Time.now + (60**2)).httpdate)
8
+ @res = Rack::Cache::Response.new(200, {'Date' => @now.httpdate}, [])
9
+ end
10
+
11
+ after :each do
12
+ @now, @res, @one_hour_ago = nil
13
+ end
14
+
15
+ it 'responds to #to_a with a Rack response tuple' do
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
141
+ end
142
+
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
157
+ end
158
+
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
177
+ end
178
+ end
@@ -0,0 +1,237 @@
1
+ require 'pp'
2
+ require 'tmpdir'
3
+ require 'stringio'
4
+
5
+ [ STDOUT, STDERR ].each { |io| io.sync = true }
6
+
7
+ begin
8
+ require 'test/spec'
9
+ rescue LoadError => boom
10
+ require 'rubygems' rescue nil
11
+ require 'test/spec'
12
+ end
13
+
14
+ # Set the MEMCACHED environment variable as follows to enable testing
15
+ # of the MemCached meta and entity stores.
16
+ ENV['MEMCACHED'] ||= 'localhost:11211'
17
+ $memcached = nil
18
+ $memcache = nil
19
+
20
+ def have_memcached?(server=ENV['MEMCACHED'])
21
+ return $memcached unless $memcached.nil?
22
+ v, $VERBOSE = $VERBOSE, nil # silence warnings from memcached
23
+ require 'memcached'
24
+ $VERBOSE = v
25
+ $memcached = Memcached.new(server)
26
+ $memcached.set('ping', '')
27
+ true
28
+ rescue LoadError => boom
29
+ $memcached = false
30
+ false
31
+ rescue => boom
32
+ STDERR.puts "memcached not working. related tests will be skipped."
33
+ $memcached = false
34
+ false
35
+ end
36
+
37
+ have_memcached?
38
+
39
+ def have_memcache?(server=ENV['MEMCACHED'])
40
+ return $memcache unless $memcache.nil?
41
+ require 'memcache'
42
+ $memcache = MemCache.new(server)
43
+ $memcache.set('ping', '')
44
+ true
45
+ rescue LoadError => boom
46
+ $memcache = false
47
+ false
48
+ rescue => boom
49
+ STDERR.puts "memcache not working. related tests will be skipped."
50
+ $memcache = false
51
+ false
52
+ end
53
+
54
+ have_memcache?
55
+
56
+ def need_memcached(forwhat)
57
+ if have_memcached?
58
+ yield
59
+ else
60
+ STDERR.puts "skipping memcached #{forwhat}"
61
+ end
62
+ end
63
+
64
+ def need_memcache(forwhat)
65
+ if have_memcache?
66
+ yield
67
+ else
68
+ STDERR.puts "skipping memcache #{forwhat}"
69
+ end
70
+ end
71
+
72
+ def need_java(forwhat)
73
+ if RUBY_PLATFORM =~ /java/
74
+ yield
75
+ else
76
+ STDERR.puts "skipping app engine #{forwhat}"
77
+ end
78
+ end
79
+
80
+
81
+ # Setup the load path ..
82
+ $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__)) + '/lib'
83
+ $LOAD_PATH.unshift File.dirname(__FILE__)
84
+
85
+ require 'rack/cache'
86
+
87
+
88
+ # Methods for constructing downstream applications / response
89
+ # generators.
90
+ module CacheContextHelpers
91
+
92
+ # The Rack::Cache::Context instance used for the most recent
93
+ # request.
94
+ attr_reader :cache
95
+
96
+ # An Array of Rack::Cache::Context instances used for each request, in
97
+ # request order.
98
+ attr_reader :caches
99
+
100
+ # The Rack::Response instance result of the most recent request.
101
+ attr_reader :response
102
+
103
+ # An Array of Rack::Response instances for each request, in request order.
104
+ attr_reader :responses
105
+
106
+ # The backend application object.
107
+ attr_reader :app
108
+
109
+ def setup_cache_context
110
+ # holds each Rack::Cache::Context
111
+ @app = nil
112
+
113
+ # each time a request is made, a clone of @cache_template is used
114
+ # and appended to @caches.
115
+ @cache_template = nil
116
+ @cache = nil
117
+ @caches = []
118
+ @errors = StringIO.new
119
+ @cache_config = nil
120
+
121
+ @called = false
122
+ @request = nil
123
+ @response = nil
124
+ @responses = []
125
+
126
+ @storage = Rack::Cache::Storage.new
127
+ end
128
+
129
+ def teardown_cache_context
130
+ @app, @cache_template, @cache, @caches, @called,
131
+ @request, @response, @responses, @cache_config = nil
132
+ end
133
+
134
+ # A basic response with 200 status code and a tiny body.
135
+ def respond_with(status=200, headers={}, body=['Hello World'])
136
+ called = false
137
+ @app =
138
+ lambda do |env|
139
+ called = true
140
+ response = Rack::Response.new(body, status, headers)
141
+ request = Rack::Request.new(env)
142
+ yield request, response if block_given?
143
+ response.finish
144
+ end
145
+ @app.meta_def(:called?) { called }
146
+ @app.meta_def(:reset!) { called = false }
147
+ @app
148
+ end
149
+
150
+ def cache_config(&block)
151
+ @cache_config = block
152
+ end
153
+
154
+ def request(method, uri='/', opts={})
155
+ opts = {
156
+ 'rack.run_once' => true,
157
+ 'rack.errors' => @errors,
158
+ 'rack-cache.storage' => @storage
159
+ }.merge(opts)
160
+
161
+ fail 'response not specified (use respond_with)' if @app.nil?
162
+ @app.reset! if @app.respond_to?(:reset!)
163
+
164
+ @cache_prototype ||= Rack::Cache::Context.new(@app, &@cache_config)
165
+ @cache = @cache_prototype.clone
166
+ @caches << @cache
167
+ @request = Rack::MockRequest.new(@cache)
168
+ yield @cache if block_given?
169
+ @response = @request.request(method.to_s.upcase, uri, opts)
170
+ @responses << @response
171
+ @response
172
+ end
173
+
174
+ def get(stem, env={}, &b)
175
+ request(:get, stem, env, &b)
176
+ end
177
+
178
+ def head(stem, env={}, &b)
179
+ request(:head, stem, env, &b)
180
+ end
181
+
182
+ def post(*args, &b)
183
+ request(:post, *args, &b)
184
+ end
185
+ end
186
+
187
+
188
+ module TestHelpers
189
+ include FileUtils
190
+ F = File
191
+
192
+ @@temp_dir_count = 0
193
+
194
+ def create_temp_directory
195
+ @@temp_dir_count += 1
196
+ path = F.join(Dir.tmpdir, "rack-cache-#{$$}-#{@@temp_dir_count}")
197
+ mkdir_p path
198
+ if block_given?
199
+ yield path
200
+ remove_entry_secure path
201
+ end
202
+ path
203
+ end
204
+
205
+ def create_temp_file(root, file, data='')
206
+ path = F.join(root, file)
207
+ mkdir_p F.dirname(path)
208
+ F.open(path, 'w') { |io| io.write(data) }
209
+ end
210
+
211
+ end
212
+
213
+ class Test::Unit::TestCase
214
+ include TestHelpers
215
+ include CacheContextHelpers
216
+ end
217
+
218
+ # Metaid == a few simple metaclass helper
219
+ # (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
220
+ class Object
221
+ # The hidden singleton lurks behind everyone
222
+ def metaclass; class << self; self; end; end
223
+ def meta_eval(&blk); metaclass.instance_eval(&blk); end
224
+ # Adds methods to a metaclass
225
+ def meta_def name, &blk
226
+ meta_eval { define_method name, &blk }
227
+ end
228
+ # Defines an instance method within a class
229
+ def class_def name, &blk
230
+ class_eval { define_method name, &blk }
231
+ end
232
+
233
+ # True when the Object is neither false or nil.
234
+ def truthy?
235
+ !!self
236
+ end
237
+ end