getProperty('_routes'); $routes_prop->setAccessible(TRUE); self::$_original_routes = $routes_prop->getValue('Route'); $routes = [ 'ko_request_clienttest' => new Route('//',['data'=>'.+']) ] + self::$_original_routes; $routes_prop->setValue('Route',$routes); } // @codingStandardsIgnoreStart - PHPUnit does not follow standards /** * Resets the application's routes to their state prior to this test case */ public static function tearDownAfterClass(): void { // @codingStandardsIgnoreEnd // Reset routes $route_class = new ReflectionClass('Route'); $routes_prop = $route_class->getProperty('_routes'); $routes_prop->setAccessible(TRUE); $routes_prop->setValue('Route',self::$_original_routes); parent::tearDownAfterClass(); } // @codingStandardsIgnoreStart - PHPUnit does not follow standards public function setUp(): void { // @codingStandardsIgnoreEnd parent::setUp(); $this->_initial_request = Request::$initial; Request::$initial = new Request('/'); } // @codingStandardsIgnoreStart - PHPUnit does not follow standards public function tearDown(): void { // @codingStandardsIgnoreEnd Request::$initial = $this->_initial_request; parent::tearDown(); } /** * Generates an internal URI to the [Controller_RequestClientDummy] shunt * controller - the URI contains an encoded form of the required server * response. * * @param string $status HTTP response code to issue * @param array $headers HTTP headers to send with the response * @param string $body A string to send back as response body (included in the JSON response) * @return string */ protected function _dummy_uri($status, $headers, $body) { $data = [ 'status' => $status, 'header' => $headers, 'body' => $body ]; return "/requestclientdummy/fake".'/'.urlencode(http_build_query($data)); } /** * Shortcut method to generate a simple redirect URI - the first request will * receive a redirect with the given HTTP status code and the second will * receive a 200 response. The 'body' data value in the first response will * be 'not-followed' and in the second response it will be 'followed'. This * allows easy assertion that a redirect has taken place. * * @param string $status HTTP response code to issue * @return string */ protected function _dummy_redirect_uri($status) { return $this->_dummy_uri($status, ['Location' => $this->_dummy_uri(200, NULL, 'followed')], 'not-followed'); } /** * Provider for test_follows_redirects * @return array */ public function provider_follows_redirects() { return [ [TRUE, $this->_dummy_uri(200, NULL, 'not-followed'), 'not-followed'], [TRUE, $this->_dummy_redirect_uri(200), 'not-followed'], [TRUE, $this->_dummy_redirect_uri(302), 'followed'], [FALSE, $this->_dummy_redirect_uri(302), 'not-followed'], ]; } /** * Tests that the client optionally follows properly formed redirects * * @dataProvider provider_follows_redirects * * @param bool $follow Option value to set * @param string $request_url URL to request initially (contains data to set up redirect etc) * @param string $expect_body Body text expected in the eventual result */ public function test_follows_redirects($follow, $request_url, $expect_body) { $response = Request::factory($request_url, ['follow' => $follow]) ->execute(); $data = json_decode($response->body(), TRUE); $this->assertEquals($expect_body, $data['body']); } /** * Tests that only specified headers are resent following a redirect */ public function test_follows_with_headers() { $response = Request::factory( $this->_dummy_redirect_uri(301), [ 'follow' => TRUE, 'follow_headers' => ['Authorization', 'X-Follow-With-Value'] ]) ->headers([ 'Authorization' => 'follow', 'X-Follow-With-Value' => 'follow', 'X-Not-In-Follow' => 'no-follow' ]) ->execute(); $data = json_decode($response->body(),TRUE); $headers = $data['rq_headers']; $this->assertEquals('followed', $data['body']); $this->assertEquals('follow', $headers['authorization']); $this->assertEquals('follow', $headers['x-follow-with-value']); $this->assertFalse(isset($headers['x-not-in-follow']), 'X-Not-In-Follow should not be passed to next request'); } /** * Tests that the follow_headers are only added to a redirect request if they were present in the original * * @ticket 4790 */ public function test_follow_does_not_add_extra_headers() { $response = Request::factory( $this->_dummy_redirect_uri(301), [ 'follow' => TRUE, 'follow_headers' => ['Authorization'] ]) ->headers([]) ->execute(); $data = json_decode($response->body(),TRUE); $headers = $data['rq_headers']; $this->assertArrayNotHasKey('authorization', $headers, 'Empty headers should not be added when following redirects'); } /** * Provider for test_follows_with_strict_method * * @return array */ public function provider_follows_with_strict_method() { return [ [201, NULL, Request::POST, Request::GET], [301, NULL, Request::GET, Request::GET], [302, TRUE, Request::POST, Request::POST], [302, FALSE, Request::POST, Request::GET], [303, NULL, Request::POST, Request::GET], [307, NULL, Request::POST, Request::POST], ]; } /** * Tests that the correct method is used (allowing for the strict_redirect setting) * for follow requests. * * @dataProvider provider_follows_with_strict_method * * @param string $status_code HTTP response code to fake * @param bool $strict_redirect Option value to set * @param string $orig_method Request method for the original request * @param string $expect_method Request method expected for the follow request */ public function test_follows_with_strict_method($status_code, $strict_redirect, $orig_method, $expect_method) { $response = Request::factory($this->_dummy_redirect_uri($status_code), [ 'follow' => TRUE, 'strict_redirect' => $strict_redirect ]) ->method($orig_method) ->execute(); $data = json_decode($response->body(), TRUE); $this->assertEquals('followed', $data['body']); $this->assertEquals($expect_method, $data['rq_method']); } /** * Provider for test_follows_with_body_if_not_get * * @return array */ public function provider_follows_with_body_if_not_get() { return [ ['GET','301',NULL], ['POST','303',NULL], ['POST','307','foo-bar'] ]; } /** * Tests that the original request body is sent when following a redirect * (unless redirect method is GET) * * @dataProvider provider_follows_with_body_if_not_get * @depends test_follows_with_strict_method * @depends test_follows_redirects * * @param string $original_method Request method to use for the original request * @param string $status Redirect status that will be issued * @param string $expect_body Expected value of body() in the second request */ public function test_follows_with_body_if_not_get($original_method, $status, $expect_body) { $response = Request::factory($this->_dummy_redirect_uri($status), ['follow' => TRUE]) ->method($original_method) ->body('foo-bar') ->execute(); $data = json_decode($response->body(), TRUE); $this->assertEquals('followed', $data['body']); $this->assertEquals($expect_body, $data['rq_body']); } /** * Provider for test_triggers_header_callbacks * * @return array */ public function provider_triggers_header_callbacks() { return [ // Straightforward response manipulation [ ['X-test-1' => function($request, $response, $client) { $response->body(json_encode(['body'=>'test1-body-changed'])); return $response; }], $this->_dummy_uri(200, ['X-test-1' => 'foo'], 'test1-body'), 'test1-body-changed' ], // Subsequent request execution [ ['X-test-2' => function($request, $response, $client) { return Request::factory($response->headers('X-test-2')); }], $this->_dummy_uri(200, ['X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')], 'test2-orig-body'), 'test2-subsequent-body' ], // No callbacks triggered [ ['X-test-3' => function ($request, $response, $client) { throw new Exception("Unexpected execution of X-test-3 callback"); }], $this->_dummy_uri(200, ['X-test-1' => 'foo'], 'test3-body'), 'test3-body' ], // Callbacks not triggered once a previous callback has created a new response [ [ 'X-test-1' => function($request, $response, $client) { return Request::factory($response->headers('X-test-1')); }, 'X-test-2' => function($request, $response, $client) { return Request::factory($response->headers('X-test-2')); } ], $this->_dummy_uri(200, [ 'X-test-1' => $this->_dummy_uri(200, NULL, 'test1-subsequent-body'), 'X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body') ], 'test2-orig-body'), 'test1-subsequent-body' ], // Nested callbacks are supported if callback creates new request [ [ 'X-test-1' => function($request, $response, $client) { return Request::factory($response->headers('X-test-1')); }, 'X-test-2' => function($request, $response, $client) { return Request::factory($response->headers('X-test-2')); } ], $this->_dummy_uri(200, [ 'X-test-1' => $this->_dummy_uri( 200, ['X-test-2' => $this->_dummy_uri(200, NULL, 'test2-subsequent-body')], 'test1-subsequent-body'), ], 'test-orig-body'), 'test2-subsequent-body' ], ]; } /** * Tests that header callbacks are triggered in sequence when specific headers * are present in the response * * @dataProvider provider_triggers_header_callbacks * * @param array $callbacks Array of header callbacks * @param array $headers Headers that will be received in the response * @param string $expect_body Response body content to expect */ public function test_triggers_header_callbacks($callbacks, $uri, $expect_body) { $response = Request::factory($uri, ['header_callbacks' => $callbacks]) ->execute(); $data = json_decode($response->body(), TRUE); $this->assertEquals($expect_body, $data['body']); } /** * Tests that the Request_Client is protected from too many recursions of * requests triggered by header callbacks. * */ public function test_deep_recursive_callbacks_are_aborted() { $uri = $this->_dummy_uri('200', ['x-cb' => '1'], 'body'); // Temporary property to track requests $this->requests_executed = 0; try { $response = Request::factory( $uri, [ 'header_callbacks' => [ 'x-cb' => function ($request, $response, $client) { $client->callback_params('testcase')->requests_executed++; // Recurse into a new request return Request::factory($request->uri()); }], 'max_callback_depth' => 2, 'callback_params' => [ 'testcase' => $this, ] ]) ->execute(); } catch (Request_Client_Recursion_Exception $e) { // Verify that two requests were executed $this->assertEquals(2, $this->requests_executed); return; } $this->fail('Expected Request_Client_Recursion_Exception was not thrown'); } /** * Header callback for testing that arbitrary callback_params are available * to the callback. * * @param Request $request * @param Response $response * @param Request_Client $client */ public function callback_assert_params($request, $response, $client) { $this->assertEquals('foo', $client->callback_params('constructor_param')); $this->assertEquals('bar', $client->callback_params('setter_param')); $response->body('assertions_ran'); } /** * Test that arbitrary callback_params can be passed to the callback through * the Request_Client and are assigned to subsequent requests */ public function test_client_can_hold_params_for_callbacks() { // Test with param in constructor $request = Request::factory( $this->_dummy_uri( 302, ['Location' => $this->_dummy_uri('200',['X-cb'=>'1'], 'followed')], 'not-followed'), [ 'follow' => TRUE, 'header_callbacks' => [ 'x-cb' => [$this, 'callback_assert_params'], 'location' => 'Request_Client::on_header_location', ], 'callback_params' => [ 'constructor_param' => 'foo' ] ]); // Test passing param to setter $request->client()->callback_params('setter_param', 'bar'); // Callback will throw assertion exceptions when executed $response = $request->execute(); $this->assertEquals('assertions_ran', $response->body()); } } // End KO7_Request_ClientTest /** * Dummy controller class that acts as a shunt - passing back request information * in the response to allow inspection. */ class Controller_RequestClientDummy extends Controller { /** * Takes a urlencoded 'data' parameter from the route and uses it to craft a * response. Redirect chains can be tested by passing another encoded uri * as a location header with an appropriate status code. */ public function action_fake() { parse_str(urldecode($this->request->param('data')), $data); $this->response->status(Arr::get($data, 'status', 200)); $this->response->headers(Arr::get($data, 'header', [])); $this->response->body(json_encode([ 'body'=> Arr::get($data,'body','ok'), 'rq_headers' => $this->request->headers(), 'rq_body' => $this->request->body(), 'rq_method' => $this->request->method(), ])); } } // End Controller_RequestClientDummy