ClientTest.php 39 KB

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