ClientTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <?php defined('SYSPATH') OR die('Kohana bootstrap needs to be included before tests run');
  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) 2008-2012 Kohana Team
  14. * @license http://kohanaframework.org/license
  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 = array(
  36. 'ko_request_clienttest' => new Route('<controller>/<action>/<data>',array('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 = array(
  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. array('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 array(
  111. array(TRUE, $this->_dummy_uri(200, NULL, 'not-followed'), 'not-followed'),
  112. array(TRUE, $this->_dummy_redirect_uri(200), 'not-followed'),
  113. array(TRUE, $this->_dummy_redirect_uri(302), 'followed'),
  114. array(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. array('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. array(
  142. 'follow' => TRUE,
  143. 'follow_headers' => array('Authorization', 'X-Follow-With-Value')
  144. ))
  145. ->headers(array(
  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. * Provider for test_follows_with_strict_method
  160. *
  161. * @return array
  162. */
  163. public function provider_follows_with_strict_method()
  164. {
  165. return array(
  166. array(201, NULL, Request::POST, Request::GET),
  167. array(301, NULL, Request::GET, Request::GET),
  168. array(302, TRUE, Request::POST, Request::POST),
  169. array(302, FALSE, Request::POST, Request::GET),
  170. array(303, NULL, Request::POST, Request::GET),
  171. array(307, NULL, Request::POST, Request::POST),
  172. );
  173. }
  174. /**
  175. * Tests that the correct method is used (allowing for the strict_redirect setting)
  176. * for follow requests.
  177. *
  178. * @dataProvider provider_follows_with_strict_method
  179. *
  180. * @param string $status_code HTTP response code to fake
  181. * @param bool $strict_redirect Option value to set
  182. * @param string $orig_method Request method for the original request
  183. * @param string $expect_method Request method expected for the follow request
  184. */
  185. public function test_follows_with_strict_method($status_code, $strict_redirect, $orig_method, $expect_method)
  186. {
  187. $response = Request::factory($this->_dummy_redirect_uri($status_code),
  188. array(
  189. 'follow' => TRUE,
  190. 'strict_redirect' => $strict_redirect
  191. ))
  192. ->method($orig_method)
  193. ->execute();
  194. $data = json_decode($response->body(), TRUE);
  195. $this->assertEquals('followed', $data['body']);
  196. $this->assertEquals($expect_method, $data['rq_method']);
  197. }
  198. /**
  199. * Provider for test_follows_with_body_if_not_get
  200. *
  201. * @return array
  202. */
  203. public function provider_follows_with_body_if_not_get()
  204. {
  205. return array(
  206. array('GET','301',NULL),
  207. array('POST','303',NULL),
  208. array('POST','307','foo-bar')
  209. );
  210. }
  211. /**
  212. * Tests that the original request body is sent when following a redirect
  213. * (unless redirect method is GET)
  214. *
  215. * @dataProvider provider_follows_with_body_if_not_get
  216. * @depends test_follows_with_strict_method
  217. * @depends test_follows_redirects
  218. *
  219. * @param string $original_method Request method to use for the original request
  220. * @param string $status Redirect status that will be issued
  221. * @param string $expect_body Expected value of body() in the second request
  222. */
  223. public function test_follows_with_body_if_not_get($original_method, $status, $expect_body)
  224. {
  225. $response = Request::factory($this->_dummy_redirect_uri($status),
  226. array('follow' => TRUE))
  227. ->method($original_method)
  228. ->body('foo-bar')
  229. ->execute();
  230. $data = json_decode($response->body(), TRUE);
  231. $this->assertEquals('followed', $data['body']);
  232. $this->assertEquals($expect_body, $data['rq_body']);
  233. }
  234. /**
  235. * Provider for test_triggers_header_callbacks
  236. *
  237. * @return array
  238. */
  239. public function provider_triggers_header_callbacks()
  240. {
  241. return array(
  242. // Straightforward response manipulation
  243. array(
  244. array('X-test-1' =>
  245. function($request, $response, $client)
  246. {
  247. $response->body(json_encode(array('body'=>'test1-body-changed')));
  248. return $response;
  249. }),
  250. $this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test1-body'),
  251. 'test1-body-changed'
  252. ),
  253. // Subsequent request execution
  254. array(
  255. array('X-test-2' =>
  256. function($request, $response, $client)
  257. {
  258. return Request::factory($response->headers('X-test-2'));
  259. }),
  260. $this->_dummy_uri(200,
  261. array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')),
  262. 'test2-orig-body'),
  263. 'test2-subsequent-body'
  264. ),
  265. // No callbacks triggered
  266. array(
  267. array('X-test-3' =>
  268. function ($request, $response, $client)
  269. {
  270. throw new Exception("Unexpected execution of X-test-3 callback");
  271. }),
  272. $this->_dummy_uri(200, array('X-test-1' => 'foo'), 'test3-body'),
  273. 'test3-body'
  274. ),
  275. // Callbacks not triggered once a previous callback has created a new response
  276. array(
  277. array(
  278. 'X-test-1' =>
  279. function($request, $response, $client)
  280. {
  281. return Request::factory($response->headers('X-test-1'));
  282. },
  283. 'X-test-2' =>
  284. function($request, $response, $client)
  285. {
  286. return Request::factory($response->headers('X-test-2'));
  287. }
  288. ),
  289. $this->_dummy_uri(200,
  290. array(
  291. 'X-test-1' => $this->_dummy_uri(200, NULL, 'test1-subsequent-body'),
  292. 'X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')
  293. ),
  294. 'test2-orig-body'),
  295. 'test1-subsequent-body'
  296. ),
  297. // Nested callbacks are supported if callback creates new request
  298. array(
  299. array(
  300. 'X-test-1' =>
  301. function($request, $response, $client)
  302. {
  303. return Request::factory($response->headers('X-test-1'));
  304. },
  305. 'X-test-2' =>
  306. function($request, $response, $client)
  307. {
  308. return Request::factory($response->headers('X-test-2'));
  309. }
  310. ),
  311. $this->_dummy_uri(200,
  312. array(
  313. 'X-test-1' => $this->_dummy_uri(
  314. 200,
  315. array('X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')),
  316. 'test1-subsequent-body'),
  317. ),
  318. 'test-orig-body'),
  319. 'test2-subsequent-body'
  320. ),
  321. );
  322. }
  323. /**
  324. * Tests that header callbacks are triggered in sequence when specific headers
  325. * are present in the response
  326. *
  327. * @dataProvider provider_triggers_header_callbacks
  328. *
  329. * @param array $callbacks Array of header callbacks
  330. * @param array $headers Headers that will be received in the response
  331. * @param string $expect_body Response body content to expect
  332. */
  333. public function test_triggers_header_callbacks($callbacks, $uri, $expect_body)
  334. {
  335. $response = Request::factory($uri,
  336. array('header_callbacks' => $callbacks))
  337. ->execute();
  338. $data = json_decode($response->body(), TRUE);
  339. $this->assertEquals($expect_body, $data['body']);
  340. }
  341. /**
  342. * Tests that the Request_Client is protected from too many recursions of
  343. * requests triggered by header callbacks.
  344. *
  345. */
  346. public function test_deep_recursive_callbacks_are_aborted()
  347. {
  348. $uri = $this->_dummy_uri('200', array('x-cb' => '1'), 'body');
  349. // Temporary property to track requests
  350. $this->requests_executed = 0;
  351. try
  352. {
  353. $response = Request::factory(
  354. $uri,
  355. array(
  356. 'header_callbacks' => array(
  357. 'x-cb' =>
  358. function ($request, $response, $client)
  359. {
  360. $client->callback_params('testcase')->requests_executed++;
  361. // Recurse into a new request
  362. return Request::factory($request->uri());
  363. }),
  364. 'max_callback_depth' => 2,
  365. 'callback_params' => array(
  366. 'testcase' => $this,
  367. )
  368. ))
  369. ->execute();
  370. }
  371. catch (Request_Client_Recursion_Exception $e)
  372. {
  373. // Verify that two requests were executed
  374. $this->assertEquals(2, $this->requests_executed);
  375. return;
  376. }
  377. $this->fail('Expected Request_Client_Recursion_Exception was not thrown');
  378. }
  379. /**
  380. * Header callback for testing that arbitrary callback_params are available
  381. * to the callback.
  382. *
  383. * @param Request $request
  384. * @param Response $response
  385. * @param Request_Client $client
  386. */
  387. public function callback_assert_params($request, $response, $client)
  388. {
  389. $this->assertEquals('foo', $client->callback_params('constructor_param'));
  390. $this->assertEquals('bar', $client->callback_params('setter_param'));
  391. $response->body('assertions_ran');
  392. }
  393. /**
  394. * Test that arbitrary callback_params can be passed to the callback through
  395. * the Request_Client and are assigned to subsequent requests
  396. */
  397. public function test_client_can_hold_params_for_callbacks()
  398. {
  399. // Test with param in constructor
  400. $request = Request::factory(
  401. $this->_dummy_uri(
  402. 302,
  403. array('Location' => $this->_dummy_uri('200',array('X-cb'=>'1'), 'followed')),
  404. 'not-followed'),
  405. array(
  406. 'follow' => TRUE,
  407. 'header_callbacks' => array(
  408. 'x-cb' => array($this, 'callback_assert_params'),
  409. 'location' => 'Request_Client::on_header_location',
  410. ),
  411. 'callback_params' => array(
  412. 'constructor_param' => 'foo'
  413. )
  414. ));
  415. // Test passing param to setter
  416. $request->client()->callback_params('setter_param', 'bar');
  417. // Callback will throw assertion exceptions when executed
  418. $response = $request->execute();
  419. $this->assertEquals('assertions_ran', $response->body());
  420. }
  421. } // End Kohana_Request_ClientTest
  422. /**
  423. * Dummy controller class that acts as a shunt - passing back request information
  424. * in the response to allow inspection.
  425. */
  426. class Controller_RequestClientDummy extends Controller {
  427. /**
  428. * Takes a urlencoded 'data' parameter from the route and uses it to craft a
  429. * response. Redirect chains can be tested by passing another encoded uri
  430. * as a location header with an appropriate status code.
  431. */
  432. public function action_fake()
  433. {
  434. parse_str(urldecode($this->request->param('data')), $data);
  435. $this->response->status(Arr::get($data, 'status', 200));
  436. $this->response->headers(Arr::get($data, 'header', array()));
  437. $this->response->body(json_encode(array(
  438. 'body'=> Arr::get($data,'body','ok'),
  439. 'rq_headers' => $this->request->headers(),
  440. 'rq_body' => $this->request->body(),
  441. 'rq_method' => $this->request->method(),
  442. )));
  443. }
  444. } // End Controller_RequestClientDummy