From 9dcb8d647a5607cd9627b5cd4579b8c662a0a642 Mon Sep 17 00:00:00 2001 From: Alexander Obuhovich Date: Tue, 28 Jun 2016 16:06:51 +0300 Subject: [PATCH] Add tests for client classes --- .travis.yml | 11 + CONTRIBUTING.md | 9 + phpunit.xml.dist | 15 +- src/Jira/Api/Client/ClientInterface.php | 7 + src/Jira/Api/Client/CurlClient.php | 31 +- src/Jira/Api/Client/PHPClient.php | 101 +++++-- .../Api/Client/AbstractClientTestCase.php | 280 ++++++++++++++++++ tests/Jira/Api/Client/CurlClientTest.php | 22 ++ tests/Jira/Api/Client/PHPClientTest.php | 22 ++ tests/debug_response.php | 57 ++++ 10 files changed, 517 insertions(+), 38 deletions(-) create mode 100644 tests/Jira/Api/Client/AbstractClientTestCase.php create mode 100644 tests/Jira/Api/Client/CurlClientTest.php create mode 100644 tests/Jira/Api/Client/PHPClientTest.php create mode 100644 tests/debug_response.php diff --git a/.travis.yml b/.travis.yml index bc08f91..9310b67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,19 @@ php: - hhvm - 7.0 +matrix: + allow_failures: + - php: hhvm + install: - composer install --prefer-dist +before_script: + - ~/.phpenv/versions/5.6/bin/php -S localhost:8000 -t $(pwd) > /dev/null 2> /tmp/webserver_output.txt & + - export REPO_URL=http://localhost:8000/ + script: - ./vendor/bin/phpunit + +after_failure: + - cat /tmp/webserver_output.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c066e62..2c6ea8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,15 @@ JIRA REST API Client is an open source, community-driven project. If you'd like 7. Create Pull Request. ## Running the Tests + +To be able to run integration tests locally (optional) please follow these steps once: + +1. make sure repository is located in web server document root (or it's sub-folder) +2. copy `phpunit.xml.dist` file into `phpunit.xml` file +3. in the `phpunit.xml` file: + * uncomment part, where `REPO_URL` environment variable is defined + * set `REPO_URL` environment variable value to URL, from where repository can be accessed (e.g. `http://localhost/path/to/repository/`) + Make sure that you don't break anything with your changes by running: ```bash diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f5f0ffb..491111a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,6 @@ - - - - + tests - \ No newline at end of file + + + diff --git a/src/Jira/Api/Client/ClientInterface.php b/src/Jira/Api/Client/ClientInterface.php index 88391be..f056e07 100644 --- a/src/Jira/Api/Client/ClientInterface.php +++ b/src/Jira/Api/Client/ClientInterface.php @@ -26,6 +26,8 @@ use chobie\Jira\Api\Authentication\AuthenticationInterface; +use chobie\Jira\Api\Exception; +use chobie\Jira\Api\UnauthorizedException; interface ClientInterface { @@ -42,6 +44,11 @@ interface ClientInterface * @param boolean $debug Debug this request. * * @return array|string + * @throws \InvalidArgumentException When non-supported implementation of AuthenticationInterface is given. + * @throws \InvalidArgumentException When data is not an array and http method is GET. + * @throws Exception When request failed due communication error. + * @throws UnauthorizedException When request failed, because user can't be authorized properly. + * @throws Exception When there was empty response instead of needed data. */ public function sendRequest( $method, diff --git a/src/Jira/Api/Client/CurlClient.php b/src/Jira/Api/Client/CurlClient.php index ce7ee83..e475ad8 100644 --- a/src/Jira/Api/Client/CurlClient.php +++ b/src/Jira/Api/Client/CurlClient.php @@ -53,11 +53,11 @@ public function __construct() * @param boolean $debug Debug this request. * * @return array|string - * @throws \Exception When non-supported implementation of AuthenticationInterface is given. - * @throws Exception When request failed due CURL error. + * @throws \InvalidArgumentException When non-supported implementation of AuthenticationInterface is given. + * @throws \InvalidArgumentException When data is not an array and http method is GET. + * @throws Exception When request failed due communication error. * @throws UnauthorizedException When request failed, because user can't be authorized properly. * @throws Exception When there was empty response instead of needed data. - * @throws \InvalidArgumentException When data is not an array and http method is GET. */ public function sendRequest( $method, @@ -69,17 +69,20 @@ public function sendRequest( $debug = false ) { if ( !($credential instanceof Basic) && !($credential instanceof Anonymous) ) { - throw new \Exception(sprintf('CurlClient does not support %s authentication.', get_class($credential))); + throw new \InvalidArgumentException(sprintf( + 'CurlClient does not support %s authentication.', + get_class($credential) + )); } $curl = curl_init(); if ( $method == 'GET' ) { - $url .= '?' . http_build_query($data); - if ( !is_array($data) ) { throw new \InvalidArgumentException('Data must be an array.'); } + + $url .= '?' . http_build_query($data); } curl_setopt($curl, CURLOPT_URL, $endpoint . $url); @@ -96,7 +99,7 @@ public function sendRequest( curl_setopt($curl, CURLOPT_VERBOSE, $debug); if ( $is_file ) { - if ( defined('CURLOPT_SAFE_UPLOAD') ) { + if ( defined('CURLOPT_SAFE_UPLOAD') && PHP_VERSION_ID < 70000 ) { curl_setopt($curl, CURLOPT_SAFE_UPLOAD, false); } @@ -122,7 +125,7 @@ public function sendRequest( curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); } - $data = curl_exec($curl); + $response = curl_exec($curl); $error_number = curl_errno($curl); @@ -139,15 +142,17 @@ public function sendRequest( throw new UnauthorizedException('Unauthorized'); } - if ( $data === '' && !in_array($http_code, array(201, 204)) ) { + if ( $response === '' && !in_array($http_code, array(201, 204)) ) { throw new Exception('JIRA Rest server returns unexpected result.'); } - if ( is_null($data) ) { + // @codeCoverageIgnoreStart + if ( is_null($response) ) { throw new Exception('JIRA Rest server returns unexpected result.'); } + // @codeCoverageIgnoreEnd - return $data; + return $response; } /** @@ -160,10 +165,10 @@ public function sendRequest( protected function getCurlValue($file_string) { if ( !function_exists('curl_file_create') ) { - return $file_string . '; filename=' . basename($file_string); + return $file_string . ';filename=' . basename($file_string); } - return curl_file_create(substr($file_string, 1), null, basename($file_string)); + return curl_file_create(substr($file_string, 1), '', basename($file_string)); } } diff --git a/src/Jira/Api/Client/PHPClient.php b/src/Jira/Api/Client/PHPClient.php index 38b976e..fe822f4 100644 --- a/src/Jira/Api/Client/PHPClient.php +++ b/src/Jira/Api/Client/PHPClient.php @@ -29,6 +29,7 @@ use chobie\Jira\Api\Authentication\AuthenticationInterface; use chobie\Jira\Api\Authentication\Basic; use chobie\Jira\Api\Exception; +use chobie\Jira\Api\UnauthorizedException; class PHPClient implements ClientInterface { @@ -40,6 +41,13 @@ class PHPClient implements ClientInterface */ protected $httpsSupport = false; + /** + * Last error message. + * + * @var string + */ + private $_lastErrorMessage = ''; + /** * Create a traditional php client. */ @@ -57,6 +65,8 @@ public function __construct() * Returns status of HTTP support. * * @return boolean + * + * @codeCoverageIgnore */ protected function isHttpsSupported() { @@ -75,7 +85,12 @@ protected function isHttpsSupported() * @param boolean $debug Debug this request. * * @return array|string - * @throws \Exception When non-supported implementation of AuthenticationInterface is given. + * @throws \InvalidArgumentException When non-supported implementation of AuthenticationInterface is given. + * @throws \InvalidArgumentException When data is not an array and http method is GET. + * @throws Exception When request failed due communication error. + * @throws UnauthorizedException When request failed, because user can't be authorized properly. + * @throws Exception When there was empty response instead of needed data. + * @throws \InvalidArgumentException When "https" wrapper is not available, but http:// is requested. */ public function sendRequest( $method, @@ -87,7 +102,10 @@ public function sendRequest( $debug = false ) { if ( !($credential instanceof Basic) && !($credential instanceof Anonymous) ) { - throw new \Exception(sprintf('PHPClient does not support %s authentication.', get_class($credential))); + throw new \InvalidArgumentException(sprintf( + 'PHPClient does not support %s authentication.', + get_class($credential) + )); } $header = array(); @@ -96,6 +114,10 @@ public function sendRequest( $header[] = 'Authorization: Basic ' . $credential->getCredential(); } + if ( !$is_file ) { + $header[] = 'Content-Type: application/json;charset=UTF-8'; + } + $context = array( 'http' => array( 'method' => $method, @@ -103,11 +125,7 @@ public function sendRequest( ), ); - if ( !$is_file ) { - $header[] = 'Content-Type: application/json;charset=UTF-8'; - } - - if ( $method == 'POST' || $method == 'PUT' ) { + if ( $method == 'POST' || $method == 'PUT' || $method == 'DELETE' ) { if ( $is_file ) { $filename = preg_replace('/^@/', '', $data['file']); $boundary = '--------------------------' . microtime(true); @@ -129,41 +147,84 @@ public function sendRequest( $context['http']['header'] = implode("\r\n", $header); $context['http']['content'] = $__data; } - else { + elseif ( $method == 'GET' ) { + if ( !is_array($data) ) { + throw new \InvalidArgumentException('Data must be an array.'); + } + $url .= '?' . http_build_query($data); } + // @codeCoverageIgnoreStart if ( strpos($endpoint, 'https://') === 0 && !$this->isHttpsSupported() ) { - throw new \Exception('does not support https wrapper. please enable openssl extension'); + throw new \InvalidArgumentException('does not support https wrapper. please enable openssl extension'); + } + // @codeCoverageIgnoreEnd + + list ($http_code, $response, $error_message) = $this->doSendRequest($endpoint . $url, $context); + + // Check for 401 code before "$error_message" checking, because it's considered as an error. + if ( $http_code == 401 ) { + throw new UnauthorizedException('Unauthorized'); + } + + if ( !empty($error_message) ) { + throw new Exception( + sprintf('Jira request failed: "%s"', $error_message) + ); + } + + if ( $response === '' && !in_array($http_code, array(201, 204)) ) { + throw new Exception('JIRA Rest server returns unexpected result.'); } + // @codeCoverageIgnoreStart + if ( is_null($response) ) { + throw new Exception('JIRA Rest server returns unexpected result.'); + } + // @codeCoverageIgnoreEnd + + return $response; + } + + /** + * Sends the request. + * + * @param string $url URL. + * @param array $context Context. + * + * @return array + */ + protected function doSendRequest($url, array $context) + { + $this->_lastErrorMessage = ''; + set_error_handler(array($this, 'errorHandler')); - $data = file_get_contents( - $endpoint . $url, - false, - stream_context_create($context) - ); + $response = file_get_contents($url, false, stream_context_create($context)); restore_error_handler(); - if ( is_null($data) ) { - throw new \Exception('JIRA Rest server returns unexpected result.'); + if ( isset($http_response_header) ) { + preg_match('#HTTP/\d+\.\d+ (\d+)#', $http_response_header[0], $matches); + $http_code = $matches[1]; + } + else { + $http_code = 0; } - return $data; + return array($http_code, $response, $this->_lastErrorMessage); } /** - * Throws exception on error. + * Remembers last error. * * @param integer $errno Error number. * @param string $errstr Error message. * * @return void - * @throws \Exception Always. */ public function errorHandler($errno, $errstr) { - throw new \Exception($errstr); + $this->_lastErrorMessage = $errstr; } } diff --git a/tests/Jira/Api/Client/AbstractClientTestCase.php b/tests/Jira/Api/Client/AbstractClientTestCase.php new file mode 100644 index 0000000..a5e4032 --- /dev/null +++ b/tests/Jira/Api/Client/AbstractClientTestCase.php @@ -0,0 +1,280 @@ +markTestSkipped('The "REPO_URL" environment variable not set.'); + } + + $this->client = $this->createClient(); + } + + public function testGetRequest() + { + $data = array('param1' => 'value1', 'param2' => 'value2'); + $trace_result = $this->traceRequest(Api::REQUEST_GET, $data); + + $this->assertEquals('GET', $trace_result['_SERVER']['REQUEST_METHOD']); + $this->assertContentType('application/json;charset=UTF-8', $trace_result); + $this->assertEquals($data, $trace_result['_GET']); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Data must be an array. + */ + public function testGetRequestError() + { + $this->traceRequest(Api::REQUEST_GET, 'param1=value1¶m2=value2'); + } + + public function testPostRequest() + { + $data = array('param1' => 'value1', 'param2' => 'value2'); + $trace_result = $this->traceRequest(Api::REQUEST_POST, $data); + + $this->assertEquals('POST', $trace_result['_SERVER']['REQUEST_METHOD']); + $this->assertContentType('application/json;charset=UTF-8', $trace_result); + $this->assertEquals(json_encode($data), $trace_result['INPUT']); + } + + public function testPutRequest() + { + $data = array('param1' => 'value1', 'param2' => 'value2'); + $trace_result = $this->traceRequest(Api::REQUEST_PUT, $data); + + $this->assertEquals('PUT', $trace_result['_SERVER']['REQUEST_METHOD']); + $this->assertContentType('application/json;charset=UTF-8', $trace_result); + $this->assertEquals(json_encode($data), $trace_result['INPUT']); + } + + public function testDeleteRequest() + { + $data = array('param1' => 'value1', 'param2' => 'value2'); + $trace_result = $this->traceRequest(Api::REQUEST_DELETE, $data); + + $this->assertEquals('DELETE', $trace_result['_SERVER']['REQUEST_METHOD']); + $this->assertContentType('application/json;charset=UTF-8', $trace_result); + $this->assertEquals(json_encode($data), $trace_result['INPUT']); + } + + public function testFileUpload() + { + $upload_file = __FILE__; + $trace_result = $this->traceRequest(Api::REQUEST_POST, array('file' => '@' . $upload_file), null, true); + + $this->assertEquals('POST', $trace_result['_SERVER']['REQUEST_METHOD']); + + $this->assertArrayHasKey('HTTP_X_ATLASSIAN_TOKEN', $trace_result['_SERVER']); + $this->assertEquals('nocheck', $trace_result['_SERVER']['HTTP_X_ATLASSIAN_TOKEN']); + + $this->assertCount( + 1, + $trace_result['_FILES'], + 'File was uploaded' + ); + $this->assertArrayHasKey( + 'file', + $trace_result['_FILES'], + 'File was uploaded under "file" field name' + ); + $this->assertEquals( + basename($upload_file), + $trace_result['_FILES']['file']['name'], + 'Basename is used as filename' + ); + $this->assertNotEmpty($trace_result['_FILES']['file']['type']); + $this->assertEquals( + UPLOAD_ERR_OK, + $trace_result['_FILES']['file']['error'], + 'No upload error happened' + ); + $this->assertGreaterThan( + 0, + $trace_result['_FILES']['file']['size'], + 'File is not empty' + ); + } + + public function testUnsupportedCredentialGiven() + { + $client_class_parts = explode('\\', get_class($this->client)); + $credential = $this->prophesize('chobie\Jira\Api\Authentication\AuthenticationInterface')->reveal(); + + $this->setExpectedException( + 'InvalidArgumentException', + end($client_class_parts) . ' does not support ' . get_class($credential) . ' authentication.' + ); + + $this->client->sendRequest(Api::REQUEST_GET, 'url', array(), 'endpoint', $credential); + } + + public function testBasicCredentialGiven() + { + $credential = new Basic('user1', 'pass1'); + + $trace_result = $this->traceRequest(Api::REQUEST_GET, array(), $credential); + + $this->assertArrayHasKey('PHP_AUTH_USER', $trace_result['_SERVER']); + $this->assertEquals('user1', $trace_result['_SERVER']['PHP_AUTH_USER']); + + $this->assertArrayHasKey('PHP_AUTH_PW', $trace_result['_SERVER']); + $this->assertEquals('pass1', $trace_result['_SERVER']['PHP_AUTH_PW']); + } + + public function testCommunicationError() + { + $this->markTestIncomplete('TODO'); + } + + /** + * @expectedException \chobie\Jira\Api\UnauthorizedException + * @expectedExceptionMessage Unauthorized + */ + public function testUnauthorizedRequest() + { + $this->traceRequest(Api::REQUEST_GET, array('http_code' => 401)); + } + + /** + * @expectedException \chobie\Jira\Api\Exception + * @expectedExceptionMessage JIRA Rest server returns unexpected result. + */ + public function testEmptyResponseWithUnknownHttpCode() + { + $this->traceRequest(Api::REQUEST_GET, array('response_mode' => 'empty')); + } + + /** + * @dataProvider emptyResponseWithKnownHttpCodeDataProvider + */ + public function testEmptyResponseWithKnownHttpCode($http_code) + { + $this->assertSame( + '', + $this->traceRequest(Api::REQUEST_GET, array('http_code' => $http_code, 'response_mode' => 'empty')) + ); + } + + public function emptyResponseWithKnownHttpCodeDataProvider() + { + return array( + 'http 201' => array(201), + 'http 204' => array(204), + ); + } + + /** + * Checks, that request contained specified content type. + * + * @param string $expected Expected. + * @param array $trace_result Trace result. + * + * @return void + */ + protected function assertContentType($expected, array $trace_result) + { + if ( array_key_exists('CONTENT_TYPE', $trace_result['_SERVER']) ) { + // Normal Web Server. + $content_type = $trace_result['_SERVER']['CONTENT_TYPE']; + } + elseif ( array_key_exists('HTTP_CONTENT_TYPE', $trace_result['_SERVER']) ) { + // PHP Built-In Web Server. + $content_type = $trace_result['_SERVER']['HTTP_CONTENT_TYPE']; + } + else { + $content_type = null; + } + + $this->assertEquals($expected, $content_type, 'Content type is correct'); + } + + /** + * Traces a request. + * + * @param string $method Request method. + * @param array $data Request data. + * @param AuthenticationInterface|null $credential Credential. + * @param boolean $is_file This is a file upload request. + * + * @return array + */ + protected function traceRequest( + $method, + $data = array(), + AuthenticationInterface $credential = null, + $is_file = false + ) { + if ( !isset($credential) ) { + $credential = new Anonymous(); + } + + $path_info_variables = array( + 'http_code' => 200, + 'response_mode' => 'trace', + ); + + if ( is_array($data) ) { + if ( isset($data['http_code']) ) { + $path_info_variables['http_code'] = $data['http_code']; + unset($data['http_code']); + } + + if ( isset($data['response_mode']) ) { + $path_info_variables['response_mode'] = $data['response_mode']; + unset($data['response_mode']); + } + } + + $path_info = array(); + + foreach ( $path_info_variables as $variable_name => $variable_value ) { + $path_info[] = $variable_name; + $path_info[] = $variable_value; + } + + $result = $this->client->sendRequest( + $method, + '/tests/debug_response.php/' . implode('/', $path_info) . '/', + $data, + rtrim($_SERVER['REPO_URL'], '/'), + $credential, + $is_file + ); + + if ( $path_info_variables['response_mode'] === 'trace' ) { + return unserialize($result); + } + + return $result; + } + + /** + * Creates client. + * + * @return ClientInterface + */ + abstract protected function createClient(); + +} diff --git a/tests/Jira/Api/Client/CurlClientTest.php b/tests/Jira/Api/Client/CurlClientTest.php new file mode 100644 index 0000000..84f7bf3 --- /dev/null +++ b/tests/Jira/Api/Client/CurlClientTest.php @@ -0,0 +1,22 @@ + $_SERVER, + '_GET' => $_GET, + '_POST' => $_POST, + '_COOKIE' => $_COOKIE, + '_FILES' => $_FILES, + 'INPUT' => file_get_contents('php://input'), +); + +$path_info_variables = array(); +$path_info = explode('/', trim($_SERVER['PATH_INFO'], '/')); + +while ( $path_info ) { + $variable_name = array_shift($path_info); + $variable_value = array_shift($path_info); + + $path_info_variables[$variable_name] = $variable_value; +} + +if ( array_key_exists('http_code', $path_info_variables) ) { + // Complete code list: https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes. + $http_code_messages = array( + 200 => 'OK', + 201 => 'Created', + 204 => 'No Content', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + ); + + $http_code = (int)$path_info_variables['http_code']; + + if ( $http_code !== 200 ) { + header('HTTP/1.1 ' . $http_code . ' ' . $http_code_messages[$http_code]); + } +} + +if ( array_key_exists('response_mode', $path_info_variables) ) { + $response_mode = $path_info_variables['response_mode']; +} +else { + $response_mode = 'trace'; +} + +if ( $response_mode === 'trace' ) { + echo serialize($result); +} +elseif ( $response_mode === 'empty' ) { + echo ''; +} +elseif ( $response_mode === 'null' ) { + echo null; +} +