diff --git a/src/Redmine/Api/AbstractApi.php b/src/Redmine/Api/AbstractApi.php index f7001a27..2cff81e0 100644 --- a/src/Redmine/Api/AbstractApi.php +++ b/src/Redmine/Api/AbstractApi.php @@ -9,6 +9,7 @@ use Redmine\Exception; use Redmine\Exception\SerializerException; use Redmine\Http\HttpClient; +use Redmine\Http\HttpFactory; use Redmine\Http\Request; use Redmine\Http\Response; use Redmine\Serializer\JsonSerializer; @@ -73,7 +74,7 @@ final protected function getHttpClient(): HttpClient final protected function getLastResponse(): Response { - return $this->lastResponse !== null ? $this->lastResponse : $this->createResponse(0, '', ''); + return $this->lastResponse !== null ? $this->lastResponse : HttpFactory::makeResponse(0, '', ''); } /** @@ -412,16 +413,12 @@ private function getResponseAsArray(Response $response): array private function handleClient(Client $client): HttpClient { - $responseFactory = Closure::fromCallable([$this, 'createResponse']); - - return new class ($client, $responseFactory) implements HttpClient { + return new class ($client) implements HttpClient { private $client; - private $responseFactory; - public function __construct(Client $client, Closure $responseFactory) + public function __construct(Client $client) { $this->client = $client; - $this->responseFactory = $responseFactory; } public function request(Request $request): Response @@ -436,7 +433,7 @@ public function request(Request $request): Response $this->client->requestGet($request->getPath()); } - return ($this->responseFactory)( + return HttpFactory::makeResponse( $this->client->getLastResponseStatusCode(), $this->client->getLastResponseContentType(), $this->client->getLastResponseBody() @@ -445,37 +442,6 @@ public function request(Request $request): Response }; } - private function createResponse(int $statusCode, string $contentType, string $body): Response - { - return new class ($statusCode, $contentType, $body) implements Response { - private $statusCode; - private $contentType; - private $body; - - public function __construct(int $statusCode, string $contentType, string $body) - { - $this->statusCode = $statusCode; - $this->contentType = $contentType; - $this->body = $body; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } - - public function getContentType(): string - { - return $this->contentType; - } - - public function getContent(): string - { - return $this->body; - } - }; - } - private function createRequest(string $method, string $path, string $contentType, string $content = ''): Request { return new class ($method, $path, $contentType, $content) implements Request { diff --git a/src/Redmine/Client/NativeCurlClient.php b/src/Redmine/Client/NativeCurlClient.php index 40709797..f2129020 100644 --- a/src/Redmine/Client/NativeCurlClient.php +++ b/src/Redmine/Client/NativeCurlClient.php @@ -5,11 +5,15 @@ namespace Redmine\Client; use Redmine\Exception\ClientException; +use Redmine\Http\HttpClient; +use Redmine\Http\HttpFactory; +use Redmine\Http\Request; +use Redmine\Http\Response; /** * Native cURL client. */ -final class NativeCurlClient implements Client +final class NativeCurlClient implements Client, HttpClient { use ClientApiTrait; @@ -55,6 +59,27 @@ public function __construct( } } + /** + * Create and send a HTTP request and return the response + * + * @throws ClientException If anything goes wrong on creating or sending the request + */ + public function request(Request $request): Response + { + $this->runRequest( + $request->getMethod(), + $request->getPath(), + $request->getContent(), + $request->getContentType() + ); + + return HttpFactory::makeResponse( + $this->lastResponseStatusCode, + $this->lastResponseContentType, + $this->lastResponseBody + ); + } + /** * Sets to an existing username so api calls can be * impersonated to this user. @@ -77,7 +102,7 @@ public function stopImpersonateUser(): void */ public function requestGet(string $path): bool { - return $this->request('get', $path); + return $this->runRequest('GET', $path); } /** @@ -85,7 +110,7 @@ public function requestGet(string $path): bool */ public function requestPost(string $path, string $body): bool { - return $this->request('post', $path, $body); + return $this->runRequest('POST', $path, $body); } /** @@ -93,7 +118,7 @@ public function requestPost(string $path, string $body): bool */ public function requestPut(string $path, string $body): bool { - return $this->request('put', $path, $body); + return $this->runRequest('PUT', $path, $body); } /** @@ -101,7 +126,7 @@ public function requestPut(string $path, string $body): bool */ public function requestDelete(string $path): bool { - return $this->request('delete', $path); + return $this->runRequest('DELETE', $path); } /** @@ -211,13 +236,13 @@ private function unsetHttpHeader(string $name): void /** * @throws ClientException If anything goes wrong on curl request */ - private function request(string $method, string $path, string $body = ''): bool + private function runRequest(string $method, string $path, string $body = '', string $contentType = ''): bool { $this->lastResponseStatusCode = 0; $this->lastResponseContentType = ''; $this->lastResponseBody = ''; - $curl = $this->createCurl($method, $path, $body); + $curl = $this->createCurl($method, $path, $body, $contentType); $response = curl_exec($curl); @@ -249,7 +274,7 @@ private function request(string $method, string $path, string $body = ''): bool * * @return \CurlHandle a cURL handle on success, FALSE on errors */ - private function createCurl(string $method, string $path, string $body = '') + private function createCurl(string $method, string $path, string $body = '', string $contentType = '') { // General cURL options $curlOptions = [ @@ -264,13 +289,13 @@ private function createCurl(string $method, string $path, string $body = '') $curlOptions[CURLOPT_URL] = $this->url . $path; // Set the HTTP request headers - $curlOptions[CURLOPT_HTTPHEADER] = $this->createHttpHeader($path); + $curlOptions[CURLOPT_HTTPHEADER] = $this->createHttpHeader($path, $contentType); unset($curlOptions[CURLOPT_CUSTOMREQUEST]); unset($curlOptions[CURLOPT_POST]); unset($curlOptions[CURLOPT_POSTFIELDS]); switch ($method) { - case 'post': + case 'POST': $curlOptions[CURLOPT_POST] = 1; if ($this->isUploadCall($path) && $this->isValidFilePath($body)) { @trigger_error('Uploading an attachment by filepath is deprecated, use file_get_contents() to upload the file content instead.', E_USER_DEPRECATED); @@ -286,13 +311,13 @@ private function createCurl(string $method, string $path, string $body = '') $curlOptions[CURLOPT_POSTFIELDS] = $body; } break; - case 'put': + case 'PUT': $curlOptions[CURLOPT_CUSTOMREQUEST] = 'PUT'; if ($body !== '') { $curlOptions[CURLOPT_POSTFIELDS] = $body; } break; - case 'delete': + case 'DELETE': $curlOptions[CURLOPT_CUSTOMREQUEST] = 'DELETE'; break; default: // GET @@ -314,7 +339,7 @@ private function createCurl(string $method, string $path, string $body = '') return $curl; } - private function createHttpHeader(string $path): array + private function createHttpHeader(string $path, string $contentType = ''): array { // Additional request headers $httpHeaders = [ @@ -352,14 +377,18 @@ private function createHttpHeader(string $path): array // Now set or reset mandatory headers // Content type headers - $tmp = parse_url($this->url . $path); - - if ($this->isUploadCall($path)) { - $httpHeaders[] = 'Content-Type: application/octet-stream'; - } elseif ('json' === substr($tmp['path'], -4)) { - $httpHeaders[] = 'Content-Type: application/json'; - } elseif ('xml' === substr($tmp['path'], -3)) { - $httpHeaders[] = 'Content-Type: text/xml'; + if ($contentType !== '') { + $httpHeaders[] = 'Content-Type: ' . $contentType; + } else { + $tmp = parse_url($this->url . $path); + + if ($this->isUploadCall($path)) { + $httpHeaders[] = 'Content-Type: application/octet-stream'; + } elseif ('json' === substr($tmp['path'], -4)) { + $httpHeaders[] = 'Content-Type: application/json'; + } elseif ('xml' === substr($tmp['path'], -3)) { + $httpHeaders[] = 'Content-Type: text/xml'; + } } return $httpHeaders; diff --git a/src/Redmine/Client/Psr18Client.php b/src/Redmine/Client/Psr18Client.php index d5b24566..f5f1899b 100644 --- a/src/Redmine/Client/Psr18Client.php +++ b/src/Redmine/Client/Psr18Client.php @@ -13,11 +13,15 @@ use Psr\Http\Message\ServerRequestFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; use Redmine\Exception\ClientException; +use Redmine\Http\HttpClient; +use Redmine\Http\HttpFactory; +use Redmine\Http\Request; +use Redmine\Http\Response; /** * Psr18 client. */ -final class Psr18Client implements Client +final class Psr18Client implements Client, HttpClient { use ClientApiTrait; @@ -73,6 +77,27 @@ public function __construct( $this->password = $password; } + /** + * Create and send a HTTP request and return the response + * + * @throws ClientException If anything goes wrong on creating or sending the request + */ + public function request(Request $request): Response + { + $response = $this->runRequest( + $request->getMethod(), + $request->getPath(), + $request->getContent(), + $request->getContentType() + ); + + return HttpFactory::makeResponse( + $response->getStatusCode(), + $response->getHeaderLine('Content-Type'), + strval($response->getBody()) + ); + } + /** * Sets to an existing username so api calls can be * impersonated to this user. @@ -92,34 +117,58 @@ public function stopImpersonateUser(): void /** * Create and send a GET request. + * + * @throws ClientException If anything goes wrong on the request + * + * @return bool true if status code of the response is not 4xx oder 5xx */ public function requestGet(string $path): bool { - return $this->runRequest('GET', $path); + $response = $this->runRequest('GET', $path); + + return $response->getStatusCode() < 400; } /** * Create and send a POST request. + * + * @throws ClientException If anything goes wrong on the request + * + * @return bool true if status code of the response is not 4xx oder 5xx */ public function requestPost(string $path, string $body): bool { - return $this->runRequest('POST', $path, $body); + $response = $this->runRequest('POST', $path, $body); + + return $response->getStatusCode() < 400; } /** * Create and send a PUT request. + * + * @throws ClientException If anything goes wrong on the request + * + * @return bool true if status code of the response is not 4xx oder 5xx */ public function requestPut(string $path, string $body): bool { - return $this->runRequest('PUT', $path, $body); + $response = $this->runRequest('PUT', $path, $body); + + return $response->getStatusCode() < 400; } /** * Create and send a DELETE request. + * + * @throws ClientException If anything goes wrong on the request + * + * @return bool true if status code of the response is not 4xx oder 5xx */ public function requestDelete(string $path): bool { - return $this->runRequest('DELETE', $path); + $response = $this->runRequest('DELETE', $path); + + return $response->getStatusCode() < 400; } /** @@ -162,14 +211,12 @@ public function getLastResponseBody(): string * Create and run a request. * * @throws ClientException If anything goes wrong on the request - * - * @return bool true if status code of the response is not 4xx oder 5xx */ - private function runRequest(string $method, string $path, string $body = ''): bool + private function runRequest(string $method, string $path, string $body = '', string $contentType = ''): ResponseInterface { $this->lastResponse = null; - $request = $this->createRequest($method, $path, $body); + $request = $this->createRequest($method, $path, $body, $contentType); try { $this->lastResponse = $this->httpClient->sendRequest($request); @@ -177,10 +224,10 @@ private function runRequest(string $method, string $path, string $body = ''): bo throw new ClientException($e->getMessage(), $e->getCode(), $e); } - return $this->lastResponse->getStatusCode() < 400; + return $this->lastResponse; } - private function createRequest(string $method, string $path, string $body = ''): RequestInterface + private function createRequest(string $method, string $path, string $body = '', string $contentType = ''): RequestInterface { $request = $this->requestFactory->createRequest( $method, @@ -228,14 +275,19 @@ private function createRequest(string $method, string $path, string $body = ''): } // set Content-Type header - $tmp = parse_url($this->url . $path); - - if ($this->isUploadCall($path)) { - $request = $request->withHeader('Content-Type', 'application/octet-stream'); - } elseif ('json' === substr($tmp['path'], -4)) { - $request = $request->withHeader('Content-Type', 'application/json'); - } elseif ('xml' === substr($tmp['path'], -3)) { - $request = $request->withHeader('Content-Type', 'text/xml'); + if ($contentType !== '') { + $request = $request->withHeader('Content-Type', $contentType); + } else { + // guess Content-Type from path + $tmp = parse_url($this->url . $path); + + if ($this->isUploadCall($path)) { + $request = $request->withHeader('Content-Type', 'application/octet-stream'); + } elseif ('json' === substr($tmp['path'], -4)) { + $request = $request->withHeader('Content-Type', 'application/json'); + } elseif ('xml' === substr($tmp['path'], -3)) { + $request = $request->withHeader('Content-Type', 'text/xml'); + } } return $request; diff --git a/src/Redmine/Http/HttpFactory.php b/src/Redmine/Http/HttpFactory.php new file mode 100644 index 00000000..61b18dc1 --- /dev/null +++ b/src/Redmine/Http/HttpFactory.php @@ -0,0 +1,44 @@ +statusCode = $statusCode; + $this->contentType = $contentType; + $this->body = $body; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getContentType(): string + { + return $this->contentType; + } + + public function getContent(): string + { + return $this->body; + } + }; + } +} diff --git a/tests/End2End/Group/GroupTest.php b/tests/End2End/Group/GroupTest.php index b2d3faf2..2f033fc9 100644 --- a/tests/End2End/Group/GroupTest.php +++ b/tests/End2End/Group/GroupTest.php @@ -29,11 +29,13 @@ public function testInteractionWithGroup(RedmineVersion $redmineVersion): void 'name' => $groupName, ]); - $groupData = json_decode(json_encode($xmlData), true); + $jsonData = json_encode($xmlData); - $this->assertIsArray($groupData, json_encode($groupData)); - $this->assertIsString($groupData['id']); - $this->assertSame($groupName, $groupData['name']); + $groupData = json_decode($jsonData, true); + + $this->assertIsArray($groupData, $jsonData); + $this->assertIsString($groupData['id'], $jsonData); + $this->assertSame($groupName, $groupData['name'], $jsonData); $groupId = (int) $groupData['id']; diff --git a/tests/Unit/Client/NativeCurlClient/RequestTest.php b/tests/Unit/Client/NativeCurlClient/RequestTest.php new file mode 100644 index 00000000..b1cf4fe5 --- /dev/null +++ b/tests/Unit/Client/NativeCurlClient/RequestTest.php @@ -0,0 +1,95 @@ +createMock(stdClass::class); + + $curlInit = $this->getFunctionMock($namespace, 'curl_init'); + $curlInit->expects($this->exactly(1))->willReturn($curl); + + $curlExec = $this->getFunctionMock($namespace, 'curl_exec'); + $curlExec->expects($this->exactly(1))->willReturn($content); + + $curlSetoptArray = $this->getFunctionMock($namespace, 'curl_setopt_array'); + + $curlGetinfo = $this->getFunctionMock($namespace, 'curl_getinfo'); + $curlGetinfo->expects($this->exactly(2))->will($this->returnValueMap(([ + [$curl, CURLINFO_HTTP_CODE, $statusCode], + [$curl, CURLINFO_CONTENT_TYPE, $contentType], + ]))); + + $curlErrno = $this->getFunctionMock($namespace, 'curl_errno'); + $curlErrno->expects($this->exactly(1))->willReturn(CURLE_OK); + + $curlClose = $this->getFunctionMock($namespace, 'curl_close'); + + $client = new NativeCurlClient( + 'http://test.local', + 'access_token' + ); + + $request = $this->createConfiguredMock(Request::class, [ + 'getMethod' => $method, + 'getPath' => '/path', + 'getContentType' => $contentType, + 'getContent' => $data, + ]); + + $response = $client->request($request); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame($statusCode, $response->getStatusCode()); + $this->assertSame($contentType, $response->getContentType()); + $this->assertSame($content, $response->getContent()); + } + + public static function getRequestReponseData(): array + { + return [ + ['GET', '', 101, '', ''], + ['GET', '', 101, 'text/plain', ''], + ['GET', '', 200, 'application/json', '{"foo_bar": 12345}'], + ['GET', '', 301, 'application/json', ''], + ['GET', '', 404, 'application/json', '{"title": "404 Not Found"}'], + ['GET', '', 500, 'text/plain', 'Internal Server Error'], + ['POST', '{"foo":"bar"}', 101, 'text/plain', ''], + ['POST', '{"foo":"bar"}', 200, 'application/json', '{"foo_bar": 12345}'], + ['POST', '{"foo":"bar"}', 301, 'application/json', ''], + ['POST', '{"foo":"bar"}', 404, 'application/json', '{"title": "404 Not Found"}'], + ['POST', '{"foo":"bar"}', 500, 'text/plain', 'Internal Server Error'], + ['PUT', '{"foo":"bar"}', 101, 'text/plain', ''], + ['PUT', '{"foo":"bar"}', 200, 'application/json', '{"foo_bar": 12345}'], + ['PUT', '{"foo":"bar"}', 301, 'application/json', ''], + ['PUT', '{"foo":"bar"}', 404, 'application/json', '{"title": "404 Not Found"}'], + ['PUT', '{"foo":"bar"}', 500, 'text/plain', 'Internal Server Error'], + ['DELETE', '', 101, 'text/plain', ''], + ['DELETE', '', 200, 'application/json', '{"foo_bar": 12345}'], + ['DELETE', '', 301, 'application/json', ''], + ['DELETE', '', 404, 'application/json', '{"title": "404 Not Found"}'], + ['DELETE', '', 500, 'text/plain', 'Internal Server Error'], + ]; + } +} diff --git a/tests/Unit/Client/NativeCurlClientTest.php b/tests/Unit/Client/NativeCurlClientTest.php index caf0ddf1..ec317453 100644 --- a/tests/Unit/Client/NativeCurlClientTest.php +++ b/tests/Unit/Client/NativeCurlClientTest.php @@ -10,6 +10,7 @@ use PHPUnit\Framework\TestCase; use Redmine\Client\Client; use Redmine\Client\NativeCurlClient; +use Redmine\Http\HttpClient; use stdClass; class NativeCurlClientTest extends TestCase @@ -44,6 +45,7 @@ public function shouldPassApiKeyToConstructor() $this->assertInstanceOf(NativeCurlClient::class, $client); $this->assertInstanceOf(Client::class, $client); + $this->assertInstanceOf(HttpClient::class, $client); } /** diff --git a/tests/Unit/Client/Psr18Client/RequestTest.php b/tests/Unit/Client/Psr18Client/RequestTest.php new file mode 100644 index 00000000..3230c888 --- /dev/null +++ b/tests/Unit/Client/Psr18Client/RequestTest.php @@ -0,0 +1,138 @@ +createConfiguredMock(ClientInterface::class, [ + 'sendRequest' => $this->createConfiguredMock(ResponseInterface::class, [ + 'getStatusCode' => $statusCode, + 'getHeaderLine' => $contentType, + 'getBody' => $this->createConfiguredMock(StreamInterface::class, [ + '__toString' => $content, + ]), + ]) + ]); + + $requestFactory = $this->createConfiguredMock(RequestFactoryInterface::class, [ + 'createRequest' => (function () { + $request = $this->createMock(RequestInterface::class); + $request->method('withHeader')->willReturn($request); + $request->method('withBody')->willReturn($request); + + return $request; + })(), + ]); + + $client = new Psr18Client( + $httpClient, + $requestFactory, + $this->createMock(StreamFactoryInterface::class), + 'http://test.local', + 'access_token' + ); + + $request = $this->createConfiguredMock(Request::class, [ + 'getMethod' => $method, + 'getPath' => '/path', + 'getContentType' => $contentType, + 'getContent' => $data, + ]); + + $response = $client->request($request); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame($statusCode, $response->getStatusCode()); + $this->assertSame($contentType, $response->getContentType()); + $this->assertSame($content, $response->getContent()); + } + + public static function getRequestReponseData(): array + { + return [ + ['GET', '', 101, '', ''], + ['GET', '', 101, 'text/plain', ''], + ['GET', '', 200, 'application/json', '{"foo_bar": 12345}'], + ['GET', '', 301, 'application/xml', ''], + ['GET', '', 404, 'application/json', '{"title": "404 Not Found"}'], + ['GET', '', 500, 'text/plain', 'Internal Server Error'], + ['POST', '{"foo":"bar"}', 101, 'text/plain', ''], + ['POST', '{"foo":"bar"}', 200, 'application/json', '{"foo_bar": 12345}'], + ['POST', '{"foo":"bar"}', 301, 'application/json', ''], + ['POST', '{"foo":"bar"}', 404, 'application/json', '{"title": "404 Not Found"}'], + ['POST', '{"foo":"bar"}', 500, 'text/plain', 'Internal Server Error'], + ['PUT', '{"foo":"bar"}', 101, 'text/plain', ''], + ['PUT', '{"foo":"bar"}', 200, 'application/json', '{"foo_bar": 12345}'], + ['PUT', '{"foo":"bar"}', 301, 'application/json', ''], + ['PUT', '{"foo":"bar"}', 404, 'application/json', '{"title": "404 Not Found"}'], + ['PUT', '{"foo":"bar"}', 500, 'text/plain', 'Internal Server Error'], + ['DELETE', '', 101, 'text/plain', ''], + ['DELETE', '', 200, 'application/json', '{"foo_bar": 12345}'], + ['DELETE', '', 301, 'application/json', ''], + ['DELETE', '', 404, 'application/json', '{"title": "404 Not Found"}'], + ['DELETE', '', 500, 'text/plain', 'Internal Server Error'], + ]; + } + + public function testRequestThrowsClientException() + { + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->expects($this->exactly(1))->method('sendRequest')->willThrowException( + new class ('error message') extends Exception implements ClientExceptionInterface {} + ); + + $requestFactory = $this->createConfiguredMock(RequestFactoryInterface::class, [ + 'createRequest' => (function () { + $request = $this->createMock(RequestInterface::class); + $request->method('withHeader')->willReturn($request); + $request->method('withBody')->willReturn($request); + + return $request; + })(), + ]); + + $client = new Psr18Client( + $httpClient, + $requestFactory, + $this->createMock(StreamFactoryInterface::class), + 'http://test.local', + 'access_token' + ); + + $request = $this->createConfiguredMock(Request::class, [ + 'getMethod' => 'GET', + 'getPath' => '/path', + 'getContentType' => 'application/json', + 'getContent' => '', + ]); + + $this->expectException(ClientException::class); + $this->expectExceptionMessage('error message'); + + $client->request($request); + } +} diff --git a/tests/Unit/Client/Psr18ClientTest.php b/tests/Unit/Client/Psr18ClientTest.php index 8e47eea2..7a9bf49b 100644 --- a/tests/Unit/Client/Psr18ClientTest.php +++ b/tests/Unit/Client/Psr18ClientTest.php @@ -2,6 +2,7 @@ namespace Redmine\Tests\Unit\Client; +use Exception; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Psr\Http\Client\ClientInterface; @@ -9,10 +10,13 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; use Redmine\Client\Client; use Redmine\Client\Psr18Client; +use Redmine\Http\HttpClient; +use stdClass; class Psr18ClientTest extends TestCase { @@ -32,17 +36,25 @@ public function shouldPassApiKeyToConstructor() $this->assertInstanceOf(Psr18Client::class, $client); $this->assertInstanceOf(Client::class, $client); + $this->assertInstanceOf(HttpClient::class, $client); } /** * @covers \Redmine\Client\Psr18Client - * @test */ - public function acceptServerRequestFactoryInConstructorForBC() + public function testServerRequestFactoryIsAcceptedInConstructorForBC() { $client = new Psr18Client( $this->createMock(ClientInterface::class), - $this->createMock(ServerRequestFactoryInterface::class), + $this->createConfiguredMock(ServerRequestFactoryInterface::class, [ + 'createServerRequest' => (function () { + $request = $this->createMock(ServerRequestInterface::class); + $request->method('withHeader')->willReturn($request); + $request->method('withBody')->willReturn($request); + + return $request; + })(), + ]), $this->createMock(StreamFactoryInterface::class), 'http://test.local', 'access_token' @@ -50,6 +62,8 @@ public function acceptServerRequestFactoryInConstructorForBC() $this->assertInstanceOf(Psr18Client::class, $client); $this->assertInstanceOf(Client::class, $client); + + $client->requestGet('/path.xml'); } /** @@ -296,6 +310,23 @@ public static function getApiClassesProvider(): array ]; } + /** + * @covers \Redmine\Client\Psr18Client::__construct + */ + public function testCreateWithoutFactoryThrowsException() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Redmine\Client\Psr18Client::__construct(): Argument #2 ($requestFactory) must be of type Psr\Http\Message\RequestFactoryInterface'); + + $client = new Psr18Client( + $this->createMock(ClientInterface::class), + new stdClass(), + $this->createMock(StreamFactoryInterface::class), + 'http://test.local', + 'access_token' + ); + } + /** * @covers \Redmine\Client\Psr18Client * @test diff --git a/tests/Unit/Http/HttpFactory/MakeResponseTest.php b/tests/Unit/Http/HttpFactory/MakeResponseTest.php new file mode 100644 index 00000000..407b90ec --- /dev/null +++ b/tests/Unit/Http/HttpFactory/MakeResponseTest.php @@ -0,0 +1,25 @@ +assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json', $response->getContentType()); + $this->assertSame('content', $response->getContent()); + } +}