ClientTest.php 15 KB

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