Pull merge.
[yaffs-website] / vendor / symfony / http-kernel / Tests / HttpCache / HttpCacheTest.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\HttpKernel\Tests\HttpCache;
13
14 use Symfony\Component\HttpFoundation\Request;
15 use Symfony\Component\HttpFoundation\Response;
16 use Symfony\Component\HttpKernel\HttpCache\Esi;
17 use Symfony\Component\HttpKernel\HttpCache\HttpCache;
18 use Symfony\Component\HttpKernel\HttpCache\Store;
19 use Symfony\Component\HttpKernel\HttpKernelInterface;
20
21 /**
22  * @group time-sensitive
23  */
24 class HttpCacheTest extends HttpCacheTestCase
25 {
26     public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
27     {
28         $storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface')
29             ->disableOriginalConstructor()
30             ->getMock();
31
32         // does not implement TerminableInterface
33         $kernel = new TestKernel();
34         $httpCache = new HttpCache($kernel, $storeMock);
35         $httpCache->terminate(Request::create('/'), new Response());
36
37         $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface');
38
39         // implements TerminableInterface
40         $kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel')
41             ->disableOriginalConstructor()
42             ->setMethods(array('terminate', 'registerBundles', 'registerContainerConfiguration'))
43             ->getMock();
44
45         $kernelMock->expects($this->once())
46             ->method('terminate');
47
48         $kernel = new HttpCache($kernelMock, $storeMock);
49         $kernel->terminate(Request::create('/'), new Response());
50     }
51
52     public function testPassesOnNonGetHeadRequests()
53     {
54         $this->setNextResponse(200);
55         $this->request('POST', '/');
56         $this->assertHttpKernelIsCalled();
57         $this->assertResponseOk();
58         $this->assertTraceContains('pass');
59         $this->assertFalse($this->response->headers->has('Age'));
60     }
61
62     public function testInvalidatesOnPostPutDeleteRequests()
63     {
64         foreach (array('post', 'put', 'delete') as $method) {
65             $this->setNextResponse(200);
66             $this->request($method, '/');
67
68             $this->assertHttpKernelIsCalled();
69             $this->assertResponseOk();
70             $this->assertTraceContains('invalidate');
71             $this->assertTraceContains('pass');
72         }
73     }
74
75     public function testDoesNotCacheWithAuthorizationRequestHeaderAndNonPublicResponse()
76     {
77         $this->setNextResponse(200, array('ETag' => '"Foo"'));
78         $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
79
80         $this->assertHttpKernelIsCalled();
81         $this->assertResponseOk();
82         $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
83
84         $this->assertTraceContains('miss');
85         $this->assertTraceNotContains('store');
86         $this->assertFalse($this->response->headers->has('Age'));
87     }
88
89     public function testDoesCacheWithAuthorizationRequestHeaderAndPublicResponse()
90     {
91         $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"Foo"'));
92         $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
93
94         $this->assertHttpKernelIsCalled();
95         $this->assertResponseOk();
96         $this->assertTraceContains('miss');
97         $this->assertTraceContains('store');
98         $this->assertTrue($this->response->headers->has('Age'));
99         $this->assertEquals('public', $this->response->headers->get('Cache-Control'));
100     }
101
102     public function testDoesNotCacheWithCookieHeaderAndNonPublicResponse()
103     {
104         $this->setNextResponse(200, array('ETag' => '"Foo"'));
105         $this->request('GET', '/', array(), array('foo' => 'bar'));
106
107         $this->assertHttpKernelIsCalled();
108         $this->assertResponseOk();
109         $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
110         $this->assertTraceContains('miss');
111         $this->assertTraceNotContains('store');
112         $this->assertFalse($this->response->headers->has('Age'));
113     }
114
115     public function testDoesNotCacheRequestsWithACookieHeader()
116     {
117         $this->setNextResponse(200);
118         $this->request('GET', '/', array(), array('foo' => 'bar'));
119
120         $this->assertHttpKernelIsCalled();
121         $this->assertResponseOk();
122         $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
123         $this->assertTraceContains('miss');
124         $this->assertTraceNotContains('store');
125         $this->assertFalse($this->response->headers->has('Age'));
126     }
127
128     public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified()
129     {
130         $time = \DateTime::createFromFormat('U', time());
131
132         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822), 'Content-Type' => 'text/plain'), 'Hello World');
133         $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
134
135         $this->assertHttpKernelIsCalled();
136         $this->assertEquals(304, $this->response->getStatusCode());
137         $this->assertEquals('', $this->response->headers->get('Content-Type'));
138         $this->assertEmpty($this->response->getContent());
139         $this->assertTraceContains('miss');
140         $this->assertTraceContains('store');
141     }
142
143     public function testRespondsWith304WhenIfNoneMatchMatchesETag()
144     {
145         $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '12345', 'Content-Type' => 'text/plain'), 'Hello World');
146         $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345'));
147
148         $this->assertHttpKernelIsCalled();
149         $this->assertEquals(304, $this->response->getStatusCode());
150         $this->assertEquals('', $this->response->headers->get('Content-Type'));
151         $this->assertTrue($this->response->headers->has('ETag'));
152         $this->assertEmpty($this->response->getContent());
153         $this->assertTraceContains('miss');
154         $this->assertTraceContains('store');
155     }
156
157     public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch()
158     {
159         $time = \DateTime::createFromFormat('U', time());
160
161         $this->setNextResponse(200, array(), '', function ($request, $response) use ($time) {
162             $response->setStatusCode(200);
163             $response->headers->set('ETag', '12345');
164             $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
165             $response->headers->set('Content-Type', 'text/plain');
166             $response->setContent('Hello World');
167         });
168
169         // only ETag matches
170         $t = \DateTime::createFromFormat('U', time() - 3600);
171         $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(DATE_RFC2822)));
172         $this->assertHttpKernelIsCalled();
173         $this->assertEquals(200, $this->response->getStatusCode());
174
175         // only Last-Modified matches
176         $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
177         $this->assertHttpKernelIsCalled();
178         $this->assertEquals(200, $this->response->getStatusCode());
179
180         // Both matches
181         $this->request('GET', '/', array('HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
182         $this->assertHttpKernelIsCalled();
183         $this->assertEquals(304, $this->response->getStatusCode());
184     }
185
186     public function testIncrementsMaxAgeWhenNoDateIsSpecifiedEventWhenUsingETag()
187     {
188         $this->setNextResponse(
189             200,
190             array(
191                 'ETag' => '1234',
192                 'Cache-Control' => 'public, s-maxage=60',
193             )
194         );
195
196         $this->request('GET', '/');
197         $this->assertHttpKernelIsCalled();
198         $this->assertEquals(200, $this->response->getStatusCode());
199         $this->assertTraceContains('miss');
200         $this->assertTraceContains('store');
201
202         sleep(2);
203
204         $this->request('GET', '/');
205         $this->assertHttpKernelIsNotCalled();
206         $this->assertEquals(200, $this->response->getStatusCode());
207         $this->assertTraceContains('fresh');
208         $this->assertEquals(2, $this->response->headers->get('Age'));
209     }
210
211     public function testValidatesPrivateResponsesCachedOnTheClient()
212     {
213         $this->setNextResponse(200, array(), '', function ($request, $response) {
214             $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH'));
215             if ($request->cookies->has('authenticated')) {
216                 $response->headers->set('Cache-Control', 'private, no-store');
217                 $response->setETag('"private tag"');
218                 if (\in_array('"private tag"', $etags)) {
219                     $response->setStatusCode(304);
220                 } else {
221                     $response->setStatusCode(200);
222                     $response->headers->set('Content-Type', 'text/plain');
223                     $response->setContent('private data');
224                 }
225             } else {
226                 $response->headers->set('Cache-Control', 'public');
227                 $response->setETag('"public tag"');
228                 if (\in_array('"public tag"', $etags)) {
229                     $response->setStatusCode(304);
230                 } else {
231                     $response->setStatusCode(200);
232                     $response->headers->set('Content-Type', 'text/plain');
233                     $response->setContent('public data');
234                 }
235             }
236         });
237
238         $this->request('GET', '/');
239         $this->assertHttpKernelIsCalled();
240         $this->assertEquals(200, $this->response->getStatusCode());
241         $this->assertEquals('"public tag"', $this->response->headers->get('ETag'));
242         $this->assertEquals('public data', $this->response->getContent());
243         $this->assertTraceContains('miss');
244         $this->assertTraceContains('store');
245
246         $this->request('GET', '/', array(), array('authenticated' => ''));
247         $this->assertHttpKernelIsCalled();
248         $this->assertEquals(200, $this->response->getStatusCode());
249         $this->assertEquals('"private tag"', $this->response->headers->get('ETag'));
250         $this->assertEquals('private data', $this->response->getContent());
251         $this->assertTraceContains('stale');
252         $this->assertTraceContains('invalid');
253         $this->assertTraceNotContains('store');
254     }
255
256     public function testStoresResponsesWhenNoCacheRequestDirectivePresent()
257     {
258         $time = \DateTime::createFromFormat('U', time() + 5);
259
260         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
261         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
262
263         $this->assertHttpKernelIsCalled();
264         $this->assertTraceContains('store');
265         $this->assertTrue($this->response->headers->has('Age'));
266     }
267
268     public function testReloadsResponsesWhenCacheHitsButNoCacheRequestDirectivePresentWhenAllowReloadIsSetTrue()
269     {
270         $count = 0;
271
272         $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
273             ++$count;
274             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
275         });
276
277         $this->request('GET', '/');
278         $this->assertEquals(200, $this->response->getStatusCode());
279         $this->assertEquals('Hello World', $this->response->getContent());
280         $this->assertTraceContains('store');
281
282         $this->request('GET', '/');
283         $this->assertEquals(200, $this->response->getStatusCode());
284         $this->assertEquals('Hello World', $this->response->getContent());
285         $this->assertTraceContains('fresh');
286
287         $this->cacheConfig['allow_reload'] = true;
288         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
289         $this->assertEquals(200, $this->response->getStatusCode());
290         $this->assertEquals('Goodbye World', $this->response->getContent());
291         $this->assertTraceContains('reload');
292         $this->assertTraceContains('store');
293     }
294
295     public function testDoesNotReloadResponsesWhenAllowReloadIsSetFalseDefault()
296     {
297         $count = 0;
298
299         $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
300             ++$count;
301             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
302         });
303
304         $this->request('GET', '/');
305         $this->assertEquals(200, $this->response->getStatusCode());
306         $this->assertEquals('Hello World', $this->response->getContent());
307         $this->assertTraceContains('store');
308
309         $this->request('GET', '/');
310         $this->assertEquals(200, $this->response->getStatusCode());
311         $this->assertEquals('Hello World', $this->response->getContent());
312         $this->assertTraceContains('fresh');
313
314         $this->cacheConfig['allow_reload'] = false;
315         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
316         $this->assertEquals(200, $this->response->getStatusCode());
317         $this->assertEquals('Hello World', $this->response->getContent());
318         $this->assertTraceNotContains('reload');
319
320         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
321         $this->assertEquals(200, $this->response->getStatusCode());
322         $this->assertEquals('Hello World', $this->response->getContent());
323         $this->assertTraceNotContains('reload');
324     }
325
326     public function testRevalidatesFreshCacheEntryWhenMaxAgeRequestDirectiveIsExceededWhenAllowRevalidateOptionIsSetTrue()
327     {
328         $count = 0;
329
330         $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
331             ++$count;
332             $response->headers->set('Cache-Control', 'public, max-age=10000');
333             $response->setETag($count);
334             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
335         });
336
337         $this->request('GET', '/');
338         $this->assertEquals(200, $this->response->getStatusCode());
339         $this->assertEquals('Hello World', $this->response->getContent());
340         $this->assertTraceContains('store');
341
342         $this->request('GET', '/');
343         $this->assertEquals(200, $this->response->getStatusCode());
344         $this->assertEquals('Hello World', $this->response->getContent());
345         $this->assertTraceContains('fresh');
346
347         $this->cacheConfig['allow_revalidate'] = true;
348         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
349         $this->assertEquals(200, $this->response->getStatusCode());
350         $this->assertEquals('Goodbye World', $this->response->getContent());
351         $this->assertTraceContains('stale');
352         $this->assertTraceContains('invalid');
353         $this->assertTraceContains('store');
354     }
355
356     public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIsSetFalseDefault()
357     {
358         $count = 0;
359
360         $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
361             ++$count;
362             $response->headers->set('Cache-Control', 'public, max-age=10000');
363             $response->setETag($count);
364             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
365         });
366
367         $this->request('GET', '/');
368         $this->assertEquals(200, $this->response->getStatusCode());
369         $this->assertEquals('Hello World', $this->response->getContent());
370         $this->assertTraceContains('store');
371
372         $this->request('GET', '/');
373         $this->assertEquals(200, $this->response->getStatusCode());
374         $this->assertEquals('Hello World', $this->response->getContent());
375         $this->assertTraceContains('fresh');
376
377         $this->cacheConfig['allow_revalidate'] = false;
378         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
379         $this->assertEquals(200, $this->response->getStatusCode());
380         $this->assertEquals('Hello World', $this->response->getContent());
381         $this->assertTraceNotContains('stale');
382         $this->assertTraceNotContains('invalid');
383         $this->assertTraceContains('fresh');
384
385         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'max-age=0'));
386         $this->assertEquals(200, $this->response->getStatusCode());
387         $this->assertEquals('Hello World', $this->response->getContent());
388         $this->assertTraceNotContains('stale');
389         $this->assertTraceNotContains('invalid');
390         $this->assertTraceContains('fresh');
391     }
392
393     public function testFetchesResponseFromBackendWhenCacheMisses()
394     {
395         $time = \DateTime::createFromFormat('U', time() + 5);
396         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
397
398         $this->request('GET', '/');
399         $this->assertEquals(200, $this->response->getStatusCode());
400         $this->assertTraceContains('miss');
401         $this->assertTrue($this->response->headers->has('Age'));
402     }
403
404     public function testDoesNotCacheSomeStatusCodeResponses()
405     {
406         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) {
407             $time = \DateTime::createFromFormat('U', time() + 5);
408             $this->setNextResponse($code, array('Expires' => $time->format(DATE_RFC2822)));
409
410             $this->request('GET', '/');
411             $this->assertEquals($code, $this->response->getStatusCode());
412             $this->assertTraceNotContains('store');
413             $this->assertFalse($this->response->headers->has('Age'));
414         }
415     }
416
417     public function testDoesNotCacheResponsesWithExplicitNoStoreDirective()
418     {
419         $time = \DateTime::createFromFormat('U', time() + 5);
420         $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'no-store'));
421
422         $this->request('GET', '/');
423         $this->assertTraceNotContains('store');
424         $this->assertFalse($this->response->headers->has('Age'));
425     }
426
427     public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator()
428     {
429         $this->setNextResponse();
430
431         $this->request('GET', '/');
432         $this->assertEquals(200, $this->response->getStatusCode());
433         $this->assertTraceNotContains('store');
434     }
435
436     public function testCachesResponsesWithExplicitNoCacheDirective()
437     {
438         $time = \DateTime::createFromFormat('U', time() + 5);
439         $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, no-cache'));
440
441         $this->request('GET', '/');
442         $this->assertTraceContains('store');
443         $this->assertTrue($this->response->headers->has('Age'));
444     }
445
446     public function testCachesResponsesWithAnExpirationHeader()
447     {
448         $time = \DateTime::createFromFormat('U', time() + 5);
449         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
450
451         $this->request('GET', '/');
452         $this->assertEquals(200, $this->response->getStatusCode());
453         $this->assertEquals('Hello World', $this->response->getContent());
454         $this->assertNotNull($this->response->headers->get('Date'));
455         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
456         $this->assertTraceContains('miss');
457         $this->assertTraceContains('store');
458
459         $values = $this->getMetaStorageValues();
460         $this->assertCount(1, $values);
461     }
462
463     public function testCachesResponsesWithAMaxAgeDirective()
464     {
465         $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=5'));
466
467         $this->request('GET', '/');
468         $this->assertEquals(200, $this->response->getStatusCode());
469         $this->assertEquals('Hello World', $this->response->getContent());
470         $this->assertNotNull($this->response->headers->get('Date'));
471         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
472         $this->assertTraceContains('miss');
473         $this->assertTraceContains('store');
474
475         $values = $this->getMetaStorageValues();
476         $this->assertCount(1, $values);
477     }
478
479     public function testCachesResponsesWithASMaxAgeDirective()
480     {
481         $this->setNextResponse(200, array('Cache-Control' => 's-maxage=5'));
482
483         $this->request('GET', '/');
484         $this->assertEquals(200, $this->response->getStatusCode());
485         $this->assertEquals('Hello World', $this->response->getContent());
486         $this->assertNotNull($this->response->headers->get('Date'));
487         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
488         $this->assertTraceContains('miss');
489         $this->assertTraceContains('store');
490
491         $values = $this->getMetaStorageValues();
492         $this->assertCount(1, $values);
493     }
494
495     public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation()
496     {
497         $time = \DateTime::createFromFormat('U', time());
498         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822)));
499
500         $this->request('GET', '/');
501         $this->assertEquals(200, $this->response->getStatusCode());
502         $this->assertEquals('Hello World', $this->response->getContent());
503         $this->assertTraceContains('miss');
504         $this->assertTraceContains('store');
505     }
506
507     public function testCachesResponsesWithAnETagValidatorButNoFreshnessInformation()
508     {
509         $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"123456"'));
510
511         $this->request('GET', '/');
512         $this->assertEquals(200, $this->response->getStatusCode());
513         $this->assertEquals('Hello World', $this->response->getContent());
514         $this->assertTraceContains('miss');
515         $this->assertTraceContains('store');
516     }
517
518     public function testHitsCachedResponsesWithExpiresHeader()
519     {
520         $time1 = \DateTime::createFromFormat('U', time() - 5);
521         $time2 = \DateTime::createFromFormat('U', time() + 5);
522         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Date' => $time1->format(DATE_RFC2822), 'Expires' => $time2->format(DATE_RFC2822)));
523
524         $this->request('GET', '/');
525         $this->assertHttpKernelIsCalled();
526         $this->assertEquals(200, $this->response->getStatusCode());
527         $this->assertNotNull($this->response->headers->get('Date'));
528         $this->assertTraceContains('miss');
529         $this->assertTraceContains('store');
530         $this->assertEquals('Hello World', $this->response->getContent());
531
532         $this->request('GET', '/');
533         $this->assertHttpKernelIsNotCalled();
534         $this->assertEquals(200, $this->response->getStatusCode());
535         $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
536         $this->assertGreaterThan(0, $this->response->headers->get('Age'));
537         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
538         $this->assertTraceContains('fresh');
539         $this->assertTraceNotContains('store');
540         $this->assertEquals('Hello World', $this->response->getContent());
541     }
542
543     public function testHitsCachedResponseWithMaxAgeDirective()
544     {
545         $time = \DateTime::createFromFormat('U', time() - 5);
546         $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, max-age=10'));
547
548         $this->request('GET', '/');
549         $this->assertHttpKernelIsCalled();
550         $this->assertEquals(200, $this->response->getStatusCode());
551         $this->assertNotNull($this->response->headers->get('Date'));
552         $this->assertTraceContains('miss');
553         $this->assertTraceContains('store');
554         $this->assertEquals('Hello World', $this->response->getContent());
555
556         $this->request('GET', '/');
557         $this->assertHttpKernelIsNotCalled();
558         $this->assertEquals(200, $this->response->getStatusCode());
559         $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
560         $this->assertGreaterThan(0, $this->response->headers->get('Age'));
561         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
562         $this->assertTraceContains('fresh');
563         $this->assertTraceNotContains('store');
564         $this->assertEquals('Hello World', $this->response->getContent());
565     }
566
567     public function testDegradationWhenCacheLocked()
568     {
569         if ('\\' === \DIRECTORY_SEPARATOR) {
570             $this->markTestSkipped('Skips on windows to avoid permissions issues.');
571         }
572
573         $this->cacheConfig['stale_while_revalidate'] = 10;
574
575         // The prescence of Last-Modified makes this cacheable (because Response::isValidateable() then).
576         $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=5', 'Last-Modified' => 'some while ago'), 'Old response');
577         $this->request('GET', '/'); // warm the cache
578
579         // Now, lock the cache
580         $concurrentRequest = Request::create('/', 'GET');
581         $this->store->lock($concurrentRequest);
582
583         /*
584          *  After 10s, the cached response has become stale. Yet, we're still within the "stale_while_revalidate"
585          *  timeout so we may serve the stale response.
586          */
587         sleep(10);
588
589         $this->request('GET', '/');
590         $this->assertHttpKernelIsNotCalled();
591         $this->assertEquals(200, $this->response->getStatusCode());
592         $this->assertTraceContains('stale-while-revalidate');
593         $this->assertEquals('Old response', $this->response->getContent());
594
595         /*
596          * Another 10s later, stale_while_revalidate is over. Resort to serving the old response, but
597          * do so with a "server unavailable" message.
598          */
599         sleep(10);
600
601         $this->request('GET', '/');
602         $this->assertHttpKernelIsNotCalled();
603         $this->assertEquals(503, $this->response->getStatusCode());
604         $this->assertEquals('Old response', $this->response->getContent());
605     }
606
607     public function testHitsCachedResponseWithSMaxAgeDirective()
608     {
609         $time = \DateTime::createFromFormat('U', time() - 5);
610         $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0'));
611
612         $this->request('GET', '/');
613         $this->assertHttpKernelIsCalled();
614         $this->assertEquals(200, $this->response->getStatusCode());
615         $this->assertNotNull($this->response->headers->get('Date'));
616         $this->assertTraceContains('miss');
617         $this->assertTraceContains('store');
618         $this->assertEquals('Hello World', $this->response->getContent());
619
620         $this->request('GET', '/');
621         $this->assertHttpKernelIsNotCalled();
622         $this->assertEquals(200, $this->response->getStatusCode());
623         $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date')));
624         $this->assertGreaterThan(0, $this->response->headers->get('Age'));
625         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
626         $this->assertTraceContains('fresh');
627         $this->assertTraceNotContains('store');
628         $this->assertEquals('Hello World', $this->response->getContent());
629     }
630
631     public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation()
632     {
633         $this->setNextResponse();
634
635         $this->cacheConfig['default_ttl'] = 10;
636         $this->request('GET', '/');
637         $this->assertHttpKernelIsCalled();
638         $this->assertTraceContains('miss');
639         $this->assertTraceContains('store');
640         $this->assertEquals('Hello World', $this->response->getContent());
641         $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
642
643         $this->cacheConfig['default_ttl'] = 10;
644         $this->request('GET', '/');
645         $this->assertHttpKernelIsNotCalled();
646         $this->assertEquals(200, $this->response->getStatusCode());
647         $this->assertTraceContains('fresh');
648         $this->assertTraceNotContains('store');
649         $this->assertEquals('Hello World', $this->response->getContent());
650         $this->assertRegExp('/s-maxage=10/', $this->response->headers->get('Cache-Control'));
651     }
652
653     public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpired()
654     {
655         $this->setNextResponse();
656
657         $this->cacheConfig['default_ttl'] = 2;
658         $this->request('GET', '/');
659         $this->assertHttpKernelIsCalled();
660         $this->assertTraceContains('miss');
661         $this->assertTraceContains('store');
662         $this->assertEquals('Hello World', $this->response->getContent());
663         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
664
665         $this->request('GET', '/');
666         $this->assertHttpKernelIsNotCalled();
667         $this->assertEquals(200, $this->response->getStatusCode());
668         $this->assertTraceContains('fresh');
669         $this->assertTraceNotContains('store');
670         $this->assertEquals('Hello World', $this->response->getContent());
671         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
672
673         // expires the cache
674         $values = $this->getMetaStorageValues();
675         $this->assertCount(1, $values);
676         $tmp = unserialize($values[0]);
677         $time = \DateTime::createFromFormat('U', time() - 5);
678         $tmp[0][1]['date'] = $time->format(DATE_RFC2822);
679         $r = new \ReflectionObject($this->store);
680         $m = $r->getMethod('save');
681         $m->setAccessible(true);
682         $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
683
684         $this->request('GET', '/');
685         $this->assertHttpKernelIsCalled();
686         $this->assertEquals(200, $this->response->getStatusCode());
687         $this->assertTraceContains('stale');
688         $this->assertTraceContains('invalid');
689         $this->assertTraceContains('store');
690         $this->assertEquals('Hello World', $this->response->getContent());
691         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
692
693         $this->setNextResponse();
694
695         $this->request('GET', '/');
696         $this->assertHttpKernelIsNotCalled();
697         $this->assertEquals(200, $this->response->getStatusCode());
698         $this->assertTraceContains('fresh');
699         $this->assertTraceNotContains('store');
700         $this->assertEquals('Hello World', $this->response->getContent());
701         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
702     }
703
704     public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpiredWithStatus304()
705     {
706         $this->setNextResponse();
707
708         $this->cacheConfig['default_ttl'] = 2;
709         $this->request('GET', '/');
710         $this->assertHttpKernelIsCalled();
711         $this->assertTraceContains('miss');
712         $this->assertTraceContains('store');
713         $this->assertEquals('Hello World', $this->response->getContent());
714         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
715
716         $this->request('GET', '/');
717         $this->assertHttpKernelIsNotCalled();
718         $this->assertEquals(200, $this->response->getStatusCode());
719         $this->assertTraceContains('fresh');
720         $this->assertTraceNotContains('store');
721         $this->assertEquals('Hello World', $this->response->getContent());
722
723         // expires the cache
724         $values = $this->getMetaStorageValues();
725         $this->assertCount(1, $values);
726         $tmp = unserialize($values[0]);
727         $time = \DateTime::createFromFormat('U', time() - 5);
728         $tmp[0][1]['date'] = $time->format(DATE_RFC2822);
729         $r = new \ReflectionObject($this->store);
730         $m = $r->getMethod('save');
731         $m->setAccessible(true);
732         $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
733
734         $this->request('GET', '/');
735         $this->assertHttpKernelIsCalled();
736         $this->assertEquals(200, $this->response->getStatusCode());
737         $this->assertTraceContains('stale');
738         $this->assertTraceContains('valid');
739         $this->assertTraceContains('store');
740         $this->assertTraceNotContains('miss');
741         $this->assertEquals('Hello World', $this->response->getContent());
742         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
743
744         $this->request('GET', '/');
745         $this->assertHttpKernelIsNotCalled();
746         $this->assertEquals(200, $this->response->getStatusCode());
747         $this->assertTraceContains('fresh');
748         $this->assertTraceNotContains('store');
749         $this->assertEquals('Hello World', $this->response->getContent());
750         $this->assertRegExp('/s-maxage=2/', $this->response->headers->get('Cache-Control'));
751     }
752
753     public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective()
754     {
755         $this->setNextResponse(200, array('Cache-Control' => 'must-revalidate'));
756
757         $this->cacheConfig['default_ttl'] = 10;
758         $this->request('GET', '/');
759         $this->assertHttpKernelIsCalled();
760         $this->assertEquals(200, $this->response->getStatusCode());
761         $this->assertTraceContains('miss');
762         $this->assertTraceNotContains('store');
763         $this->assertNotRegExp('/s-maxage/', $this->response->headers->get('Cache-Control'));
764         $this->assertEquals('Hello World', $this->response->getContent());
765     }
766
767     public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent()
768     {
769         $time = \DateTime::createFromFormat('U', time() + 5);
770         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
771
772         // build initial request
773         $this->request('GET', '/');
774         $this->assertHttpKernelIsCalled();
775         $this->assertEquals(200, $this->response->getStatusCode());
776         $this->assertNotNull($this->response->headers->get('Date'));
777         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
778         $this->assertNotNull($this->response->headers->get('Age'));
779         $this->assertTraceContains('miss');
780         $this->assertTraceContains('store');
781         $this->assertEquals('Hello World', $this->response->getContent());
782
783         // go in and play around with the cached metadata directly ...
784         $values = $this->getMetaStorageValues();
785         $this->assertCount(1, $values);
786         $tmp = unserialize($values[0]);
787         $time = \DateTime::createFromFormat('U', time());
788         $tmp[0][1]['expires'] = $time->format(DATE_RFC2822);
789         $r = new \ReflectionObject($this->store);
790         $m = $r->getMethod('save');
791         $m->setAccessible(true);
792         $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp));
793
794         // build subsequent request; should be found but miss due to freshness
795         $this->request('GET', '/');
796         $this->assertHttpKernelIsCalled();
797         $this->assertEquals(200, $this->response->getStatusCode());
798         $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
799         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
800         $this->assertTraceContains('stale');
801         $this->assertTraceNotContains('fresh');
802         $this->assertTraceNotContains('miss');
803         $this->assertTraceContains('store');
804         $this->assertEquals('Hello World', $this->response->getContent());
805     }
806
807     public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInformation()
808     {
809         $time = \DateTime::createFromFormat('U', time());
810         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
811             $response->headers->set('Cache-Control', 'public');
812             $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
813             if ($time->format(DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) {
814                 $response->setStatusCode(304);
815                 $response->setContent('');
816             }
817         });
818
819         // build initial request
820         $this->request('GET', '/');
821         $this->assertHttpKernelIsCalled();
822         $this->assertEquals(200, $this->response->getStatusCode());
823         $this->assertNotNull($this->response->headers->get('Last-Modified'));
824         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
825         $this->assertEquals('Hello World', $this->response->getContent());
826         $this->assertTraceContains('miss');
827         $this->assertTraceContains('store');
828         $this->assertTraceNotContains('stale');
829
830         // build subsequent request; should be found but miss due to freshness
831         $this->request('GET', '/');
832         $this->assertHttpKernelIsCalled();
833         $this->assertEquals(200, $this->response->getStatusCode());
834         $this->assertNotNull($this->response->headers->get('Last-Modified'));
835         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
836         $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
837         $this->assertEquals('Hello World', $this->response->getContent());
838         $this->assertTraceContains('stale');
839         $this->assertTraceContains('valid');
840         $this->assertTraceContains('store');
841         $this->assertTraceNotContains('miss');
842     }
843
844     public function testValidatesCachedResponsesUseSameHttpMethod()
845     {
846         $test = $this;
847
848         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($test) {
849             $test->assertSame('OPTIONS', $request->getMethod());
850         });
851
852         // build initial request
853         $this->request('OPTIONS', '/');
854
855         // build subsequent request
856         $this->request('OPTIONS', '/');
857     }
858
859     public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation()
860     {
861         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
862             $response->headers->set('Cache-Control', 'public');
863             $response->headers->set('ETag', '"12345"');
864             if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) {
865                 $response->setStatusCode(304);
866                 $response->setContent('');
867             }
868         });
869
870         // build initial request
871         $this->request('GET', '/');
872         $this->assertHttpKernelIsCalled();
873         $this->assertEquals(200, $this->response->getStatusCode());
874         $this->assertNotNull($this->response->headers->get('ETag'));
875         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
876         $this->assertEquals('Hello World', $this->response->getContent());
877         $this->assertTraceContains('miss');
878         $this->assertTraceContains('store');
879
880         // build subsequent request; should be found but miss due to freshness
881         $this->request('GET', '/');
882         $this->assertHttpKernelIsCalled();
883         $this->assertEquals(200, $this->response->getStatusCode());
884         $this->assertNotNull($this->response->headers->get('ETag'));
885         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
886         $this->assertLessThanOrEqual(1, $this->response->headers->get('Age'));
887         $this->assertEquals('Hello World', $this->response->getContent());
888         $this->assertTraceContains('stale');
889         $this->assertTraceContains('valid');
890         $this->assertTraceContains('store');
891         $this->assertTraceNotContains('miss');
892     }
893
894     public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInformation()
895     {
896         $time = \DateTime::createFromFormat('U', time());
897
898         $this->setNextResponse(200, array(), 'Hello World', function (Request $request, Response $response) use ($time) {
899             $response->setSharedMaxAge(10);
900             $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
901         });
902
903         // prime the cache
904         $this->request('GET', '/');
905
906         // next request before s-maxage has expired: Serve from cache
907         // without hitting the backend
908         $this->request('GET', '/');
909         $this->assertHttpKernelIsNotCalled();
910         $this->assertEquals(200, $this->response->getStatusCode());
911         $this->assertEquals('Hello World', $this->response->getContent());
912         $this->assertTraceContains('fresh');
913
914         sleep(15); // expire the cache
915
916         $this->setNextResponse(304, array(), '', function (Request $request, Response $response) use ($time) {
917             $this->assertEquals($time->format(DATE_RFC2822), $request->headers->get('IF_MODIFIED_SINCE'));
918         });
919
920         $this->request('GET', '/');
921         $this->assertHttpKernelIsCalled();
922         $this->assertEquals(200, $this->response->getStatusCode());
923         $this->assertEquals('Hello World', $this->response->getContent());
924         $this->assertTraceContains('stale');
925         $this->assertTraceContains('valid');
926     }
927
928     public function testReplacesCachedResponsesWhenValidationResultsInNon304Response()
929     {
930         $time = \DateTime::createFromFormat('U', time());
931         $count = 0;
932         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time, &$count) {
933             $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
934             $response->headers->set('Cache-Control', 'public');
935             switch (++$count) {
936                 case 1:
937                     $response->setContent('first response');
938                     break;
939                 case 2:
940                     $response->setContent('second response');
941                     break;
942                 case 3:
943                     $response->setContent('');
944                     $response->setStatusCode(304);
945                     break;
946             }
947         });
948
949         // first request should fetch from backend and store in cache
950         $this->request('GET', '/');
951         $this->assertEquals(200, $this->response->getStatusCode());
952         $this->assertEquals('first response', $this->response->getContent());
953
954         // second request is validated, is invalid, and replaces cached entry
955         $this->request('GET', '/');
956         $this->assertEquals(200, $this->response->getStatusCode());
957         $this->assertEquals('second response', $this->response->getContent());
958
959         // third response is validated, valid, and returns cached entry
960         $this->request('GET', '/');
961         $this->assertEquals(200, $this->response->getStatusCode());
962         $this->assertEquals('second response', $this->response->getContent());
963
964         $this->assertEquals(3, $count);
965     }
966
967     public function testPassesHeadRequestsThroughDirectlyOnPass()
968     {
969         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
970             $response->setContent('');
971             $response->setStatusCode(200);
972             $this->assertEquals('HEAD', $request->getMethod());
973         });
974
975         $this->request('HEAD', '/', array('HTTP_EXPECT' => 'something ...'));
976         $this->assertHttpKernelIsCalled();
977         $this->assertEquals('', $this->response->getContent());
978     }
979
980     public function testUsesCacheToRespondToHeadRequestsWhenFresh()
981     {
982         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
983             $response->headers->set('Cache-Control', 'public, max-age=10');
984             $response->setContent('Hello World');
985             $response->setStatusCode(200);
986             $this->assertNotEquals('HEAD', $request->getMethod());
987         });
988
989         $this->request('GET', '/');
990         $this->assertHttpKernelIsCalled();
991         $this->assertEquals('Hello World', $this->response->getContent());
992
993         $this->request('HEAD', '/');
994         $this->assertHttpKernelIsNotCalled();
995         $this->assertEquals(200, $this->response->getStatusCode());
996         $this->assertEquals('', $this->response->getContent());
997         $this->assertEquals(\strlen('Hello World'), $this->response->headers->get('Content-Length'));
998     }
999
1000     public function testSendsNoContentWhenFresh()
1001     {
1002         $time = \DateTime::createFromFormat('U', time());
1003         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($time) {
1004             $response->headers->set('Cache-Control', 'public, max-age=10');
1005             $response->headers->set('Last-Modified', $time->format(DATE_RFC2822));
1006         });
1007
1008         $this->request('GET', '/');
1009         $this->assertHttpKernelIsCalled();
1010         $this->assertEquals('Hello World', $this->response->getContent());
1011
1012         $this->request('GET', '/', array('HTTP_IF_MODIFIED_SINCE' => $time->format(DATE_RFC2822)));
1013         $this->assertHttpKernelIsNotCalled();
1014         $this->assertEquals(304, $this->response->getStatusCode());
1015         $this->assertEquals('', $this->response->getContent());
1016     }
1017
1018     public function testInvalidatesCachedResponsesOnPost()
1019     {
1020         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
1021             if ('GET' == $request->getMethod()) {
1022                 $response->setStatusCode(200);
1023                 $response->headers->set('Cache-Control', 'public, max-age=500');
1024                 $response->setContent('Hello World');
1025             } elseif ('POST' == $request->getMethod()) {
1026                 $response->setStatusCode(303);
1027                 $response->headers->set('Location', '/');
1028                 $response->headers->remove('Cache-Control');
1029                 $response->setContent('');
1030             }
1031         });
1032
1033         // build initial request to enter into the cache
1034         $this->request('GET', '/');
1035         $this->assertHttpKernelIsCalled();
1036         $this->assertEquals(200, $this->response->getStatusCode());
1037         $this->assertEquals('Hello World', $this->response->getContent());
1038         $this->assertTraceContains('miss');
1039         $this->assertTraceContains('store');
1040
1041         // make sure it is valid
1042         $this->request('GET', '/');
1043         $this->assertHttpKernelIsNotCalled();
1044         $this->assertEquals(200, $this->response->getStatusCode());
1045         $this->assertEquals('Hello World', $this->response->getContent());
1046         $this->assertTraceContains('fresh');
1047
1048         // now POST to same URL
1049         $this->request('POST', '/helloworld');
1050         $this->assertHttpKernelIsCalled();
1051         $this->assertEquals('/', $this->response->headers->get('Location'));
1052         $this->assertTraceContains('invalidate');
1053         $this->assertTraceContains('pass');
1054         $this->assertEquals('', $this->response->getContent());
1055
1056         // now make sure it was actually invalidated
1057         $this->request('GET', '/');
1058         $this->assertHttpKernelIsCalled();
1059         $this->assertEquals(200, $this->response->getStatusCode());
1060         $this->assertEquals('Hello World', $this->response->getContent());
1061         $this->assertTraceContains('stale');
1062         $this->assertTraceContains('invalid');
1063         $this->assertTraceContains('store');
1064     }
1065
1066     public function testServesFromCacheWhenHeadersMatch()
1067     {
1068         $count = 0;
1069         $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
1070             $response->headers->set('Vary', 'Accept User-Agent Foo');
1071             $response->headers->set('Cache-Control', 'public, max-age=10');
1072             $response->headers->set('X-Response-Count', ++$count);
1073             $response->setContent($request->headers->get('USER_AGENT'));
1074         });
1075
1076         $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1077         $this->assertEquals(200, $this->response->getStatusCode());
1078         $this->assertEquals('Bob/1.0', $this->response->getContent());
1079         $this->assertTraceContains('miss');
1080         $this->assertTraceContains('store');
1081
1082         $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1083         $this->assertEquals(200, $this->response->getStatusCode());
1084         $this->assertEquals('Bob/1.0', $this->response->getContent());
1085         $this->assertTraceContains('fresh');
1086         $this->assertTraceNotContains('store');
1087         $this->assertNotNull($this->response->headers->get('X-Content-Digest'));
1088     }
1089
1090     public function testStoresMultipleResponsesWhenHeadersDiffer()
1091     {
1092         $count = 0;
1093         $this->setNextResponse(200, array('Cache-Control' => 'max-age=10000'), '', function ($request, $response) use (&$count) {
1094             $response->headers->set('Vary', 'Accept User-Agent Foo');
1095             $response->headers->set('Cache-Control', 'public, max-age=10');
1096             $response->headers->set('X-Response-Count', ++$count);
1097             $response->setContent($request->headers->get('USER_AGENT'));
1098         });
1099
1100         $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1101         $this->assertEquals(200, $this->response->getStatusCode());
1102         $this->assertEquals('Bob/1.0', $this->response->getContent());
1103         $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
1104
1105         $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
1106         $this->assertEquals(200, $this->response->getStatusCode());
1107         $this->assertTraceContains('miss');
1108         $this->assertTraceContains('store');
1109         $this->assertEquals('Bob/2.0', $this->response->getContent());
1110         $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
1111
1112         $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0'));
1113         $this->assertTraceContains('fresh');
1114         $this->assertEquals('Bob/1.0', $this->response->getContent());
1115         $this->assertEquals(1, $this->response->headers->get('X-Response-Count'));
1116
1117         $this->request('GET', '/', array('HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0'));
1118         $this->assertTraceContains('fresh');
1119         $this->assertEquals('Bob/2.0', $this->response->getContent());
1120         $this->assertEquals(2, $this->response->headers->get('X-Response-Count'));
1121
1122         $this->request('GET', '/', array('HTTP_USER_AGENT' => 'Bob/2.0'));
1123         $this->assertTraceContains('miss');
1124         $this->assertEquals('Bob/2.0', $this->response->getContent());
1125         $this->assertEquals(3, $this->response->headers->get('X-Response-Count'));
1126     }
1127
1128     public function testShouldCatchExceptions()
1129     {
1130         $this->catchExceptions();
1131
1132         $this->setNextResponse();
1133         $this->request('GET', '/');
1134
1135         $this->assertExceptionsAreCaught();
1136     }
1137
1138     public function testShouldCatchExceptionsWhenReloadingAndNoCacheRequest()
1139     {
1140         $this->catchExceptions();
1141
1142         $this->setNextResponse();
1143         $this->cacheConfig['allow_reload'] = true;
1144         $this->request('GET', '/', array(), array(), false, array('Pragma' => 'no-cache'));
1145
1146         $this->assertExceptionsAreCaught();
1147     }
1148
1149     public function testShouldNotCatchExceptions()
1150     {
1151         $this->catchExceptions(false);
1152
1153         $this->setNextResponse();
1154         $this->request('GET', '/');
1155
1156         $this->assertExceptionsAreNotCaught();
1157     }
1158
1159     public function testEsiCacheSendsTheLowestTtl()
1160     {
1161         $responses = array(
1162             array(
1163                 'status' => 200,
1164                 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1165                 'headers' => array(
1166                     'Cache-Control' => 's-maxage=300',
1167                     'Surrogate-Control' => 'content="ESI/1.0"',
1168                 ),
1169             ),
1170             array(
1171                 'status' => 200,
1172                 'body' => 'Hello World!',
1173                 'headers' => array('Cache-Control' => 's-maxage=200'),
1174             ),
1175             array(
1176                 'status' => 200,
1177                 'body' => 'My name is Bobby.',
1178                 'headers' => array('Cache-Control' => 's-maxage=100'),
1179             ),
1180         );
1181
1182         $this->setNextResponses($responses);
1183
1184         $this->request('GET', '/', array(), array(), true);
1185         $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1186
1187         $this->assertEquals(100, $this->response->getTtl());
1188     }
1189
1190     public function testEsiCacheSendsTheLowestTtlForHeadRequests()
1191     {
1192         $responses = array(
1193             array(
1194                 'status' => 200,
1195                 'body' => 'I am a long-lived master response, but I embed a short-lived resource: <esi:include src="/foo" />',
1196                 'headers' => array(
1197                     'Cache-Control' => 's-maxage=300',
1198                     'Surrogate-Control' => 'content="ESI/1.0"',
1199                 ),
1200             ),
1201             array(
1202                 'status' => 200,
1203                 'body' => 'I am a short-lived resource',
1204                 'headers' => array('Cache-Control' => 's-maxage=100'),
1205             ),
1206         );
1207
1208         $this->setNextResponses($responses);
1209
1210         $this->request('HEAD', '/', array(), array(), true);
1211
1212         $this->assertEmpty($this->response->getContent());
1213         $this->assertEquals(100, $this->response->getTtl());
1214     }
1215
1216     public function testEsiCacheForceValidation()
1217     {
1218         $responses = array(
1219             array(
1220                 'status' => 200,
1221                 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1222                 'headers' => array(
1223                     'Cache-Control' => 's-maxage=300',
1224                     'Surrogate-Control' => 'content="ESI/1.0"',
1225                 ),
1226             ),
1227             array(
1228                 'status' => 200,
1229                 'body' => 'Hello World!',
1230                 'headers' => array('ETag' => 'foobar'),
1231             ),
1232             array(
1233                 'status' => 200,
1234                 'body' => 'My name is Bobby.',
1235                 'headers' => array('Cache-Control' => 's-maxage=100'),
1236             ),
1237         );
1238
1239         $this->setNextResponses($responses);
1240
1241         $this->request('GET', '/', array(), array(), true);
1242         $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1243         $this->assertNull($this->response->getTtl());
1244         $this->assertTrue($this->response->mustRevalidate());
1245         $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
1246         $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
1247     }
1248
1249     public function testEsiCacheForceValidationForHeadRequests()
1250     {
1251         $responses = array(
1252             array(
1253                 'status' => 200,
1254                 'body' => 'I am the master response and use expiration caching, but I embed another resource: <esi:include src="/foo" />',
1255                 'headers' => array(
1256                     'Cache-Control' => 's-maxage=300',
1257                     'Surrogate-Control' => 'content="ESI/1.0"',
1258                 ),
1259             ),
1260             array(
1261                 'status' => 200,
1262                 'body' => 'I am the embedded resource and use validation caching',
1263                 'headers' => array('ETag' => 'foobar'),
1264             ),
1265         );
1266
1267         $this->setNextResponses($responses);
1268
1269         $this->request('HEAD', '/', array(), array(), true);
1270
1271         // The response has been assembled from expiration and validation based resources
1272         // This can neither be cached nor revalidated, so it should be private/no cache
1273         $this->assertEmpty($this->response->getContent());
1274         $this->assertNull($this->response->getTtl());
1275         $this->assertTrue($this->response->mustRevalidate());
1276         $this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
1277         $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
1278     }
1279
1280     public function testEsiRecalculateContentLengthHeader()
1281     {
1282         $responses = array(
1283             array(
1284                 'status' => 200,
1285                 'body' => '<esi:include src="/foo" />',
1286                 'headers' => array(
1287                     'Content-Length' => 26,
1288                     'Surrogate-Control' => 'content="ESI/1.0"',
1289                 ),
1290             ),
1291             array(
1292                 'status' => 200,
1293                 'body' => 'Hello World!',
1294                 'headers' => array(),
1295             ),
1296         );
1297
1298         $this->setNextResponses($responses);
1299
1300         $this->request('GET', '/', array(), array(), true);
1301         $this->assertEquals('Hello World!', $this->response->getContent());
1302         $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1303     }
1304
1305     public function testEsiRecalculateContentLengthHeaderForHeadRequest()
1306     {
1307         $responses = array(
1308             array(
1309                 'status' => 200,
1310                 'body' => '<esi:include src="/foo" />',
1311                 'headers' => array(
1312                     'Content-Length' => 26,
1313                     'Surrogate-Control' => 'content="ESI/1.0"',
1314                 ),
1315             ),
1316             array(
1317                 'status' => 200,
1318                 'body' => 'Hello World!',
1319                 'headers' => array(),
1320             ),
1321         );
1322
1323         $this->setNextResponses($responses);
1324
1325         $this->request('HEAD', '/', array(), array(), true);
1326
1327         // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13
1328         // "The Content-Length entity-header field indicates the size of the entity-body,
1329         // in decimal number of OCTETs, sent to the recipient or, in the case of the HEAD
1330         // method, the size of the entity-body that would have been sent had the request
1331         // been a GET."
1332         $this->assertEmpty($this->response->getContent());
1333         $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1334     }
1335
1336     public function testClientIpIsAlwaysLocalhostForForwardedRequests()
1337     {
1338         $this->setNextResponse();
1339         $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1340
1341         $this->kernel->assert(function ($backendRequest) {
1342             $this->assertSame('127.0.0.1', $backendRequest->server->get('REMOTE_ADDR'));
1343         });
1344     }
1345
1346     /**
1347      * @dataProvider getTrustedProxyData
1348      */
1349     public function testHttpCacheIsSetAsATrustedProxy(array $existing)
1350     {
1351         Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_ALL);
1352
1353         $this->setNextResponse();
1354         $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1355         $this->assertSame($existing, Request::getTrustedProxies());
1356
1357         $existing = array_unique(array_merge($existing, array('127.0.0.1')));
1358         $this->kernel->assert(function ($backendRequest) use ($existing) {
1359             $this->assertSame($existing, Request::getTrustedProxies());
1360             $this->assertsame('10.0.0.1', $backendRequest->getClientIp());
1361         });
1362
1363         Request::setTrustedProxies(array(), -1);
1364     }
1365
1366     public function getTrustedProxyData()
1367     {
1368         return array(
1369             array(array()),
1370             array(array('10.0.0.2')),
1371             array(array('10.0.0.2', '127.0.0.1')),
1372         );
1373     }
1374
1375     /**
1376      * @dataProvider getForwardedData
1377      */
1378     public function testForwarderHeaderForForwardedRequests($forwarded, $expected)
1379     {
1380         $this->setNextResponse();
1381         $server = array('REMOTE_ADDR' => '10.0.0.1');
1382         if (null !== $forwarded) {
1383             Request::setTrustedProxies($server, -1);
1384             $server['HTTP_FORWARDED'] = $forwarded;
1385         }
1386         $this->request('GET', '/', $server);
1387
1388         $this->kernel->assert(function ($backendRequest) use ($expected) {
1389             $this->assertSame($expected, $backendRequest->headers->get('Forwarded'));
1390         });
1391
1392         Request::setTrustedProxies(array(), -1);
1393     }
1394
1395     public function getForwardedData()
1396     {
1397         return array(
1398             array(null, 'for="10.0.0.1";host="localhost";proto=http'),
1399             array('for=10.0.0.2', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.1"'),
1400             array('for=10.0.0.2, for=10.0.0.3', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.3", for="10.0.0.1"'),
1401         );
1402     }
1403
1404     public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses()
1405     {
1406         $time = \DateTime::createFromFormat('U', time());
1407
1408         $responses = array(
1409             array(
1410                 'status' => 200,
1411                 'body' => '<esi:include src="/hey" />',
1412                 'headers' => array(
1413                     'Surrogate-Control' => 'content="ESI/1.0"',
1414                     'ETag' => 'hey',
1415                     'Last-Modified' => $time->format(DATE_RFC2822),
1416                 ),
1417             ),
1418             array(
1419                 'status' => 200,
1420                 'body' => 'Hey!',
1421                 'headers' => array(),
1422             ),
1423         );
1424
1425         $this->setNextResponses($responses);
1426
1427         $this->request('GET', '/', array(), array(), true);
1428         $this->assertNull($this->response->getETag());
1429         $this->assertNull($this->response->getLastModified());
1430     }
1431
1432     public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadRequest()
1433     {
1434         $time = \DateTime::createFromFormat('U', time());
1435
1436         $responses = array(
1437             array(
1438                 'status' => 200,
1439                 'body' => '<esi:include src="/hey" />',
1440                 'headers' => array(
1441                     'Surrogate-Control' => 'content="ESI/1.0"',
1442                     'ETag' => 'hey',
1443                     'Last-Modified' => $time->format(DATE_RFC2822),
1444                 ),
1445             ),
1446             array(
1447                 'status' => 200,
1448                 'body' => 'Hey!',
1449                 'headers' => array(),
1450             ),
1451         );
1452
1453         $this->setNextResponses($responses);
1454
1455         $this->request('HEAD', '/', array(), array(), true);
1456         $this->assertEmpty($this->response->getContent());
1457         $this->assertNull($this->response->getETag());
1458         $this->assertNull($this->response->getLastModified());
1459     }
1460
1461     public function testDoesNotCacheOptionsRequest()
1462     {
1463         $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'get');
1464         $this->request('GET', '/');
1465         $this->assertHttpKernelIsCalled();
1466
1467         $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'options');
1468         $this->request('OPTIONS', '/');
1469         $this->assertHttpKernelIsCalled();
1470
1471         $this->request('GET', '/');
1472         $this->assertHttpKernelIsNotCalled();
1473         $this->assertSame('get', $this->response->getContent());
1474     }
1475
1476     public function testUsesOriginalRequestForSurrogate()
1477     {
1478         $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock();
1479         $store = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpCache\StoreInterface')->getMock();
1480
1481         $kernel
1482             ->expects($this->exactly(2))
1483             ->method('handle')
1484             ->willReturnCallback(function (Request $request) {
1485                 $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
1486
1487                 return new Response();
1488             });
1489
1490         $cache = new HttpCache($kernel,
1491             $store,
1492             new Esi()
1493         );
1494
1495         $request = Request::create('/');
1496         $request->server->set('REMOTE_ADDR', '10.0.0.1');
1497
1498         // Main request
1499         $cache->handle($request, HttpKernelInterface::MASTER_REQUEST);
1500
1501         // Main request was now modified by HttpCache
1502         // The surrogate will ask for the request using $this->cache->getRequest()
1503         // which MUST return the original request so the surrogate
1504         // can actually behave like a reverse proxy like e.g. Varnish would.
1505         $this->assertSame('10.0.0.1', $cache->getRequest()->getClientIp());
1506         $this->assertSame('10.0.0.1', $cache->getRequest()->server->get('REMOTE_ADDR'));
1507
1508         // Surrogate request
1509         $cache->handle($request, HttpKernelInterface::SUB_REQUEST);
1510     }
1511 }
1512
1513 class TestKernel implements HttpKernelInterface
1514 {
1515     public $terminateCalled = false;
1516
1517     public function terminate(Request $request, Response $response)
1518     {
1519         $this->terminateCalled = true;
1520     }
1521
1522     public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
1523     {
1524     }
1525 }