ClientTest.php 20 KB

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