ClientTest.php 37 KB

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