4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\HttpKernel\Tests\HttpCache;
14 use Symfony\Component\HttpKernel\HttpCache\HttpCache;
15 use Symfony\Component\HttpFoundation\Request;
16 use Symfony\Component\HttpFoundation\Response;
17 use Symfony\Component\HttpKernel\HttpKernelInterface;
20 * @group time-sensitive
22 class HttpCacheTest extends HttpCacheTestCase
24 public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
26 $storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface')
27 ->disableOriginalConstructor()
30 // does not implement TerminableInterface
31 $kernel = new TestKernel();
32 $httpCache = new HttpCache($kernel, $storeMock);
33 $httpCache->terminate(Request::create('/'), new Response());
35 $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface');
37 // implements TerminableInterface
38 $kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel')
39 ->disableOriginalConstructor()
40 ->setMethods(array('terminate', 'registerBundles', 'registerContainerConfiguration'))
43 $kernelMock->expects($this->once())
44 ->method('terminate');
46 $kernel = new HttpCache($kernelMock, $storeMock);
47 $kernel->terminate(Request::create('/'), new Response());
50 public function testPassesOnNonGetHeadRequests()
52 $this->setNextResponse(200);
53 $this->request('POST', '/');
54 $this->assertHttpKernelIsCalled();
55 $this->assertResponseOk();
56 $this->assertTraceContains('pass');
57 $this->assertFalse($this->response->headers->has('Age'));
60 public function testInvalidatesOnPostPutDeleteRequests()
62 foreach (array('post', 'put', 'delete') as $method) {
63 $this->setNextResponse(200);
64 $this->request($method, '/');
66 $this->assertHttpKernelIsCalled();
67 $this->assertResponseOk();
68 $this->assertTraceContains('invalidate');
69 $this->assertTraceContains('pass');
73 public function testDoesNotCacheWithAuthorizationRequestHeaderAndNonPublicResponse()
75 $this->setNextResponse(200, array('ETag' => '"Foo"'));
76 $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
78 $this->assertHttpKernelIsCalled();
79 $this->assertResponseOk();
80 $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
82 $this->assertTraceContains('miss');
83 $this->assertTraceNotContains('store');
84 $this->assertFalse($this->response->headers->has('Age'));
87 public function testDoesCacheWithAuthorizationRequestHeaderAndPublicResponse()
89 $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"Foo"'));
90 $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
92 $this->assertHttpKernelIsCalled();
93 $this->assertResponseOk();
94 $this->assertTraceContains('miss');
95 $this->assertTraceContains('store');
96 $this->assertTrue($this->response->headers->has('Age'));
97 $this->assertEquals('public', $this->response->headers->get('Cache-Control'));
100 public function testDoesNotCacheWithCookieHeaderAndNonPublicResponse()
102 $this->setNextResponse(200, array('ETag' => '"Foo"'));
103 $this->request('GET', '/', array(), array('foo' => 'bar'));
105 $this->assertHttpKernelIsCalled();
106 $this->assertResponseOk();
107 $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
108 $this->assertTraceContains('miss');
109 $this->assertTraceNotContains('store');
110 $this->assertFalse($this->response->headers->has('Age'));
113 public function testDoesNotCacheRequestsWithACookieHeader()
115 $this->setNextResponse(200);
116 $this->request('GET', '/', array(), array('foo' => 'bar'));
118 $this->assertHttpKernelIsCalled();
119 $this->assertResponseOk();
120 $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
121 $this->assertTraceContains('miss');
122 $this->assertTraceNotContains('store');
123 $this->assertFalse($this->response->headers->has('Age'));
126 public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified()
128 $time = \DateTime::createFromFormat('U', time());
130 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822), 'Content-Type' => 'text/plain'), 'Hello World');
131 $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
133 $this->assertHttpKernelIsCalled();
134 $this->assertEquals(304, $this->response->getStatusCode());
135 $this->assertEquals('', $this->response->headers->get('Content-Type'));
136 $this->assertEmpty($this->response->getContent());
137 $this->assertTraceContains('miss');
138 $this->assertTraceContains('store');
141 public function testRespondsWith304WhenIfNoneMatchMatchesETag()
143 $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '12345', 'Content-Type' => 'text/plain'), 'Hello World');
144 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345'));
146 $this->assertHttpKernelIsCalled();
147 $this->assertEquals(304, $this->response->getStatusCode());
148 $this->assertEquals('', $this->response->headers->get('Content-Type'));
149 $this->assertTrue($this->response->headers->has('ETag'));
150 $this->assertEmpty($this->response->getContent());
151 $this->assertTraceContains('miss');
152 $this->assertTraceContains('store');
155 public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch()
157 $time = \DateTime::createFromFormat('U', time());
159 $this->setNextResponse(200, array(), '', function ($request, $response) use ($time) {
160 $response->setStatusCode(200);
161 $response->headers->set('ETag', '12345');
162 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
163 $response->headers->set('Content-Type', 'text/plain');
164 $response->setContent('Hello World');
168 $t = \DateTime::createFromFormat('U', time() - 3600);
169 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(DATE_RFC2822)));
170 $this->assertHttpKernelIsCalled();
171 $this->assertEquals(200, $this->response->getStatusCode());
173 // only Last-Modified matches
174 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
175 $this->assertHttpKernelIsCalled();
176 $this->assertEquals(200, $this->response->getStatusCode());
179 $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
180 $this->assertHttpKernelIsCalled();
181 $this->assertEquals(304, $this->response->getStatusCode());
184 public function testIncrementsMaxAgeWhenNoDateIsSpecifiedEventWhenUsingETag()
186 $this->setNextResponse(
190 'Cache-Control' => 'public, s-maxage=60',
194 $this->request('GET', '/');
195 $this->assertHttpKernelIsCalled();
196 $this->assertEquals(200, $this->response->getStatusCode());
197 $this->assertTraceContains('miss');
198 $this->assertTraceContains('store');
202 $this->request('GET', '/');
203 $this->assertHttpKernelIsNotCalled();
204 $this->assertEquals(200, $this->response->getStatusCode());
205 $this->assertTraceContains('fresh');
206 $this->assertEquals(2, $this->response->headers->get('Age'));
209 public function testValidatesPrivateResponsesCachedOnTheClient()
211 $this->setNextResponse(200, array(), '', function ($request, $response) {
212 $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH'));
213 if ($request->cookies->has('authenticated')) {
214 $response->headers->set('Cache-Control', 'private, no-store');
215 $response->setETag('"private tag"');
216 if (in_array('"private tag"', $etags)) {
217 $response->setStatusCode(304);
219 $response->setStatusCode(200);
220 $response->headers->set('Content-Type', 'text/plain');
221 $response->setContent('private data');
224 $response->headers->set('Cache-Control', 'public');
225 $response->setETag('"public tag"');
226 if (in_array('"public tag"', $etags)) {
227 $response->setStatusCode(304);
229 $response->setStatusCode(200);
230 $response->headers->set('Content-Type', 'text/plain');
231 $response->setContent('public data');
236 $this->request('GET', '/');
237 $this->assertHttpKernelIsCalled();
238 $this->assertEquals(200, $this->response->getStatusCode());
239 $this->assertEquals('"public tag"', $this->response->headers->get('ETag'));
240 $this->assertEquals('public data', $this->response->getContent());
241 $this->assertTraceContains('miss');
242 $this->assertTraceContains('store');
244 $this->request('GET', '/', array(), array('authenticated' => ''));
245 $this->assertHttpKernelIsCalled();
246 $this->assertEquals(200, $this->response->getStatusCode());
247 $this->assertEquals('"private tag"', $this->response->headers->get('ETag'));
248 $this->assertEquals('private data', $this->response->getContent());
249 $this->assertTraceContains('stale');
250 $this->assertTraceContains('invalid');
251 $this->assertTraceNotContains('store');
254 public function testStoresResponsesWhenNoCacheRequestDirectivePresent()
256 $time = \DateTime::createFromFormat('U', time() + 5);
258 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
259 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
261 $this->assertHttpKernelIsCalled();
262 $this->assertTraceContains('store');
263 $this->assertTrue($this->response->headers->has('Age'));
266 public function testReloadsResponsesWhenCacheHitsButNoCacheRequestDirectivePresentWhenAllowReloadIsSetTrue()
270 $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
272 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
275 $this->request('GET', '/');
276 $this->assertEquals(200, $this->response->getStatusCode());
277 $this->assertEquals('Hello World', $this->response->getContent());
278 $this->assertTraceContains('store');
280 $this->request('GET', '/');
281 $this->assertEquals(200, $this->response->getStatusCode());
282 $this->assertEquals('Hello World', $this->response->getContent());
283 $this->assertTraceContains('fresh');
285 $this->cacheConfig['allow_reload'] = true;
286 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
287 $this->assertEquals(200, $this->response->getStatusCode());
288 $this->assertEquals('Goodbye World', $this->response->getContent());
289 $this->assertTraceContains('reload');
290 $this->assertTraceContains('store');
293 public function testDoesNotReloadResponsesWhenAllowReloadIsSetFalseDefault()
297 $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
299 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
302 $this->request('GET', '/');
303 $this->assertEquals(200, $this->response->getStatusCode());
304 $this->assertEquals('Hello World', $this->response->getContent());
305 $this->assertTraceContains('store');
307 $this->request('GET', '/');
308 $this->assertEquals(200, $this->response->getStatusCode());
309 $this->assertEquals('Hello World', $this->response->getContent());
310 $this->assertTraceContains('fresh');
312 $this->cacheConfig['allow_reload'] = false;
313 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
314 $this->assertEquals(200, $this->response->getStatusCode());
315 $this->assertEquals('Hello World', $this->response->getContent());
316 $this->assertTraceNotContains('reload');
318 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
319 $this->assertEquals(200, $this->response->getStatusCode());
320 $this->assertEquals('Hello World', $this->response->getContent());
321 $this->assertTraceNotContains('reload');
324 public function testRevalidatesFreshCacheEntryWhenMaxAgeRequestDirectiveIsExceededWhenAllowRevalidateOptionIsSetTrue()
328 $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
330 $response->headers->set('Cache-Control', 'public, max-age=10000');
331 $response->setETag($count);
332 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
335 $this->request('GET', '/');
336 $this->assertEquals(200, $this->response->getStatusCode());
337 $this->assertEquals('Hello World', $this->response->getContent());
338 $this->assertTraceContains('store');
340 $this->request('GET', '/');
341 $this->assertEquals(200, $this->response->getStatusCode());
342 $this->assertEquals('Hello World', $this->response->getContent());
343 $this->assertTraceContains('fresh');
345 $this->cacheConfig['allow_revalidate'] = true;
346 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
347 $this->assertEquals(200, $this->response->getStatusCode());
348 $this->assertEquals('Goodbye World', $this->response->getContent());
349 $this->assertTraceContains('stale');
350 $this->assertTraceContains('invalid');
351 $this->assertTraceContains('store');
354 public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIsSetFalseDefault()
358 $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
360 $response->headers->set('Cache-Control', 'public, max-age=10000');
361 $response->setETag($count);
362 $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
365 $this->request('GET', '/');
366 $this->assertEquals(200, $this->response->getStatusCode());
367 $this->assertEquals('Hello World', $this->response->getContent());
368 $this->assertTraceContains('store');
370 $this->request('GET', '/');
371 $this->assertEquals(200, $this->response->getStatusCode());
372 $this->assertEquals('Hello World', $this->response->getContent());
373 $this->assertTraceContains('fresh');
375 $this->cacheConfig['allow_revalidate'] = false;
376 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
377 $this->assertEquals(200, $this->response->getStatusCode());
378 $this->assertEquals('Hello World', $this->response->getContent());
379 $this->assertTraceNotContains('stale');
380 $this->assertTraceNotContains('invalid');
381 $this->assertTraceContains('fresh');
383 $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
384 $this->assertEquals(200, $this->response->getStatusCode());
385 $this->assertEquals('Hello World', $this->response->getContent());
386 $this->assertTraceNotContains('stale');
387 $this->assertTraceNotContains('invalid');
388 $this->assertTraceContains('fresh');
391 public function testFetchesResponseFromBackendWhenCacheMisses()
393 $time = \DateTime::createFromFormat('U', time() + 5);
394 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
396 $this->request('GET', '/');
397 $this->assertEquals(200, $this->response->getStatusCode());
398 $this->assertTraceContains('miss');
399 $this->assertTrue($this->response->headers->has('Age'));
402 public function testDoesNotCacheSomeStatusCodeResponses()
404 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) {
405 $time = \DateTime::createFromFormat('U', time() + 5);
406 $this->setNextResponse($code, array('Expires' => $time->format(DATE_RFC2822)));
408 $this->request('GET', '/');
409 $this->assertEquals($code, $this->response->getStatusCode());
410 $this->assertTraceNotContains('store');
411 $this->assertFalse($this->response->headers->has('Age'));
415 public function testDoesNotCacheResponsesWithExplicitNoStoreDirective()
417 $time = \DateTime::createFromFormat('U', time() + 5);
418 $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'no-store'));
420 $this->request('GET', '/');
421 $this->assertTraceNotContains('store');
422 $this->assertFalse($this->response->headers->has('Age'));
425 public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator()
427 $this->setNextResponse();
429 $this->request('GET', '/');
430 $this->assertEquals(200, $this->response->getStatusCode());
431 $this->assertTraceNotContains('store');
434 public function testCachesResponsesWithExplicitNoCacheDirective()
436 $time = \DateTime::createFromFormat('U', time() + 5);
437 $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, no-cache'));
439 $this->request('GET', '/');
440 $this->assertTraceContains('store');
441 $this->assertTrue($this->response->headers->has('Age'));
444 public function testCachesResponsesWithAnExpirationHeader()
446 $time = \DateTime::createFromFormat('U', time() + 5);
447 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
449 $this->request('GET', '/');
450 $this->assertEquals(200, $this->response->getStatusCode());
451 $this->assertEquals('Hello World', $this->response->getContent());
452 $this->assertNotNull($this->response->headers->get('Date'));
453 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
454 $this->assertTraceContains('miss');
455 $this->assertTraceContains('store');
457 $values = $this->getMetaStorageValues();
458 $this->assertCount(1, $values);
461 public function testCachesResponsesWithAMaxAgeDirective()
463 $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=5'));
465 $this->request('GET', '/');
466 $this->assertEquals(200, $this->response->getStatusCode());
467 $this->assertEquals('Hello World', $this->response->getContent());
468 $this->assertNotNull($this->response->headers->get('Date'));
469 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
470 $this->assertTraceContains('miss');
471 $this->assertTraceContains('store');
473 $values = $this->getMetaStorageValues();
474 $this->assertCount(1, $values);
477 public function testCachesResponsesWithASMaxAgeDirective()
479 $this->setNextResponse(200, array('Cache-Control' => 's-maxage=5'));
481 $this->request('GET', '/');
482 $this->assertEquals(200, $this->response->getStatusCode());
483 $this->assertEquals('Hello World', $this->response->getContent());
484 $this->assertNotNull($this->response->headers->get('Date'));
485 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
486 $this->assertTraceContains('miss');
487 $this->assertTraceContains('store');
489 $values = $this->getMetaStorageValues();
490 $this->assertCount(1, $values);
493 public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation()
495 $time = \DateTime::createFromFormat('U', time());
496 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822)));
498 $this->request('GET', '/');
499 $this->assertEquals(200, $this->response->getStatusCode());
500 $this->assertEquals('Hello World', $this->response->getContent());
501 $this->assertTraceContains('miss');
502 $this->assertTraceContains('store');
505 public function testCachesResponsesWithAnETagValidatorButNoFreshnessInformation()
507 $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"123456"'));
509 $this->request('GET', '/');
510 $this->assertEquals(200, $this->response->getStatusCode());
511 $this->assertEquals('Hello World', $this->response->getContent());
512 $this->assertTraceContains('miss');
513 $this->assertTraceContains('store');
516 public function testHitsCachedResponsesWithExpiresHeader()
518 $time1 = \DateTime::createFromFormat('U', time() - 5);
519 $time2 = \DateTime::createFromFormat('U', time() + 5);
520 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Date' => $time1->format(DATE_RFC2822), 'Expires' => $time2->format(DATE_RFC2822)));
522 $this->request('GET', '/');
523 $this->assertHttpKernelIsCalled();
524 $this->assertEquals(200, $this->response->getStatusCode());
525 $this->assertNotNull($this->response->headers->get('Date'));
526 $this->assertTraceContains('miss');
527 $this->assertTraceContains('store');
528 $this->assertEquals('Hello World', $this->response->getContent());
530 $this->request('GET', '/');
531 $this->assertHttpKernelIsNotCalled();
532 $this->assertEquals(200, $this->response->getStatusCode());
533 $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
534 $this->assertGreaterThan(0, $this->response->headers->get('Age'));
535 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
536 $this->assertTraceContains('fresh');
537 $this->assertTraceNotContains('store');
538 $this->assertEquals('Hello World', $this->response->getContent());
541 public function testHitsCachedResponseWithMaxAgeDirective()
543 $time = \DateTime::createFromFormat('U', time() - 5);
544 $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, max-age=10'));
546 $this->request('GET', '/');
547 $this->assertHttpKernelIsCalled();
548 $this->assertEquals(200, $this->response->getStatusCode());
549 $this->assertNotNull($this->response->headers->get('Date'));
550 $this->assertTraceContains('miss');
551 $this->assertTraceContains('store');
552 $this->assertEquals('Hello World', $this->response->getContent());
554 $this->request('GET', '/');
555 $this->assertHttpKernelIsNotCalled();
556 $this->assertEquals(200, $this->response->getStatusCode());
557 $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
558 $this->assertGreaterThan(0, $this->response->headers->get('Age'));
559 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
560 $this->assertTraceContains('fresh');
561 $this->assertTraceNotContains('store');
562 $this->assertEquals('Hello World', $this->response->getContent());
565 public function testDegradationWhenCacheLocked()
567 if ('\\' === DIRECTORY_SEPARATOR) {
568 $this->markTestSkipped('Skips on windows to avoid permissions issues.');
571 $this->cacheConfig['stale_while_revalidate'] = 10;
573 // The prescence of Last-Modified makes this cacheable (because Response::isValidateable() then).
574 $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=5', 'Last-Modified' => 'some while ago'), 'Old response');
575 $this->request('GET', '/'); // warm the cache
577 // Now, lock the cache
578 $concurrentRequest = Request::create('/', 'GET');
579 $this->store->lock($concurrentRequest);
582 * After 10s, the cached response has become stale. Yet, we're still within the "stale_while_revalidate"
583 * timeout so we may serve the stale response.
587 $this->request('GET', '/');
588 $this->assertHttpKernelIsNotCalled();
589 $this->assertEquals(200, $this->response->getStatusCode());
590 $this->assertTraceContains('stale-while-revalidate');
591 $this->assertEquals('Old response', $this->response->getContent());
594 * Another 10s later, stale_while_revalidate is over. Resort to serving the old response, but
595 * do so with a "server unavailable" message.
599 $this->request('GET', '/');
600 $this->assertHttpKernelIsNotCalled();
601 $this->assertEquals(503, $this->response->getStatusCode());
602 $this->assertEquals('Old response', $this->response->getContent());
605 public function testHitsCachedResponseWithSMaxAgeDirective()
607 $time = \DateTime::createFromFormat('U', time() - 5);
608 $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0'));
610 $this->request('GET', '/');
611 $this->assertHttpKernelIsCalled();
612 $this->assertEquals(200, $this->response->getStatusCode());
613 $this->assertNotNull($this->response->headers->get('Date'));
614 $this->assertTraceContains('miss');
615 $this->assertTraceContains('store');
616 $this->assertEquals('Hello World', $this->response->getContent());
618 $this->request('GET', '/');
619 $this->assertHttpKernelIsNotCalled();
620 $this->assertEquals(200, $this->response->getStatusCode());
621 $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
622 $this->assertGreaterThan(0, $this->response->headers->get('Age'));
623 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
624 $this->assertTraceContains('fresh');
625 $this->assertTraceNotContains('store');
626 $this->assertEquals('Hello World', $this->response->getContent());
629 public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation()
631 $this->setNextResponse();
633 $this->cacheConfig['default_ttl'] = 10;
634 $this->request('GET', '/');
635 $this->assertHttpKernelIsCalled();
636 $this->assertTraceContains('miss');
637 $this->assertTraceContains('store');
638 $this->assertEquals('Hello World', $this->response->getContent());
639 $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
641 $this->cacheConfig['default_ttl'] = 10;
642 $this->request('GET', '/');
643 $this->assertHttpKernelIsNotCalled();
644 $this->assertEquals(200, $this->response->getStatusCode());
645 $this->assertTraceContains('fresh');
646 $this->assertTraceNotContains('store');
647 $this->assertEquals('Hello World', $this->response->getContent());
648 $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
651 public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpired()
653 $this->setNextResponse();
655 $this->cacheConfig['default_ttl'] = 2;
656 $this->request('GET', '/');
657 $this->assertHttpKernelIsCalled();
658 $this->assertTraceContains('miss');
659 $this->assertTraceContains('store');
660 $this->assertEquals('Hello World', $this->response->getContent());
661 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
663 $this->request('GET', '/');
664 $this->assertHttpKernelIsNotCalled();
665 $this->assertEquals(200, $this->response->getStatusCode());
666 $this->assertTraceContains('fresh');
667 $this->assertTraceNotContains('store');
668 $this->assertEquals('Hello World', $this->response->getContent());
669 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
672 $values = $this->getMetaStorageValues();
673 $this->assertCount(1, $values);
674 $tmp = unserialize($values[0]);
675 $time = \DateTime::createFromFormat('U', time() - 5);
676 $tmp[0][1]['date'] = $time->format(DATE_RFC2822);
677 $r = new \ReflectionObject($this->store);
678 $m = $r->getMethod('save');
679 $m->setAccessible(true);
680 $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
682 $this->request('GET', '/');
683 $this->assertHttpKernelIsCalled();
684 $this->assertEquals(200, $this->response->getStatusCode());
685 $this->assertTraceContains('stale');
686 $this->assertTraceContains('invalid');
687 $this->assertTraceContains('store');
688 $this->assertEquals('Hello World', $this->response->getContent());
689 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
691 $this->setNextResponse();
693 $this->request('GET', '/');
694 $this->assertHttpKernelIsNotCalled();
695 $this->assertEquals(200, $this->response->getStatusCode());
696 $this->assertTraceContains('fresh');
697 $this->assertTraceNotContains('store');
698 $this->assertEquals('Hello World', $this->response->getContent());
699 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
702 public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpiredWithStatus304()
704 $this->setNextResponse();
706 $this->cacheConfig['default_ttl'] = 2;
707 $this->request('GET', '/');
708 $this->assertHttpKernelIsCalled();
709 $this->assertTraceContains('miss');
710 $this->assertTraceContains('store');
711 $this->assertEquals('Hello World', $this->response->getContent());
712 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
714 $this->request('GET', '/');
715 $this->assertHttpKernelIsNotCalled();
716 $this->assertEquals(200, $this->response->getStatusCode());
717 $this->assertTraceContains('fresh');
718 $this->assertTraceNotContains('store');
719 $this->assertEquals('Hello World', $this->response->getContent());
722 $values = $this->getMetaStorageValues();
723 $this->assertCount(1, $values);
724 $tmp = unserialize($values[0]);
725 $time = \DateTime::createFromFormat('U', time() - 5);
726 $tmp[0][1]['date'] = $time->format(DATE_RFC2822);
727 $r = new \ReflectionObject($this->store);
728 $m = $r->getMethod('save');
729 $m->setAccessible(true);
730 $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
732 $this->request('GET', '/');
733 $this->assertHttpKernelIsCalled();
734 $this->assertEquals(200, $this->response->getStatusCode());
735 $this->assertTraceContains('stale');
736 $this->assertTraceContains('valid');
737 $this->assertTraceContains('store');
738 $this->assertTraceNotContains('miss');
739 $this->assertEquals('Hello World', $this->response->getContent());
740 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
742 $this->request('GET', '/');
743 $this->assertHttpKernelIsNotCalled();
744 $this->assertEquals(200, $this->response->getStatusCode());
745 $this->assertTraceContains('fresh');
746 $this->assertTraceNotContains('store');
747 $this->assertEquals('Hello World', $this->response->getContent());
748 $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
751 public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective()
753 $this->setNextResponse(200, array('Cache-Control' => 'must-revalidate'));
755 $this->cacheConfig['default_ttl'] = 10;
756 $this->request('GET', '/');
757 $this->assertHttpKernelIsCalled();
758 $this->assertEquals(200, $this->response->getStatusCode());
759 $this->assertTraceContains('miss');
760 $this->assertTraceNotContains('store');
761 $this->assertNotRegExp('/s-maxage/', $this->response->headers->get('Cache-Control'));
762 $this->assertEquals('Hello World', $this->response->getContent());
765 public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent()
767 $time = \DateTime::createFromFormat('U', time() + 5);
768 $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
770 // build initial request
771 $this->request('GET', '/');
772 $this->assertHttpKernelIsCalled();
773 $this->assertEquals(200, $this->response->getStatusCode());
774 $this->assertNotNull($this->response->headers->get('Date'));
775 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
776 $this->assertNotNull($this->response->headers->get('Age'));
777 $this->assertTraceContains('miss');
778 $this->assertTraceContains('store');
779 $this->assertEquals('Hello World', $this->response->getContent());
781 // go in and play around with the cached metadata directly ...
782 $values = $this->getMetaStorageValues();
783 $this->assertCount(1, $values);
784 $tmp = unserialize($values[0]);
785 $time = \DateTime::createFromFormat('U', time());
786 $tmp[0][1]['expires'] = $time->format(DATE_RFC2822);
787 $r = new \ReflectionObject($this->store);
788 $m = $r->getMethod('save');
789 $m->setAccessible(true);
790 $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
792 // build subsequent request; should be found but miss due to freshness
793 $this->request('GET', '/');
794 $this->assertHttpKernelIsCalled();
795 $this->assertEquals(200, $this->response->getStatusCode());
796 $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
797 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
798 $this->assertTraceContains('stale');
799 $this->assertTraceNotContains('fresh');
800 $this->assertTraceNotContains('miss');
801 $this->assertTraceContains('store');
802 $this->assertEquals('Hello World', $this->response->getContent());
805 public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInformation()
807 $time = \DateTime::createFromFormat('U', time());
808 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
809 $response->headers->set('Cache-Control', 'public');
810 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
811 if ($time->format(DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) {
812 $response->setStatusCode(304);
813 $response->setContent('');
817 // build initial request
818 $this->request('GET', '/');
819 $this->assertHttpKernelIsCalled();
820 $this->assertEquals(200, $this->response->getStatusCode());
821 $this->assertNotNull($this->response->headers->get('Last-Modified'));
822 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
823 $this->assertEquals('Hello World', $this->response->getContent());
824 $this->assertTraceContains('miss');
825 $this->assertTraceContains('store');
826 $this->assertTraceNotContains('stale');
828 // build subsequent request; should be found but miss due to freshness
829 $this->request('GET', '/');
830 $this->assertHttpKernelIsCalled();
831 $this->assertEquals(200, $this->response->getStatusCode());
832 $this->assertNotNull($this->response->headers->get('Last-Modified'));
833 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
834 $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
835 $this->assertEquals('Hello World', $this->response->getContent());
836 $this->assertTraceContains('stale');
837 $this->assertTraceContains('valid');
838 $this->assertTraceContains('store');
839 $this->assertTraceNotContains('miss');
842 public function testValidatesCachedResponsesUseSameHttpMethod()
846 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($test) {
847 $test->assertSame('OPTIONS', $request->getMethod());
850 // build initial request
851 $this->request('OPTIONS', '/');
853 // build subsequent request
854 $this->request('OPTIONS', '/');
857 public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation()
859 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
860 $response->headers->set('Cache-Control', 'public');
861 $response->headers->set('ETag', '"12345"');
862 if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) {
863 $response->setStatusCode(304);
864 $response->setContent('');
868 // build initial request
869 $this->request('GET', '/');
870 $this->assertHttpKernelIsCalled();
871 $this->assertEquals(200, $this->response->getStatusCode());
872 $this->assertNotNull($this->response->headers->get('ETag'));
873 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
874 $this->assertEquals('Hello World', $this->response->getContent());
875 $this->assertTraceContains('miss');
876 $this->assertTraceContains('store');
878 // build subsequent request; should be found but miss due to freshness
879 $this->request('GET', '/');
880 $this->assertHttpKernelIsCalled();
881 $this->assertEquals(200, $this->response->getStatusCode());
882 $this->assertNotNull($this->response->headers->get('ETag'));
883 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
884 $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
885 $this->assertEquals('Hello World', $this->response->getContent());
886 $this->assertTraceContains('stale');
887 $this->assertTraceContains('valid');
888 $this->assertTraceContains('store');
889 $this->assertTraceNotContains('miss');
892 public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInformation()
894 $time = \DateTime::createFromFormat('U', time());
896 $this->setNextResponse(200, array(), 'Hello World', function (Request $request, Response $response) use ($time) {
897 $response->setSharedMaxAge(10);
898 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
902 $this->request('GET', '/');
904 // next request before s-maxage has expired: Serve from cache
905 // without hitting the backend
906 $this->request('GET', '/');
907 $this->assertHttpKernelIsNotCalled();
908 $this->assertEquals(200, $this->response->getStatusCode());
909 $this->assertEquals('Hello World', $this->response->getContent());
910 $this->assertTraceContains('fresh');
912 sleep(15); // expire the cache
914 $this->setNextResponse(304, array(), '', function (Request $request, Response $response) use ($time) {
915 $this->assertEquals($time->format(DATE_RFC2822), $request->headers->get('IF_MODIFIED_SINCE'));
918 $this->request('GET', '/');
919 $this->assertHttpKernelIsCalled();
920 $this->assertEquals(200, $this->response->getStatusCode());
921 $this->assertEquals('Hello World', $this->response->getContent());
922 $this->assertTraceContains('stale');
923 $this->assertTraceContains('valid');
926 public function testReplacesCachedResponsesWhenValidationResultsInNon304Response()
928 $time = \DateTime::createFromFormat('U', time());
930 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time, &$count) {
931 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
932 $response->headers->set('Cache-Control', 'public');
935 $response->setContent('first response');
938 $response->setContent('second response');
941 $response->setContent('');
942 $response->setStatusCode(304);
947 // first request should fetch from backend and store in cache
948 $this->request('GET', '/');
949 $this->assertEquals(200, $this->response->getStatusCode());
950 $this->assertEquals('first response', $this->response->getContent());
952 // second request is validated, is invalid, and replaces cached entry
953 $this->request('GET', '/');
954 $this->assertEquals(200, $this->response->getStatusCode());
955 $this->assertEquals('second response', $this->response->getContent());
957 // third response is validated, valid, and returns cached entry
958 $this->request('GET', '/');
959 $this->assertEquals(200, $this->response->getStatusCode());
960 $this->assertEquals('second response', $this->response->getContent());
962 $this->assertEquals(3, $count);
965 public function testPassesHeadRequestsThroughDirectlyOnPass()
967 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
968 $response->setContent('');
969 $response->setStatusCode(200);
970 $this->assertEquals('HEAD', $request->getMethod());
973 $this->request('HEAD', '/', array('HTTP_EXPECT' => 'something ...'));
974 $this->assertHttpKernelIsCalled();
975 $this->assertEquals('', $this->response->getContent());
978 public function testUsesCacheToRespondToHeadRequestsWhenFresh()
980 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
981 $response->headers->set('Cache-Control', 'public, max-age=10');
982 $response->setContent('Hello World');
983 $response->setStatusCode(200);
984 $this->assertNotEquals('HEAD', $request->getMethod());
987 $this->request('GET', '/');
988 $this->assertHttpKernelIsCalled();
989 $this->assertEquals('Hello World', $this->response->getContent());
991 $this->request('HEAD', '/');
992 $this->assertHttpKernelIsNotCalled();
993 $this->assertEquals(200, $this->response->getStatusCode());
994 $this->assertEquals('', $this->response->getContent());
995 $this->assertEquals(strlen('Hello World'), $this->response->headers->get('Content-Length'));
998 public function testSendsNoContentWhenFresh()
1000 $time = \DateTime::createFromFormat('U', time());
1001 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
1002 $response->headers->set('Cache-Control', 'public, max-age=10');
1003 $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
1006 $this->request('GET', '/');
1007 $this->assertHttpKernelIsCalled();
1008 $this->assertEquals('Hello World', $this->response->getContent());
1010 $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
1011 $this->assertHttpKernelIsNotCalled();
1012 $this->assertEquals(304, $this->response->getStatusCode());
1013 $this->assertEquals('', $this->response->getContent());
1016 public function testInvalidatesCachedResponsesOnPost()
1018 $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
1019 if ('GET' == $request->getMethod()) {
1020 $response->setStatusCode(200);
1021 $response->headers->set('Cache-Control', 'public, max-age=500');
1022 $response->setContent('Hello World');
1023 } elseif ('POST' == $request->getMethod()) {
1024 $response->setStatusCode(303);
1025 $response->headers->set('Location', '/');
1026 $response->headers->remove('Cache-Control');
1027 $response->setContent('');
1031 // build initial request to enter into the cache
1032 $this->request('GET', '/');
1033 $this->assertHttpKernelIsCalled();
1034 $this->assertEquals(200, $this->response->getStatusCode());
1035 $this->assertEquals('Hello World', $this->response->getContent());
1036 $this->assertTraceContains('miss');
1037 $this->assertTraceContains('store');
1039 // make sure it is valid
1040 $this->request('GET', '/');
1041 $this->assertHttpKernelIsNotCalled();
1042 $this->assertEquals(200, $this->response->getStatusCode());
1043 $this->assertEquals('Hello World', $this->response->getContent());
1044 $this->assertTraceContains('fresh');
1046 // now POST to same URL
1047 $this->request('POST', '/helloworld');
1048 $this->assertHttpKernelIsCalled();
1049 $this->assertEquals('/', $this->response->headers->get('Location'));
1050 $this->assertTraceContains('invalidate');
1051 $this->assertTraceContains('pass');
1052 $this->assertEquals('', $this->response->getContent());
1054 // now make sure it was actually invalidated
1055 $this->request('GET', '/');
1056 $this->assertHttpKernelIsCalled();
1057 $this->assertEquals(200, $this->response->getStatusCode());
1058 $this->assertEquals('Hello World', $this->response->getContent());
1059 $this->assertTraceContains('stale');
1060 $this->assertTraceContains('invalid');
1061 $this->assertTraceContains('store');
1064 public function testServesFromCacheWhenHeadersMatch()
1067 $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
1068 $response->headers->set('Vary', 'Accept User-Agent Foo');
1069 $response->headers->set('Cache-Control', 'public, max-age=10');
1070 $response->headers->set('X-Response-Count', ++$count);
1071 $response->setContent($request->headers->get('USER_AGENT'));
1074 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1075 $this->assertEquals(200, $this->response->getStatusCode());
1076 $this->assertEquals('Bob/1.0', $this->response->getContent());
1077 $this->assertTraceContains('miss');
1078 $this->assertTraceContains('store');
1080 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1081 $this->assertEquals(200, $this->response->getStatusCode());
1082 $this->assertEquals('Bob/1.0', $this->response->getContent());
1083 $this->assertTraceContains('fresh');
1084 $this->assertTraceNotContains('store');
1085 $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
1088 public function testStoresMultipleResponsesWhenHeadersDiffer()
1091 $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
1092 $response->headers->set('Vary', 'Accept User-Agent Foo');
1093 $response->headers->set('Cache-Control', 'public, max-age=10');
1094 $response->headers->set('X-Response-Count', ++$count);
1095 $response->setContent($request->headers->get('USER_AGENT'));
1098 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1099 $this->assertEquals(200, $this->response->getStatusCode());
1100 $this->assertEquals('Bob/1.0', $this->response->getContent());
1101 $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
1103 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
1104 $this->assertEquals(200, $this->response->getStatusCode());
1105 $this->assertTraceContains('miss');
1106 $this->assertTraceContains('store');
1107 $this->assertEquals('Bob/2.0', $this->response->getContent());
1108 $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
1110 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1111 $this->assertTraceContains('fresh');
1112 $this->assertEquals('Bob/1.0', $this->response->getContent());
1113 $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
1115 $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
1116 $this->assertTraceContains('fresh');
1117 $this->assertEquals('Bob/2.0', $this->response->getContent());
1118 $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
1120 $this->request('GET', '/', array('HTTP_USER_AGENT' => 'Bob/2.0'));
1121 $this->assertTraceContains('miss');
1122 $this->assertEquals('Bob/2.0', $this->response->getContent());
1123 $this->assertEquals(3, $this->response->headers->get('X-Response-Count'));
1126 public function testShouldCatchExceptions()
1128 $this->catchExceptions();
1130 $this->setNextResponse();
1131 $this->request('GET', '/');
1133 $this->assertExceptionsAreCaught();
1136 public function testShouldCatchExceptionsWhenReloadingAndNoCacheRequest()
1138 $this->catchExceptions();
1140 $this->setNextResponse();
1141 $this->cacheConfig['allow_reload'] = true;
1142 $this->request('GET', '/', array(), array(), false, array('Pragma' => 'no-cache'));
1144 $this->assertExceptionsAreCaught();
1147 public function testShouldNotCatchExceptions()
1149 $this->catchExceptions(false);
1151 $this->setNextResponse();
1152 $this->request('GET', '/');
1154 $this->assertExceptionsAreNotCaught();
1157 public function testEsiCacheSendsTheLowestTtl()
1162 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1164 'Cache-Control' => 's-maxage=300',
1165 'Surrogate-Control' => 'content="ESI/1.0"',
1170 'body' => 'Hello World!',
1171 'headers' => array('Cache-Control' => 's-maxage=200'),
1175 'body' => 'My name is Bobby.',
1176 'headers' => array('Cache-Control' => 's-maxage=100'),
1180 $this->setNextResponses($responses);
1182 $this->request('GET', '/', array(), array(), true);
1183 $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1185 $this->assertEquals(100, $this->response->getTtl());
1188 public function testEsiCacheSendsTheLowestTtlForHeadRequests()
1193 'body' => 'I am a long-lived master response, but I embed a short-lived resource: <esi:include src="/foo" />',
1195 'Cache-Control' => 's-maxage=300',
1196 'Surrogate-Control' => 'content="ESI/1.0"',
1201 'body' => 'I am a short-lived resource',
1202 'headers' => array('Cache-Control' => 's-maxage=100'),
1206 $this->setNextResponses($responses);
1208 $this->request('HEAD', '/', array(), array(), true);
1210 $this->assertEmpty($this->response->getContent());
1211 $this->assertEquals(100, $this->response->getTtl());
1214 public function testEsiCacheForceValidation()
1219 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1221 'Cache-Control' => 's-maxage=300',
1222 'Surrogate-Control' => 'content="ESI/1.0"',
1227 'body' => 'Hello World!',
1228 'headers' => array('ETag' => 'foobar'),
1232 'body' => 'My name is Bobby.',
1233 'headers' => array('Cache-Control' => 's-maxage=100'),
1237 $this->setNextResponses($responses);
1239 $this->request('GET', '/', array(), array(), true);
1240 $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1241 $this->assertNull($this->response->getTtl());
1242 $this->assertTrue($this->response->mustRevalidate());
1243 $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
1244 $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
1247 public function testEsiCacheForceValidationForHeadRequests()
1252 'body' => 'I am the master response and use expiration caching, but I embed another resource: <esi:include src="/foo" />',
1254 'Cache-Control' => 's-maxage=300',
1255 'Surrogate-Control' => 'content="ESI/1.0"',
1260 'body' => 'I am the embedded resource and use validation caching',
1261 'headers' => array('ETag' => 'foobar'),
1265 $this->setNextResponses($responses);
1267 $this->request('HEAD', '/', array(), array(), true);
1269 // The response has been assembled from expiration and validation based resources
1270 // This can neither be cached nor revalidated, so it should be private/no cache
1271 $this->assertEmpty($this->response->getContent());
1272 $this->assertNull($this->response->getTtl());
1273 $this->assertTrue($this->response->mustRevalidate());
1274 $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
1275 $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
1278 public function testEsiRecalculateContentLengthHeader()
1283 'body' => '<esi:include src="/foo" />',
1285 'Content-Length' => 26,
1286 'Surrogate-Control' => 'content="ESI/1.0"',
1291 'body' => 'Hello World!',
1292 'headers' => array(),
1296 $this->setNextResponses($responses);
1298 $this->request('GET', '/', array(), array(), true);
1299 $this->assertEquals('Hello World!', $this->response->getContent());
1300 $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1303 public function testEsiRecalculateContentLengthHeaderForHeadRequest()
1308 'body' => '<esi:include src="/foo" />',
1310 'Content-Length' => 26,
1311 'Surrogate-Control' => 'content="ESI/1.0"',
1316 'body' => 'Hello World!',
1317 'headers' => array(),
1321 $this->setNextResponses($responses);
1323 $this->request('HEAD', '/', array(), array(), true);
1325 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13
1326 // "The Content-Length entity-header field indicates the size of the entity-body,
1327 // in decimal number of OCTETs, sent to the recipient or, in the case of the HEAD
1328 // method, the size of the entity-body that would have been sent had the request
1330 $this->assertEmpty($this->response->getContent());
1331 $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1334 public function testClientIpIsAlwaysLocalhostForForwardedRequests()
1336 $this->setNextResponse();
1337 $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1339 $this->assertEquals('127.0.0.1', $this->kernel->getBackendRequest()->server->get('REMOTE_ADDR'));
1343 * @dataProvider getTrustedProxyData
1345 public function testHttpCacheIsSetAsATrustedProxy(array $existing, array $expected)
1347 Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_ALL);
1349 $this->setNextResponse();
1350 $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1352 $this->assertEquals($expected, Request::getTrustedProxies());
1355 public function getTrustedProxyData()
1358 array(array(), array('127.0.0.1')),
1359 array(array('10.0.0.2'), array('10.0.0.2', '127.0.0.1')),
1360 array(array('10.0.0.2', '127.0.0.1'), array('10.0.0.2', '127.0.0.1')),
1365 * @dataProvider getXForwardedForData
1367 public function testXForwarderForHeaderForForwardedRequests($xForwardedFor, $expected)
1369 $this->setNextResponse();
1370 $server = array('REMOTE_ADDR' => '10.0.0.1');
1371 if (false !== $xForwardedFor) {
1372 $server['HTTP_X_FORWARDED_FOR'] = $xForwardedFor;
1374 $this->request('GET', '/', $server);
1376 $this->assertEquals($expected, $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
1379 public function getXForwardedForData()
1382 array(false, '10.0.0.1'),
1383 array('10.0.0.2', '10.0.0.2, 10.0.0.1'),
1384 array('10.0.0.2, 10.0.0.3', '10.0.0.2, 10.0.0.3, 10.0.0.1'),
1388 public function testXForwarderForHeaderForPassRequests()
1390 $this->setNextResponse();
1391 $server = array('REMOTE_ADDR' => '10.0.0.1');
1392 $this->request('POST', '/', $server);
1394 $this->assertEquals('10.0.0.1', $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
1397 public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses()
1399 $time = \DateTime::createFromFormat('U', time());
1404 'body' => '<esi:include src="/hey" />',
1406 'Surrogate-Control' => 'content="ESI/1.0"',
1408 'Last-Modified' => $time->format(DATE_RFC2822),
1414 'headers' => array(),
1418 $this->setNextResponses($responses);
1420 $this->request('GET', '/', array(), array(), true);
1421 $this->assertNull($this->response->getETag());
1422 $this->assertNull($this->response->getLastModified());
1425 public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadRequest()
1427 $time = \DateTime::createFromFormat('U', time());
1432 'body' => '<esi:include src="/hey" />',
1434 'Surrogate-Control' => 'content="ESI/1.0"',
1436 'Last-Modified' => $time->format(DATE_RFC2822),
1442 'headers' => array(),
1446 $this->setNextResponses($responses);
1448 $this->request('HEAD', '/', array(), array(), true);
1449 $this->assertEmpty($this->response->getContent());
1450 $this->assertNull($this->response->getETag());
1451 $this->assertNull($this->response->getLastModified());
1454 public function testDoesNotCacheOptionsRequest()
1456 $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'get');
1457 $this->request('GET', '/');
1458 $this->assertHttpKernelIsCalled();
1460 $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'options');
1461 $this->request('OPTIONS', '/');
1462 $this->assertHttpKernelIsCalled();
1464 $this->request('GET', '/');
1465 $this->assertHttpKernelIsNotCalled();
1466 $this->assertSame('get', $this->response->getContent());
1470 class TestKernel implements HttpKernelInterface
1472 public $terminateCalled = false;
1474 public function terminate(Request $request, Response $response)
1476 $this->terminateCalled = true;
1479 public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)