2a9a30d9ca4092dfe255e9a36615285fd836dfa0
[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\HttpKernel\HttpCache\HttpCache;
15 use Symfony\Component\HttpFoundation\Request;
16 use Symfony\Component\HttpFoundation\Response;
17 use Symfony\Component\HttpKernel\HttpKernelInterface;
18
19 /**
20  * @group time-sensitive
21  */
22 class HttpCacheTest extends HttpCacheTestCase
23 {
24     public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
25     {
26         $storeMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\HttpCache\\StoreInterface')
27             ->disableOriginalConstructor()
28             ->getMock();
29
30         // does not implement TerminableInterface
31         $kernel = new TestKernel();
32         $httpCache = new HttpCache($kernel, $storeMock);
33         $httpCache->terminate(Request::create('/'), new Response());
34
35         $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface');
36
37         // implements TerminableInterface
38         $kernelMock = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Kernel')
39             ->disableOriginalConstructor()
40             ->setMethods(array('terminate', 'registerBundles', 'registerContainerConfiguration'))
41             ->getMock();
42
43         $kernelMock->expects($this->once())
44             ->method('terminate');
45
46         $kernel = new HttpCache($kernelMock, $storeMock);
47         $kernel->terminate(Request::create('/'), new Response());
48     }
49
50     public function testPassesOnNonGetHeadRequests()
51     {
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'));
58     }
59
60     public function testInvalidatesOnPostPutDeleteRequests()
61     {
62         foreach (array('post', 'put', 'delete') as $method) {
63             $this->setNextResponse(200);
64             $this->request($method, '/');
65
66             $this->assertHttpKernelIsCalled();
67             $this->assertResponseOk();
68             $this->assertTraceContains('invalidate');
69             $this->assertTraceContains('pass');
70         }
71     }
72
73     public function testDoesNotCacheWithAuthorizationRequestHeaderAndNonPublicResponse()
74     {
75         $this->setNextResponse(200, array('ETag' => '"Foo"'));
76         $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
77
78         $this->assertHttpKernelIsCalled();
79         $this->assertResponseOk();
80         $this->assertEquals('private', $this->response->headers->get('Cache-Control'));
81
82         $this->assertTraceContains('miss');
83         $this->assertTraceNotContains('store');
84         $this->assertFalse($this->response->headers->has('Age'));
85     }
86
87     public function testDoesCacheWithAuthorizationRequestHeaderAndPublicResponse()
88     {
89         $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"Foo"'));
90         $this->request('GET', '/', array('HTTP_AUTHORIZATION' => 'basic foobarbaz'));
91
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'));
98     }
99
100     public function testDoesNotCacheWithCookieHeaderAndNonPublicResponse()
101     {
102         $this->setNextResponse(200, array('ETag' => '"Foo"'));
103         $this->request('GET', '/', array(), array('foo' => 'bar'));
104
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'));
111     }
112
113     public function testDoesNotCacheRequestsWithACookieHeader()
114     {
115         $this->setNextResponse(200);
116         $this->request('GET', '/', array(), array('foo' => 'bar'));
117
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'));
124     }
125
126     public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified()
127     {
128         $time = \DateTime::createFromFormat('U', time());
129
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)));
132
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');
139     }
140
141     public function testRespondsWith304WhenIfNoneMatchMatchesETag()
142     {
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'));
145
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');
153     }
154
155     public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch()
156     {
157         $time = \DateTime::createFromFormat('U', time());
158
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');
165         });
166
167         // only ETag matches
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());
172
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());
177
178         // Both matches
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());
182     }
183
184     public function testIncrementsMaxAgeWhenNoDateIsSpecifiedEventWhenUsingETag()
185     {
186         $this->setNextResponse(
187             200,
188             array(
189                 'ETag' => '1234',
190                 'Cache-Control' => 'public, s-maxage=60',
191             )
192         );
193
194         $this->request('GET', '/');
195         $this->assertHttpKernelIsCalled();
196         $this->assertEquals(200, $this->response->getStatusCode());
197         $this->assertTraceContains('miss');
198         $this->assertTraceContains('store');
199
200         sleep(2);
201
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'));
207     }
208
209     public function testValidatesPrivateResponsesCachedOnTheClient()
210     {
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);
218                 } else {
219                     $response->setStatusCode(200);
220                     $response->headers->set('Content-Type', 'text/plain');
221                     $response->setContent('private data');
222                 }
223             } else {
224                 $response->headers->set('Cache-Control', 'public');
225                 $response->setETag('"public tag"');
226                 if (in_array('"public tag"', $etags)) {
227                     $response->setStatusCode(304);
228                 } else {
229                     $response->setStatusCode(200);
230                     $response->headers->set('Content-Type', 'text/plain');
231                     $response->setContent('public data');
232                 }
233             }
234         });
235
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');
243
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');
252     }
253
254     public function testStoresResponsesWhenNoCacheRequestDirectivePresent()
255     {
256         $time = \DateTime::createFromFormat('U', time() + 5);
257
258         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
259         $this->request('GET', '/', array('HTTP_CACHE_CONTROL' => 'no-cache'));
260
261         $this->assertHttpKernelIsCalled();
262         $this->assertTraceContains('store');
263         $this->assertTrue($this->response->headers->has('Age'));
264     }
265
266     public function testReloadsResponsesWhenCacheHitsButNoCacheRequestDirectivePresentWhenAllowReloadIsSetTrue()
267     {
268         $count = 0;
269
270         $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
271             ++$count;
272             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
273         });
274
275         $this->request('GET', '/');
276         $this->assertEquals(200, $this->response->getStatusCode());
277         $this->assertEquals('Hello World', $this->response->getContent());
278         $this->assertTraceContains('store');
279
280         $this->request('GET', '/');
281         $this->assertEquals(200, $this->response->getStatusCode());
282         $this->assertEquals('Hello World', $this->response->getContent());
283         $this->assertTraceContains('fresh');
284
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');
291     }
292
293     public function testDoesNotReloadResponsesWhenAllowReloadIsSetFalseDefault()
294     {
295         $count = 0;
296
297         $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=10000'), '', function ($request, $response) use (&$count) {
298             ++$count;
299             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
300         });
301
302         $this->request('GET', '/');
303         $this->assertEquals(200, $this->response->getStatusCode());
304         $this->assertEquals('Hello World', $this->response->getContent());
305         $this->assertTraceContains('store');
306
307         $this->request('GET', '/');
308         $this->assertEquals(200, $this->response->getStatusCode());
309         $this->assertEquals('Hello World', $this->response->getContent());
310         $this->assertTraceContains('fresh');
311
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');
317
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');
322     }
323
324     public function testRevalidatesFreshCacheEntryWhenMaxAgeRequestDirectiveIsExceededWhenAllowRevalidateOptionIsSetTrue()
325     {
326         $count = 0;
327
328         $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
329             ++$count;
330             $response->headers->set('Cache-Control', 'public, max-age=10000');
331             $response->setETag($count);
332             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
333         });
334
335         $this->request('GET', '/');
336         $this->assertEquals(200, $this->response->getStatusCode());
337         $this->assertEquals('Hello World', $this->response->getContent());
338         $this->assertTraceContains('store');
339
340         $this->request('GET', '/');
341         $this->assertEquals(200, $this->response->getStatusCode());
342         $this->assertEquals('Hello World', $this->response->getContent());
343         $this->assertTraceContains('fresh');
344
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');
352     }
353
354     public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIsSetFalseDefault()
355     {
356         $count = 0;
357
358         $this->setNextResponse(200, array(), '', function ($request, $response) use (&$count) {
359             ++$count;
360             $response->headers->set('Cache-Control', 'public, max-age=10000');
361             $response->setETag($count);
362             $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World');
363         });
364
365         $this->request('GET', '/');
366         $this->assertEquals(200, $this->response->getStatusCode());
367         $this->assertEquals('Hello World', $this->response->getContent());
368         $this->assertTraceContains('store');
369
370         $this->request('GET', '/');
371         $this->assertEquals(200, $this->response->getStatusCode());
372         $this->assertEquals('Hello World', $this->response->getContent());
373         $this->assertTraceContains('fresh');
374
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');
382
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');
389     }
390
391     public function testFetchesResponseFromBackendWhenCacheMisses()
392     {
393         $time = \DateTime::createFromFormat('U', time() + 5);
394         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
395
396         $this->request('GET', '/');
397         $this->assertEquals(200, $this->response->getStatusCode());
398         $this->assertTraceContains('miss');
399         $this->assertTrue($this->response->headers->has('Age'));
400     }
401
402     public function testDoesNotCacheSomeStatusCodeResponses()
403     {
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)));
407
408             $this->request('GET', '/');
409             $this->assertEquals($code, $this->response->getStatusCode());
410             $this->assertTraceNotContains('store');
411             $this->assertFalse($this->response->headers->has('Age'));
412         }
413     }
414
415     public function testDoesNotCacheResponsesWithExplicitNoStoreDirective()
416     {
417         $time = \DateTime::createFromFormat('U', time() + 5);
418         $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'no-store'));
419
420         $this->request('GET', '/');
421         $this->assertTraceNotContains('store');
422         $this->assertFalse($this->response->headers->has('Age'));
423     }
424
425     public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator()
426     {
427         $this->setNextResponse();
428
429         $this->request('GET', '/');
430         $this->assertEquals(200, $this->response->getStatusCode());
431         $this->assertTraceNotContains('store');
432     }
433
434     public function testCachesResponsesWithExplicitNoCacheDirective()
435     {
436         $time = \DateTime::createFromFormat('U', time() + 5);
437         $this->setNextResponse(200, array('Expires' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, no-cache'));
438
439         $this->request('GET', '/');
440         $this->assertTraceContains('store');
441         $this->assertTrue($this->response->headers->has('Age'));
442     }
443
444     public function testCachesResponsesWithAnExpirationHeader()
445     {
446         $time = \DateTime::createFromFormat('U', time() + 5);
447         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
448
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');
456
457         $values = $this->getMetaStorageValues();
458         $this->assertCount(1, $values);
459     }
460
461     public function testCachesResponsesWithAMaxAgeDirective()
462     {
463         $this->setNextResponse(200, array('Cache-Control' => 'public, max-age=5'));
464
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');
472
473         $values = $this->getMetaStorageValues();
474         $this->assertCount(1, $values);
475     }
476
477     public function testCachesResponsesWithASMaxAgeDirective()
478     {
479         $this->setNextResponse(200, array('Cache-Control' => 's-maxage=5'));
480
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');
488
489         $values = $this->getMetaStorageValues();
490         $this->assertCount(1, $values);
491     }
492
493     public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation()
494     {
495         $time = \DateTime::createFromFormat('U', time());
496         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Last-Modified' => $time->format(DATE_RFC2822)));
497
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');
503     }
504
505     public function testCachesResponsesWithAnETagValidatorButNoFreshnessInformation()
506     {
507         $this->setNextResponse(200, array('Cache-Control' => 'public', 'ETag' => '"123456"'));
508
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');
514     }
515
516     public function testHitsCachedResponsesWithExpiresHeader()
517     {
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)));
521
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());
529
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());
539     }
540
541     public function testHitsCachedResponseWithMaxAgeDirective()
542     {
543         $time = \DateTime::createFromFormat('U', time() - 5);
544         $this->setNextResponse(200, array('Date' => $time->format(DATE_RFC2822), 'Cache-Control' => 'public, max-age=10'));
545
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());
553
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());
563     }
564
565     public function testDegradationWhenCacheLocked()
566     {
567         if ('\\' === DIRECTORY_SEPARATOR) {
568             $this->markTestSkipped('Skips on windows to avoid permissions issues.');
569         }
570
571         $this->cacheConfig['stale_while_revalidate'] = 10;
572
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
576
577         // Now, lock the cache
578         $concurrentRequest = Request::create('/', 'GET');
579         $this->store->lock($concurrentRequest);
580
581         /*
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.
584          */
585         sleep(10);
586
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());
592
593         /*
594          * Another 10s later, stale_while_revalidate is over. Resort to serving the old response, but
595          * do so with a "server unavailable" message.
596          */
597         sleep(10);
598
599         $this->request('GET', '/');
600         $this->assertHttpKernelIsNotCalled();
601         $this->assertEquals(503, $this->response->getStatusCode());
602         $this->assertEquals('Old response', $this->response->getContent());
603     }
604
605     public function testHitsCachedResponseWithSMaxAgeDirective()
606     {
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'));
609
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());
617
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());
627     }
628
629     public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation()
630     {
631         $this->setNextResponse();
632
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'));
640
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'));
649     }
650
651     public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpired()
652     {
653         $this->setNextResponse();
654
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'));
662
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'));
670
671         // expires the cache
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));
681
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'));
690
691         $this->setNextResponse();
692
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'));
700     }
701
702     public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpiredWithStatus304()
703     {
704         $this->setNextResponse();
705
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'));
713
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());
720
721         // expires the cache
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));
731
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'));
741
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'));
749     }
750
751     public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective()
752     {
753         $this->setNextResponse(200, array('Cache-Control' => 'must-revalidate'));
754
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());
763     }
764
765     public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent()
766     {
767         $time = \DateTime::createFromFormat('U', time() + 5);
768         $this->setNextResponse(200, array('Cache-Control' => 'public', 'Expires' => $time->format(DATE_RFC2822)));
769
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());
780
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));
791
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());
803     }
804
805     public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInformation()
806     {
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('');
814             }
815         });
816
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');
827
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');
840     }
841
842     public function testValidatesCachedResponsesUseSameHttpMethod()
843     {
844         $test = $this;
845
846         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) use ($test) {
847             $test->assertSame('OPTIONS', $request->getMethod());
848         });
849
850         // build initial request
851         $this->request('OPTIONS', '/');
852
853         // build subsequent request
854         $this->request('OPTIONS', '/');
855     }
856
857     public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation()
858     {
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('');
865             }
866         });
867
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');
877
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');
890     }
891
892     public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInformation()
893     {
894         $time = \DateTime::createFromFormat('U', time());
895
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));
899         });
900
901         // prime the cache
902         $this->request('GET', '/');
903
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');
911
912         sleep(15); // expire the cache
913
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'));
916         });
917
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');
924     }
925
926     public function testReplacesCachedResponsesWhenValidationResultsInNon304Response()
927     {
928         $time = \DateTime::createFromFormat('U', time());
929         $count = 0;
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');
933             switch (++$count) {
934                 case 1:
935                     $response->setContent('first response');
936                     break;
937                 case 2:
938                     $response->setContent('second response');
939                     break;
940                 case 3:
941                     $response->setContent('');
942                     $response->setStatusCode(304);
943                     break;
944             }
945         });
946
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());
951
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());
956
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());
961
962         $this->assertEquals(3, $count);
963     }
964
965     public function testPassesHeadRequestsThroughDirectlyOnPass()
966     {
967         $this->setNextResponse(200, array(), 'Hello World', function ($request, $response) {
968             $response->setContent('');
969             $response->setStatusCode(200);
970             $this->assertEquals('HEAD', $request->getMethod());
971         });
972
973         $this->request('HEAD', '/', array('HTTP_EXPECT' => 'something ...'));
974         $this->assertHttpKernelIsCalled();
975         $this->assertEquals('', $this->response->getContent());
976     }
977
978     public function testUsesCacheToRespondToHeadRequestsWhenFresh()
979     {
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());
985         });
986
987         $this->request('GET', '/');
988         $this->assertHttpKernelIsCalled();
989         $this->assertEquals('Hello World', $this->response->getContent());
990
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'));
996     }
997
998     public function testSendsNoContentWhenFresh()
999     {
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));
1004         });
1005
1006         $this->request('GET', '/');
1007         $this->assertHttpKernelIsCalled();
1008         $this->assertEquals('Hello World', $this->response->getContent());
1009
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());
1014     }
1015
1016     public function testInvalidatesCachedResponsesOnPost()
1017     {
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('');
1028             }
1029         });
1030
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');
1038
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');
1045
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());
1053
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');
1062     }
1063
1064     public function testServesFromCacheWhenHeadersMatch()
1065     {
1066         $count = 0;
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'));
1072         });
1073
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');
1079
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'));
1086     }
1087
1088     public function testStoresMultipleResponsesWhenHeadersDiffer()
1089     {
1090         $count = 0;
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'));
1096         });
1097
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'));
1102
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'));
1109
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'));
1114
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'));
1119
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'));
1124     }
1125
1126     public function testShouldCatchExceptions()
1127     {
1128         $this->catchExceptions();
1129
1130         $this->setNextResponse();
1131         $this->request('GET', '/');
1132
1133         $this->assertExceptionsAreCaught();
1134     }
1135
1136     public function testShouldCatchExceptionsWhenReloadingAndNoCacheRequest()
1137     {
1138         $this->catchExceptions();
1139
1140         $this->setNextResponse();
1141         $this->cacheConfig['allow_reload'] = true;
1142         $this->request('GET', '/', array(), array(), false, array('Pragma' => 'no-cache'));
1143
1144         $this->assertExceptionsAreCaught();
1145     }
1146
1147     public function testShouldNotCatchExceptions()
1148     {
1149         $this->catchExceptions(false);
1150
1151         $this->setNextResponse();
1152         $this->request('GET', '/');
1153
1154         $this->assertExceptionsAreNotCaught();
1155     }
1156
1157     public function testEsiCacheSendsTheLowestTtl()
1158     {
1159         $responses = array(
1160             array(
1161                 'status' => 200,
1162                 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1163                 'headers' => array(
1164                     'Cache-Control' => 's-maxage=300',
1165                     'Surrogate-Control' => 'content="ESI/1.0"',
1166                 ),
1167             ),
1168             array(
1169                 'status' => 200,
1170                 'body' => 'Hello World!',
1171                 'headers' => array('Cache-Control' => 's-maxage=200'),
1172             ),
1173             array(
1174                 'status' => 200,
1175                 'body' => 'My name is Bobby.',
1176                 'headers' => array('Cache-Control' => 's-maxage=100'),
1177             ),
1178         );
1179
1180         $this->setNextResponses($responses);
1181
1182         $this->request('GET', '/', array(), array(), true);
1183         $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
1184
1185         $this->assertEquals(100, $this->response->getTtl());
1186     }
1187
1188     public function testEsiCacheSendsTheLowestTtlForHeadRequests()
1189     {
1190         $responses = array(
1191             array(
1192                 'status' => 200,
1193                 'body' => 'I am a long-lived master response, but I embed a short-lived resource: <esi:include src="/foo" />',
1194                 'headers' => array(
1195                     'Cache-Control' => 's-maxage=300',
1196                     'Surrogate-Control' => 'content="ESI/1.0"',
1197                 ),
1198             ),
1199             array(
1200                 'status' => 200,
1201                 'body' => 'I am a short-lived resource',
1202                 'headers' => array('Cache-Control' => 's-maxage=100'),
1203             ),
1204         );
1205
1206         $this->setNextResponses($responses);
1207
1208         $this->request('HEAD', '/', array(), array(), true);
1209
1210         $this->assertEmpty($this->response->getContent());
1211         $this->assertEquals(100, $this->response->getTtl());
1212     }
1213
1214     public function testEsiCacheForceValidation()
1215     {
1216         $responses = array(
1217             array(
1218                 'status' => 200,
1219                 'body' => '<esi:include src="/foo" /> <esi:include src="/bar" />',
1220                 'headers' => array(
1221                     'Cache-Control' => 's-maxage=300',
1222                     'Surrogate-Control' => 'content="ESI/1.0"',
1223                 ),
1224             ),
1225             array(
1226                 'status' => 200,
1227                 'body' => 'Hello World!',
1228                 'headers' => array('ETag' => 'foobar'),
1229             ),
1230             array(
1231                 'status' => 200,
1232                 'body' => 'My name is Bobby.',
1233                 'headers' => array('Cache-Control' => 's-maxage=100'),
1234             ),
1235         );
1236
1237         $this->setNextResponses($responses);
1238
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'));
1245     }
1246
1247     public function testEsiCacheForceValidationForHeadRequests()
1248     {
1249         $responses = array(
1250             array(
1251                 'status' => 200,
1252                 'body' => 'I am the master response and use expiration caching, but I embed another resource: <esi:include src="/foo" />',
1253                 'headers' => array(
1254                     'Cache-Control' => 's-maxage=300',
1255                     'Surrogate-Control' => 'content="ESI/1.0"',
1256                 ),
1257             ),
1258             array(
1259                 'status' => 200,
1260                 'body' => 'I am the embedded resource and use validation caching',
1261                 'headers' => array('ETag' => 'foobar'),
1262             ),
1263         );
1264
1265         $this->setNextResponses($responses);
1266
1267         $this->request('HEAD', '/', array(), array(), true);
1268
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'));
1276     }
1277
1278     public function testEsiRecalculateContentLengthHeader()
1279     {
1280         $responses = array(
1281             array(
1282                 'status' => 200,
1283                 'body' => '<esi:include src="/foo" />',
1284                 'headers' => array(
1285                     'Content-Length' => 26,
1286                     'Surrogate-Control' => 'content="ESI/1.0"',
1287                 ),
1288             ),
1289             array(
1290                 'status' => 200,
1291                 'body' => 'Hello World!',
1292                 'headers' => array(),
1293             ),
1294         );
1295
1296         $this->setNextResponses($responses);
1297
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'));
1301     }
1302
1303     public function testEsiRecalculateContentLengthHeaderForHeadRequest()
1304     {
1305         $responses = array(
1306             array(
1307                 'status' => 200,
1308                 'body' => '<esi:include src="/foo" />',
1309                 'headers' => array(
1310                     'Content-Length' => 26,
1311                     'Surrogate-Control' => 'content="ESI/1.0"',
1312                 ),
1313             ),
1314             array(
1315                 'status' => 200,
1316                 'body' => 'Hello World!',
1317                 'headers' => array(),
1318             ),
1319         );
1320
1321         $this->setNextResponses($responses);
1322
1323         $this->request('HEAD', '/', array(), array(), true);
1324
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
1329         // been a GET."
1330         $this->assertEmpty($this->response->getContent());
1331         $this->assertEquals(12, $this->response->headers->get('Content-Length'));
1332     }
1333
1334     public function testClientIpIsAlwaysLocalhostForForwardedRequests()
1335     {
1336         $this->setNextResponse();
1337         $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1338
1339         $this->assertEquals('127.0.0.1', $this->kernel->getBackendRequest()->server->get('REMOTE_ADDR'));
1340     }
1341
1342     /**
1343      * @dataProvider getTrustedProxyData
1344      */
1345     public function testHttpCacheIsSetAsATrustedProxy(array $existing, array $expected)
1346     {
1347         Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_ALL);
1348
1349         $this->setNextResponse();
1350         $this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1351
1352         $this->assertEquals($expected, Request::getTrustedProxies());
1353     }
1354
1355     public function getTrustedProxyData()
1356     {
1357         return array(
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')),
1361         );
1362     }
1363
1364     /**
1365      * @dataProvider getXForwardedForData
1366      */
1367     public function testXForwarderForHeaderForForwardedRequests($xForwardedFor, $expected)
1368     {
1369         $this->setNextResponse();
1370         $server = array('REMOTE_ADDR' => '10.0.0.1');
1371         if (false !== $xForwardedFor) {
1372             $server['HTTP_X_FORWARDED_FOR'] = $xForwardedFor;
1373         }
1374         $this->request('GET', '/', $server);
1375
1376         $this->assertEquals($expected, $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
1377     }
1378
1379     public function getXForwardedForData()
1380     {
1381         return array(
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'),
1385         );
1386     }
1387
1388     public function testXForwarderForHeaderForPassRequests()
1389     {
1390         $this->setNextResponse();
1391         $server = array('REMOTE_ADDR' => '10.0.0.1');
1392         $this->request('POST', '/', $server);
1393
1394         $this->assertEquals('10.0.0.1', $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
1395     }
1396
1397     public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses()
1398     {
1399         $time = \DateTime::createFromFormat('U', time());
1400
1401         $responses = array(
1402             array(
1403                 'status' => 200,
1404                 'body' => '<esi:include src="/hey" />',
1405                 'headers' => array(
1406                     'Surrogate-Control' => 'content="ESI/1.0"',
1407                     'ETag' => 'hey',
1408                     'Last-Modified' => $time->format(DATE_RFC2822),
1409                 ),
1410             ),
1411             array(
1412                 'status' => 200,
1413                 'body' => 'Hey!',
1414                 'headers' => array(),
1415             ),
1416         );
1417
1418         $this->setNextResponses($responses);
1419
1420         $this->request('GET', '/', array(), array(), true);
1421         $this->assertNull($this->response->getETag());
1422         $this->assertNull($this->response->getLastModified());
1423     }
1424
1425     public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadRequest()
1426     {
1427         $time = \DateTime::createFromFormat('U', time());
1428
1429         $responses = array(
1430             array(
1431                 'status' => 200,
1432                 'body' => '<esi:include src="/hey" />',
1433                 'headers' => array(
1434                     'Surrogate-Control' => 'content="ESI/1.0"',
1435                     'ETag' => 'hey',
1436                     'Last-Modified' => $time->format(DATE_RFC2822),
1437                 ),
1438             ),
1439             array(
1440                 'status' => 200,
1441                 'body' => 'Hey!',
1442                 'headers' => array(),
1443             ),
1444         );
1445
1446         $this->setNextResponses($responses);
1447
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());
1452     }
1453
1454     public function testDoesNotCacheOptionsRequest()
1455     {
1456         $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'get');
1457         $this->request('GET', '/');
1458         $this->assertHttpKernelIsCalled();
1459
1460         $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=60'), 'options');
1461         $this->request('OPTIONS', '/');
1462         $this->assertHttpKernelIsCalled();
1463
1464         $this->request('GET', '/');
1465         $this->assertHttpKernelIsNotCalled();
1466         $this->assertSame('get', $this->response->getContent());
1467     }
1468 }
1469
1470 class TestKernel implements HttpKernelInterface
1471 {
1472     public $terminateCalled = false;
1473
1474     public function terminate(Request $request, Response $response)
1475     {
1476         $this->terminateCalled = true;
1477     }
1478
1479     public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
1480     {
1481     }
1482 }