ClientTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <?php
  2. /**
  3. * Unit tests for generic Request_Client class
  4. *
  5. * @group ko7
  6. * @group ko7.core
  7. * @group ko7.core.request
  8. *
  9. * @package KO7
  10. * @category Tests
  11. *
  12. * @author Andrew Coulton
  13. * @copyright (c) 2007-2016 Kohana Team
  14. * @copyright (c) since 2016 Koseven Team
  15. * @license https://koseven.dev/LICENSE
  16. */
  17. class KO7_Request_ClientTest extends Unittest_TestCase
  18. {
  19. protected $_inital_request;
  20. protected static $_original_routes;
  21. // @codingStandardsIgnoreStart - PHPUnit does not follow standards
  22. /**
  23. * Sets up a new route to ensure that we have a matching route for our
  24. * Controller_RequestClientDummy class.
  25. */
  26. public static function setUpBeforeClass() : void
  27. {
  28. // @codingStandardsIgnoreEnd
  29. parent::setUpBeforeClass();
  30. // Set a new Route to the ClientTest controller as the first route
  31. // This requires reflection as the API for editing defined routes is limited
  32. $route_class = new ReflectionClass('Route');
  33. $routes_prop = $route_class->getProperty('_routes');
  34. $routes_prop->setAccessible(TRUE);
  35. self::$_original_routes = $routes_prop->getValue('Route');
  36. $routes = [
  37. 'ko_request_clienttest' => new Route('<controller>/<action>/<data>',['data'=>'.+'])
  38. ] + self::$_original_routes;
  39. $routes_prop->setValue('Route',$routes);
  40. }
  41. // @codingStandardsIgnoreStart - PHPUnit does not follow standards
  42. /**
  43. * Resets the application's routes to their state prior to this test case
  44. */
  45. public static function tearDownAfterClass(): void
  46. {
  47. // @codingStandardsIgnoreEnd
  48. // Reset routes
  49. $route_class = new ReflectionClass('Route');
  50. $routes_prop = $route_class->getProperty('_routes');
  51. $routes_prop->setAccessible(TRUE);
  52. $routes_prop->setValue('Route',self::$_original_routes);
  53. parent::tearDownAfterClass();
  54. }
  55. // @codingStandardsIgnoreStart - PHPUnit does not follow standards
  56. public function setUp(): void
  57. {
  58. // @codingStandardsIgnoreEnd
  59. parent::setUp();
  60. $this->_initial_request = Request::$initial;
  61. Request::$initial = new Request('/');
  62. }
  63. // @codingStandardsIgnoreStart - PHPUnit does not follow standards
  64. public function tearDown(): void
  65. {
  66. // @codingStandardsIgnoreEnd
  67. Request::$initial = $this->_initial_request;
  68. parent::tearDown();
  69. }
  70. /**
  71. * Generates an internal URI to the [Controller_RequestClientDummy] shunt
  72. * controller - the URI contains an encoded form of the required server
  73. * response.
  74. *
  75. * @param string $status HTTP response code to issue
  76. * @param array $headers HTTP headers to send with the response
  77. * @param string $body A string to send back as response body (included in the JSON response)
  78. * @return string
  79. */
  80. protected function _dummy_uri($status, $headers, $body)
  81. {
  82. $data = [
  83. 'status' => $status,
  84. 'header' => $headers,
  85. 'body' => $body
  86. ];
  87. return "/requestclientdummy/fake".'/'.urlencode(http_build_query($data));
  88. }
  89. /**
  90. * Shortcut method to generate a simple redirect URI - the first request will
  91. * receive a redirect with the given HTTP status code and the second will
  92. * receive a 200 response. The 'body' data value in the first response will
  93. * be 'not-followed' and in the second response it will be 'followed'. This
  94. * allows easy assertion that a redirect has taken place.
  95. *
  96. * @param string $status HTTP response code to issue
  97. * @return string
  98. */
  99. protected function _dummy_redirect_uri($status)
  100. {
  101. return $this->_dummy_uri($status,
  102. ['Location' => $this->_dummy_uri(200, NULL, 'followed')],
  103. 'not-followed');
  104. }
  105. /**
  106. * Provider for test_follows_redirects
  107. * @return array
  108. */
  109. public function provider_follows_redirects()
  110. {
  111. return [
  112. [TRUE, $this->_dummy_uri(200, NULL, 'not-followed'), 'not-followed'],
  113. [TRUE, $this->_dummy_redirect_uri(200), 'not-followed'],
  114. [TRUE, $this->_dummy_redirect_uri(302), 'followed'],
  115. [FALSE, $this->_dummy_redirect_uri(302), 'not-followed'],
  116. ];
  117. }
  118. /**
  119. * Tests that the client optionally follows properly formed redirects
  120. *
  121. * @dataProvider provider_follows_redirects
  122. *
  123. * @param bool $follow Option value to set
  124. * @param string $request_url URL to request initially (contains data to set up redirect etc)
  125. * @param string $expect_body Body text expected in the eventual result
  126. */
  127. public function test_follows_redirects($follow, $request_url, $expect_body)
  128. {
  129. $response = Request::factory($request_url,
  130. ['follow' => $follow])
  131. ->execute();
  132. $data = json_decode($response->body(), TRUE);
  133. $this->assertEquals($expect_body, $data['body']);
  134. }
  135. /**
  136. * Tests that only specified headers are resent following a redirect
  137. */
  138. public function test_follows_with_headers()
  139. {
  140. $response = Request::factory(
  141. $this->_dummy_redirect_uri(301),
  142. [
  143. 'follow' => TRUE,
  144. 'follow_headers' => ['Authorization', 'X-Follow-With-Value']
  145. ])
  146. ->headers([
  147. 'Authorization' => 'follow',
  148. 'X-Follow-With-Value' => 'follow',
  149. 'X-Not-In-Follow' => 'no-follow'
  150. ])
  151. ->execute();
  152. $data = json_decode($response->body(),TRUE);
  153. $headers = $data['rq_headers'];
  154. $this->assertEquals('followed', $data['body']);
  155. $this->assertEquals('follow', $headers['authorization']);
  156. $this->assertEquals('follow', $headers['x-follow-with-value']);
  157. $this->assertFalse(isset($headers['x-not-in-follow']), 'X-Not-In-Follow should not be passed to next request');
  158. }
  159. /**
  160. * Tests that the follow_headers are only added to a redirect request if they were present in the original
  161. *
  162. * @ticket 4790
  163. */
  164. public function test_follow_does_not_add_extra_headers()
  165. {
  166. $response = Request::factory(
  167. $this->_dummy_redirect_uri(301),
  168. [
  169. 'follow' => TRUE,
  170. 'follow_headers' => ['Authorization']
  171. ])
  172. ->headers([])
  173. ->execute();
  174. $data = json_decode($response->body(),TRUE);
  175. $headers = $data['rq_headers'];
  176. $this->assertArrayNotHasKey('authorization', $headers, 'Empty headers should not be added when following redirects');
  177. }
  178. /**
  179. * Provider for test_follows_with_strict_method
  180. *
  181. * @return array
  182. */
  183. public function provider_follows_with_strict_method()
  184. {
  185. return [
  186. [201, NULL, Request::POST, Request::GET],
  187. [301, NULL, Request::GET, Request::GET],
  188. [302, TRUE, Request::POST, Request::POST],
  189. [302, FALSE, Request::POST, Request::GET],
  190. [303, NULL, Request::POST, Request::GET],
  191. [307, NULL, Request::POST, Request::POST],
  192. ];
  193. }
  194. /**
  195. * Tests that the correct method is used (allowing for the strict_redirect setting)
  196. * for follow requests.
  197. *
  198. * @dataProvider provider_follows_with_strict_method
  199. *
  200. * @param string $status_code HTTP response code to fake
  201. * @param bool $strict_redirect Option value to set
  202. * @param string $orig_method Request method for the original request
  203. * @param string $expect_method Request method expected for the follow request
  204. */
  205. public function test_follows_with_strict_method($status_code, $strict_redirect, $orig_method, $expect_method)
  206. {
  207. $response = Request::factory($this->_dummy_redirect_uri($status_code),
  208. [
  209. 'follow' => TRUE,
  210. 'strict_redirect' => $strict_redirect
  211. ])
  212. ->method($orig_method)
  213. ->execute();
  214. $data = json_decode($response->body(), TRUE);
  215. $this->assertEquals('followed', $data['body']);
  216. $this->assertEquals($expect_method, $data['rq_method']);
  217. }
  218. /**
  219. * Provider for test_follows_with_body_if_not_get
  220. *
  221. * @return array
  222. */
  223. public function provider_follows_with_body_if_not_get()
  224. {
  225. return [
  226. ['GET','301',NULL],
  227. ['POST','303',NULL],
  228. ['POST','307','foo-bar']
  229. ];
  230. }
  231. /**
  232. * Tests that the original request body is sent when following a redirect
  233. * (unless redirect method is GET)
  234. *
  235. * @dataProvider provider_follows_with_body_if_not_get
  236. * @depends test_follows_with_strict_method
  237. * @depends test_follows_redirects
  238. *
  239. * @param string $original_method Request method to use for the original request
  240. * @param string $status Redirect status that will be issued
  241. * @param string $expect_body Expected value of body() in the second request
  242. */
  243. public function test_follows_with_body_if_not_get($original_method, $status, $expect_body)
  244. {
  245. $response = Request::factory($this->_dummy_redirect_uri($status),
  246. ['follow' => TRUE])
  247. ->method($original_method)
  248. ->body('foo-bar')
  249. ->execute();
  250. $data = json_decode($response->body(), TRUE);
  251. $this->assertEquals('followed', $data['body']);
  252. $this->assertEquals($expect_body, $data['rq_body']);
  253. }
  254. /**
  255. * Provider for test_triggers_header_callbacks
  256. *
  257. * @return array
  258. */
  259. public function provider_triggers_header_callbacks()
  260. {
  261. return [
  262. // Straightforward response manipulation
  263. [
  264. ['X-test-1' =>
  265. function($request, $response, $client)
  266. {
  267. $response->body(json_encode(['body'=>'test1-body-changed']));
  268. return $response;
  269. }],
  270. $this->_dummy_uri(200, ['X-test-1' => 'foo'], 'test1-body'),
  271. 'test1-body-changed'
  272. ],
  273. // Subsequent request execution
  274. [
  275. ['X-test-2' =>
  276. function($request, $response, $client)
  277. {
  278. return Request::factory($response->headers('X-test-2'));
  279. }],
  280. $this->_dummy_uri(200,
  281. ['X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')],
  282. 'test2-orig-body'),
  283. 'test2-subsequent-body'
  284. ],
  285. // No callbacks triggered
  286. [
  287. ['X-test-3' =>
  288. function ($request, $response, $client)
  289. {
  290. throw new Exception("Unexpected execution of X-test-3 callback");
  291. }],
  292. $this->_dummy_uri(200, ['X-test-1' => 'foo'], 'test3-body'),
  293. 'test3-body'
  294. ],
  295. // Callbacks not triggered once a previous callback has created a new response
  296. [
  297. [
  298. 'X-test-1' =>
  299. function($request, $response, $client)
  300. {
  301. return Request::factory($response->headers('X-test-1'));
  302. },
  303. 'X-test-2' =>
  304. function($request, $response, $client)
  305. {
  306. return Request::factory($response->headers('X-test-2'));
  307. }
  308. ],
  309. $this->_dummy_uri(200,
  310. [
  311. 'X-test-1' => $this->_dummy_uri(200, NULL, 'test1-subsequent-body'),
  312. 'X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')
  313. ],
  314. 'test2-orig-body'),
  315. 'test1-subsequent-body'
  316. ],
  317. // Nested callbacks are supported if callback creates new request
  318. [
  319. [
  320. 'X-test-1' =>
  321. function($request, $response, $client)
  322. {
  323. return Request::factory($response->headers('X-test-1'));
  324. },
  325. 'X-test-2' =>
  326. function($request, $response, $client)
  327. {
  328. return Request::factory($response->headers('X-test-2'));
  329. }
  330. ],
  331. $this->_dummy_uri(200,
  332. [
  333. 'X-test-1' => $this->_dummy_uri(
  334. 200,
  335. ['X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')],
  336. 'test1-subsequent-body'),
  337. ],
  338. 'test-orig-body'),
  339. 'test2-subsequent-body'
  340. ],
  341. ];
  342. }
  343. /**
  344. * Tests that header callbacks are triggered in sequence when specific headers
  345. * are present in the response
  346. *
  347. * @dataProvider provider_triggers_header_callbacks
  348. *
  349. * @param array $callbacks Array of header callbacks
  350. * @param array $headers Headers that will be received in the response
  351. * @param string $expect_body Response body content to expect
  352. */
  353. public function test_triggers_header_callbacks($callbacks, $uri, $expect_body)
  354. {
  355. $response = Request::factory($uri,
  356. ['header_callbacks' => $callbacks])
  357. ->execute();
  358. $data = json_decode($response->body(), TRUE);
  359. $this->assertEquals($expect_body, $data['body']);
  360. }
  361. /**
  362. * Tests that the Request_Client is protected from too many recursions of
  363. * requests triggered by header callbacks.
  364. *
  365. */
  366. public function test_deep_recursive_callbacks_are_aborted()
  367. {
  368. $uri = $this->_dummy_uri('200', ['x-cb' => '1'], 'body');
  369. // Temporary property to track requests
  370. $this->requests_executed = 0;
  371. try
  372. {
  373. $response = Request::factory(
  374. $uri,
  375. [
  376. 'header_callbacks' => [
  377. 'x-cb' =>
  378. function ($request, $response, $client)
  379. {
  380. $client->callback_params('testcase')->requests_executed++;
  381. // Recurse into a new request
  382. return Request::factory($request->uri());
  383. }],
  384. 'max_callback_depth' => 2,
  385. 'callback_params' => [
  386. 'testcase' => $this,
  387. ]
  388. ])
  389. ->execute();
  390. }
  391. catch (Request_Client_Recursion_Exception $e)
  392. {
  393. // Verify that two requests were executed
  394. $this->assertEquals(2, $this->requests_executed);
  395. return;
  396. }
  397. $this->fail('Expected Request_Client_Recursion_Exception was not thrown');
  398. }
  399. /**
  400. * Header callback for testing that arbitrary callback_params are available
  401. * to the callback.
  402. *
  403. * @param Request $request
  404. * @param Response $response
  405. * @param Request_Client $client
  406. */
  407. public function callback_assert_params($request, $response, $client)
  408. {
  409. $this->assertEquals('foo', $client->callback_params('constructor_param'));
  410. $this->assertEquals('bar', $client->callback_params('setter_param'));
  411. $response->body('assertions_ran');
  412. }
  413. /**
  414. * Test that arbitrary callback_params can be passed to the callback through
  415. * the Request_Client and are assigned to subsequent requests
  416. */
  417. public function test_client_can_hold_params_for_callbacks()
  418. {
  419. // Test with param in constructor
  420. $request = Request::factory(
  421. $this->_dummy_uri(
  422. 302,
  423. ['Location' => $this->_dummy_uri('200',['X-cb'=>'1'], 'followed')],
  424. 'not-followed'),
  425. [
  426. 'follow' => TRUE,
  427. 'header_callbacks' => [
  428. 'x-cb' => [$this, 'callback_assert_params'],
  429. 'location' => 'Request_Client::on_header_location',
  430. ],
  431. 'callback_params' => [
  432. 'constructor_param' => 'foo'
  433. ]
  434. ]);
  435. // Test passing param to setter
  436. $request->client()->callback_params('setter_param', 'bar');
  437. // Callback will throw assertion exceptions when executed
  438. $response = $request->execute();
  439. $this->assertEquals('assertions_ran', $response->body());
  440. }
  441. } // End KO7_Request_ClientTest
  442. /**
  443. * Dummy controller class that acts as a shunt - passing back request information
  444. * in the response to allow inspection.
  445. */
  446. class Controller_RequestClientDummy extends Controller {
  447. /**
  448. * Takes a urlencoded 'data' parameter from the route and uses it to craft a
  449. * response. Redirect chains can be tested by passing another encoded uri
  450. * as a location header with an appropriate status code.
  451. */
  452. public function action_fake()
  453. {
  454. parse_str(urldecode($this->request->param('data')), $data);
  455. $this->response->status(Arr::get($data, 'status', 200));
  456. $this->response->headers(Arr::get($data, 'header', []));
  457. $this->response->body(json_encode([
  458. 'body'=> Arr::get($data,'body','ok'),
  459. 'rq_headers' => $this->request->headers(),
  460. 'rq_body' => $this->request->body(),
  461. 'rq_method' => $this->request->method(),
  462. ]));
  463. }
  464. } // End Controller_RequestClientDummy