ClientTest.php 39 KB

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