1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
|
require_relative 'test_helper'
require 'rack/cache/meta_store'
require 'rack/cache/entity_store'
module RackCacheMetaStoreImplementation
def self.included(base)
base.class_eval do
###
# Helpers
def mock_request(uri, opts)
env = Rack::MockRequest.env_for(uri, opts || {})
Rack::Cache::Request.new(env)
end
def mock_response(status, headers, body)
headers ||= {}
Rack::Cache::Response.new(status, headers, body)
end
def slurp(body)
buf = ''
body.each { |part| buf << part }
buf
end
# Stores an entry for the given request args, returns a url encoded cache key
# for the request.
def store_simple_entry(path=nil, headers=nil, body=['test'])
@request = mock_request(path || '/test', headers || {})
@response = mock_response(200, {'cache-control' => 'max-age=420'}, body)
body = @response.body
cache_key = @store.store(@request, @response, @entity_store)
# atm we always read back the body from the cache, this is a workaround to deal with
# bodies that can only be read once
@response.body.object_id.wont_equal body.object_id
cache_key
end
before do
@request = mock_request('/', {})
@response = mock_response(200, {}, ['hello world'])
end
after do
@store = nil
@entity_store = nil
end
# Low-level implementation methods ===========================================
it 'writes a list of negotation tuples with #write' do
@store.write('/test', [[{}, {}]])
end
it 'reads a list of negotation tuples with #read' do
@store.write('/test', [[{},{}],[{},{}]])
tuples = @store.read('/test')
tuples.must_equal [ [{},{}], [{},{}] ]
end
it 'reads an empty list with #read when nothing cached at key' do
assert @store.read('/nothing').empty?
end
it 'removes entries for key with #purge' do
@store.write('/test', [[{},{}]])
refute @store.read('/test').empty?
@store.purge('/test')
assert @store.read('/test').empty?
end
it 'succeeds when purging non-existing entries' do
assert @store.read('/test').empty?
@store.purge('/test')
end
it 'returns nil from #purge' do
@store.write('/test', [[{},{}]])
@store.purge('/test').must_be_nil
@store.read('/test').must_equal []
end
%w[/test http://example.com:8080/ /test?x=y /test?x=y&p=q].each do |key|
it "can read and write key: '#{key}'" do
@store.write(key, [[{},{}]])
@store.read(key).must_equal [[{},{}]]
end
end
it "can read and write fairly large keys" do
key = "b" * 4096
@store.write(key, [[{},{}]])
@store.read(key).must_equal [[{},{}]]
end
it "allows custom cache keys from block" do
request = mock_request('/test', {})
request.env['rack-cache.cache_key'] =
lambda { |request| request.path_info.reverse }
@store.cache_key(request).must_equal 'tset/'
end
it "allows custom cache keys from class" do
request = mock_request('/test', {})
request.env['rack-cache.cache_key'] = Class.new do
def self.call(request); request.path_info.reverse end
end
@store.cache_key(request).must_equal 'tset/'
end
it 'does not blow up when given a non-marhsalable object with an ALL_CAPS key' do
store_simple_entry('/bad', { 'SOME_THING' => Proc.new {} })
end
it 'supports a ttl parameter for #write' do
@store.write('/test', [[{},{}],[{},{}]], 0)
tuples = @store.read('/test')
tuples.must_equal [ [{},{}], [{},{}] ]
end
# Abstract methods ===========================================================
it 'stores a cache entry' do
cache_key = store_simple_entry
refute @store.read(cache_key).empty?
end
it 'can handle objects that can only be read once' do
io = StringIO.new("TEST")
store_simple_entry nil, nil, io
# was stored correctly in entity store
key = @response.headers.fetch('x-content-digest')
@entity_store.read(key).must_equal "TEST"
# io is closed, so that file descriptors are released
assert io.closed?
# rendered body is the same content as the cache
@response.body.to_a.must_equal ["TEST"]
end
it 'sets the x-content-digest response header before storing' do
cache_key = store_simple_entry
req, res = @store.read(cache_key).first
res['x-content-digest'].must_equal 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
end
it 'finds a stored entry with #lookup' do
store_simple_entry
response = @store.lookup(@request, @entity_store)
refute response.nil?
response.class.must_equal Rack::Cache::Response
end
it 'does not find an entry with #lookup when none exists' do
req = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
@store.lookup(req, @entity_store).must_be_nil
end
it "canonizes urls for cache keys" do
store_simple_entry(path='/test?x=y&p=q')
hits_req = mock_request(path, {})
miss_req = mock_request('/test?p=x', {})
@store.lookup(hits_req, @entity_store).wont_equal nil
@store.lookup(miss_req, @entity_store).must_be_nil
end
it 'does not find an entry with #lookup when the body does not exist' do
store_simple_entry
refute @response.headers['x-content-digest'].nil?
@entity_store.purge(@response.headers['x-content-digest'])
@store.lookup(@request, @entity_store).must_be_nil
end
it 'purges meta store entry when the body does not exist' do
store_simple_entry
@entity_store.purge(@response.headers['x-content-digest'])
mock = Minitest::Mock.new
mock.expect :call, nil, [@store.cache_key(@request)]
@store.stub(:purge, mock) do
@store.lookup(@request, @entity_store)
end
mock.verify
end
it 'purges meta store entry when the entry does not contain the digest header' do
cache_key = store_simple_entry
meta_entry = @store.read(cache_key)
meta_entry.grep(Array).flatten.each { |h| h.is_a?(Hash) && h.delete('x-content-digest') }
@store.write(cache_key, meta_entry)
mock = Minitest::Mock.new
mock.expect :call, nil, [@store.cache_key(@request)]
@store.stub(:purge, mock) do
@store.lookup(@request, nil)
end
mock.verify
end
it 'warns once if purge is not implemented' do
store_simple_entry
assert @response.headers['x-content-digest']
@entity_store.purge(@response.headers['x-content-digest'])
def @store.purge(key); raise NotImplementedError; end
@store.lookup(@request, @entity_store).must_be_nil
@store.lookup(@request, @entity_store).must_be_nil
end
it 'restores response headers properly with #lookup' do
store_simple_entry
response = @store.lookup(@request, @entity_store)
response.headers.
must_equal @response.headers.merge('content-length' => '4')
end
it 'restores response body from entity store with #lookup' do
store_simple_entry
response = @store.lookup(@request, @entity_store)
body = '' ; response.body.each {|p| body << p}
body.must_equal 'test'
end
it 'invalidates meta and entity store entries with #invalidate' do
store_simple_entry
@store.invalidate(@request, @entity_store)
response = @store.lookup(@request, @entity_store)
response.class.must_equal Rack::Cache::Response
refute response.fresh?
end
it 'succeeds quietly when #invalidate called with no matching entries' do
req = mock_request('/test', {})
@store.invalidate(req, @entity_store)
@store.lookup(@request, @entity_store).must_be_nil
end
it 'does not remove response x-status with #invalidate' do
request = mock_request('/test', {})
# non-fresh response
response = mock_response(200, {'Expires' => (Time.now - 3600).httpdate, 'vary' => 'Foo'}, ['test'])
cache_key = @store.store(request, response, @entity_store)
# fresh response
response = mock_response(200, {'Expires' => (Time.now + 3600).httpdate, 'vary' => 'Bar'}, ['test'])
@store.store(request, response, @entity_store)
@store.invalidate(request, @entity_store)
@store.read(cache_key).each do |_, res|
res.must_include 'x-status'
end
end
it 'gracefully degrades if the cache store stops working' do
@store = Class.new(Rack::Cache::MetaStore) do
def purge(*args); nil end
def read(*args); [] end
def write(*args); nil end
end.new
@entity_store = Class.new(Rack::Cache::EntityStore) do
def exists?(*args); false end
def open(*args); nil end
def read(*args); nil end
def write(*args); nil end
def purge(*args); nil end
end.new
request = mock_request('/test', {})
response = mock_response(200, {}, ['test'])
@store.store(request, response, @entity_store)
response.body.must_equal ['test']
end
# Vary =======================================================================
%w[Vary vary].each do |vary|
it 'does not return entries that Vary with #lookup' do
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
res = mock_response(200, {vary => 'Foo Bar'}, ['test'])
@store.store(req1, res, @entity_store)
@store.lookup(req2, @entity_store).must_be_nil
end
it 'stores multiple responses for each Vary combination' do
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
res1 = mock_response(200, {vary => 'Foo Bar'}, ['test 1'])
key = @store.store(req1, res1, @entity_store)
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
res2 = mock_response(200, {vary => 'Foo Bar'}, ['test 2'])
@store.store(req2, res2, @entity_store)
req3 = mock_request('/test', {'HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'})
res3 = mock_response(200, {vary => 'Foo Bar'}, ['test 3'])
@store.store(req3, res3, @entity_store)
slurp(@store.lookup(req3, @entity_store).body).must_equal 'test 3'
slurp(@store.lookup(req1, @entity_store).body).must_equal 'test 1'
slurp(@store.lookup(req2, @entity_store).body).must_equal 'test 2'
@store.read(key).length.must_equal 3
end
it 'overwrites non-varying responses with #store' do
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
res1 = mock_response(200, {vary => 'Foo Bar'}, ['test 1'])
key = @store.store(req1, res1, @entity_store)
slurp(@store.lookup(req1, @entity_store).body).must_equal 'test 1'
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
res2 = mock_response(200, {vary => 'Foo Bar'}, ['test 2'])
@store.store(req2, res2, @entity_store)
slurp(@store.lookup(req2, @entity_store).body).must_equal 'test 2'
req3 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
res3 = mock_response(200, {vary => 'Foo Bar'}, ['test 3'])
@store.store(req3, res3, @entity_store)
slurp(@store.lookup(req1, @entity_store).body).must_equal 'test 3'
@store.read(key).length.must_equal 2
end
end
# Age ====================================================================
%w[Age age].each do |age|
it 'removes the Age response header before storing' do
response = mock_response(200, {age => "100"}, ['foo'])
@store.store(@request, response, @entity_store)
@store.lookup(@request, @entity_store).headers['age'].must_be_nil
end
end
# TTL ====================================================================
context 'logging writes' do
it 'passes a TTL to the stores is use_native_ttl is truthy' do
request = mock_request('/test', { 'rack-cache.use_native_ttl' => true })
response = mock_response(200, {'cache-control' => 'max-age=42'}, ['foo'])
@entity_store.expects(:write).with(['foo'], 42).returns ['foobar']
@store.expects(:write).with(anything, anything, 42).returns ['foobar']
@store.store(request, response, @entity_store)
end
end
end
end
end
describe Rack::Cache::MetaStore do
{read: [1], write: [1,2], purge: [1]}.each do |method, args|
it "has not implemented #{method}" do
assert_raises NotImplementedError do
Rack::Cache::MetaStore.new.send(method, *args)
end
end
end
describe 'Heap' do
before do
@store = Rack::Cache::MetaStore::Heap.new
@entity_store = Rack::Cache::EntityStore::Heap.new
end
include RackCacheMetaStoreImplementation
end
describe 'Disk' do
before do
@temp_dir = create_temp_directory
@store = Rack::Cache::MetaStore::Disk.new("#{@temp_dir}/meta")
@entity_store = Rack::Cache::EntityStore::Disk.new("#{@temp_dir}/entity")
end
after do
remove_entry_secure @temp_dir
end
include RackCacheMetaStoreImplementation
end
need_memcached 'metastore tests' do
describe 'MemCached' do
before do
@temp_dir = create_temp_directory
$memcached.flush
@store = Rack::Cache::MetaStore::MemCached.new($memcached)
@entity_store = Rack::Cache::EntityStore::Heap.new
end
include RackCacheMetaStoreImplementation
end
describe 'options parsing' do
before do
uri = URI.parse("memcached://#{ENV['MEMCACHED']}/meta_ns1?show_backtraces=true")
@memcached_metastore = Rack::Cache::MetaStore::MemCached.resolve uri
end
it 'passes options from uri' do
@memcached_metastore.cache.instance_variable_get(:@options)[:show_backtraces].must_equal true
end
it 'takes namespace into account' do
@memcached_metastore.cache.instance_variable_get(:@options)[:prefix_key].must_equal 'meta_ns1'
end
end
end
need_dalli 'metastore tests' do
describe 'Dalli' do
before do
@temp_dir = create_temp_directory
$dalli.flush_all
@store = Rack::Cache::MetaStore::Dalli.new($dalli)
@entity_store = Rack::Cache::EntityStore::Heap.new
end
include RackCacheMetaStoreImplementation
end
describe 'options parsing' do
before do
uri = URI.parse("memcached://#{ENV['MEMCACHED']}/meta_ns1?show_backtraces=true")
@dalli_metastore = Rack::Cache::MetaStore::Dalli.resolve uri
end
it 'passes options from uri' do
@dalli_metastore.cache.instance_variable_get(:@options)[:show_backtraces].must_equal true
end
it 'takes namespace into account' do
@dalli_metastore.cache.instance_variable_get(:@options)[:namespace].must_equal 'meta_ns1'
end
end
end
need_java 'entity store testing' do
module Rack::Cache::AppEngine
module MC
class << (Service = {})
def contains(key); include?(key); end
def get(key); self[key]; end;
def put(key, value, ttl = nil)
self[key] = value
end
end
end
end
describe 'GAEStore' do
before :each do
Rack::Cache::AppEngine::MC::Service.clear
@store = Rack::Cache::MetaStore::GAEStore.new
@entity_store = Rack::Cache::EntityStore::Heap.new
end
include RackCacheMetaStoreImplementation
end
end
end
|