ClientTest.php 37 KB

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