Skip to content

Commit 4bf191d

Browse files
Nyholmsagikazarmark
authored andcommitted
Added support for Last-Modified and ETag (#8)
* Added support for Last-Modified and ETag * style fix * Return the 304 response if we do not have a response in cache * Bugfix and started on the tests * Style fixes * Tests are passing * Style fix * Added tests * Applied fixes from StyleCI [ci skip] [skip ci] * Updated comment * Made the code easier to read * Updated hash * Minor * cs * Removed return statement * cs * Added comment * Removed period * Added docs about isset
1 parent acc6599 commit 4bf191d

File tree

3 files changed

+315
-18
lines changed

3 files changed

+315
-18
lines changed

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
"Http\\Client\\Common\\Plugin\\": "src/"
2727
}
2828
},
29+
"autoload-dev": {
30+
"psr-4": {
31+
"spec\\Http\\Client\\Common\\Plugin\\": "spec/"
32+
}
33+
},
2934
"scripts": {
3035
"test": "vendor/bin/phpspec run",
3136
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"

spec/CachePluginSpec.php

Lines changed: 195 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace spec\Http\Client\Common\Plugin;
44

5+
use Prophecy\Argument;
56
use Http\Message\StreamFactory;
67
use Http\Promise\FulfilledPromise;
78
use PhpSpec\ObjectBehavior;
@@ -15,7 +16,10 @@ class CachePluginSpec extends ObjectBehavior
1516
{
1617
function let(CacheItemPoolInterface $pool, StreamFactory $streamFactory)
1718
{
18-
$this->beConstructedWith($pool, $streamFactory, ['default_ttl'=>60]);
19+
$this->beConstructedWith($pool, $streamFactory, [
20+
'default_ttl' => 60,
21+
'cache_lifetime' => 1000
22+
]);
1923
}
2024

2125
function it_is_initializable(CacheItemPoolInterface $pool)
@@ -39,14 +43,22 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i
3943
$request->getUri()->willReturn('/');
4044
$response->getStatusCode()->willReturn(200);
4145
$response->getBody()->willReturn($stream);
42-
$response->getHeader('Cache-Control')->willReturn(array());
43-
$response->getHeader('Expires')->willReturn(array());
46+
$response->getHeader('Cache-Control')->willReturn(array())->shouldBeCalled();
47+
$response->getHeader('Expires')->willReturn(array())->shouldBeCalled();
48+
$response->getHeader('ETag')->willReturn(array())->shouldBeCalled();
4449

4550
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
4651
$item->isHit()->willReturn(false);
47-
$item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled();
48-
$item->expiresAfter(60)->willReturn($item)->shouldBeCalled();
49-
$pool->save($item)->shouldBeCalled();
52+
$item->expiresAfter(1060)->willReturn($item)->shouldBeCalled();
53+
54+
$item->set($this->getCacheItemMatcher([
55+
'response' => $response->getWrappedObject(),
56+
'body' => $httpBody,
57+
'expiresAt' => 0,
58+
'createdAt' => 0,
59+
'etag' => []
60+
]))->willReturn($item)->shouldBeCalled();
61+
$pool->save(Argument::any())->shouldBeCalled();
5062

5163
$next = function (RequestInterface $request) use ($response) {
5264
return new FulfilledPromise($response->getWrappedObject());
@@ -100,13 +112,20 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI
100112
$response->getHeader('Cache-Control')->willReturn(array('max-age=40'));
101113
$response->getHeader('Age')->willReturn(array('15'));
102114
$response->getHeader('Expires')->willReturn(array());
115+
$response->getHeader('ETag')->willReturn(array());
103116

104117
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
105118
$item->isHit()->willReturn(false);
106119

107-
// 40-15 should be 25
108-
$item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled();
109-
$item->expiresAfter(25)->willReturn($item)->shouldBeCalled();
120+
$item->set($this->getCacheItemMatcher([
121+
'response' => $response->getWrappedObject(),
122+
'body' => $httpBody,
123+
'expiresAt' => 0,
124+
'createdAt' => 0,
125+
'etag' => []
126+
]))->willReturn($item)->shouldBeCalled();
127+
// 40-15 should be 25 + the default 1000
128+
$item->expiresAfter(1025)->willReturn($item)->shouldBeCalled();
110129
$pool->save($item)->shouldBeCalled();
111130

112131
$next = function (RequestInterface $request) use ($response) {
@@ -115,4 +134,171 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI
115134

116135
$this->handleRequest($request, $next, function () {});
117136
}
137+
138+
function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
139+
{
140+
$httpBody = 'body';
141+
$stream->__toString()->willReturn($httpBody);
142+
$stream->isSeekable()->willReturn(true);
143+
$stream->rewind()->shouldBeCalled();
144+
145+
$request->getMethod()->willReturn('GET');
146+
$request->getUri()->willReturn('/');
147+
$response->getStatusCode()->willReturn(200);
148+
$response->getBody()->willReturn($stream);
149+
$response->getHeader('Cache-Control')->willReturn(array());
150+
$response->getHeader('Expires')->willReturn(array());
151+
$response->getHeader('ETag')->willReturn(array('foo_etag'));
152+
153+
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
154+
$item->isHit()->willReturn(false);
155+
$item->expiresAfter(1060)->willReturn($item);
156+
157+
$item->set($this->getCacheItemMatcher([
158+
'response' => $response->getWrappedObject(),
159+
'body' => $httpBody,
160+
'expiresAt' => 0,
161+
'createdAt' => 0,
162+
'etag' => ['foo_etag']
163+
]))->willReturn($item)->shouldBeCalled();
164+
$pool->save(Argument::any())->shouldBeCalled();
165+
166+
$next = function (RequestInterface $request) use ($response) {
167+
return new FulfilledPromise($response->getWrappedObject());
168+
};
169+
170+
$this->handleRequest($request, $next, function () {});
171+
}
172+
173+
function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream)
174+
{
175+
$httpBody = 'body';
176+
177+
$request->getMethod()->willReturn('GET');
178+
$request->getUri()->willReturn('/');
179+
180+
$request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request);
181+
$request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request);
182+
183+
$response->getStatusCode()->willReturn(304);
184+
185+
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
186+
$item->isHit()->willReturn(true, false);
187+
$item->get()->willReturn([
188+
'response' => $response,
189+
'body' => $httpBody,
190+
'expiresAt' => 0,
191+
'createdAt' => 4711,
192+
'etag' => ['foo_etag']
193+
])->shouldBeCalled();
194+
195+
$next = function (RequestInterface $request) use ($response) {
196+
return new FulfilledPromise($response->getWrappedObject());
197+
};
198+
199+
$this->handleRequest($request, $next, function () {});
200+
}
201+
202+
function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory)
203+
{
204+
$httpBody = 'body';
205+
206+
$request->getMethod()->willReturn('GET');
207+
$request->getUri()->willReturn('/');
208+
209+
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
210+
$item->isHit()->willReturn(true);
211+
$item->get()->willReturn([
212+
'response' => $response,
213+
'body' => $httpBody,
214+
'expiresAt' => time()+1000000, //It is in the future
215+
'createdAt' => 4711,
216+
'etag' => []
217+
])->shouldBeCalled();
218+
219+
// Make sure we add back the body
220+
$response->withBody($stream)->willReturn($response)->shouldBeCalled();
221+
$streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream);
222+
223+
$next = function (RequestInterface $request) use ($response) {
224+
return new FulfilledPromise($response->getWrappedObject());
225+
};
226+
227+
$this->handleRequest($request, $next, function () {});
228+
}
229+
230+
function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory)
231+
{
232+
$httpBody = 'body';
233+
234+
$request->getMethod()->willReturn('GET');
235+
$request->getUri()->willReturn('/');
236+
237+
$request->withHeader(Argument::any(), Argument::any())->willReturn($request);
238+
$request->withHeader(Argument::any(), Argument::any())->willReturn($request);
239+
240+
$response->getStatusCode()->willReturn(304);
241+
$response->getHeader('Cache-Control')->willReturn(array());
242+
$response->getHeader('Expires')->willReturn(array())->shouldBeCalled();
243+
244+
// Make sure we add back the body
245+
$response->withBody($stream)->willReturn($response)->shouldBeCalled();
246+
247+
$pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item);
248+
$item->isHit()->willReturn(true, true);
249+
$item->expiresAfter(1060)->willReturn($item)->shouldBeCalled();
250+
$item->get()->willReturn([
251+
'response' => $response,
252+
'body' => $httpBody,
253+
'expiresAt' => 0,
254+
'createdAt' => 4711,
255+
'etag' => ['foo_etag']
256+
])->shouldBeCalled();
257+
258+
$item->set($this->getCacheItemMatcher([
259+
'response' => $response->getWrappedObject(),
260+
'body' => $httpBody,
261+
'expiresAt' => 0,
262+
'createdAt' => 0,
263+
'etag' => ['foo_etag']
264+
]))->willReturn($item)->shouldBeCalled();
265+
$pool->save(Argument::any())->shouldBeCalled();
266+
267+
$streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream);
268+
269+
$next = function (RequestInterface $request) use ($response) {
270+
return new FulfilledPromise($response->getWrappedObject());
271+
};
272+
273+
$this->handleRequest($request, $next, function () {});
274+
}
275+
276+
277+
/**
278+
* Private function to match cache item data.
279+
*
280+
* @param array $expectedData
281+
*
282+
* @return \Closure
283+
*/
284+
private function getCacheItemMatcher(array $expectedData)
285+
{
286+
return Argument::that(function(array $actualData) use ($expectedData) {
287+
foreach ($expectedData as $key => $value) {
288+
if (!isset($actualData[$key])) {
289+
return false;
290+
}
291+
292+
if ($key === 'expiresAt' || $key === 'createdAt') {
293+
// We do not need to validate the value of these fields.
294+
continue;
295+
}
296+
297+
if ($actualData[$key] !== $value) {
298+
return false;
299+
}
300+
}
301+
return true;
302+
});
303+
}
118304
}

0 commit comments

Comments
 (0)