HttpCacheTest.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Tests\Component\HttpKernel\HttpCache;
  11. require_once __DIR__.'/HttpCacheTestCase.php';
  12. class HttpCacheTest extends HttpCacheTestCase
  13. {
  14. public function testPassesOnNonGetHeadRequests()
  15. {
  16. $this->setNextResponse(200);
  17. $this->request('POST', '/');
  18. $this->assertHttpKernelIsCalled();
  19. $this->assertResponseOk();
  20. $this->assertTraceContains('pass');
  21. $this->assertFalse($this->response->headers->has('Age'));
  22. }
  23. public function testInvalidatesOnPostPutDeleteRequests()
  24. {
  25. foreach (array('post', 'put', 'delete') as $method) {
  26. $this->setNextResponse(200);
  27. $this->request($method, '/');
  28. $this->assertHttpKernelIsCalled();
  29. $this->assertResponseOk();
  30. $this->assertTraceContains('invalidate');
  31. $this->assertTraceContains('pass');
  32. }
  33. }
  34. public function testDoesNotCacheWithAuthorizationRequestHeaderAndNonPublicResponse()
  35. {
  36. $this->setNextResponse(200, array('ETag' => '"Foo"'));
  37. $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
  38. $this->assertHttpKernelIsCalled();
  39. $this->assertResponseOk();
  40. $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
  41. $this->assertTraceContains('miss');
  42. $this->assertTraceNotContains('store');
  43. $this->assertFalse($this->response->headers->has('Age'));
  44. }
  45. public function testDoesCacheWithAuthorizationRequestHeaderAndPublicResponse()
  46. {
  47. $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"Foo"'));
  48. $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
  49. $this->assertHttpKernelIsCalled();
  50. $this->assertResponseOk();
  51. $this->assertTraceContains('miss');
  52. $this->assertTraceContains('store');
  53. $this->assertTrue($this->response->headers->has('Age'));
  54. $this->assertEquals('public', $this->response->headers->get('Cache-Control'));
  55. }
  56. public function testDoesNotCacheWithCookieHeaderAndNonPublicResponse()
  57. {
  58. $this->setNextResponse(200, array('ETag' => '"Foo"'));
  59. $this->request('GET', '/', array(), array('foo' => 'bar'));
  60. $this->assertHttpKernelIsCalled();
  61. $this->assertResponseOk();
  62. $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
  63. $this->assertTraceContains('miss');
  64. $this->assertTraceNotContains('store');
  65. $this->assertFalse($this->response->headers->has('Age'));
  66. }
  67. public function testDoesNotCacheRequestsWithACookieHeader()
  68. {
  69. $this->setNextResponse(200);
  70. $this->request('GET', '/', array(), array('foo' => 'bar'));
  71. $this->assertHttpKernelIsCalled();
  72. $this->assertResponseOk();
  73. $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
  74. $this->assertTraceContains('miss');
  75. $this->assertTraceNotContains('store');
  76. $this->assertFalse($this->response->headers->has('Age'));
  77. }
  78. public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified()
  79. {
  80. $time = new \DateTime();
  81. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822), 'Content-Type' => 'text/plain'), 'Hello World');
  82. $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
  83. $this->assertHttpKernelIsCalled();
  84. $this->assertEquals(304, $this->response->getStatusCode());
  85. $this->assertEquals('text/html; charset=UTF-8', $this->response->headers->get('Content-Type'));
  86. $this->assertEmpty($this->response->getContent());
  87. $this->assertTraceContains('miss');
  88. $this->assertTraceContains('store');
  89. }
  90. public function testRespondsWith304WhenIfNoneMatchMatchesETag()
  91. {
  92. $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '12345', 'Content-Type' => 'text/plain'), 'Hello World');
  93. $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345'));
  94. $this->assertHttpKernelIsCalled();
  95. $this->assertEquals(304, $this->response->getStatusCode());
  96. $this->assertEquals('text/html; charset=UTF-8', $this->response->headers->get('Content-Type'));
  97. $this->assertTrue($this->response->headers->has('ETag'));
  98. $this->assertEmpty($this->response->getContent());
  99. $this->assertTraceContains('miss');
  100. $this->assertTraceContains('store');
  101. }
  102. public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch()
  103. {
  104. $time = new \DateTime();
  105. $this->setNextResponse(200, array(), '', function ($request, $response) use ($time) {
  106. $response->setStatusCode(200);
  107. $response->headers->set('ETag', '12345');
  108. $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
  109. $response->headers->set('Content-Type', 'text/plain');
  110. $response->setContent('Hello World');
  111. });
  112. // only ETag matches
  113. $t = \DateTime::createFromFormat('U', time() - 3600);
  114. $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(DATE_RFC2822)));
  115. $this->assertHttpKernelIsCalled();
  116. $this->assertEquals(200, $this->response->getStatusCode());
  117. // only Last-Modified matches
  118. $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
  119. $this->assertHttpKernelIsCalled();
  120. $this->assertEquals(200, $this->response->getStatusCode());
  121. // Both matches
  122. $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
  123. $this->assertHttpKernelIsCalled();
  124. $this->assertEquals(304, $this->response->getStatusCode());
  125. }
  126. public function testValidatesPrivateResponsesCachedOnTheClient()
  127. {
  128. $this->setNextResponse(200, array(), '', function ($request, $response) {
  129. $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH'));
  130. if ($request->cookies->has('authenticated')) {
  131. $response->headers->set('Cache-Control', 'private, no-store');
  132. $response->setETag('"private tag"');
  133. if (in_array('"private tag"', $etags)) {
  134. $response->setStatusCode(304);
  135. } else {
  136. $response->setStatusCode(200);
  137. $response->headers->set('Content-Type', 'text/plain');
  138. $response->setContent('private data');
  139. }
  140. } else {
  141. $response->headers->set('Cache-Control', 'public');
  142. $response->setETag('"public tag"');
  143. if (in_array('"public tag"', $etags)) {
  144. $response->setStatusCode(304);
  145. } else {
  146. $response->setStatusCode(200);
  147. $response->headers->set('Content-Type', 'text/plain');
  148. $response->setContent('public data');
  149. }
  150. }
  151. });
  152. $this->request('GET', '/');
  153. $this->assertHttpKernelIsCalled();
  154. $this->assertEquals(200, $this->response->getStatusCode());
  155. $this->assertEquals('"public tag"', $this->response->headers->get('ETag'));
  156. $this->assertEquals('public data', $this->response->getContent());
  157. $this->assertTraceContains('miss');
  158. $this->assertTraceContains('store');
  159. $this->request('GET', '/', array(), array('authenticated' => ''));
  160. $this->assertHttpKernelIsCalled();
  161. $this->assertEquals(200, $this->response->getStatusCode());
  162. $this->assertEquals('"private tag"', $this->response->headers->get('ETag'));
  163. $this->assertEquals('private data', $this->response->getContent());
  164. $this->assertTraceContains('stale');
  165. $this->assertTraceContains('invalid');
  166. $this->assertTraceNotContains('store');
  167. }
  168. public function testStoresResponsesWhenNoCacheRequestDirectivePresent()
  169. {
  170. $time = \DateTime::createFromFormat('U', time() + 5);
  171. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
  172. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
  173. $this->assertHttpKernelIsCalled();
  174. $this->assertTraceContains('store');
  175. $this->assertTrue($this->response->headers->has('Age'));
  176. }
  177. public function testReloadsResponsesWhenCacheHitsButNoCacheRequestDirectivePresentWhenAllowReloadIsSetTrue()
  178. {
  179. $count = 0;
  180. $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
  181. ++$count;
  182. $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
  183. });
  184. $this->request('GET', '/');
  185. $this->assertEquals(200, $this->response->getStatusCode());
  186. $this->assertEquals('Hello World', $this->response->getContent());
  187. $this->assertTraceContains('store');
  188. $this->request('GET', '/');
  189. $this->assertEquals(200, $this->response->getStatusCode());
  190. $this->assertEquals('Hello World', $this->response->getContent());
  191. $this->assertTraceContains('fresh');
  192. $this->cacheConfig['allow_reload'] = true;
  193. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
  194. $this->assertEquals(200, $this->response->getStatusCode());
  195. $this->assertEquals('Goodbye World', $this->response->getContent());
  196. $this->assertTraceContains('reload');
  197. $this->assertTraceContains('store');
  198. }
  199. public function testDoesNotReloadResponsesWhenAllowReloadIsSetFalseDefault()
  200. {
  201. $count = 0;
  202. $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
  203. ++$count;
  204. $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
  205. });
  206. $this->request('GET', '/');
  207. $this->assertEquals(200, $this->response->getStatusCode());
  208. $this->assertEquals('Hello World', $this->response->getContent());
  209. $this->assertTraceContains('store');
  210. $this->request('GET', '/');
  211. $this->assertEquals(200, $this->response->getStatusCode());
  212. $this->assertEquals('Hello World', $this->response->getContent());
  213. $this->assertTraceContains('fresh');
  214. $this->cacheConfig['allow_reload'] = false;
  215. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
  216. $this->assertEquals(200, $this->response->getStatusCode());
  217. $this->assertEquals('Hello World', $this->response->getContent());
  218. $this->assertTraceNotContains('reload');
  219. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
  220. $this->assertEquals(200, $this->response->getStatusCode());
  221. $this->assertEquals('Hello World', $this->response->getContent());
  222. $this->assertTraceNotContains('reload');
  223. }
  224. public function testRevalidatesFreshCacheEntryWhenMaxAgeRequestDirectiveIsExceededWhenAllowRevalidateOptionIsSetTrue()
  225. {
  226. $count = 0;
  227. $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
  228. ++$count;
  229. $response->headers->set('Cache-Control', 'public, max-age=10000');
  230. $response->setETag($count);
  231. $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
  232. });
  233. $this->request('GET', '/');
  234. $this->assertEquals(200, $this->response->getStatusCode());
  235. $this->assertEquals('Hello World', $this->response->getContent());
  236. $this->assertTraceContains('store');
  237. $this->request('GET', '/');
  238. $this->assertEquals(200, $this->response->getStatusCode());
  239. $this->assertEquals('Hello World', $this->response->getContent());
  240. $this->assertTraceContains('fresh');
  241. $this->cacheConfig['allow_revalidate'] = true;
  242. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
  243. $this->assertEquals(200, $this->response->getStatusCode());
  244. $this->assertEquals('Goodbye World', $this->response->getContent());
  245. $this->assertTraceContains('stale');
  246. $this->assertTraceContains('invalid');
  247. $this->assertTraceContains('store');
  248. }
  249. public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIsSetFalseDefault()
  250. {
  251. $count = 0;
  252. $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
  253. ++$count;
  254. $response->headers->set('Cache-Control', 'public, max-age=10000');
  255. $response->setETag($count);
  256. $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
  257. });
  258. $this->request('GET', '/');
  259. $this->assertEquals(200, $this->response->getStatusCode());
  260. $this->assertEquals('Hello World', $this->response->getContent());
  261. $this->assertTraceContains('store');
  262. $this->request('GET', '/');
  263. $this->assertEquals(200, $this->response->getStatusCode());
  264. $this->assertEquals('Hello World', $this->response->getContent());
  265. $this->assertTraceContains('fresh');
  266. $this->cacheConfig['allow_revalidate'] = false;
  267. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
  268. $this->assertEquals(200, $this->response->getStatusCode());
  269. $this->assertEquals('Hello World', $this->response->getContent());
  270. $this->assertTraceNotContains('stale');
  271. $this->assertTraceNotContains('invalid');
  272. $this->assertTraceContains('fresh');
  273. $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
  274. $this->assertEquals(200, $this->response->getStatusCode());
  275. $this->assertEquals('Hello World', $this->response->getContent());
  276. $this->assertTraceNotContains('stale');
  277. $this->assertTraceNotContains('invalid');
  278. $this->assertTraceContains('fresh');
  279. }
  280. public function testFetchesResponseFromBackendWhenCacheMisses()
  281. {
  282. $time = \DateTime::createFromFormat('U', time() + 5);
  283. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
  284. $this->request('GET', '/');
  285. $this->assertEquals(200, $this->response->getStatusCode());
  286. $this->assertTraceContains('miss');
  287. $this->assertTrue($this->response->headers->has('Age'));
  288. }
  289. public function testDoesNotCacheSomeStatusCodeResponses()
  290. {
  291. foreach (array_merge(range(201, 202), range(204, 206), range(303, 305), range(400, 403), range(405, 409), range(411, 417), range(500, 505)) as $code) {
  292. $time = \DateTime::createFromFormat('U', time() + 5);
  293. $this->setNextResponse($code, array('Expires' => $time->format(DATE_RFC2822)));
  294. $this->request('GET', '/');
  295. $this->assertEquals($code, $this->response->getStatusCode());
  296. $this->assertTraceNotContains('store');
  297. $this->assertFalse($this->response->headers->has('Age'));
  298. }
  299. }
  300. public function testDoesNotCacheResponsesWithExplicitNoStoreDirective()
  301. {
  302. $time = \DateTime::createFromFormat('U', time() + 5);
  303. $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'no-store'));
  304. $this->request('GET', '/');
  305. $this->assertTraceNotContains('store');
  306. $this->assertFalse($this->response->headers->has('Age'));
  307. }
  308. public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator()
  309. {
  310. $this->setNextResponse();
  311. $this->request('GET', '/');
  312. $this->assertEquals(200, $this->response->getStatusCode());
  313. $this->assertTraceNotContains('store');
  314. }
  315. public function testCachesResponsesWithExplicitNoCacheDirective()
  316. {
  317. $time = \DateTime::createFromFormat('U', time() + 5);
  318. $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, no-cache'));
  319. $this->request('GET', '/');
  320. $this->assertTraceContains('store');
  321. $this->assertTrue($this->response->headers->has('Age'));
  322. }
  323. public function testCachesResponsesWithAnExpirationHeader()
  324. {
  325. $time = \DateTime::createFromFormat('U', time() + 5);
  326. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
  327. $this->request('GET', '/');
  328. $this->assertEquals(200, $this->response->getStatusCode());
  329. $this->assertEquals('Hello World', $this->response->getContent());
  330. $this->assertNotNull($this->response->headers->get('Date'));
  331. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  332. $this->assertTraceContains('miss');
  333. $this->assertTraceContains('store');
  334. $values = $this->getMetaStorageValues();
  335. $this->assertEquals(1, count($values));
  336. }
  337. public function testCachesResponsesWithAMaxAgeDirective()
  338. {
  339. $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=5'));
  340. $this->request('GET', '/');
  341. $this->assertEquals(200, $this->response->getStatusCode());
  342. $this->assertEquals('Hello World', $this->response->getContent());
  343. $this->assertNotNull($this->response->headers->get('Date'));
  344. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  345. $this->assertTraceContains('miss');
  346. $this->assertTraceContains('store');
  347. $values = $this->getMetaStorageValues();
  348. $this->assertEquals(1, count($values));
  349. }
  350. public function testCachesResponsesWithASMaxAgeDirective()
  351. {
  352. $this->setNextResponse(200, array('Cache-Control' => 's-maxage=5'));
  353. $this->request('GET', '/');
  354. $this->assertEquals(200, $this->response->getStatusCode());
  355. $this->assertEquals('Hello World', $this->response->getContent());
  356. $this->assertNotNull($this->response->headers->get('Date'));
  357. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  358. $this->assertTraceContains('miss');
  359. $this->assertTraceContains('store');
  360. $values = $this->getMetaStorageValues();
  361. $this->assertEquals(1, count($values));
  362. }
  363. public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation()
  364. {
  365. $time = \DateTime::createFromFormat('U', time());
  366. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822)));
  367. $this->request('GET', '/');
  368. $this->assertEquals(200, $this->response->getStatusCode());
  369. $this->assertEquals('Hello World', $this->response->getContent());
  370. $this->assertTraceContains('miss');
  371. $this->assertTraceContains('store');
  372. }
  373. public function testCachesResponsesWithAnETagValidatorButNoFreshnessInformation()
  374. {
  375. $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"123456"'));
  376. $this->request('GET', '/');
  377. $this->assertEquals(200, $this->response->getStatusCode());
  378. $this->assertEquals('Hello World', $this->response->getContent());
  379. $this->assertTraceContains('miss');
  380. $this->assertTraceContains('store');
  381. }
  382. public function testHitsCachedResponsesWithExpiresHeader()
  383. {
  384. $time1 = \DateTime::createFromFormat('U', time() - 5);
  385. $time2 = \DateTime::createFromFormat('U', time() + 5);
  386. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Date' => $time1->format(DATE_RFC2822), 'Expires' => $time2->format(DATE_RFC2822)));
  387. $this->request('GET', '/');
  388. $this->assertHttpKernelIsCalled();
  389. $this->assertEquals(200, $this->response->getStatusCode());
  390. $this->assertNotNull($this->response->headers->get('Date'));
  391. $this->assertTraceContains('miss');
  392. $this->assertTraceContains('store');
  393. $this->assertEquals('Hello World', $this->response->getContent());
  394. $this->request('GET', '/');
  395. $this->assertHttpKernelIsNotCalled();
  396. $this->assertEquals(200, $this->response->getStatusCode());
  397. $this->assertTrue(strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')) < 2);
  398. $this->assertTrue($this->response->headers->get('Age') > 0);
  399. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  400. $this->assertTraceContains('fresh');
  401. $this->assertTraceNotContains('store');
  402. $this->assertEquals('Hello World', $this->response->getContent());
  403. }
  404. public function testHitsCachedResponseWithMaxAgeDirective()
  405. {
  406. $time = \DateTime::createFromFormat('U', time() - 5);
  407. $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, max-age=10'));
  408. $this->request('GET', '/');
  409. $this->assertHttpKernelIsCalled();
  410. $this->assertEquals(200, $this->response->getStatusCode());
  411. $this->assertNotNull($this->response->headers->get('Date'));
  412. $this->assertTraceContains('miss');
  413. $this->assertTraceContains('store');
  414. $this->assertEquals('Hello World', $this->response->getContent());
  415. $this->request('GET', '/');
  416. $this->assertHttpKernelIsNotCalled();
  417. $this->assertEquals(200, $this->response->getStatusCode());
  418. $this->assertTrue(strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')) < 2);
  419. $this->assertTrue($this->response->headers->get('Age') > 0);
  420. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  421. $this->assertTraceContains('fresh');
  422. $this->assertTraceNotContains('store');
  423. $this->assertEquals('Hello World', $this->response->getContent());
  424. }
  425. public function testHitsCachedResponseWithSMaxAgeDirective()
  426. {
  427. $time = \DateTime::createFromFormat('U', time() - 5);
  428. $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0'));
  429. $this->request('GET', '/');
  430. $this->assertHttpKernelIsCalled();
  431. $this->assertEquals(200, $this->response->getStatusCode());
  432. $this->assertNotNull($this->response->headers->get('Date'));
  433. $this->assertTraceContains('miss');
  434. $this->assertTraceContains('store');
  435. $this->assertEquals('Hello World', $this->response->getContent());
  436. $this->request('GET', '/');
  437. $this->assertHttpKernelIsNotCalled();
  438. $this->assertEquals(200, $this->response->getStatusCode());
  439. $this->assertTrue(strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')) < 2);
  440. $this->assertTrue($this->response->headers->get('Age') > 0);
  441. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  442. $this->assertTraceContains('fresh');
  443. $this->assertTraceNotContains('store');
  444. $this->assertEquals('Hello World', $this->response->getContent());
  445. }
  446. public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation()
  447. {
  448. $this->setNextResponse();
  449. $this->cacheConfig['default_ttl'] = 10;
  450. $this->request('GET', '/');
  451. $this->assertHttpKernelIsCalled();
  452. $this->assertTraceContains('miss');
  453. $this->assertTraceContains('store');
  454. $this->assertEquals('Hello World', $this->response->getContent());
  455. $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
  456. $this->cacheConfig['default_ttl'] = 10;
  457. $this->request('GET', '/');
  458. $this->assertHttpKernelIsNotCalled();
  459. $this->assertEquals(200, $this->response->getStatusCode());
  460. $this->assertTraceContains('fresh');
  461. $this->assertTraceNotContains('store');
  462. $this->assertEquals('Hello World', $this->response->getContent());
  463. }
  464. public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective()
  465. {
  466. $this->setNextResponse(200, array('Cache-Control' => 'must-revalidate'));
  467. $this->cacheConfig['default_ttl'] = 10;
  468. $this->request('GET', '/');
  469. $this->assertHttpKernelIsCalled();
  470. $this->assertEquals(200, $this->response->getStatusCode());
  471. $this->assertTraceContains('miss');
  472. $this->assertTraceNotContains('store');
  473. $this->assertNotRegExp('/s-maxage/', $this->response->headers->get('Cache-Control'));
  474. $this->assertEquals('Hello World', $this->response->getContent());
  475. }
  476. public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent()
  477. {
  478. $time = \DateTime::createFromFormat('U', time() + 5);
  479. $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
  480. // build initial request
  481. $this->request('GET', '/');
  482. $this->assertHttpKernelIsCalled();
  483. $this->assertEquals(200, $this->response->getStatusCode());
  484. $this->assertNotNull($this->response->headers->get('Date'));
  485. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  486. $this->assertNotNull($this->response->headers->get('Age'));
  487. $this->assertTraceContains('miss');
  488. $this->assertTraceContains('store');
  489. $this->assertEquals('Hello World', $this->response->getContent());
  490. # go in and play around with the cached metadata directly ...
  491. $values = $this->getMetaStorageValues();
  492. $this->assertEquals(1, count($values));
  493. $tmp = unserialize($values[0]);
  494. $time = \DateTime::createFromFormat('U', time());
  495. $tmp[0][1]['expires'] = $time->format(DATE_RFC2822);
  496. $r = new \ReflectionObject($this->store);
  497. $m = $r->getMethod('save');
  498. $m->setAccessible(true);
  499. $m->invoke($this->store, 'md'.sha1('http://localhost/'), serialize($tmp));
  500. // build subsequent request; should be found but miss due to freshness
  501. $this->request('GET', '/');
  502. $this->assertHttpKernelIsCalled();
  503. $this->assertEquals(200, $this->response->getStatusCode());
  504. $this->assertTrue($this->response->headers->get('Age') <= 1);
  505. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  506. $this->assertTraceContains('stale');
  507. $this->assertTraceNotContains('fresh');
  508. $this->assertTraceNotContains('miss');
  509. $this->assertTraceContains('store');
  510. $this->assertEquals('Hello World', $this->response->getContent());
  511. }
  512. public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInformation()
  513. {
  514. $time = \DateTime::createFromFormat('U', time());
  515. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
  516. $response->headers->set('Cache-Control', 'public');
  517. $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
  518. if ($time->format(DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) {
  519. $response->setStatusCode(304);
  520. $response->setContent('');
  521. }
  522. });
  523. // build initial request
  524. $this->request('GET', '/');
  525. $this->assertHttpKernelIsCalled();
  526. $this->assertEquals(200, $this->response->getStatusCode());
  527. $this->assertNotNull($this->response->headers->get('Last-Modified'));
  528. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  529. $this->assertEquals('Hello World', $this->response->getContent());
  530. $this->assertTraceContains('miss');
  531. $this->assertTraceContains('store');
  532. $this->assertTraceNotContains('stale');
  533. // build subsequent request; should be found but miss due to freshness
  534. $this->request('GET', '/');
  535. $this->assertHttpKernelIsCalled();
  536. $this->assertEquals(200, $this->response->getStatusCode());
  537. $this->assertNotNull($this->response->headers->get('Last-Modified'));
  538. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  539. $this->assertTrue($this->response->headers->get('Age') <= 1);
  540. $this->assertEquals('Hello World', $this->response->getContent());
  541. $this->assertTraceContains('stale');
  542. $this->assertTraceContains('valid');
  543. $this->assertTraceContains('store');
  544. $this->assertTraceNotContains('miss');
  545. }
  546. public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation()
  547. {
  548. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
  549. $response->headers->set('Cache-Control', 'public');
  550. $response->headers->set('ETag', '"12345"');
  551. if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) {
  552. $response->setStatusCode(304);
  553. $response->setContent('');
  554. }
  555. });
  556. // build initial request
  557. $this->request('GET', '/');
  558. $this->assertHttpKernelIsCalled();
  559. $this->assertEquals(200, $this->response->getStatusCode());
  560. $this->assertNotNull($this->response->headers->get('ETag'));
  561. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  562. $this->assertEquals('Hello World', $this->response->getContent());
  563. $this->assertTraceContains('miss');
  564. $this->assertTraceContains('store');
  565. // build subsequent request; should be found but miss due to freshness
  566. $this->request('GET', '/');
  567. $this->assertHttpKernelIsCalled();
  568. $this->assertEquals(200, $this->response->getStatusCode());
  569. $this->assertNotNull($this->response->headers->get('ETag'));
  570. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  571. $this->assertTrue($this->response->headers->get('Age') <= 1);
  572. $this->assertEquals('Hello World', $this->response->getContent());
  573. $this->assertTraceContains('stale');
  574. $this->assertTraceContains('valid');
  575. $this->assertTraceContains('store');
  576. $this->assertTraceNotContains('miss');
  577. }
  578. public function testReplacesCachedResponsesWhenValidationResultsInNon304Response()
  579. {
  580. $time = \DateTime::createFromFormat('U', time());
  581. $count = 0;
  582. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time, &$count) {
  583. $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
  584. $response->headers->set('Cache-Control', 'public');
  585. switch (++$count) {
  586. case 1:
  587. $response->setContent('first response');
  588. break;
  589. case 2:
  590. $response->setContent('second response');
  591. break;
  592. case 3:
  593. $response->setContent('');
  594. $response->setStatusCode(304);
  595. break;
  596. }
  597. });
  598. // first request should fetch from backend and store in cache
  599. $this->request('GET', '/');
  600. $this->assertEquals(200, $this->response->getStatusCode());
  601. $this->assertEquals('first response', $this->response->getContent());
  602. // second request is validated, is invalid, and replaces cached entry
  603. $this->request('GET', '/');
  604. $this->assertEquals(200, $this->response->getStatusCode());
  605. $this->assertEquals('second response', $this->response->getContent());
  606. // third response is validated, valid, and returns cached entry
  607. $this->request('GET', '/');
  608. $this->assertEquals(200, $this->response->getStatusCode());
  609. $this->assertEquals('second response', $this->response->getContent());
  610. $this->assertEquals(3, $count);
  611. }
  612. public function testPassesHeadRequestsThroughDirectlyOnPass()
  613. {
  614. $that = $this;
  615. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($that) {
  616. $response->setContent('');
  617. $response->setStatusCode(200);
  618. $that->assertEquals('HEAD', $request->getMethod());
  619. });
  620. $this->request('HEAD', '/', array('HTTP_EXPECT' => 'something ...'));
  621. $this->assertHttpKernelIsCalled();
  622. $this->assertEquals('', $this->response->getContent());
  623. }
  624. public function testUsesCacheToRespondToHeadRequestsWhenFresh()
  625. {
  626. $that = $this;
  627. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($that) {
  628. $response->headers->set('Cache-Control', 'public, max-age=10');
  629. $response->setContent('Hello World');
  630. $response->setStatusCode(200);
  631. $that->assertNotEquals('HEAD', $request->getMethod());
  632. });
  633. $this->request('GET', '/');
  634. $this->assertHttpKernelIsCalled();
  635. $this->assertEquals('Hello World', $this->response->getContent());
  636. $this->request('HEAD', '/');
  637. $this->assertHttpKernelIsNotCalled();
  638. $this->assertEquals(200, $this->response->getStatusCode());
  639. $this->assertEquals('', $this->response->getContent());
  640. $this->assertEquals(strlen('Hello World'), $this->response->headers->get('Content-Length'));
  641. }
  642. public function testSendsNoContentWhenFresh()
  643. {
  644. $time = \DateTime::createFromFormat('U', time());
  645. $that = $this;
  646. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($that, $time) {
  647. $response->headers->set('Cache-Control', 'public, max-age=10');
  648. $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
  649. });
  650. $this->request('GET', '/');
  651. $this->assertHttpKernelIsCalled();
  652. $this->assertEquals('Hello World', $this->response->getContent());
  653. $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
  654. $this->assertHttpKernelIsNotCalled();
  655. $this->assertEquals(304, $this->response->getStatusCode());
  656. $this->assertEquals('', $this->response->getContent());
  657. }
  658. public function testInvalidatesCachedResponsesOnPost()
  659. {
  660. $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
  661. if ('GET' == $request->getMethod()) {
  662. $response->setStatusCode(200);
  663. $response->headers->set('Cache-Control', 'public, max-age=500');
  664. $response->setContent('Hello World');
  665. } elseif ('POST' == $request->getMethod()) {
  666. $response->setStatusCode(303);
  667. $response->headers->set('Location', '/');
  668. $response->headers->remove('Cache-Control');
  669. $response->setContent('');
  670. }
  671. });
  672. // build initial request to enter into the cache
  673. $this->request('GET', '/');
  674. $this->assertHttpKernelIsCalled();
  675. $this->assertEquals(200, $this->response->getStatusCode());
  676. $this->assertEquals('Hello World', $this->response->getContent());
  677. $this->assertTraceContains('miss');
  678. $this->assertTraceContains('store');
  679. // make sure it is valid
  680. $this->request('GET', '/');
  681. $this->assertHttpKernelIsNotCalled();
  682. $this->assertEquals(200, $this->response->getStatusCode());
  683. $this->assertEquals('Hello World', $this->response->getContent());
  684. $this->assertTraceContains('fresh');
  685. // now POST to same URL
  686. $this->request('POST', '/');
  687. $this->assertHttpKernelIsCalled();
  688. $this->assertEquals('/', $this->response->headers->get('Location'));
  689. $this->assertTraceContains('invalidate');
  690. $this->assertTraceContains('pass');
  691. $this->assertEquals('', $this->response->getContent());
  692. // now make sure it was actually invalidated
  693. $this->request('GET', '/');
  694. $this->assertHttpKernelIsCalled();
  695. $this->assertEquals(200, $this->response->getStatusCode());
  696. $this->assertEquals('Hello World', $this->response->getContent());
  697. $this->assertTraceContains('stale');
  698. $this->assertTraceContains('invalid');
  699. $this->assertTraceContains('store');
  700. }
  701. public function testServesFromCacheWhenHeadersMatch()
  702. {
  703. $count = 0;
  704. $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
  705. $response->headers->set('Vary', 'Accept User-Agent Foo');
  706. $response->headers->set('Cache-Control', 'public, max-age=10');
  707. $response->headers->set('X-Response-Count', ++$count);
  708. $response->setContent($request->headers->get('USER_AGENT'));
  709. });
  710. $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
  711. $this->assertEquals(200, $this->response->getStatusCode());
  712. $this->assertEquals('Bob/1.0', $this->response->getContent());
  713. $this->assertTraceContains('miss');
  714. $this->assertTraceContains('store');
  715. $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
  716. $this->assertEquals(200, $this->response->getStatusCode());
  717. $this->assertEquals('Bob/1.0', $this->response->getContent());
  718. $this->assertTraceContains('fresh');
  719. $this->assertTraceNotContains('store');
  720. $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
  721. }
  722. public function testStoresMultipleResponsesWhenHeadersDiffer()
  723. {
  724. $count = 0;
  725. $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
  726. $response->headers->set('Vary', 'Accept User-Agent Foo');
  727. $response->headers->set('Cache-Control', 'public, max-age=10');
  728. $response->headers->set('X-Response-Count', ++$count);
  729. $response->setContent($request->headers->get('USER_AGENT'));
  730. });
  731. $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
  732. $this->assertEquals(200, $this->response->getStatusCode());
  733. $this->assertEquals('Bob/1.0', $this->response->getContent());
  734. $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
  735. $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
  736. $this->assertEquals(200, $this->response->getStatusCode());
  737. $this->assertTraceContains('miss');
  738. $this->assertTraceContains('store');
  739. $this->assertEquals('Bob/2.0', $this->response->getContent());
  740. $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
  741. $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
  742. $this->assertTraceContains('fresh');
  743. $this->assertEquals('Bob/1.0', $this->response->getContent());
  744. $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
  745. $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
  746. $this->assertTraceContains('fresh');
  747. $this->assertEquals('Bob/2.0', $this->response->getContent());
  748. $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
  749. $this->request('GET', '/', array('HTTP_USER_AGENT' => 'Bob/2.0'));
  750. $this->assertTraceContains('miss');
  751. $this->assertEquals('Bob/2.0', $this->response->getContent());
  752. $this->assertEquals(3, $this->response->headers->get('X-Response-Count'));
  753. }
  754. public function testShouldCatchExceptions()
  755. {
  756. $this->catchExceptions();
  757. $this->setNextResponse();
  758. $this->request('GET', '/');
  759. $this->assertExceptionsAreCaught();
  760. }
  761. public function testShouldNotCatchExceptions()
  762. {
  763. $this->catchExceptions(false);
  764. $this->setNextResponse();
  765. $this->request('GET', '/');
  766. $this->assertExceptionsAreNotCaught();
  767. }
  768. public function testEsiCacheSendsTheLowestTtl()
  769. {
  770. $responses = array(
  771. array(
  772. 'status' => 200,
  773. 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
  774. 'headers' => array(
  775. 'Cache-Control' => 's-maxage=300',
  776. 'Surrogate-Control' => 'content="ESI/1.0"',
  777. ),
  778. ),
  779. array(
  780. 'status' => 200,
  781. 'body' => 'Hello World!',
  782. 'headers' => array('Cache-Control' => 's-maxage=300'),
  783. ),
  784. array(
  785. 'status' => 200,
  786. 'body' => 'My name is Bobby.',
  787. 'headers' => array('Cache-Control' => 's-maxage=100'),
  788. ),
  789. );
  790. $this->setNextResponses($responses);
  791. $this->request('GET', '/', array(), array(), true);
  792. $this->assertEquals("Hello World! My name is Bobby.", $this->response->getContent());
  793. // check for 100 or 99 as the test can be executed after a second change
  794. $this->assertTrue(in_array($this->response->getTtl(), array(99, 100)));
  795. }
  796. public function testEsiCacheForceValidation()
  797. {
  798. $responses = array(
  799. array(
  800. 'status' => 200,
  801. 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
  802. 'headers' => array(
  803. 'Cache-Control' => 's-maxage=300',
  804. 'Surrogate-Control' => 'content="ESI/1.0"',
  805. ),
  806. ),
  807. array(
  808. 'status' => 200,
  809. 'body' => 'Hello World!',
  810. 'headers' => array('ETag' => 'foobar'),
  811. ),
  812. array(
  813. 'status' => 200,
  814. 'body' => 'My name is Bobby.',
  815. 'headers' => array('Cache-Control' => 's-maxage=100'),
  816. ),
  817. );
  818. $this->setNextResponses($responses);
  819. $this->request('GET', '/', array(), array(), true);
  820. $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
  821. $this->assertEquals(null, $this->response->getTtl());
  822. $this->assertTrue($this->response->mustRevalidate());
  823. $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
  824. $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
  825. }
  826. public function testEsiRecalculateContentLengthHeader()
  827. {
  828. $responses = array(
  829. array(
  830. 'status' => 200,
  831. 'body' => '<esi:include src="/foo" />',
  832. 'headers' => array(
  833. 'Content-Length' => 26,
  834. 'Cache-Control' => 's-maxage=300',
  835. 'Surrogate-Control' => 'content="ESI/1.0"',
  836. ),
  837. ),
  838. array(
  839. 'status' => 200,
  840. 'body' => 'Hello World!',
  841. 'headers' => array(),
  842. ),
  843. );
  844. $this->setNextResponses($responses);
  845. $this->request('GET', '/', array(), array(), true);
  846. $this->assertEquals('Hello World!', $this->response->getContent());
  847. $this->assertEquals(12, $this->response->headers->get('Content-Length'));
  848. }
  849. }