1<?php
2namespace GuzzleHttp\Tests\Ring\Client;
3
4use GuzzleHttp\Ring\Client\ClientUtils;
5use GuzzleHttp\Ring\Core;
6use GuzzleHttp\Ring\Client\StreamHandler;
7
8class StreamHandlerTest extends \PHPUnit_Framework_TestCase
9{
10    public function testReturnsResponseForSuccessfulRequest()
11    {
12        $this->queueRes();
13        $handler = new StreamHandler();
14        $response = $handler([
15            'http_method' => 'GET',
16            'uri'         => '/',
17            'headers'     => [
18                'host' => [Server::$host],
19                'Foo' => ['Bar'],
20            ],
21        ]);
22
23        $this->assertEquals('1.1', $response['version']);
24        $this->assertEquals(200, $response['status']);
25        $this->assertEquals('OK', $response['reason']);
26        $this->assertEquals(['Bar'], $response['headers']['Foo']);
27        $this->assertEquals(['8'], $response['headers']['Content-Length']);
28        $this->assertEquals('hi there', Core::body($response));
29
30        $sent = Server::received()[0];
31        $this->assertEquals('GET', $sent['http_method']);
32        $this->assertEquals('/', $sent['resource']);
33        $this->assertEquals(['127.0.0.1:8125'], $sent['headers']['host']);
34        $this->assertEquals('Bar', Core::header($sent, 'foo'));
35    }
36
37    public function testAddsErrorToResponse()
38    {
39        $handler = new StreamHandler();
40        $result = $handler([
41            'http_method' => 'GET',
42            'headers'     => ['host' => ['localhost:123']],
43            'client'      => ['timeout' => 0.01],
44        ]);
45        $this->assertInstanceOf(
46            'GuzzleHttp\Ring\Future\CompletedFutureArray',
47            $result
48        );
49        $this->assertNull($result['status']);
50        $this->assertNull($result['body']);
51        $this->assertEquals([], $result['headers']);
52        $this->assertInstanceOf(
53            'GuzzleHttp\Ring\Exception\RingException',
54            $result['error']
55        );
56    }
57
58    public function testEnsuresTheHttpProtocol()
59    {
60        $handler = new StreamHandler();
61        $result = $handler([
62            'http_method' => 'GET',
63            'url'         => 'ftp://localhost:123',
64        ]);
65        $this->assertArrayHasKey('error', $result);
66        $this->assertContains(
67            'URL is invalid: ftp://localhost:123',
68            $result['error']->getMessage()
69        );
70    }
71
72    public function testStreamAttributeKeepsStreamOpen()
73    {
74        $this->queueRes();
75        $handler = new StreamHandler();
76        $response = $handler([
77            'http_method'  => 'PUT',
78            'uri'          => '/foo',
79            'query_string' => 'baz=bar',
80            'headers'      => [
81                'host' => [Server::$host],
82                'Foo'  => ['Bar'],
83            ],
84            'body'         => 'test',
85            'client'       => ['stream' => true],
86        ]);
87
88        $this->assertEquals(200, $response['status']);
89        $this->assertEquals('OK', $response['reason']);
90        $this->assertEquals('8', Core::header($response, 'Content-Length'));
91        $body = $response['body'];
92        $this->assertTrue(is_resource($body));
93        $this->assertEquals('http', stream_get_meta_data($body)['wrapper_type']);
94        $this->assertEquals('hi there', stream_get_contents($body));
95        fclose($body);
96        $sent = Server::received()[0];
97        $this->assertEquals('PUT', $sent['http_method']);
98        $this->assertEquals('/foo', $sent['uri']);
99        $this->assertEquals('baz=bar', $sent['query_string']);
100        $this->assertEquals('/foo?baz=bar', $sent['resource']);
101        $this->assertEquals('127.0.0.1:8125', Core::header($sent, 'host'));
102        $this->assertEquals('Bar', Core::header($sent, 'foo'));
103    }
104
105    public function testDrainsResponseIntoTempStream()
106    {
107        $this->queueRes();
108        $handler = new StreamHandler();
109        $response = $handler([
110            'http_method' => 'GET',
111            'uri'         => '/',
112            'headers'     => ['host' => [Server::$host]],
113        ]);
114        $body = $response['body'];
115        $this->assertEquals('php://temp', stream_get_meta_data($body)['uri']);
116        $this->assertEquals('hi', fread($body, 2));
117        fclose($body);
118    }
119
120    public function testDrainsResponseIntoSaveToBody()
121    {
122        $r = fopen('php://temp', 'r+');
123        $this->queueRes();
124        $handler = new StreamHandler();
125        $response = $handler([
126            'http_method' => 'GET',
127            'uri' => '/',
128            'headers' => ['host' => [Server::$host]],
129            'client' => ['save_to' => $r],
130        ]);
131        $body = $response['body'];
132        $this->assertEquals('php://temp', stream_get_meta_data($body)['uri']);
133        $this->assertEquals('hi', fread($body, 2));
134        $this->assertEquals(' there', stream_get_contents($r));
135        fclose($r);
136    }
137
138    public function testDrainsResponseIntoSaveToBodyAtPath()
139    {
140        $tmpfname = tempnam('/tmp', 'save_to_path');
141        $this->queueRes();
142        $handler = new StreamHandler();
143        $response = $handler([
144            'http_method' => 'GET',
145            'uri' => '/',
146            'headers' => ['host' => [Server::$host]],
147            'client' => ['save_to' => $tmpfname],
148        ]);
149        $body = $response['body'];
150        $this->assertInstanceOf('GuzzleHttp\Stream\StreamInterface', $body);
151        $this->assertEquals($tmpfname, $body->getMetadata('uri'));
152        $this->assertEquals('hi', $body->read(2));
153        $body->close();
154        unlink($tmpfname);
155    }
156
157    public function testAutomaticallyDecompressGzip()
158    {
159        Server::flush();
160        $content = gzencode('test');
161        Server::enqueue([
162            [
163                'status' => 200,
164                'reason' => 'OK',
165                'headers' => [
166                    'Content-Encoding' => ['gzip'],
167                    'Content-Length' => [strlen($content)],
168                ],
169                'body' => $content,
170            ],
171        ]);
172
173        $handler = new StreamHandler();
174        $response = $handler([
175            'http_method' => 'GET',
176            'headers'     => ['host' => [Server::$host]],
177            'uri'         => '/',
178            'client'      => ['decode_content' => true],
179        ]);
180        $this->assertEquals('test', Core::body($response));
181    }
182
183    public function testDoesNotForceGzipDecode()
184    {
185        Server::flush();
186        $content = gzencode('test');
187        Server::enqueue([
188            [
189                'status' => 200,
190                'reason' => 'OK',
191                'headers' => [
192                    'Content-Encoding' => ['gzip'],
193                    'Content-Length'   => [strlen($content)],
194                ],
195                'body' => $content,
196            ],
197        ]);
198
199        $handler = new StreamHandler();
200        $response = $handler([
201            'http_method' => 'GET',
202            'headers'     => ['host' => [Server::$host]],
203            'uri'         => '/',
204            'client'      => ['stream' => true, 'decode_content' => false],
205        ]);
206        $this->assertSame($content, Core::body($response));
207    }
208
209    public function testProtocolVersion()
210    {
211        $this->queueRes();
212        $handler = new StreamHandler();
213        $handler([
214            'http_method' => 'GET',
215            'uri'         => '/',
216            'headers'     => ['host' => [Server::$host]],
217            'version'     => 1.0,
218        ]);
219
220        $this->assertEquals(1.0, Server::received()[0]['version']);
221    }
222
223    protected function getSendResult(array $opts)
224    {
225        $this->queueRes();
226        $handler = new StreamHandler();
227        $opts['stream'] = true;
228        return $handler([
229            'http_method' => 'GET',
230            'uri'         => '/',
231            'headers'     => ['host' => [Server::$host]],
232            'client'      => $opts,
233        ]);
234    }
235
236    public function testAddsProxy()
237    {
238        $res = $this->getSendResult(['stream' => true, 'proxy' => '127.0.0.1:8125']);
239        $opts = stream_context_get_options($res['body']);
240        $this->assertEquals('127.0.0.1:8125', $opts['http']['proxy']);
241    }
242
243    public function testAddsTimeout()
244    {
245        $res = $this->getSendResult(['stream' => true, 'timeout' => 200]);
246        $opts = stream_context_get_options($res['body']);
247        $this->assertEquals(200, $opts['http']['timeout']);
248    }
249
250    public function testVerifiesVerifyIsValidIfPath()
251    {
252        $res = $this->getSendResult(['verify' => '/does/not/exist']);
253        $this->assertContains(
254            'SSL CA bundle not found: /does/not/exist',
255            (string) $res['error']
256        );
257    }
258
259    public function testVerifyCanBeDisabled()
260    {
261        $res = $this->getSendResult(['verify' => false]);
262        $this->assertArrayNotHasKey('error', $res);
263    }
264
265    public function testVerifiesCertIfValidPath()
266    {
267        $res = $this->getSendResult(['cert' => '/does/not/exist']);
268        $this->assertContains(
269            'SSL certificate not found: /does/not/exist',
270            (string) $res['error']
271        );
272    }
273
274    public function testVerifyCanBeSetToPath()
275    {
276        $path = $path = ClientUtils::getDefaultCaBundle();
277        $res = $this->getSendResult(['verify' => $path]);
278        $this->assertArrayNotHasKey('error', $res);
279        $opts = stream_context_get_options($res['body']);
280        $this->assertEquals(true, $opts['ssl']['verify_peer']);
281        $this->assertEquals($path, $opts['ssl']['cafile']);
282        $this->assertTrue(file_exists($opts['ssl']['cafile']));
283    }
284
285    public function testUsesSystemDefaultBundle()
286    {
287        $path = $path = ClientUtils::getDefaultCaBundle();
288        $res = $this->getSendResult(['verify' => true]);
289        $this->assertArrayNotHasKey('error', $res);
290        $opts = stream_context_get_options($res['body']);
291        if (PHP_VERSION_ID < 50600) {
292            $this->assertEquals($path, $opts['ssl']['cafile']);
293        }
294    }
295
296    public function testEnsuresVerifyOptionIsValid()
297    {
298        $res = $this->getSendResult(['verify' => 10]);
299        $this->assertContains(
300            'Invalid verify request option',
301            (string) $res['error']
302        );
303    }
304
305    public function testCanSetPasswordWhenSettingCert()
306    {
307        $path = __FILE__;
308        $res = $this->getSendResult(['cert' => [$path, 'foo']]);
309        $opts = stream_context_get_options($res['body']);
310        $this->assertEquals($path, $opts['ssl']['local_cert']);
311        $this->assertEquals('foo', $opts['ssl']['passphrase']);
312    }
313
314    public function testDebugAttributeWritesToStream()
315    {
316        $this->queueRes();
317        $f = fopen('php://temp', 'w+');
318        $this->getSendResult(['debug' => $f]);
319        fseek($f, 0);
320        $contents = stream_get_contents($f);
321        $this->assertContains('<GET http://127.0.0.1:8125/> [CONNECT]', $contents);
322        $this->assertContains('<GET http://127.0.0.1:8125/> [FILE_SIZE_IS]', $contents);
323        $this->assertContains('<GET http://127.0.0.1:8125/> [PROGRESS]', $contents);
324    }
325
326    public function testDebugAttributeWritesStreamInfoToBuffer()
327    {
328        $called = false;
329        $this->queueRes();
330        $buffer = fopen('php://temp', 'r+');
331        $this->getSendResult([
332            'progress' => function () use (&$called) { $called = true; },
333            'debug' => $buffer,
334        ]);
335        fseek($buffer, 0);
336        $contents = stream_get_contents($buffer);
337        $this->assertContains('<GET http://127.0.0.1:8125/> [CONNECT]', $contents);
338        $this->assertContains('<GET http://127.0.0.1:8125/> [FILE_SIZE_IS] message: "Content-Length: 8"', $contents);
339        $this->assertContains('<GET http://127.0.0.1:8125/> [PROGRESS] bytes_max: "8"', $contents);
340        $this->assertTrue($called);
341    }
342
343    public function testEmitsProgressInformation()
344    {
345        $called = [];
346        $this->queueRes();
347        $this->getSendResult([
348            'progress' => function () use (&$called) {
349                $called[] = func_get_args();
350            },
351        ]);
352        $this->assertNotEmpty($called);
353        $this->assertEquals(8, $called[0][0]);
354        $this->assertEquals(0, $called[0][1]);
355    }
356
357    public function testEmitsProgressInformationAndDebugInformation()
358    {
359        $called = [];
360        $this->queueRes();
361        $buffer = fopen('php://memory', 'w+');
362        $this->getSendResult([
363            'debug'    => $buffer,
364            'progress' => function () use (&$called) {
365                $called[] = func_get_args();
366            },
367        ]);
368        $this->assertNotEmpty($called);
369        $this->assertEquals(8, $called[0][0]);
370        $this->assertEquals(0, $called[0][1]);
371        rewind($buffer);
372        $this->assertNotEmpty(stream_get_contents($buffer));
373        fclose($buffer);
374    }
375
376    public function testAddsProxyByProtocol()
377    {
378        $url = str_replace('http', 'tcp', Server::$url);
379        $res = $this->getSendResult(['proxy' => ['http' => $url]]);
380        $opts = stream_context_get_options($res['body']);
381        $this->assertEquals($url, $opts['http']['proxy']);
382    }
383
384    public function testPerformsShallowMergeOfCustomContextOptions()
385    {
386        $res = $this->getSendResult([
387            'stream_context' => [
388                'http' => [
389                    'request_fulluri' => true,
390                    'method' => 'HEAD',
391                ],
392                'socket' => [
393                    'bindto' => '127.0.0.1:0',
394                ],
395                'ssl' => [
396                    'verify_peer' => false,
397                ],
398            ],
399        ]);
400
401        $opts = stream_context_get_options($res['body']);
402        $this->assertEquals('HEAD', $opts['http']['method']);
403        $this->assertTrue($opts['http']['request_fulluri']);
404        $this->assertFalse($opts['ssl']['verify_peer']);
405        $this->assertEquals('127.0.0.1:0', $opts['socket']['bindto']);
406    }
407
408    public function testEnsuresThatStreamContextIsAnArray()
409    {
410        $res = $this->getSendResult(['stream_context' => 'foo']);
411        $this->assertContains(
412            'stream_context must be an array',
413            (string) $res['error']
414        );
415    }
416
417    public function testDoesNotAddContentTypeByDefault()
418    {
419        $this->queueRes();
420        $handler = new StreamHandler();
421        $handler([
422            'http_method' => 'PUT',
423            'uri' => '/',
424            'headers' => ['host' => [Server::$host], 'content-length' => [3]],
425            'body' => 'foo',
426        ]);
427        $req = Server::received()[0];
428        $this->assertEquals('', Core::header($req, 'Content-Type'));
429        $this->assertEquals(3, Core::header($req, 'Content-Length'));
430    }
431
432    private function queueRes()
433    {
434        Server::flush();
435        Server::enqueue([
436            [
437                'status' => 200,
438                'reason' => 'OK',
439                'headers' => [
440                    'Foo' => ['Bar'],
441                    'Content-Length' => [8],
442                ],
443                'body' => 'hi there',
444            ],
445        ]);
446    }
447
448    public function testSupports100Continue()
449    {
450        Server::flush();
451        Server::enqueue([
452            [
453                'status' => '200',
454                'reason' => 'OK',
455                'headers' => [
456                    'Test' => ['Hello'],
457                    'Content-Length' => ['4'],
458                ],
459                'body' => 'test',
460            ],
461        ]);
462
463        $request = [
464            'http_method' => 'PUT',
465            'headers'     => [
466                'Host'   => [Server::$host],
467                'Expect' => ['100-Continue'],
468            ],
469            'body'        => 'test',
470        ];
471
472        $handler = new StreamHandler();
473        $response = $handler($request);
474        $this->assertEquals(200, $response['status']);
475        $this->assertEquals('OK', $response['reason']);
476        $this->assertEquals(['Hello'], $response['headers']['Test']);
477        $this->assertEquals(['4'], $response['headers']['Content-Length']);
478        $this->assertEquals('test', Core::body($response));
479    }
480}
481