1<?php
2
3namespace Sabre\HTTP;
4
5class ClientTest extends \PHPUnit_Framework_TestCase {
6
7    protected $client;
8
9    function testCreateCurlSettingsArrayGET() {
10
11        $client = new ClientMock();
12        $client->addCurlSetting(CURLOPT_POSTREDIR, 0);
13
14        $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']);
15
16        $settings = [
17                CURLOPT_RETURNTRANSFER => true,
18                CURLOPT_HEADER         => true,
19                CURLOPT_POSTREDIR      => 0,
20                CURLOPT_HTTPHEADER     => ['X-Foo: bar'],
21                CURLOPT_NOBODY         => false,
22                CURLOPT_URL            => 'http://example.org/',
23                CURLOPT_CUSTOMREQUEST  => 'GET',
24                CURLOPT_POSTFIELDS     => null,
25                CURLOPT_PUT            => false,
26            ];
27
28        // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
29        // at least if this unit test fails in the future we know it is :)
30        if (defined('HHVM_VERSION') === false) {
31            $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
32            $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
33        }
34
35
36        $this->assertEquals($settings, $client->createCurlSettingsArray($request));
37
38    }
39
40    function testCreateCurlSettingsArrayHEAD() {
41
42        $client = new ClientMock();
43        $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']);
44
45
46        $settings = [
47                CURLOPT_RETURNTRANSFER => true,
48                CURLOPT_HEADER         => true,
49                CURLOPT_NOBODY         => true,
50                CURLOPT_CUSTOMREQUEST  => 'HEAD',
51                CURLOPT_HTTPHEADER     => ['X-Foo: bar'],
52                CURLOPT_URL            => 'http://example.org/',
53                CURLOPT_POSTFIELDS     => '',
54                CURLOPT_PUT            => false,
55            ];
56
57        // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
58        // at least if this unit test fails in the future we know it is :)
59        if (defined('HHVM_VERSION') === false) {
60            $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
61            $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
62        }
63
64        $this->assertEquals($settings, $client->createCurlSettingsArray($request));
65
66    }
67
68    function testCreateCurlSettingsArrayGETAfterHEAD() {
69
70        $client = new ClientMock();
71        $request = new Request('HEAD', 'http://example.org/', ['X-Foo' => 'bar']);
72
73        // Parsing the settings for this method, and discarding the result.
74        // This will cause the client to automatically persist previous
75        // settings and will help us detect problems.
76        $client->createCurlSettingsArray($request);
77
78        // This is the real request.
79        $request = new Request('GET', 'http://example.org/', ['X-Foo' => 'bar']);
80
81        $settings = [
82                CURLOPT_CUSTOMREQUEST  => 'GET',
83                CURLOPT_RETURNTRANSFER => true,
84                CURLOPT_HEADER         => true,
85                CURLOPT_HTTPHEADER     => ['X-Foo: bar'],
86                CURLOPT_NOBODY         => false,
87                CURLOPT_URL            => 'http://example.org/',
88                CURLOPT_POSTFIELDS     => '',
89                CURLOPT_PUT            => false,
90            ];
91
92        // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
93        // at least if this unit test fails in the future we know it is :)
94        if (defined('HHVM_VERSION') === false) {
95            $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
96            $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
97        }
98
99        $this->assertEquals($settings, $client->createCurlSettingsArray($request));
100
101    }
102
103    function testCreateCurlSettingsArrayPUTStream() {
104
105        $client = new ClientMock();
106
107        $h = fopen('php://memory', 'r+');
108        fwrite($h, 'booh');
109        $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], $h);
110
111        $settings = [
112                CURLOPT_RETURNTRANSFER => true,
113                CURLOPT_HEADER         => true,
114                CURLOPT_PUT            => true,
115                CURLOPT_INFILE         => $h,
116                CURLOPT_NOBODY         => false,
117                CURLOPT_CUSTOMREQUEST  => 'PUT',
118                CURLOPT_HTTPHEADER     => ['X-Foo: bar'],
119                CURLOPT_URL            => 'http://example.org/',
120            ];
121
122        // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
123        // at least if this unit test fails in the future we know it is :)
124        if (defined('HHVM_VERSION') === false) {
125            $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
126            $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
127        }
128
129        $this->assertEquals($settings, $client->createCurlSettingsArray($request));
130
131    }
132
133    function testCreateCurlSettingsArrayPUTString() {
134
135        $client = new ClientMock();
136        $request = new Request('PUT', 'http://example.org/', ['X-Foo' => 'bar'], 'boo');
137
138        $settings = [
139                CURLOPT_RETURNTRANSFER => true,
140                CURLOPT_HEADER         => true,
141                CURLOPT_NOBODY         => false,
142                CURLOPT_POSTFIELDS     => 'boo',
143                CURLOPT_CUSTOMREQUEST  => 'PUT',
144                CURLOPT_HTTPHEADER     => ['X-Foo: bar'],
145                CURLOPT_URL            => 'http://example.org/',
146            ];
147
148        // FIXME: CURLOPT_PROTOCOLS and CURLOPT_REDIR_PROTOCOLS are currently unsupported by HHVM
149        // at least if this unit test fails in the future we know it is :)
150        if (defined('HHVM_VERSION') === false) {
151            $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
152            $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
153        }
154
155        $this->assertEquals($settings, $client->createCurlSettingsArray($request));
156
157    }
158
159    function testSend() {
160
161        $client = new ClientMock();
162        $request = new Request('GET', 'http://example.org/');
163
164        $client->on('doRequest', function($request, &$response) {
165            $response = new Response(200);
166        });
167
168        $response = $client->send($request);
169
170        $this->assertEquals(200, $response->getStatus());
171
172    }
173
174    function testSendClientError() {
175
176        $client = new ClientMock();
177        $request = new Request('GET', 'http://example.org/');
178
179        $client->on('doRequest', function($request, &$response) {
180            throw new ClientException('aaah', 1);
181        });
182        $called = false;
183        $client->on('exception', function() use (&$called) {
184            $called = true;
185        });
186
187        try {
188            $client->send($request);
189            $this->fail('send() should have thrown an exception');
190        } catch (ClientException $e) {
191
192        }
193        $this->assertTrue($called);
194
195    }
196
197    function testSendHttpError() {
198
199        $client = new ClientMock();
200        $request = new Request('GET', 'http://example.org/');
201
202        $client->on('doRequest', function($request, &$response) {
203            $response = new Response(404);
204        });
205        $called = 0;
206        $client->on('error', function() use (&$called) {
207            $called++;
208        });
209        $client->on('error:404', function() use (&$called) {
210            $called++;
211        });
212
213        $client->send($request);
214        $this->assertEquals(2, $called);
215
216    }
217
218    function testSendRetry() {
219
220        $client = new ClientMock();
221        $request = new Request('GET', 'http://example.org/');
222
223        $called = 0;
224        $client->on('doRequest', function($request, &$response) use (&$called) {
225            $called++;
226            if ($called < 3) {
227                $response = new Response(404);
228            } else {
229                $response = new Response(200);
230            }
231        });
232
233        $errorCalled = 0;
234        $client->on('error', function($request, $response, &$retry, $retryCount) use (&$errorCalled) {
235
236            $errorCalled++;
237            $retry = true;
238
239        });
240
241        $response = $client->send($request);
242        $this->assertEquals(3, $called);
243        $this->assertEquals(2, $errorCalled);
244        $this->assertEquals(200, $response->getStatus());
245
246    }
247
248    function testHttpErrorException() {
249
250        $client = new ClientMock();
251        $client->setThrowExceptions(true);
252        $request = new Request('GET', 'http://example.org/');
253
254        $client->on('doRequest', function($request, &$response) {
255            $response = new Response(404);
256        });
257
258        try {
259            $client->send($request);
260            $this->fail('An exception should have been thrown');
261        } catch (ClientHttpException $e) {
262            $this->assertEquals(404, $e->getHttpStatus());
263            $this->assertInstanceOf('Sabre\HTTP\Response', $e->getResponse());
264        }
265
266    }
267
268    function testParseCurlResult() {
269
270        $client = new ClientMock();
271        $client->on('curlStuff', function(&$return) {
272
273            $return = [
274                [
275                    'header_size' => 33,
276                    'http_code'   => 200,
277                ],
278                0,
279                '',
280            ];
281
282        });
283
284        $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo";
285        $result = $client->parseCurlResult($body, 'foobar');
286
287        $this->assertEquals(Client::STATUS_SUCCESS, $result['status']);
288        $this->assertEquals(200, $result['http_code']);
289        $this->assertEquals(200, $result['response']->getStatus());
290        $this->assertEquals(['Header1' => ['Val1']], $result['response']->getHeaders());
291        $this->assertEquals('Foo', $result['response']->getBodyAsString());
292
293    }
294
295    function testParseCurlError() {
296
297        $client = new ClientMock();
298        $client->on('curlStuff', function(&$return) {
299
300            $return = [
301                [],
302                1,
303                'Curl error',
304            ];
305
306        });
307
308        $body = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo";
309        $result = $client->parseCurlResult($body, 'foobar');
310
311        $this->assertEquals(Client::STATUS_CURLERROR, $result['status']);
312        $this->assertEquals(1, $result['curl_errno']);
313        $this->assertEquals('Curl error', $result['curl_errmsg']);
314
315    }
316
317    function testDoRequest() {
318
319        $client = new ClientMock();
320        $request = new Request('GET', 'http://example.org/');
321        $client->on('curlExec', function(&$return) {
322
323            $return = "HTTP/1.1 200 OK\r\nHeader1:Val1\r\n\r\nFoo";
324
325        });
326        $client->on('curlStuff', function(&$return) {
327
328            $return = [
329                [
330                    'header_size' => 33,
331                    'http_code'   => 200,
332                ],
333                0,
334                '',
335            ];
336
337        });
338        $response = $client->doRequest($request);
339        $this->assertEquals(200, $response->getStatus());
340        $this->assertEquals(['Header1' => ['Val1']], $response->getHeaders());
341        $this->assertEquals('Foo', $response->getBodyAsString());
342
343    }
344
345    function testDoRequestCurlError() {
346
347        $client = new ClientMock();
348        $request = new Request('GET', 'http://example.org/');
349        $client->on('curlExec', function(&$return) {
350
351            $return = "";
352
353        });
354        $client->on('curlStuff', function(&$return) {
355
356            $return = [
357                [],
358                1,
359                'Curl error',
360            ];
361
362        });
363
364        try {
365            $response = $client->doRequest($request);
366            $this->fail('This should have thrown an exception');
367        } catch (ClientException $e) {
368            $this->assertEquals(1, $e->getCode());
369            $this->assertEquals('Curl error', $e->getMessage());
370        }
371
372    }
373
374}
375
376class ClientMock extends Client {
377
378    protected $persistedSettings = [];
379
380    /**
381     * Making this method public.
382     *
383     * We are also going to persist all settings this method generates. While
384     * the underlying object doesn't behave exactly the same, it helps us
385     * simulate what curl does internally, and helps us identify problems with
386     * settings that are set by _some_ methods and not correctly reset by other
387     * methods after subsequent use.
388     * forces
389     */
390    function createCurlSettingsArray(RequestInterface $request) {
391
392        $settings = parent::createCurlSettingsArray($request);
393        $settings = $settings + $this->persistedSettings;
394        $this->persistedSettings = $settings;
395        return $settings;
396
397    }
398    /**
399     * Making this method public.
400     */
401    function parseCurlResult($response, $curlHandle) {
402
403        return parent::parseCurlResult($response, $curlHandle);
404
405    }
406
407    /**
408     * This method is responsible for performing a single request.
409     *
410     * @param RequestInterface $request
411     * @return ResponseInterface
412     */
413    function doRequest(RequestInterface $request) {
414
415        $response = null;
416        $this->emit('doRequest', [$request, &$response]);
417
418        // If nothing modified $response, we're using the default behavior.
419        if (is_null($response)) {
420            return parent::doRequest($request);
421        } else {
422            return $response;
423        }
424
425    }
426
427    /**
428     * Returns a bunch of information about a curl request.
429     *
430     * This method exists so it can easily be overridden and mocked.
431     *
432     * @param resource $curlHandle
433     * @return array
434     */
435    protected function curlStuff($curlHandle) {
436
437        $return = null;
438        $this->emit('curlStuff', [&$return]);
439
440        // If nothing modified $return, we're using the default behavior.
441        if (is_null($return)) {
442            return parent::curlStuff($curlHandle);
443        } else {
444            return $return;
445        }
446
447    }
448
449    /**
450     * Calls curl_exec
451     *
452     * This method exists so it can easily be overridden and mocked.
453     *
454     * @param resource $curlHandle
455     * @return string
456     */
457    protected function curlExec($curlHandle) {
458
459        $return = null;
460        $this->emit('curlExec', [&$return]);
461
462        // If nothing modified $return, we're using the default behavior.
463        if (is_null($return)) {
464            return parent::curlExec($curlHandle);
465        } else {
466            return $return;
467        }
468
469    }
470
471}
472