ClientTest.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Http;
  16. use Cake\Core\Exception\CakeException;
  17. use Cake\Http\Client;
  18. use Cake\Http\Client\Adapter\Stream;
  19. use Cake\Http\Client\Exception\MissingResponseException;
  20. use Cake\Http\Client\Request;
  21. use Cake\Http\Client\Response;
  22. use Cake\Http\Cookie\Cookie;
  23. use Cake\Http\Cookie\CookieCollection;
  24. use Cake\TestSuite\TestCase;
  25. use InvalidArgumentException;
  26. use Laminas\Diactoros\Request as LaminasRequest;
  27. /**
  28. * HTTP client test.
  29. */
  30. class ClientTest extends TestCase
  31. {
  32. public function tearDown(): void
  33. {
  34. parent::tearDown();
  35. Client::clearMockResponses();
  36. }
  37. /**
  38. * Test storing config options and modifying them.
  39. */
  40. public function testConstructConfig(): void
  41. {
  42. $config = [
  43. 'scheme' => 'http',
  44. 'host' => 'example.org',
  45. 'basePath' => '/api/v1',
  46. ];
  47. $http = new Client($config);
  48. $result = $http->getConfig();
  49. foreach ($config as $key => $val) {
  50. $this->assertEquals($val, $result[$key]);
  51. }
  52. $result = $http->setConfig([
  53. 'auth' => ['username' => 'mark', 'password' => 'secret'],
  54. ]);
  55. $this->assertSame($result, $http);
  56. $result = $http->getConfig();
  57. $expected = [
  58. 'scheme' => 'http',
  59. 'host' => 'example.org',
  60. 'auth' => ['username' => 'mark', 'password' => 'secret'],
  61. 'protocolVersion' => '1.1',
  62. ];
  63. foreach ($expected as $key => $val) {
  64. $this->assertEquals($val, $result[$key]);
  65. }
  66. }
  67. /**
  68. * Data provider for buildUrl() tests
  69. *
  70. * @return array
  71. */
  72. public static function urlProvider(): array
  73. {
  74. return [
  75. [
  76. 'http://example.com/test.html',
  77. 'http://example.com/test.html',
  78. [],
  79. null,
  80. 'Null options',
  81. ],
  82. [
  83. 'http://example.com/test.html',
  84. 'http://example.com/test.html',
  85. [],
  86. [],
  87. 'Simple string',
  88. ],
  89. [
  90. 'http://example.com/test.html',
  91. '/test.html',
  92. [],
  93. ['host' => 'example.com'],
  94. 'host name option',
  95. ],
  96. [
  97. 'https://example.com/test.html',
  98. '/test.html',
  99. [],
  100. ['host' => 'example.com', 'scheme' => 'https'],
  101. 'HTTPS',
  102. ],
  103. [
  104. 'https://example.com/api/v1/foo/test.html',
  105. '/foo/test.html',
  106. [],
  107. ['host' => 'example.com', 'scheme' => 'https', 'basePath' => '/api/v1'],
  108. 'Base path included',
  109. ],
  110. [
  111. 'https://example.com/api/v1/foo/test.html',
  112. '/foo/test.html',
  113. [],
  114. ['host' => 'example.com', 'scheme' => 'https', 'basePath' => '/api/v1/'],
  115. 'Base path with trailing forward slash',
  116. ],
  117. [
  118. 'https://example.com/api/v1/foo/test.html',
  119. '/foo/test.html',
  120. [],
  121. ['host' => 'example.com', 'scheme' => 'https', 'basePath' => 'api/v1/'],
  122. 'Base path with no prepended forward slash',
  123. ],
  124. [
  125. 'http://example.com:8080/test.html',
  126. '/test.html',
  127. [],
  128. ['host' => 'example.com', 'port' => '8080'],
  129. 'Non standard port',
  130. ],
  131. [
  132. 'http://example.com/test.html',
  133. '/test.html',
  134. [],
  135. ['host' => 'example.com', 'port' => '80'],
  136. 'standard port, does not display',
  137. ],
  138. [
  139. 'https://example.com/test.html',
  140. '/test.html',
  141. [],
  142. ['host' => 'example.com', 'scheme' => 'https', 'port' => '443'],
  143. 'standard port, does not display',
  144. ],
  145. [
  146. 'http://example.com/test.html',
  147. 'http://example.com/test.html',
  148. [],
  149. ['host' => 'example.com', 'scheme' => 'https'],
  150. 'options do not duplicate',
  151. ],
  152. [
  153. 'http://example.com/search?q=hi%20there&cat%5Bid%5D%5B0%5D=2&cat%5Bid%5D%5B1%5D=3',
  154. 'http://example.com/search',
  155. ['q' => 'hi there', 'cat' => ['id' => [2, 3]]],
  156. [],
  157. 'query string data.',
  158. ],
  159. [
  160. 'http://example.com/search?q=hi+there&id=12',
  161. 'http://example.com/search?q=hi+there',
  162. ['id' => '12'],
  163. [],
  164. 'query string data with some already on the url.',
  165. ],
  166. [
  167. 'http://example.com/test.html',
  168. '//test.html',
  169. [],
  170. [
  171. 'scheme' => 'http',
  172. 'host' => 'example.com',
  173. 'protocolRelative' => false,
  174. ],
  175. 'url with a double slash',
  176. ],
  177. [
  178. 'http://example.com/test.html',
  179. '//example.com/test.html',
  180. [],
  181. [
  182. 'scheme' => 'http',
  183. 'protocolRelative' => true,
  184. ],
  185. 'protocol relative url',
  186. ],
  187. [
  188. 'https://example.com/operations?%24filter=operation_id%20eq%2012',
  189. 'https://example.com/operations',
  190. ['$filter' => 'operation_id eq 12'],
  191. [],
  192. 'check the RFC 3986 query encoding',
  193. ],
  194. ];
  195. }
  196. /**
  197. * @dataProvider urlProvider
  198. */
  199. public function testBuildUrl(string $expected, string $url, array $query, ?array $opts): void
  200. {
  201. $http = new Client();
  202. $result = $http->buildUrl($url, $query, (array)$opts);
  203. $this->assertEquals($expected, $result);
  204. }
  205. /**
  206. * test simple get request with headers & cookies.
  207. */
  208. public function testGetSimpleWithHeadersAndCookies(): void
  209. {
  210. $response = new Response();
  211. $headers = [
  212. 'User-Agent' => 'Cake',
  213. 'Connection' => 'close',
  214. 'Content-Type' => 'application/x-www-form-urlencoded',
  215. ];
  216. $cookies = [
  217. 'split' => 'value',
  218. ];
  219. $mock = $this->getMockBuilder(Stream::class)
  220. ->onlyMethods(['send'])
  221. ->getMock();
  222. $mock->expects($this->once())
  223. ->method('send')
  224. ->with($this->callback(function ($request) use ($headers) {
  225. $this->assertInstanceOf('Cake\Http\Client\Request', $request);
  226. $this->assertSame(Request::METHOD_GET, $request->getMethod());
  227. $this->assertSame('2', $request->getProtocolVersion());
  228. $this->assertSame('http://cakephp.org/test.html', $request->getUri() . '');
  229. $this->assertSame('split=value', $request->getHeaderLine('Cookie'));
  230. $this->assertSame($headers['Content-Type'], $request->getHeaderLine('content-type'));
  231. $this->assertSame($headers['Connection'], $request->getHeaderLine('connection'));
  232. return true;
  233. }))
  234. ->will($this->returnValue([$response]));
  235. $http = new Client(['adapter' => $mock, 'protocolVersion' => '2']);
  236. $result = $http->get('http://cakephp.org/test.html', [], [
  237. 'headers' => $headers,
  238. 'cookies' => $cookies,
  239. ]);
  240. $this->assertSame($result, $response);
  241. }
  242. /**
  243. * test get request with no data
  244. */
  245. public function testGetNoData(): void
  246. {
  247. $response = new Response();
  248. $mock = $this->getMockBuilder(Stream::class)
  249. ->onlyMethods(['send'])
  250. ->getMock();
  251. $mock->expects($this->once())
  252. ->method('send')
  253. ->with($this->callback(function ($request) {
  254. $this->assertSame(Request::METHOD_GET, $request->getMethod());
  255. $this->assertEmpty($request->getHeaderLine('Content-Type'), 'Should have no content-type set');
  256. $this->assertSame(
  257. 'http://cakephp.org/search',
  258. $request->getUri() . ''
  259. );
  260. return true;
  261. }))
  262. ->will($this->returnValue([$response]));
  263. $http = new Client([
  264. 'host' => 'cakephp.org',
  265. 'adapter' => $mock,
  266. ]);
  267. $result = $http->get('/search');
  268. $this->assertSame($result, $response);
  269. }
  270. /**
  271. * test get request with querystring data
  272. */
  273. public function testGetQuerystring(): void
  274. {
  275. $response = new Response();
  276. $mock = $this->getMockBuilder(Stream::class)
  277. ->onlyMethods(['send'])
  278. ->getMock();
  279. $mock->expects($this->once())
  280. ->method('send')
  281. ->with($this->callback(function ($request) {
  282. $this->assertSame(Request::METHOD_GET, $request->getMethod());
  283. $this->assertSame(
  284. 'http://cakephp.org/search?q=hi%20there&Category%5Bid%5D%5B0%5D=2&Category%5Bid%5D%5B1%5D=3',
  285. $request->getUri() . ''
  286. );
  287. return true;
  288. }))
  289. ->will($this->returnValue([$response]));
  290. $http = new Client([
  291. 'host' => 'cakephp.org',
  292. 'adapter' => $mock,
  293. ]);
  294. $result = $http->get('/search', [
  295. 'q' => 'hi there',
  296. 'Category' => ['id' => [2, 3]],
  297. ]);
  298. $this->assertSame($result, $response);
  299. }
  300. /**
  301. * test get request with string of query data.
  302. */
  303. public function testGetQuerystringString(): void
  304. {
  305. $response = new Response();
  306. $mock = $this->getMockBuilder(Stream::class)
  307. ->onlyMethods(['send'])
  308. ->getMock();
  309. $mock->expects($this->once())
  310. ->method('send')
  311. ->with($this->callback(function ($request) {
  312. $this->assertSame(
  313. 'http://cakephp.org/search?q=hi+there&Category%5Bid%5D%5B0%5D=2&Category%5Bid%5D%5B1%5D=3',
  314. $request->getUri() . ''
  315. );
  316. return true;
  317. }))
  318. ->will($this->returnValue([$response]));
  319. $http = new Client([
  320. 'host' => 'cakephp.org',
  321. 'adapter' => $mock,
  322. ]);
  323. $data = [
  324. 'q' => 'hi there',
  325. 'Category' => ['id' => [2, 3]],
  326. ];
  327. $result = $http->get('/search', http_build_query($data));
  328. $this->assertSame($response, $result);
  329. }
  330. /**
  331. * Test a GET with a request body. Services like
  332. * elasticsearch use this feature.
  333. */
  334. public function testGetWithContent(): void
  335. {
  336. $response = new Response();
  337. $mock = $this->getMockBuilder(Stream::class)
  338. ->onlyMethods(['send'])
  339. ->getMock();
  340. $mock->expects($this->once())
  341. ->method('send')
  342. ->with($this->callback(function ($request) {
  343. $this->assertSame(Request::METHOD_GET, $request->getMethod());
  344. $this->assertSame('http://cakephp.org/search', '' . $request->getUri());
  345. $this->assertSame('some data', '' . $request->getBody());
  346. return true;
  347. }))
  348. ->will($this->returnValue([$response]));
  349. $http = new Client([
  350. 'host' => 'cakephp.org',
  351. 'adapter' => $mock,
  352. ]);
  353. $result = $http->get('/search', [
  354. '_content' => 'some data',
  355. ]);
  356. $this->assertSame($result, $response);
  357. }
  358. /**
  359. * Test invalid authentication types throw exceptions.
  360. */
  361. public function testInvalidAuthenticationType(): void
  362. {
  363. $this->expectException(CakeException::class);
  364. $mock = $this->getMockBuilder(Stream::class)
  365. ->onlyMethods(['send'])
  366. ->getMock();
  367. $mock->expects($this->never())
  368. ->method('send');
  369. $http = new Client([
  370. 'host' => 'cakephp.org',
  371. 'adapter' => $mock,
  372. ]);
  373. $http->get('/', [], [
  374. 'auth' => ['type' => 'horribly broken'],
  375. ]);
  376. }
  377. /**
  378. * Test setting basic authentication with get
  379. */
  380. public function testGetWithAuthenticationAndProxy(): void
  381. {
  382. $response = new Response();
  383. $mock = $this->getMockBuilder(Stream::class)
  384. ->onlyMethods(['send'])
  385. ->getMock();
  386. $headers = [
  387. 'Authorization' => 'Basic ' . base64_encode('mark:secret'),
  388. 'Proxy-Authorization' => 'Basic ' . base64_encode('mark:pass'),
  389. ];
  390. $mock->expects($this->once())
  391. ->method('send')
  392. ->with($this->callback(function ($request) use ($headers) {
  393. $this->assertSame(Request::METHOD_GET, $request->getMethod());
  394. $this->assertSame('http://cakephp.org/', '' . $request->getUri());
  395. $this->assertSame($headers['Authorization'], $request->getHeaderLine('Authorization'));
  396. $this->assertSame($headers['Proxy-Authorization'], $request->getHeaderLine('Proxy-Authorization'));
  397. return true;
  398. }))
  399. ->will($this->returnValue([$response]));
  400. $http = new Client([
  401. 'host' => 'cakephp.org',
  402. 'adapter' => $mock,
  403. ]);
  404. $result = $http->get('/', [], [
  405. 'auth' => ['username' => 'mark', 'password' => 'secret'],
  406. 'proxy' => ['username' => 'mark', 'password' => 'pass'],
  407. ]);
  408. $this->assertSame($result, $response);
  409. }
  410. /**
  411. * Return a list of HTTP methods.
  412. *
  413. * @return array
  414. */
  415. public static function methodProvider(): array
  416. {
  417. return [
  418. [Request::METHOD_GET],
  419. [Request::METHOD_POST],
  420. [Request::METHOD_PUT],
  421. [Request::METHOD_DELETE],
  422. [Request::METHOD_PATCH],
  423. [Request::METHOD_OPTIONS],
  424. [Request::METHOD_TRACE],
  425. ];
  426. }
  427. /**
  428. * test simple POST request.
  429. *
  430. * @dataProvider methodProvider
  431. */
  432. public function testMethodsSimple(string $method): void
  433. {
  434. $response = new Response();
  435. $mock = $this->getMockBuilder(Stream::class)
  436. ->onlyMethods(['send'])
  437. ->getMock();
  438. $mock->expects($this->once())
  439. ->method('send')
  440. ->with($this->callback(function ($request) use ($method) {
  441. $this->assertInstanceOf('Cake\Http\Client\Request', $request);
  442. $this->assertEquals($method, $request->getMethod());
  443. $this->assertSame('http://cakephp.org/projects/add', '' . $request->getUri());
  444. return true;
  445. }))
  446. ->will($this->returnValue([$response]));
  447. $http = new Client([
  448. 'host' => 'cakephp.org',
  449. 'adapter' => $mock,
  450. ]);
  451. $result = $http->{$method}('/projects/add');
  452. $this->assertSame($result, $response);
  453. }
  454. /**
  455. * Provider for testing the type option.
  456. *
  457. * @return array
  458. */
  459. public static function typeProvider(): array
  460. {
  461. return [
  462. ['application/json', 'application/json'],
  463. ['json', 'application/json'],
  464. ['xml', 'application/xml'],
  465. ['application/xml', 'application/xml'],
  466. ];
  467. }
  468. /**
  469. * Test that using the 'type' option sets the correct headers
  470. *
  471. * @dataProvider typeProvider
  472. */
  473. public function testPostWithTypeKey(string $type, string $mime): void
  474. {
  475. $response = new Response();
  476. $data = 'some data';
  477. $headers = [
  478. 'Content-Type' => $mime,
  479. 'Accept' => $mime,
  480. ];
  481. $mock = $this->getMockBuilder(Stream::class)
  482. ->onlyMethods(['send'])
  483. ->getMock();
  484. $mock->expects($this->once())
  485. ->method('send')
  486. ->with($this->callback(function ($request) use ($headers) {
  487. $this->assertSame(Request::METHOD_POST, $request->getMethod());
  488. $this->assertEquals($headers['Content-Type'], $request->getHeaderLine('Content-Type'));
  489. $this->assertEquals($headers['Accept'], $request->getHeaderLine('Accept'));
  490. return true;
  491. }))
  492. ->will($this->returnValue([$response]));
  493. $http = new Client([
  494. 'host' => 'cakephp.org',
  495. 'adapter' => $mock,
  496. ]);
  497. $http->post('/projects/add', $data, ['type' => $type]);
  498. }
  499. /**
  500. * Test that string payloads with no content type have a default content-type set.
  501. */
  502. public function testPostWithStringDataDefaultsToFormEncoding(): void
  503. {
  504. $response = new Response();
  505. $data = 'some=value&more=data';
  506. $mock = $this->getMockBuilder(Stream::class)
  507. ->onlyMethods(['send'])
  508. ->getMock();
  509. $mock->expects($this->any())
  510. ->method('send')
  511. ->with($this->callback(function ($request) use ($data) {
  512. $this->assertSame($data, '' . $request->getBody());
  513. $this->assertSame('application/x-www-form-urlencoded', $request->getHeaderLine('content-type'));
  514. return true;
  515. }))
  516. ->will($this->returnValue([$response]));
  517. $http = new Client([
  518. 'host' => 'cakephp.org',
  519. 'adapter' => $mock,
  520. ]);
  521. $http->post('/projects/add', $data);
  522. $http->put('/projects/add', $data);
  523. $http->delete('/projects/add', $data);
  524. }
  525. /**
  526. * Test that exceptions are raised on invalid types.
  527. */
  528. public function testExceptionOnUnknownType(): void
  529. {
  530. $this->expectException(CakeException::class);
  531. $mock = $this->getMockBuilder(Stream::class)
  532. ->onlyMethods(['send'])
  533. ->getMock();
  534. $mock->expects($this->never())
  535. ->method('send');
  536. $http = new Client([
  537. 'host' => 'cakephp.org',
  538. 'adapter' => $mock,
  539. ]);
  540. $http->post('/projects/add', 'it works', ['type' => 'invalid']);
  541. }
  542. /**
  543. * Test that Client stores cookies
  544. */
  545. public function testCookieStorage(): void
  546. {
  547. $adapter = $this->getMockBuilder(Stream::class)
  548. ->onlyMethods(['send'])
  549. ->getMock();
  550. $headers = [
  551. 'HTTP/1.0 200 Ok',
  552. 'Set-Cookie: first=1',
  553. 'Set-Cookie: expiring=now; Expires=Wed, 09-Jun-1999 10:18:14 GMT',
  554. ];
  555. $response = new Response($headers, '');
  556. $adapter->expects($this->once())
  557. ->method('send')
  558. ->will($this->returnValue([$response]));
  559. $http = new Client([
  560. 'host' => 'cakephp.org',
  561. 'adapter' => $adapter,
  562. ]);
  563. $http->get('/projects');
  564. $cookies = $http->cookies();
  565. $this->assertCount(1, $cookies);
  566. $this->assertTrue($cookies->has('first'));
  567. $this->assertFalse($cookies->has('expiring'));
  568. }
  569. /**
  570. * Test cookieJar config option.
  571. */
  572. public function testCookieJar(): void
  573. {
  574. $jar = new CookieCollection();
  575. $http = new Client([
  576. 'cookieJar' => $jar,
  577. ]);
  578. $this->assertSame($jar, $http->cookies());
  579. }
  580. /**
  581. * Test addCookie() method.
  582. */
  583. public function testAddCookie(): void
  584. {
  585. $client = new Client();
  586. $cookie = new Cookie('foo', '', null, '/', 'example.com');
  587. $this->assertFalse($client->cookies()->has('foo'));
  588. $client->addCookie($cookie);
  589. $this->assertTrue($client->cookies()->has('foo'));
  590. }
  591. /**
  592. * Test addCookie() method without a domain.
  593. */
  594. public function testAddCookieWithoutDomain(): void
  595. {
  596. $this->expectException(InvalidArgumentException::class);
  597. $this->expectExceptionMessage('Cookie must have a domain and a path set.');
  598. $client = new Client();
  599. $cookie = new Cookie('foo', '', null, '/', '');
  600. $this->assertFalse($client->cookies()->has('foo'));
  601. $client->addCookie($cookie);
  602. $this->assertTrue($client->cookies()->has('foo'));
  603. }
  604. /**
  605. * Test addCookie() method without a path.
  606. */
  607. public function testAddCookieWithoutPath(): void
  608. {
  609. $this->expectException(InvalidArgumentException::class);
  610. $this->expectExceptionMessage('Cookie must have a domain and a path set.');
  611. $client = new Client();
  612. $cookie = new Cookie('foo', '', null, '', 'example.com');
  613. $this->assertFalse($client->cookies()->has('foo'));
  614. $client->addCookie($cookie);
  615. $this->assertTrue($client->cookies()->has('foo'));
  616. }
  617. /**
  618. * test head request with querystring data
  619. */
  620. public function testHeadQuerystring(): void
  621. {
  622. $response = new Response();
  623. $mock = $this->getMockBuilder(Stream::class)
  624. ->onlyMethods(['send'])
  625. ->getMock();
  626. $mock->expects($this->once())
  627. ->method('send')
  628. ->with($this->callback(function ($request) {
  629. $this->assertInstanceOf('Cake\Http\Client\Request', $request);
  630. $this->assertSame(Request::METHOD_HEAD, $request->getMethod());
  631. $this->assertSame('http://cakephp.org/search?q=hi%20there', '' . $request->getUri());
  632. return true;
  633. }))
  634. ->will($this->returnValue([$response]));
  635. $http = new Client([
  636. 'host' => 'cakephp.org',
  637. 'adapter' => $mock,
  638. ]);
  639. $result = $http->head('/search', [
  640. 'q' => 'hi there',
  641. ]);
  642. $this->assertSame($result, $response);
  643. }
  644. /**
  645. * test redirects
  646. */
  647. public function testRedirects(): void
  648. {
  649. $url = 'http://cakephp.org';
  650. $adapter = $this->getMockBuilder(Client\Adapter\Stream::class)
  651. ->onlyMethods(['send'])
  652. ->getMock();
  653. $redirect = new Response([
  654. 'HTTP/1.0 301',
  655. 'Location: http://cakephp.org/redirect1?foo=bar',
  656. 'Set-Cookie: redirect1=true;path=/',
  657. ]);
  658. $redirect2 = new Response([
  659. 'HTTP/1.0 301',
  660. 'Location: /redirect2#foo',
  661. 'Set-Cookie: redirect2=true;path=/',
  662. ]);
  663. $response = new Response([
  664. 'HTTP/1.0 200',
  665. ]);
  666. $adapter->expects($this->exactly(3))
  667. ->method('send')
  668. ->with(
  669. ...self::withConsecutive(
  670. [
  671. $this->callback(function (Request $request) use ($url) {
  672. $this->assertInstanceOf(Request::class, $request);
  673. $this->assertSame($url, (string)$request->getUri());
  674. return true;
  675. }),
  676. $this->callback(function ($options) {
  677. $this->assertArrayNotHasKey('redirect', $options);
  678. return true;
  679. }),
  680. ],
  681. [
  682. $this->callback(function (Request $request) use ($url) {
  683. $this->assertInstanceOf(Request::class, $request);
  684. $this->assertSame($url . '/redirect1?foo=bar', (string)$request->getUri());
  685. return true;
  686. }),
  687. $this->callback(function ($options) {
  688. $this->assertArrayNotHasKey('redirect', $options);
  689. return true;
  690. }),
  691. ],
  692. [
  693. $this->callback(function (Request $request) use ($url) {
  694. $this->assertInstanceOf(Request::class, $request);
  695. $this->assertSame($url . '/redirect2#foo', (string)$request->getUri());
  696. return true;
  697. }),
  698. [],
  699. ]
  700. )
  701. )
  702. ->will($this->onConsecutiveCalls([$redirect], [$redirect2], [$response]));
  703. $client = new Client([
  704. 'adapter' => $adapter,
  705. ]);
  706. $result = $client->send(new Request($url), [
  707. 'redirect' => 10,
  708. ]);
  709. $this->assertInstanceOf(Response::class, $result);
  710. $this->assertTrue($result->isOk());
  711. $cookies = $client->cookies();
  712. $this->assertTrue($cookies->has('redirect1'));
  713. $this->assertTrue($cookies->has('redirect2'));
  714. }
  715. /**
  716. * testSendRequest
  717. */
  718. public function testSendRequest(): void
  719. {
  720. $response = new Response();
  721. $headers = [
  722. 'User-Agent' => 'Cake',
  723. 'Connection' => 'close',
  724. 'Content-Type' => 'application/x-www-form-urlencoded',
  725. ];
  726. $mock = $this->getMockBuilder(Stream::class)
  727. ->onlyMethods(['send'])
  728. ->getMock();
  729. $mock->expects($this->once())
  730. ->method('send')
  731. ->with($this->callback(function ($request) use ($headers) {
  732. $this->assertInstanceOf('Laminas\Diactoros\Request', $request);
  733. $this->assertSame(Request::METHOD_GET, $request->getMethod());
  734. $this->assertSame('http://cakephp.org/test.html', $request->getUri() . '');
  735. $this->assertSame($headers['Content-Type'], $request->getHeaderLine('content-type'));
  736. $this->assertSame($headers['Connection'], $request->getHeaderLine('connection'));
  737. return true;
  738. }))
  739. ->will($this->returnValue([$response]));
  740. $http = new Client(['adapter' => $mock]);
  741. $request = new LaminasRequest(
  742. 'http://cakephp.org/test.html',
  743. Request::METHOD_GET,
  744. 'php://temp',
  745. $headers
  746. );
  747. $result = $http->sendRequest($request);
  748. $this->assertSame($result, $response);
  749. }
  750. /**
  751. * test redirect across sub domains
  752. */
  753. public function testRedirectDifferentSubDomains(): void
  754. {
  755. $adapter = $this->getMockBuilder(Client\Adapter\Stream::class)
  756. ->onlyMethods(['send'])
  757. ->getMock();
  758. $url = 'http://auth.example.org';
  759. $redirect = new Response([
  760. 'HTTP/1.0 301',
  761. 'Location: http://backstage.example.org',
  762. ]);
  763. $response = new Response([
  764. 'HTTP/1.0 200',
  765. ]);
  766. $adapter->expects($this->exactly(2))
  767. ->method('send')
  768. ->with(
  769. ...self::withConsecutive(
  770. [$this->anything()],
  771. [
  772. $this->callback(function ($request) {
  773. $this->assertSame('http://backstage.example.org', (string)$request->getUri());
  774. $this->assertSame('session=backend', $request->getHeaderLine('Cookie'));
  775. return true;
  776. }),
  777. ]
  778. )
  779. )
  780. ->will($this->OnConsecutiveCalls([$redirect], [$response]));
  781. $client = new Client([
  782. 'adapter' => $adapter,
  783. ]);
  784. $client->addCookie(new Cookie('session', 'backend', null, '/', 'backstage.example.org'));
  785. $client->addCookie(new Cookie('session', 'authz', null, '/', 'auth.example.org'));
  786. $result = $client->send(new Request($url), [
  787. 'redirect' => 10,
  788. ]);
  789. $this->assertInstanceOf(Response::class, $result);
  790. $this->assertSame($response, $result);
  791. }
  792. /**
  793. * Scheme is set when passed to client in string
  794. */
  795. public function testCreateFromUrlSetsScheme(): void
  796. {
  797. $client = Client::createFromUrl('https://example.co/');
  798. $this->assertSame('https', $client->getConfig('scheme'));
  799. }
  800. /**
  801. * Host is set when passed to client in string
  802. */
  803. public function testCreateFromUrlSetsHost(): void
  804. {
  805. $client = Client::createFromUrl('https://example.co/');
  806. $this->assertSame('example.co', $client->getConfig('host'));
  807. }
  808. /**
  809. * basePath is set when passed to client in string
  810. */
  811. public function testCreateFromUrlSetsBasePath(): void
  812. {
  813. $client = Client::createFromUrl('https://example.co/api/v1');
  814. $this->assertSame('/api/v1', $client->getConfig('basePath'));
  815. }
  816. /**
  817. * Test exception is thrown when URL cannot be parsed
  818. */
  819. public function testCreateFromUrlThrowsInvalidExceptionWhenUrlCannotBeParsed(): void
  820. {
  821. $this->expectException(InvalidArgumentException::class);
  822. Client::createFromUrl('htps://');
  823. $message = $this->getExpectedExceptionMessage();
  824. $this->assertTextContains('did not parse', $message);
  825. }
  826. /**
  827. * Port is set when passed to client in string
  828. */
  829. public function testCreateFromUrlSetsPort(): void
  830. {
  831. $client = Client::createFromUrl('https://example.co:8765/');
  832. $this->assertSame(8765, $client->getConfig('port'));
  833. }
  834. /**
  835. * Test exception is throw when no scheme is provided.
  836. */
  837. public function testCreateFromUrlThrowsInvalidArgumentExceptionWhenNoSchemeProvided(): void
  838. {
  839. $this->expectException(InvalidArgumentException::class);
  840. Client::createFromUrl('example.co');
  841. $message = $this->getExpectedExceptionMessage();
  842. $this->assertSame('The URL was parsed but did not contain a scheme or host', $message);
  843. }
  844. /**
  845. * Test exception is thrown if passed URL has no domain
  846. */
  847. public function testCreateFromUrlThrowsInvalidArgumentExceptionWhenNoDomainProvided(): void
  848. {
  849. $this->expectException(InvalidArgumentException::class);
  850. Client::createFromUrl('/api/v1');
  851. $message = $this->getExpectedExceptionMessage();
  852. $this->assertSame('The URL was parsed but did not contain a scheme or host', $message);
  853. }
  854. /**
  855. * Test that the passed parsed URL parts won't override other constructor defaults
  856. * or add undefined configuration
  857. */
  858. public function testCreateFromUrlOnlySetSchemePortHostBasePath(): void
  859. {
  860. $client = Client::createFromUrl('http://example.co:80/some/uri/?foo=bar');
  861. $config = $client->getConfig();
  862. $expected = [
  863. 'auth' => null,
  864. 'adapter' => null,
  865. 'host' => 'example.co',
  866. 'port' => 80,
  867. 'scheme' => 'http',
  868. 'basePath' => '/some/uri/',
  869. 'timeout' => 30,
  870. 'ssl_verify_peer' => true,
  871. 'ssl_verify_peer_name' => true,
  872. 'ssl_verify_depth' => 5,
  873. 'ssl_verify_host' => true,
  874. 'redirect' => false,
  875. 'protocolVersion' => '1.1',
  876. ];
  877. $this->assertSame($expected, $config);
  878. }
  879. /**
  880. * Test adding and sending to a mocked URL.
  881. */
  882. public function testAddMockResponseSimpleMatch(): void
  883. {
  884. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  885. Client::addMockResponse('POST', 'http://example.com/path', $stub);
  886. $client = new Client();
  887. $response = $client->post('http://example.com/path');
  888. $this->assertSame($stub, $response);
  889. }
  890. /**
  891. * When there are multiple matches for a URL the responses should
  892. * be used in a cycle.
  893. */
  894. public function testAddMockResponseMultipleMatches(): void
  895. {
  896. $one = new Response(['HTTP/1.0 200'], 'one');
  897. Client::addMockResponse('GET', 'http://example.com/info', $one);
  898. $two = new Response(['HTTP/1.0 200'], 'two');
  899. Client::addMockResponse('GET', 'http://example.com/info', $two);
  900. $client = new Client();
  901. $response = $client->get('http://example.com/info');
  902. $this->assertSame($one, $response);
  903. $response = $client->get('http://example.com/info');
  904. $this->assertSame($two, $response);
  905. $response = $client->get('http://example.com/info');
  906. $this->assertSame($one, $response);
  907. }
  908. /**
  909. * When there are multiple matches with custom match functions
  910. */
  911. public function testAddMockResponseMultipleMatchesCustom(): void
  912. {
  913. $one = new Response(['HTTP/1.0 200'], 'one');
  914. Client::addMockResponse('GET', 'http://example.com/info', $one, [
  915. 'match' => function ($request) {
  916. return false;
  917. },
  918. ]);
  919. $two = new Response(['HTTP/1.0 200'], 'two');
  920. Client::addMockResponse('GET', 'http://example.com/info', $two);
  921. $client = new Client();
  922. $response = $client->get('http://example.com/info');
  923. $this->assertSame($two, $response);
  924. $response = $client->get('http://example.com/info');
  925. $this->assertSame($two, $response);
  926. }
  927. /**
  928. * Mock match failures should result in the request being sent
  929. */
  930. public function testAddMockResponseMethodMatchFailure(): void
  931. {
  932. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  933. Client::addMockResponse('POST', 'http://example.com/path', $stub);
  934. $client = new Client();
  935. $this->expectException(MissingResponseException::class);
  936. $this->expectExceptionMessage('Unable to find a mock');
  937. $client->get('http://example.com/path');
  938. }
  939. /**
  940. * Trailing /* patterns should work
  941. */
  942. public function testAddMockResponseGlobMatch(): void
  943. {
  944. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  945. Client::addMockResponse('POST', 'http://example.com/path/*', $stub);
  946. $client = new Client();
  947. $response = $client->post('http://example.com/path/more/thing');
  948. $this->assertSame($stub, $response);
  949. $client = new Client();
  950. $response = $client->post('http://example.com/path/?query=value');
  951. $this->assertSame($stub, $response);
  952. }
  953. /**
  954. * Custom match methods must be closures
  955. */
  956. public function testAddMockResponseInvalidMatch(): void
  957. {
  958. $this->expectException(InvalidArgumentException::class);
  959. $this->expectExceptionMessage('The `match` option must be a `Closure`.');
  960. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  961. Client::addMockResponse('POST', 'http://example.com/path', $stub, [
  962. 'match' => 'oops',
  963. ]);
  964. }
  965. /**
  966. * Custom matchers should get a request.
  967. */
  968. public function testAddMockResponseCustomMatch(): void
  969. {
  970. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  971. Client::addMockResponse('POST', 'http://example.com/path', $stub, [
  972. 'match' => function ($request) {
  973. $this->assertInstanceOf(Request::class, $request);
  974. $uri = $request->getUri();
  975. $this->assertEquals('/path', $uri->getPath());
  976. $this->assertEquals('example.com', $uri->getHost());
  977. return true;
  978. },
  979. ]);
  980. $client = new Client();
  981. $response = $client->post('http://example.com/path');
  982. $this->assertSame($stub, $response);
  983. }
  984. /**
  985. * Custom matchers can fail the match
  986. */
  987. public function testAddMockResponseCustomNoMatch(): void
  988. {
  989. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  990. Client::addMockResponse('POST', 'http://example.com/path', $stub, [
  991. 'match' => function () {
  992. return false;
  993. },
  994. ]);
  995. $client = new Client();
  996. $this->expectException(MissingResponseException::class);
  997. $this->expectExceptionMessage('Unable to find a mock');
  998. $client->post('http://example.com/path');
  999. }
  1000. /**
  1001. * Custom matchers must return a boolean
  1002. */
  1003. public function testAddMockResponseCustomInvalidDecision(): void
  1004. {
  1005. $stub = new Response(['HTTP/1.0 200'], 'hello world');
  1006. Client::addMockResponse('POST', 'http://example.com/path', $stub, [
  1007. 'match' => function ($request) {
  1008. return 'invalid';
  1009. },
  1010. ]);
  1011. $client = new Client();
  1012. $this->expectException(InvalidArgumentException::class);
  1013. $this->expectExceptionMessage('Match callback must');
  1014. $client->post('http://example.com/path');
  1015. }
  1016. }