context_test.rb 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. require "#{File.dirname(__FILE__)}/spec_setup"
  2. require 'rack/cache/context'
  3. describe 'Rack::Cache::Context' do
  4. before { setup_cache_context }
  5. after { teardown_cache_context }
  6. it 'passes on non-GET/HEAD requests' do
  7. respond_with 200
  8. post '/'
  9. app.should.be.called
  10. response.should.be.ok
  11. cache.trace.should.include :pass
  12. response.headers.should.not.include 'Age'
  13. end
  14. it 'passes on rack-cache.force-pass' do
  15. respond_with 200
  16. get '/', {"rack-cache.force-pass" => true}
  17. app.should.be.called
  18. response.should.be.ok
  19. cache.trace.should == [:pass]
  20. response.headers.should.not.include 'Age'
  21. end
  22. %w[post put delete].each do |request_method|
  23. it "invalidates on #{request_method} requests" do
  24. respond_with 200
  25. request request_method, '/'
  26. app.should.be.called
  27. response.should.be.ok
  28. cache.trace.should.include :invalidate
  29. cache.trace.should.include :pass
  30. end
  31. end
  32. it 'does not cache with Authorization request header and non public response' do
  33. respond_with 200, 'ETag' => '"FOO"'
  34. get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
  35. app.should.be.called
  36. response.should.be.ok
  37. response.headers['Cache-Control'].should.equal 'private'
  38. cache.trace.should.include :miss
  39. cache.trace.should.not.include :store
  40. response.headers.should.not.include 'Age'
  41. end
  42. it 'does cache with Authorization request header and public response' do
  43. respond_with 200, 'Cache-Control' => 'public', 'ETag' => '"FOO"'
  44. get '/', 'HTTP_AUTHORIZATION' => 'basic foobarbaz'
  45. app.should.be.called
  46. response.should.be.ok
  47. cache.trace.should.include :miss
  48. cache.trace.should.include :store
  49. cache.trace.should.not.include :ignore
  50. response.headers.should.include 'Age'
  51. response.headers['Cache-Control'].should.equal 'public'
  52. end
  53. it 'does not cache with Cookie header and non public response' do
  54. respond_with 200, 'ETag' => '"FOO"'
  55. get '/', 'HTTP_COOKIE' => 'foo=bar'
  56. app.should.be.called
  57. response.should.be.ok
  58. response.headers['Cache-Control'].should.equal 'private'
  59. cache.trace.should.include :miss
  60. cache.trace.should.not.include :store
  61. response.headers.should.not.include 'Age'
  62. end
  63. it 'does not cache requests with a Cookie header' do
  64. respond_with 200
  65. get '/', 'HTTP_COOKIE' => 'foo=bar'
  66. response.should.be.ok
  67. app.should.be.called
  68. cache.trace.should.include :miss
  69. cache.trace.should.not.include :store
  70. response.headers.should.not.include 'Age'
  71. response.headers['Cache-Control'].should.equal 'private'
  72. end
  73. it 'does remove Set-Cookie response header from a cacheable response' do
  74. respond_with 200, 'Cache-Control' => 'public', 'ETag' => '"FOO"', 'Set-Cookie' => 'TestCookie=OK'
  75. get '/'
  76. app.should.be.called
  77. response.should.be.ok
  78. cache.trace.should.include :store
  79. cache.trace.should.include :ignore
  80. response.headers['Set-Cookie'].should.be.nil
  81. end
  82. it 'does remove all configured ignore_headers from a cacheable response' do
  83. respond_with 200, 'Cache-Control' => 'public', 'ETag' => '"FOO"', 'SET-COOKIE' => 'TestCookie=OK', 'X-Strip-Me' => 'Secret'
  84. get '/', 'rack-cache.ignore_headers' => ['set-cookie', 'x-strip-me']
  85. app.should.be.called
  86. response.should.be.ok
  87. cache.trace.should.include :store
  88. cache.trace.should.include :ignore
  89. response.headers['Set-Cookie'].should.be.nil
  90. response.headers['x-strip-me'].should.be.nil
  91. end
  92. it 'does not remove Set-Cookie response header from a private response' do
  93. respond_with 200, 'Cache-Control' => 'private', 'Set-Cookie' => 'TestCookie=OK'
  94. get '/'
  95. app.should.be.called
  96. response.should.be.ok
  97. cache.trace.should.not.include :store
  98. cache.trace.should.not.include :ignore
  99. response.headers['Set-Cookie'].should.equal 'TestCookie=OK'
  100. end
  101. it 'responds with 304 when If-Modified-Since matches Last-Modified' do
  102. timestamp = Time.now.httpdate
  103. respond_with do |req,res|
  104. res.status = 200
  105. res['Last-Modified'] = timestamp
  106. res['Content-Type'] = 'text/plain'
  107. res.body = ['Hello World']
  108. end
  109. get '/',
  110. 'HTTP_IF_MODIFIED_SINCE' => timestamp
  111. app.should.be.called
  112. response.status.should.equal 304
  113. response.original_headers.should.not.include 'Content-Length'
  114. response.original_headers.should.not.include 'Content-Type'
  115. response.body.should.empty
  116. cache.trace.should.include :miss
  117. cache.trace.should.include :store
  118. end
  119. it 'responds with 304 when If-None-Match matches ETag' do
  120. respond_with do |req,res|
  121. res.status = 200
  122. res['ETag'] = '12345'
  123. res['Content-Type'] = 'text/plain'
  124. res.body = ['Hello World']
  125. end
  126. get '/',
  127. 'HTTP_IF_NONE_MATCH' => '12345'
  128. app.should.be.called
  129. response.status.should.equal 304
  130. response.original_headers.should.not.include 'Content-Length'
  131. response.original_headers.should.not.include 'Content-Type'
  132. response.headers.should.include 'ETag'
  133. response.body.should.empty
  134. cache.trace.should.include :miss
  135. cache.trace.should.include :store
  136. end
  137. it 'responds with 304 only if If-None-Match and If-Modified-Since both match' do
  138. timestamp = Time.now
  139. respond_with do |req,res|
  140. res.status = 200
  141. res['ETag'] = '12345'
  142. res['Last-Modified'] = timestamp.httpdate
  143. res['Content-Type'] = 'text/plain'
  144. res.body = ['Hello World']
  145. end
  146. # Only etag matches
  147. get '/',
  148. 'HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => (timestamp - 1).httpdate
  149. app.should.be.called
  150. response.status.should.equal 200
  151. # Only last-modified matches
  152. get '/',
  153. 'HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => timestamp.httpdate
  154. app.should.be.called
  155. response.status.should.equal 200
  156. # Both matches
  157. get '/',
  158. 'HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => timestamp.httpdate
  159. app.should.be.called
  160. response.status.should.equal 304
  161. end
  162. it 'validates private responses cached on the client' do
  163. respond_with do |req,res|
  164. etags = req.env['HTTP_IF_NONE_MATCH'].to_s.split(/\s*,\s*/)
  165. if req.env['HTTP_COOKIE'] == 'authenticated'
  166. res['Cache-Control'] = 'private, no-store'
  167. res['ETag'] = '"private tag"'
  168. if etags.include?('"private tag"')
  169. res.status = 304
  170. else
  171. res.status = 200
  172. res['Content-Type'] = 'text/plain'
  173. res.body = ['private data']
  174. end
  175. else
  176. res['ETag'] = '"public tag"'
  177. if etags.include?('"public tag"')
  178. res.status = 304
  179. else
  180. res.status = 200
  181. res['Content-Type'] = 'text/plain'
  182. res.body = ['public data']
  183. end
  184. end
  185. end
  186. get '/'
  187. app.should.be.called
  188. response.status.should.equal 200
  189. response.headers['ETag'].should == '"public tag"'
  190. response.body.should == 'public data'
  191. cache.trace.should.include :miss
  192. cache.trace.should.include :store
  193. get '/', 'HTTP_COOKIE' => 'authenticated'
  194. app.should.be.called
  195. response.status.should.equal 200
  196. response.headers['ETag'].should == '"private tag"'
  197. response.body.should == 'private data'
  198. cache.trace.should.include :stale
  199. cache.trace.should.include :invalid
  200. cache.trace.should.not.include :store
  201. get '/',
  202. 'HTTP_IF_NONE_MATCH' => '"public tag"'
  203. app.should.be.called
  204. response.status.should.equal 304
  205. response.headers['ETag'].should == '"public tag"'
  206. cache.trace.should.include :stale
  207. cache.trace.should.include :valid
  208. cache.trace.should.include :store
  209. get '/',
  210. 'HTTP_IF_NONE_MATCH' => '"private tag"',
  211. 'HTTP_COOKIE' => 'authenticated'
  212. app.should.be.called
  213. response.status.should.equal 304
  214. response.headers['ETag'].should == '"private tag"'
  215. cache.trace.should.include :valid
  216. cache.trace.should.not.include :store
  217. end
  218. it 'stores responses when no-cache request directive present' do
  219. respond_with 200, 'Expires' => (Time.now + 5).httpdate
  220. get '/', 'HTTP_CACHE_CONTROL' => 'no-cache'
  221. response.should.be.ok
  222. cache.trace.should.include :store
  223. response.headers.should.include 'Age'
  224. end
  225. it 'reloads responses when cache hits but no-cache request directive present ' +
  226. 'when allow_reload is set true' do
  227. count = 0
  228. respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
  229. count+= 1
  230. res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
  231. end
  232. get '/'
  233. response.should.be.ok
  234. response.body.should.equal 'Hello World'
  235. cache.trace.should.include :store
  236. get '/'
  237. response.should.be.ok
  238. response.body.should.equal 'Hello World'
  239. cache.trace.should.include :fresh
  240. get '/',
  241. 'rack-cache.allow_reload' => true,
  242. 'HTTP_CACHE_CONTROL' => 'no-cache'
  243. response.should.be.ok
  244. response.body.should.equal 'Goodbye World'
  245. cache.trace.should.include :reload
  246. cache.trace.should.include :store
  247. end
  248. it 'does not reload responses when allow_reload is set false (default)' do
  249. count = 0
  250. respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
  251. count+= 1
  252. res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
  253. end
  254. get '/'
  255. response.should.be.ok
  256. response.body.should.equal 'Hello World'
  257. cache.trace.should.include :store
  258. get '/'
  259. response.should.be.ok
  260. response.body.should.equal 'Hello World'
  261. cache.trace.should.include :fresh
  262. get '/',
  263. 'rack-cache.allow_reload' => false,
  264. 'HTTP_CACHE_CONTROL' => 'no-cache'
  265. response.should.be.ok
  266. response.body.should.equal 'Hello World'
  267. cache.trace.should.not.include :reload
  268. # test again without explicitly setting the allow_reload option to false
  269. get '/',
  270. 'HTTP_CACHE_CONTROL' => 'no-cache'
  271. response.should.be.ok
  272. response.body.should.equal 'Hello World'
  273. cache.trace.should.not.include :reload
  274. end
  275. it 'revalidates fresh cache entry when max-age request directive is exceeded ' +
  276. 'when allow_revalidate option is set true' do
  277. count = 0
  278. respond_with do |req,res|
  279. count+= 1
  280. res['Cache-Control'] = 'max-age=10000'
  281. res['ETag'] = count.to_s
  282. res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
  283. end
  284. get '/'
  285. response.should.be.ok
  286. response.body.should.equal 'Hello World'
  287. cache.trace.should.include :store
  288. get '/'
  289. response.should.be.ok
  290. response.body.should.equal 'Hello World'
  291. cache.trace.should.include :fresh
  292. get '/',
  293. 'rack-cache.allow_revalidate' => true,
  294. 'HTTP_CACHE_CONTROL' => 'max-age=0'
  295. response.should.be.ok
  296. response.body.should.equal 'Goodbye World'
  297. cache.trace.should.include :stale
  298. cache.trace.should.include :invalid
  299. cache.trace.should.include :store
  300. end
  301. it 'does not revalidate fresh cache entry when enable_revalidate option is set false (default)' do
  302. count = 0
  303. respond_with do |req,res|
  304. count+= 1
  305. res['Cache-Control'] = 'max-age=10000'
  306. res['ETag'] = count.to_s
  307. res.body = (count == 1) ? ['Hello World'] : ['Goodbye World']
  308. end
  309. get '/'
  310. response.should.be.ok
  311. response.body.should.equal 'Hello World'
  312. cache.trace.should.include :store
  313. get '/'
  314. response.should.be.ok
  315. response.body.should.equal 'Hello World'
  316. cache.trace.should.include :fresh
  317. get '/',
  318. 'rack-cache.allow_revalidate' => false,
  319. 'HTTP_CACHE_CONTROL' => 'max-age=0'
  320. response.should.be.ok
  321. response.body.should.equal 'Hello World'
  322. cache.trace.should.not.include :stale
  323. cache.trace.should.not.include :invalid
  324. cache.trace.should.include :fresh
  325. # test again without explicitly setting the allow_revalidate option to false
  326. get '/',
  327. 'HTTP_CACHE_CONTROL' => 'max-age=0'
  328. response.should.be.ok
  329. response.body.should.equal 'Hello World'
  330. cache.trace.should.not.include :stale
  331. cache.trace.should.not.include :invalid
  332. cache.trace.should.include :fresh
  333. end
  334. it 'fetches response from backend when cache misses' do
  335. respond_with 200, 'Expires' => (Time.now + 5).httpdate
  336. get '/'
  337. response.should.be.ok
  338. cache.trace.should.include :miss
  339. response.headers.should.include 'Age'
  340. end
  341. [(201..202),(204..206),(303..305),(400..403),(405..409),(411..417),(500..505)].each do |range|
  342. range.each do |response_code|
  343. it "does not cache #{response_code} responses" do
  344. respond_with response_code, 'Expires' => (Time.now + 5).httpdate
  345. get '/'
  346. cache.trace.should.not.include :store
  347. response.status.should.equal response_code
  348. response.headers.should.not.include 'Age'
  349. end
  350. end
  351. end
  352. it "does not cache responses with explicit no-store directive" do
  353. respond_with 200,
  354. 'Expires' => (Time.now + 5).httpdate,
  355. 'Cache-Control' => 'no-store'
  356. get '/'
  357. response.should.be.ok
  358. cache.trace.should.not.include :store
  359. response.headers.should.not.include 'Age'
  360. end
  361. it 'does not cache responses without freshness information or a validator' do
  362. respond_with 200
  363. get '/'
  364. response.should.be.ok
  365. cache.trace.should.not.include :store
  366. end
  367. it "caches responses with explicit no-cache directive" do
  368. respond_with 200,
  369. 'Expires' => (Time.now + 5).httpdate,
  370. 'Cache-Control' => 'no-cache'
  371. get '/'
  372. response.should.be.ok
  373. cache.trace.should.include :store
  374. response.headers.should.include 'Age'
  375. end
  376. it 'caches responses with an Expiration header' do
  377. respond_with 200, 'Expires' => (Time.now + 5).httpdate
  378. get '/'
  379. response.should.be.ok
  380. response.body.should.equal 'Hello World'
  381. response.headers.should.include 'Date'
  382. response['Age'].should.not.be.nil
  383. response['X-Content-Digest'].should.not.be.nil
  384. cache.trace.should.include :miss
  385. cache.trace.should.include :store
  386. cache.metastore.to_hash.keys.length.should.equal 1
  387. end
  388. it 'caches responses with a max-age directive' do
  389. respond_with 200, 'Cache-Control' => 'max-age=5'
  390. get '/'
  391. response.should.be.ok
  392. response.body.should.equal 'Hello World'
  393. response.headers.should.include 'Date'
  394. response['Age'].should.not.be.nil
  395. response['X-Content-Digest'].should.not.be.nil
  396. cache.trace.should.include :miss
  397. cache.trace.should.include :store
  398. cache.metastore.to_hash.keys.length.should.equal 1
  399. end
  400. it 'caches responses with a s-maxage directive' do
  401. respond_with 200, 'Cache-Control' => 's-maxage=5'
  402. get '/'
  403. response.should.be.ok
  404. response.body.should.equal 'Hello World'
  405. response.headers.should.include 'Date'
  406. response['Age'].should.not.be.nil
  407. response['X-Content-Digest'].should.not.be.nil
  408. cache.trace.should.include :miss
  409. cache.trace.should.include :store
  410. cache.metastore.to_hash.keys.length.should.equal 1
  411. end
  412. it 'caches responses with a Last-Modified validator but no freshness information' do
  413. respond_with 200, 'Last-Modified' => Time.now.httpdate
  414. get '/'
  415. response.should.be.ok
  416. response.body.should.equal 'Hello World'
  417. cache.trace.should.include :miss
  418. cache.trace.should.include :store
  419. end
  420. it 'caches responses with an ETag validator but no freshness information' do
  421. respond_with 200, 'ETag' => '"123456"'
  422. get '/'
  423. response.should.be.ok
  424. response.body.should.equal 'Hello World'
  425. cache.trace.should.include :miss
  426. cache.trace.should.include :store
  427. end
  428. it 'hits cached response with Expires header' do
  429. respond_with 200,
  430. 'Date' => (Time.now - 5).httpdate,
  431. 'Expires' => (Time.now + 5).httpdate
  432. get '/'
  433. app.should.be.called
  434. response.should.be.ok
  435. response.headers.should.include 'Date'
  436. cache.trace.should.include :miss
  437. cache.trace.should.include :store
  438. response.body.should.equal 'Hello World'
  439. get '/'
  440. response.should.be.ok
  441. app.should.not.be.called
  442. response['Date'].should.equal responses.first['Date']
  443. response['Age'].to_i.should.satisfy { |age| age > 0 }
  444. response['X-Content-Digest'].should.not.be.nil
  445. cache.trace.should.include :fresh
  446. cache.trace.should.not.include :store
  447. response.body.should.equal 'Hello World'
  448. end
  449. it 'hits cached response with max-age directive' do
  450. respond_with 200,
  451. 'Date' => (Time.now - 5).httpdate,
  452. 'Cache-Control' => 'max-age=10'
  453. get '/'
  454. app.should.be.called
  455. response.should.be.ok
  456. response.headers.should.include 'Date'
  457. cache.trace.should.include :miss
  458. cache.trace.should.include :store
  459. response.body.should.equal 'Hello World'
  460. get '/'
  461. response.should.be.ok
  462. app.should.not.be.called
  463. response['Date'].should.equal responses.first['Date']
  464. response['Age'].to_i.should.satisfy { |age| age > 0 }
  465. response['X-Content-Digest'].should.not.be.nil
  466. cache.trace.should.include :fresh
  467. cache.trace.should.not.include :store
  468. response.body.should.equal 'Hello World'
  469. end
  470. it 'hits cached response with s-maxage directive' do
  471. respond_with 200,
  472. 'Date' => (Time.now - 5).httpdate,
  473. 'Cache-Control' => 's-maxage=10, max-age=0'
  474. get '/'
  475. app.should.be.called
  476. response.should.be.ok
  477. response.headers.should.include 'Date'
  478. cache.trace.should.include :miss
  479. cache.trace.should.include :store
  480. response.body.should.equal 'Hello World'
  481. get '/'
  482. response.should.be.ok
  483. app.should.not.be.called
  484. response['Date'].should.equal responses.first['Date']
  485. response['Age'].to_i.should.satisfy { |age| age > 0 }
  486. response['X-Content-Digest'].should.not.be.nil
  487. cache.trace.should.include :fresh
  488. cache.trace.should.not.include :store
  489. response.body.should.equal 'Hello World'
  490. end
  491. it 'assigns default_ttl when response has no freshness information' do
  492. respond_with 200
  493. get '/', 'rack-cache.default_ttl' => 10
  494. app.should.be.called
  495. response.should.be.ok
  496. cache.trace.should.include :miss
  497. cache.trace.should.include :store
  498. response.body.should.equal 'Hello World'
  499. response['Cache-Control'].should.include 's-maxage=10'
  500. get '/', 'rack-cache.default_ttl' => 10
  501. response.should.be.ok
  502. app.should.not.be.called
  503. cache.trace.should.include :fresh
  504. cache.trace.should.not.include :store
  505. response.body.should.equal 'Hello World'
  506. end
  507. it 'does not assign default_ttl when response has must-revalidate directive' do
  508. respond_with 200,
  509. 'Cache-Control' => 'must-revalidate'
  510. get '/', 'rack-cache.default_ttl' => 10
  511. app.should.be.called
  512. response.should.be.ok
  513. cache.trace.should.include :miss
  514. cache.trace.should.not.include :store
  515. response['Cache-Control'].should.not.include 's-maxage'
  516. response.body.should.equal 'Hello World'
  517. end
  518. it 'fetches full response when cache stale and no validators present' do
  519. respond_with 200, 'Expires' => (Time.now + 5).httpdate
  520. # build initial request
  521. get '/'
  522. app.should.be.called
  523. response.should.be.ok
  524. response.headers.should.include 'Date'
  525. response.headers.should.include 'X-Content-Digest'
  526. response.headers.should.include 'Age'
  527. cache.trace.should.include :miss
  528. cache.trace.should.include :store
  529. response.body.should.equal 'Hello World'
  530. # go in and play around with the cached metadata directly ...
  531. # XXX find some other way to do this
  532. hash = cache.metastore.to_hash
  533. hash.values.length.should.equal 1
  534. entries = Marshal.load(hash.values.first)
  535. entries.length.should.equal 1
  536. req, res = entries.first
  537. res['Expires'] = (Time.now - 1).httpdate
  538. hash[hash.keys.first] = Marshal.dump([[req, res]])
  539. # build subsequent request; should be found but miss due to freshness
  540. get '/'
  541. app.should.be.called
  542. response.should.be.ok
  543. response['Age'].to_i.should.equal 0
  544. response.headers.should.include 'X-Content-Digest'
  545. cache.trace.should.include :stale
  546. cache.trace.should.not.include :fresh
  547. cache.trace.should.not.include :miss
  548. cache.trace.should.include :store
  549. response.body.should.equal 'Hello World'
  550. end
  551. it 'validates cached responses with Last-Modified and no freshness information' do
  552. timestamp = Time.now.httpdate
  553. respond_with do |req,res|
  554. res['Last-Modified'] = timestamp
  555. if req.env['HTTP_IF_MODIFIED_SINCE'] == timestamp
  556. res.status = 304
  557. res.body = []
  558. end
  559. end
  560. # build initial request
  561. get '/'
  562. app.should.be.called
  563. response.should.be.ok
  564. response.headers.should.include 'Last-Modified'
  565. response.headers.should.include 'X-Content-Digest'
  566. response.body.should.equal 'Hello World'
  567. cache.trace.should.include :miss
  568. cache.trace.should.include :store
  569. cache.trace.should.not.include :stale
  570. # build subsequent request; should be found but miss due to freshness
  571. get '/'
  572. app.should.be.called
  573. response.should.be.ok
  574. response.headers.should.include 'Last-Modified'
  575. response.headers.should.include 'X-Content-Digest'
  576. response['Age'].to_i.should.equal 0
  577. response.body.should.equal 'Hello World'
  578. cache.trace.should.include :stale
  579. cache.trace.should.include :valid
  580. cache.trace.should.include :store
  581. cache.trace.should.not.include :miss
  582. end
  583. it 'validates cached responses with ETag and no freshness information' do
  584. timestamp = Time.now.httpdate
  585. respond_with do |req,res|
  586. res['ETAG'] = '"12345"'
  587. if req.env['HTTP_IF_NONE_MATCH'] == res['Etag']
  588. res.status = 304
  589. res.body = []
  590. end
  591. end
  592. # build initial request
  593. get '/'
  594. app.should.be.called
  595. response.should.be.ok
  596. response.headers.should.include 'ETag'
  597. response.headers.should.include 'X-Content-Digest'
  598. response.body.should.equal 'Hello World'
  599. cache.trace.should.include :miss
  600. cache.trace.should.include :store
  601. # build subsequent request; should be found but miss due to freshness
  602. get '/'
  603. app.should.be.called
  604. response.should.be.ok
  605. response.headers.should.include 'ETag'
  606. response.headers.should.include 'X-Content-Digest'
  607. response['Age'].to_i.should.equal 0
  608. response.body.should.equal 'Hello World'
  609. cache.trace.should.include :stale
  610. cache.trace.should.include :valid
  611. cache.trace.should.include :store
  612. cache.trace.should.not.include :miss
  613. end
  614. it 'replaces cached responses when validation results in non-304 response' do
  615. timestamp = Time.now.httpdate
  616. count = 0
  617. respond_with do |req,res|
  618. res['Last-Modified'] = timestamp
  619. case (count+=1)
  620. when 1 ; res.body = ['first response']
  621. when 2 ; res.body = ['second response']
  622. when 3
  623. res.body = []
  624. res.status = 304
  625. end
  626. end
  627. # first request should fetch from backend and store in cache
  628. get '/'
  629. response.status.should.equal 200
  630. response.body.should.equal 'first response'
  631. # second request is validated, is invalid, and replaces cached entry
  632. get '/'
  633. response.status.should.equal 200
  634. response.body.should.equal 'second response'
  635. # third respone is validated, valid, and returns cached entry
  636. get '/'
  637. response.status.should.equal 200
  638. response.body.should.equal 'second response'
  639. count.should.equal 3
  640. end
  641. it 'passes HEAD requests through directly on pass' do
  642. respond_with do |req,res|
  643. res.status = 200
  644. res.body = []
  645. req.request_method.should.equal 'HEAD'
  646. end
  647. head '/', 'HTTP_EXPECT' => 'something ...'
  648. app.should.be.called
  649. response.body.should.equal ''
  650. end
  651. it 'uses cache to respond to HEAD requests when fresh' do
  652. respond_with do |req,res|
  653. res['Cache-Control'] = 'max-age=10'
  654. res.body = ['Hello World']
  655. req.request_method.should.not.equal 'HEAD'
  656. end
  657. get '/'
  658. app.should.be.called
  659. response.status.should.equal 200
  660. response.body.should.equal 'Hello World'
  661. head '/'
  662. app.should.not.be.called
  663. response.status.should.equal 200
  664. response.body.should.equal ''
  665. response['Content-Length'].should.equal 'Hello World'.length.to_s
  666. end
  667. it 'invalidates cached responses on POST' do
  668. respond_with do |req,res|
  669. if req.request_method == 'GET'
  670. res.status = 200
  671. res['Cache-Control'] = 'public, max-age=500'
  672. res.body = ['Hello World']
  673. elsif req.request_method == 'POST'
  674. res.status = 303
  675. res['Location'] = '/'
  676. res.headers.delete('Cache-Control')
  677. res.body = []
  678. end
  679. end
  680. # build initial request to enter into the cache
  681. get '/'
  682. app.should.be.called
  683. response.should.be.ok
  684. response.body.should.equal 'Hello World'
  685. cache.trace.should.include :miss
  686. cache.trace.should.include :store
  687. # make sure it is valid
  688. get '/'
  689. app.should.not.called
  690. response.should.be.ok
  691. response.body.should.equal 'Hello World'
  692. cache.trace.should.include :fresh
  693. # now POST to same URL
  694. post '/'
  695. app.should.be.called
  696. response.should.be.redirect
  697. response['Location'].should.equal '/'
  698. cache.trace.should.include :invalidate
  699. cache.trace.should.include :pass
  700. response.body.should.equal ''
  701. # now make sure it was actually invalidated
  702. get '/'
  703. app.should.be.called
  704. response.should.be.ok
  705. response.body.should.equal 'Hello World'
  706. cache.trace.should.include :stale
  707. cache.trace.should.include :invalid
  708. cache.trace.should.include :store
  709. end
  710. describe 'with responses that include a Vary header' do
  711. before do
  712. count = 0
  713. respond_with 200 do |req,res|
  714. res['Vary'] = 'Accept User-Agent Foo'
  715. res['Cache-Control'] = 'max-age=10'
  716. res['X-Response-Count'] = (count+=1).to_s
  717. res.body = [req.env['HTTP_USER_AGENT']]
  718. end
  719. end
  720. it 'serves from cache when headers match' do
  721. get '/',
  722. 'HTTP_ACCEPT' => 'text/html',
  723. 'HTTP_USER_AGENT' => 'Bob/1.0'
  724. response.should.be.ok
  725. response.body.should.equal 'Bob/1.0'
  726. cache.trace.should.include :miss
  727. cache.trace.should.include :store
  728. get '/',
  729. 'HTTP_ACCEPT' => 'text/html',
  730. 'HTTP_USER_AGENT' => 'Bob/1.0'
  731. response.should.be.ok
  732. response.body.should.equal 'Bob/1.0'
  733. cache.trace.should.include :fresh
  734. cache.trace.should.not.include :store
  735. response.headers.should.include 'X-Content-Digest'
  736. end
  737. it 'stores multiple responses when headers differ' do
  738. get '/',
  739. 'HTTP_ACCEPT' => 'text/html',
  740. 'HTTP_USER_AGENT' => 'Bob/1.0'
  741. response.should.be.ok
  742. response.body.should.equal 'Bob/1.0'
  743. response['X-Response-Count'].should.equal '1'
  744. get '/',
  745. 'HTTP_ACCEPT' => 'text/html',
  746. 'HTTP_USER_AGENT' => 'Bob/2.0'
  747. cache.trace.should.include :miss
  748. cache.trace.should.include :store
  749. response.body.should.equal 'Bob/2.0'
  750. response['X-Response-Count'].should.equal '2'
  751. get '/',
  752. 'HTTP_ACCEPT' => 'text/html',
  753. 'HTTP_USER_AGENT' => 'Bob/1.0'
  754. cache.trace.should.include :fresh
  755. response.body.should.equal 'Bob/1.0'
  756. response['X-Response-Count'].should.equal '1'
  757. get '/',
  758. 'HTTP_ACCEPT' => 'text/html',
  759. 'HTTP_USER_AGENT' => 'Bob/2.0'
  760. cache.trace.should.include :fresh
  761. response.body.should.equal 'Bob/2.0'
  762. response['X-Response-Count'].should.equal '2'
  763. get '/',
  764. 'HTTP_USER_AGENT' => 'Bob/2.0'
  765. cache.trace.should.include :miss
  766. response.body.should.equal 'Bob/2.0'
  767. response['X-Response-Count'].should.equal '3'
  768. end
  769. end
  770. it 'passes if there was a metastore exception' do
  771. respond_with 200, 'Cache-Control' => 'max-age=10000' do |req,res|
  772. res.body = ['Hello World']
  773. end
  774. get '/'
  775. response.should.be.ok
  776. response.body.should.equal 'Hello World'
  777. cache.trace.should.include :store
  778. get '/' do |cache|
  779. cache.meta_def(:metastore) { raise Timeout::Error }
  780. end
  781. response.should.be.ok
  782. response.body.should.equal 'Hello World'
  783. cache.trace.should.include :pass
  784. post '/' do |cache|
  785. cache.meta_def(:metastore) { raise Timeout::Error }
  786. end
  787. response.should.be.ok
  788. response.body.should.equal 'Hello World'
  789. cache.trace.should.include :pass
  790. end
  791. end