From 6a095e56f9a6c4089b294f9de1a25e51b84279e6 Mon Sep 17 00:00:00 2001 From: Filda Date: Mon, 3 Aug 2020 12:41:55 +0200 Subject: [PATCH 01/12] Update AbstractRequester.php $requestBody parameter can be also array --- src/AbstractRequester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AbstractRequester.php b/src/AbstractRequester.php index bacf51e..42f9cd6 100644 --- a/src/AbstractRequester.php +++ b/src/AbstractRequester.php @@ -125,7 +125,7 @@ public function withQuery($query) } /** - * @param null $requestBody + * @param array|null $requestBody * @return $this */ public function withRequestBody($requestBody) From 4300039fbb5e907e84352b71c968d8a862cc1bb3 Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Thu, 17 Sep 2020 16:39:29 +0200 Subject: [PATCH 02/12] Add basic support for additional properties settings --- src/Base/Body.php | 23 +++++++- tests/OpenApiRequestBodyTest.php | 54 ++++++++++++++++++ tests/example/openapi.json | 94 +++++++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 2 deletions(-) diff --git a/src/Base/Body.php b/src/Base/Body.php index 77b2abb..325a89c 100644 --- a/src/Base/Body.php +++ b/src/Base/Body.php @@ -17,6 +17,7 @@ abstract class Body { const SWAGGER_PROPERTIES="properties"; const SWAGGER_REQUIRED="required"; + const SWAGGER_ADDITIONAL_PROPERTIES = "additionalProperties"; /** * @var Schema @@ -224,7 +225,27 @@ function () use ($name, $schema, $body, $type) */ public function matchObjectProperties($name, $schema, $body) { - if (!isset($schema[self::SWAGGER_PROPERTIES])) { + $hasNoProps = !isset($schema[self::SWAGGER_PROPERTIES]); + + if (isset($schema[self::SWAGGER_ADDITIONAL_PROPERTIES])) { + if (!is_array($body)) { + throw new InvalidRequestException( + "I expected an array here, but I got an string. Maybe you did wrong request?", + $body + ); + } + $additionalPropsSettings = $schema[self::SWAGGER_ADDITIONAL_PROPERTIES]; + + // Empty object or true means no validation + if ($additionalPropsSettings === true || empty($additionalPropsSettings)) { + // TODO: handle additionalProps with properties + return true; + } else { + throw new GenericSwaggerException('Type check of additional properties not implemented'); + } + } + + if ($hasNoProps) { return null; } diff --git a/tests/OpenApiRequestBodyTest.php b/tests/OpenApiRequestBodyTest.php index 52bec97..35976ce 100644 --- a/tests/OpenApiRequestBodyTest.php +++ b/tests/OpenApiRequestBodyTest.php @@ -273,4 +273,58 @@ public function testMatchRequestBodyRequired_Issue21_Required() $requestParameter = $this->openApiSchema2()->getRequestParameters('/accounts/create', 'post'); $requestParameter->match($body); } + + public function testAdditionalPropertiesWithTrue() + { + // Missing Request + $body = [ + "anything" => "2013-02-12", + ]; + + $requestParameter = $this->openApiSchema()->getRequestParameters('/test-additional-props', 'post'); + $this->assertTrue($requestParameter->match($body)); + } + + /** + * @throws \ByJG\ApiTools\Exception\DefinitionNotFoundException + * @throws \ByJG\ApiTools\Exception\GenericSwaggerException + * @throws \ByJG\ApiTools\Exception\HttpMethodNotFoundException + * @throws \ByJG\ApiTools\Exception\InvalidDefinitionException + * @throws \ByJG\ApiTools\Exception\InvalidRequestException + * @throws \ByJG\ApiTools\Exception\NotMatchedException + * @throws \ByJG\ApiTools\Exception\PathNotFoundException + * @throws \ByJG\ApiTools\Exception\RequiredArgumentNotFound + */ + public function testAdditionalPropertiesWithEmptyObject() + { + // Missing Request + $body = [ + "anything" => "2013-02-12", + ]; + + $requestParameter = $this->openApiSchema()->getRequestParameters('/test-additional-props-empty-object', 'post'); + $this->assertTrue($requestParameter->match($body)); + } + + /** + * @throws \ByJG\ApiTools\Exception\DefinitionNotFoundException + * @throws \ByJG\ApiTools\Exception\GenericSwaggerException + * @throws \ByJG\ApiTools\Exception\HttpMethodNotFoundException + * @throws \ByJG\ApiTools\Exception\InvalidDefinitionException + * @throws \ByJG\ApiTools\Exception\InvalidRequestException + * @throws \ByJG\ApiTools\Exception\NotMatchedException + * @throws \ByJG\ApiTools\Exception\PathNotFoundException + * @throws \ByJG\ApiTools\Exception\RequiredArgumentNotFound + */ + public function testAdditionalPropertiesWithTypeString() + { + // Missing Request + $body = [ + "anything" => "2013-02-12", + ]; + + $requestParameter = $this->openApiSchema()->getRequestParameters('/test-additional-props-type-string', 'post'); + $this->expectExceptionMessage('Type check of additional properties not implemented'); + $this->assertTrue($requestParameter->match($body)); + } } diff --git a/tests/example/openapi.json b/tests/example/openapi.json index 458a8fc..56db757 100644 --- a/tests/example/openapi.json +++ b/tests/example/openapi.json @@ -891,6 +891,98 @@ } } } + }, + "/test-additional-props": { + "post": { + "summary": "TestAdditionalProps", + "description": "", + "operationId": "additionalProps", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + } + } + }, + "/test-additional-props-empty-object": { + "post": { + "summary": "TestAdditionalPropsObject", + "description": "", + "operationId": "additionalPropsEmptyObject", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": {} + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + } + } + }, + "/test-additional-props-type-string": { + "post": { + "summary": "TestAdditionalPropsTypeString", + "description": "", + "operationId": "additionalPropsTypeString", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + } + } } }, "externalDocs": { @@ -1234,4 +1326,4 @@ } } } -} \ No newline at end of file +} From 70695c928e03a37bad69a18a415001177aa6f3cf Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Fri, 25 Sep 2020 11:08:03 +0200 Subject: [PATCH 03/12] Check if the string type value is string --- src/Base/Body.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Base/Body.php b/src/Base/Body.php index 325a89c..b75dc12 100644 --- a/src/Base/Body.php +++ b/src/Base/Body.php @@ -92,6 +92,10 @@ protected function matchString($name, $schema, $body, $type) return null; } + if (!is_string($body)) { + throw new NotMatchedException("Value in '$name' is not string.", $this->structure); + } + if (isset($schema['enum']) && !in_array($body, $schema['enum'])) { throw new NotMatchedException("Value '$body' in '$name' not matched in ENUM. ", $this->structure); } From 3316c0688ca07f9f025bb8d0e41c4d965c9f8698 Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Fri, 25 Sep 2020 11:23:05 +0200 Subject: [PATCH 04/12] Add support for allOf https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ --- src/Base/Body.php | 38 +++++++++++++ tests/OpenApiResponseAllOffTest.php | 64 +++++++++++++++++++++ tests/example/openapi_allOf.json | 88 +++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 tests/OpenApiResponseAllOffTest.php create mode 100644 tests/example/openapi_allOf.json diff --git a/src/Base/Body.php b/src/Base/Body.php index b75dc12..2f16f2a 100644 --- a/src/Base/Body.php +++ b/src/Base/Body.php @@ -320,6 +320,12 @@ protected function matchSchema($name, $schema, $body) return true; } + // All of + // @link https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/ + if (isset($schema['allOf'])) { + return $this->matchAllOfSchema($name, $schema['allOf'], $body); + } + if(!isset($schema['$ref']) && isset($schema['content'])) { $schema['$ref'] = $schema['content'][key($schema['content'])]['schema']['$ref']; } @@ -348,6 +354,38 @@ protected function matchSchema($name, $schema, $body) throw new GenericSwaggerException("Not all cases are defined. Please open an issue about this. Schema: $name"); } + /** + * @param string $name + * @param array|mixed $allOf + * @param array $body + * + * @return bool + * @throws DefinitionNotFoundException + * @throws GenericSwaggerException + * @throws InvalidDefinitionException + * @throws InvalidRequestException + * @throws NotMatchedException + */ + protected function matchAllOfSchema($name, $allOf, $body) + { + if (!is_array($allOf) || empty($allOf)) { + return false; + } + + // Merge the schemas + $mergedSchemas = []; + foreach ($allOf as $itemSchema) { + if (isset($itemSchema['type']) && $itemSchema['type'] !== 'object') { + throw new GenericSwaggerException('An entry of allOf must be object in '.$name); + } + + $mergedSchemas = array_merge_recursive($mergedSchemas, $itemSchema); + } + + $mergedSchemas['type'] = 'object'; + return $this->matchSchema($name, $mergedSchemas, $body); + } + /** * @param $name * @param $body diff --git a/tests/OpenApiResponseAllOffTest.php b/tests/OpenApiResponseAllOffTest.php new file mode 100644 index 0000000..92c692f --- /dev/null +++ b/tests/OpenApiResponseAllOffTest.php @@ -0,0 +1,64 @@ +openapiObject = new OpenApiSchema(file_get_contents(__DIR__ . '/example/openapi_allOf.json')); + } + + public function testAllOf() + { + $body = [ + 'this' => 'is', + 'the' => 'way', + ]; + $responseParameters = $this->openapiObject->getResponseParameters('/allOf', 'get', 200); + $this->assertTrue($responseParameters->match($body)); + } + + public function testAllOfWithMissingRequiredAttribute() + { + $body = [ + 'this' => 'is', + ]; + $responseParameters = $this->openapiObject->getResponseParameters('/allOf', 'get', 200); + $this->expectExceptionMessage("Required property 'the' in 'get 200 /allOf' not found in object"); + $responseParameters->match($body); + } + + public function testAllOfInvalidOptionalProperty() + { + $body = [ + 'this' => 'is', + 'the' => 'way', + 'jedi' => ['sith'] + ]; + $responseParameters = $this->openapiObject->getResponseParameters('/allOf', 'get', 200); + $this->expectExceptionMessage("Value in 'jedi' is not string"); + $this->assertFalse($responseParameters->match($body)); + } + + public function testAllOfInvalid() + { + $body = [ + 'this' => 'is', + 'the' => 'way', + ]; + $responseParameters = $this->openapiObject->getResponseParameters('/allOfInvalid', 'get', 200); + $this->expectExceptionMessage('An entry of allOf must be object in get 200 /allOfInvalid'); + $responseParameters->match($body); + } +} diff --git a/tests/example/openapi_allOf.json b/tests/example/openapi_allOf.json new file mode 100644 index 0000000..babec9f --- /dev/null +++ b/tests/example/openapi_allOf.json @@ -0,0 +1,88 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Validate OpenApi Schema #27", + "version": "1.0.0", + "title": "Validate OpenApi Schema #27", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "paths": { + "/allOf": { + "get": { + "responses": { + "200": { + "description": "Empty response", + "content": { + "application/json": { + "schema": { + "type": "object", + "allOf": [ + { + "type": "object", + "required": ["this"], + "properties": { + "this": { + "type": "string" + } + } + }, + { + "type": "object", + "required": ["the"], + "properties": { + "the": { + "type": "string" + }, + "jedi": { + "type": "string", + "nullable": false + } + } + } + ] + } + } + } + } + } + } + }, + "/allOfInvalid": { + "get": { + "responses": { + "200": { + "description": "Empty response", + "content": { + "application/json": { + "schema": { + "type": "object", + "allOf": [ + { + "type": "object", + "required": ["this"], + "properties": { + "this": { + "type": "string" + } + } + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } +} From 58430041fa5bf643aa1567f7b26eef8fa6a37bea Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Mon, 20 Apr 2020 23:16:15 +0200 Subject: [PATCH 05/12] Add trait for asserting schema requests By default we can extend provided test case, for situations where we can't extend the test case we can use trait AssertRequestAgainstSchema. --- src/ApiTestCase.php | 114 +-------------------------- src/AssertRequestAgainstSchema.php | 121 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 113 deletions(-) create mode 100644 src/AssertRequestAgainstSchema.php diff --git a/src/ApiTestCase.php b/src/ApiTestCase.php index 7b01001..d65769a 100644 --- a/src/ApiTestCase.php +++ b/src/ApiTestCase.php @@ -3,122 +3,10 @@ namespace ByJG\ApiTools; use ByJG\ApiTools\Base\BaseTestCase; -use ByJG\ApiTools\Base\Schema; -use ByJG\ApiTools\Exception\DefinitionNotFoundException; -use ByJG\ApiTools\Exception\GenericSwaggerException; -use ByJG\ApiTools\Exception\HttpMethodNotFoundException; -use ByJG\ApiTools\Exception\InvalidDefinitionException; -use ByJG\ApiTools\Exception\NotMatchedException; -use ByJG\ApiTools\Exception\PathNotFoundException; -use ByJG\ApiTools\Exception\StatusCodeNotMatchedException; use GuzzleHttp\GuzzleException; use PHPUnit\Framework\TestCase; abstract class ApiTestCase extends TestCase { - /** - * @var Schema - */ - protected $schema; - - /** - * configure the schema to use for requests - * - * When set, all requests without an own schema use this one instead. - * - * @param Schema|null $schema - */ - public function setSchema($schema) - { - $this->schema = $schema; - } - - /** - * @param string $method The HTTP Method: GET, PUT, DELETE, POST, etc - * @param string $path The REST path call - * @param int $statusExpected - * @param array|null $query - * @param array|null $requestBody - * @param array $requestHeader - * @return mixed - * @throws DefinitionNotFoundException - * @throws GenericSwaggerException - * @throws HttpMethodNotFoundException - * @throws InvalidDefinitionException - * @throws NotMatchedException - * @throws PathNotFoundException - * @throws StatusCodeNotMatchedException - * @throws \GuzzleHttp\Exception\GuzzleException - * @deprecated Use assertRequest instead - */ - protected function makeRequest( - $method, - $path, - $statusExpected = 200, - $query = null, - $requestBody = null, - $requestHeader = [] - ) { - $this->checkSchema(); - $requester = new ApiRequester(); - $body = $requester - ->withSchema($this->schema) - ->withMethod($method) - ->withPath($path) - ->withQuery($query) - ->withRequestBody($requestBody) - ->withRequestHeader($requestHeader) - ->assertResponseCode($statusExpected) - ->send(); - - // Note: - // This code is only reached if the send is successful and - // all matches are satisfied. Otherwise an error is throwed before - // reach this - $this->assertTrue(true); - - return $body; - } - - /** - * @param AbstractRequester $request - * @return mixed - * @throws DefinitionNotFoundException - * @throws GenericSwaggerException - * @throws HttpMethodNotFoundException - * @throws InvalidDefinitionException - * @throws NotMatchedException - * @throws PathNotFoundException - * @throws StatusCodeNotMatchedException - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function assertRequest(AbstractRequester $request) - { - // Add own schema if nothing is passed. - if (!$request->hasSchema()) { - $this->checkSchema(); - $request->withSchema($this->schema); - } - - // Request based on the Swagger Request definitios - $body = $request->send(); - - // Note: - // This code is only reached if the send is successful and - // all matches are satisfied. Otherwise an error is throwed before - // reach this - $this->assertTrue(true); - - return $body; - } - - /** - * @throws GenericSwaggerException - */ - protected function checkSchema() - { - if (!$this->schema) { - throw new GenericSwaggerException('You have to configure a schema for either the request or the testcase'); - } - } + use AssertRequestAgainstSchema; } diff --git a/src/AssertRequestAgainstSchema.php b/src/AssertRequestAgainstSchema.php new file mode 100644 index 0000000..f7b3252 --- /dev/null +++ b/src/AssertRequestAgainstSchema.php @@ -0,0 +1,121 @@ +schema = $schema; + } + + /** + * @param string $method The HTTP Method: GET, PUT, DELETE, POST, etc + * @param string $path The REST path call + * @param int $statusExpected + * @param array|null $query + * @param array|null $requestBody + * @param array $requestHeader + * @return mixed + * @throws DefinitionNotFoundException + * @throws GenericSwaggerException + * @throws HttpMethodNotFoundException + * @throws InvalidDefinitionException + * @throws NotMatchedException + * @throws PathNotFoundException + * @throws StatusCodeNotMatchedException + * @throws \GuzzleHttp\Exception\GuzzleException + * @deprecated Use assertRequest instead + */ + protected function makeRequest( + $method, + $path, + $statusExpected = 200, + $query = null, + $requestBody = null, + $requestHeader = [] + ) { + $this->checkSchema(); + $requester = new ApiRequester(); + $body = $requester + ->withSchema($this->schema) + ->withMethod($method) + ->withPath($path) + ->withQuery($query) + ->withRequestBody($requestBody) + ->withRequestHeader($requestHeader) + ->assertResponseCode($statusExpected) + ->send(); + + // Note: + // This code is only reached if the send is successful and + // all matches are satisfied. Otherwise an error is throwed before + // reach this + $this->assertTrue(true); + + return $body; + } + + /** + * @param AbstractRequester $request + * @return mixed + * @throws DefinitionNotFoundException + * @throws GenericSwaggerException + * @throws HttpMethodNotFoundException + * @throws InvalidDefinitionException + * @throws NotMatchedException + * @throws PathNotFoundException + * @throws StatusCodeNotMatchedException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function assertRequest(AbstractRequester $request) + { + // Add own schema if nothing is passed. + if (!$request->hasSchema()) { + $this->checkSchema(); + $request->withSchema($this->schema); + } + + // Request based on the Swagger Request definitios + $body = $request->send(); + + // Note: + // This code is only reached if the send is successful and + // all matches are satisfied. Otherwise an error is throwed before + // reach this + $this->assertTrue(true); + + return $body; + } + + /** + * @throws GenericSwaggerException + */ + protected function checkSchema() + { + if (!$this->schema) { + throw new GenericSwaggerException('You have to configure a schema for either the request or the testcase'); + } + } +} From b425193b62fa4eba755da8a488c02cd7557e7d30 Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Wed, 22 Apr 2020 12:39:45 +0200 Subject: [PATCH 06/12] Make AbstractRequester more abstract for custom non-psr clients To allow custom requester that is not making a http request (like laravel's test cases) --- src/AbstractRequester.php | 45 +++++++++++------------------- src/ApiRequester.php | 31 +++++++++++++++++--- src/Response/PsrResponse.php | 30 ++++++++++++++++++++ src/Response/ResponseInterface.php | 11 ++++++++ 4 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 src/Response/PsrResponse.php create mode 100644 src/Response/ResponseInterface.php diff --git a/src/AbstractRequester.php b/src/AbstractRequester.php index 42f9cd6..b166a47 100644 --- a/src/AbstractRequester.php +++ b/src/AbstractRequester.php @@ -5,11 +5,7 @@ use ByJG\ApiTools\Base\Schema; use ByJG\ApiTools\Exception\NotMatchedException; use ByJG\ApiTools\Exception\StatusCodeNotMatchedException; -use GuzzleHttp\Exception\BadResponseException; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Psr7\Request; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use ByJG\ApiTools\Response\ResponseInterface; /** * Abstract baseclass for request handlers. @@ -43,13 +39,16 @@ public function __construct() /** * abstract function to be implemented by derived classes * - * This function must be implemented by derived classes. It should process - * the given request and return an according response. + * This function must be implemented by derived classes. It should build a + * request to given path and headers and return a ResponseInterface (even for failed + * request). + * + * @param string $path + * @param array $headers * - * @param RequestInterface $request * @return ResponseInterface */ - abstract protected function handleRequest(RequestInterface $request); + abstract protected function handleRequest($path, $headers); /** * @param Schema $schema @@ -155,7 +154,7 @@ public function assertHeaderContains($header, $contains) * @throws Exception\HttpMethodNotFoundException * @throws Exception\InvalidDefinitionException * @throws Exception\PathNotFoundException - * @throws GuzzleException + * @throws Exception\GenericSwaggerException * @throws NotMatchedException * @throws StatusCodeNotMatchedException */ @@ -179,7 +178,6 @@ public function send() ); // Defining Variables - $serverUrl = $this->schema->getServerUrl(); $basePath = $this->schema->getBasePath(); $pathName = $this->path; @@ -187,25 +185,14 @@ public function send() $bodyRequestDef = $this->schema->getRequestParameters("$basePath$pathName", $this->method); $bodyRequestDef->match($this->requestBody); - // Make the request - $request = new Request( - $this->method, - $serverUrl . $pathName . $paramInQuery, - $header, - json_encode($this->requestBody) - ); - $statusReturned = null; - try { - $response = $this->handleRequest($request); - $responseHeader = $response->getHeaders(); - $responseBody = json_decode((string) $response->getBody(), true); - $statusReturned = $response->getStatusCode(); - } catch (BadResponseException $ex) { - $responseHeader = $ex->getResponse()->getHeaders(); - $responseBody = json_decode((string) $ex->getResponse()->getBody(), true); - $statusReturned = $ex->getResponse()->getStatusCode(); - } + // Run the request + $response = $this->handleRequest($pathName . $paramInQuery, $header); + + // Get the response + $responseHeader = $response->getHeaders(); + $responseBody = json_decode((string) $response->getBody(), true); + $statusReturned = $response->getStatusCode(); // Assert results if ($this->statusExpected != $statusReturned) { diff --git a/src/ApiRequester.php b/src/ApiRequester.php index 9b77b9e..87248b4 100644 --- a/src/ApiRequester.php +++ b/src/ApiRequester.php @@ -2,10 +2,13 @@ namespace ByJG\ApiTools; +use ByJG\ApiTools\Response\ResponseInterface; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Exception\BadResponseException; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Request; +use ByJG\ApiTools\Response\PsrResponse; /** * Request handler based on a Guzzle client. @@ -17,11 +20,31 @@ class ApiRequester extends AbstractRequester public function __construct() { + parent::__construct(); $this->guzzleHttpClient = new Client(['headers' => ['User-Agent' => 'Swagger Test']]); } - protected function handleRequest(RequestInterface $request) + /** + * @param string $path + * @param array $headers + * + * @return ResponseInterface + * @throws GuzzleException + */ + protected function handleRequest($path, $headers) { - return $this->guzzleHttpClient->send($request, ['allow_redirects' => false]); + // Make the request + $request = new Request( + $this->method, + $this->schema->getServerUrl() . $path, + $headers, + json_encode($this->requestBody) + ); + + try { + return new PsrResponse($this->guzzleHttpClient->send($request, ['allow_redirects' => false])); + } catch (BadResponseException $ex) { + return new PsrResponse($ex->getResponse()); + } } } diff --git a/src/Response/PsrResponse.php b/src/Response/PsrResponse.php new file mode 100644 index 0000000..e9609f4 --- /dev/null +++ b/src/Response/PsrResponse.php @@ -0,0 +1,30 @@ +interface = $interface; + } + + public function getHeaders() + { + return $this->interface->getHeaders(); + } + + public function getStatusCode() + { + return $this->interface->getStatusCode(); + } + + public function getBody() + { + return $this->interface->getBody(); + } + +} diff --git a/src/Response/ResponseInterface.php b/src/Response/ResponseInterface.php new file mode 100644 index 0000000..27551a8 --- /dev/null +++ b/src/Response/ResponseInterface.php @@ -0,0 +1,11 @@ + Date: Mon, 15 Jun 2020 22:08:33 +0200 Subject: [PATCH 07/12] Support date schema match --- src/Base/Body.php | 29 ++++++++++++++++++++++++ tests/OpenApiRequestBodyTest.php | 27 ++++++++++++++++++++++ tests/example/openapi.json | 39 ++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/src/Base/Body.php b/src/Base/Body.php index 2f16f2a..43259e4 100644 --- a/src/Base/Body.php +++ b/src/Base/Body.php @@ -170,6 +170,30 @@ protected function matchArray($name, $schema, $body, $type) return true; } + /** + * Checks if the value is valid date. + * + * @param $name + * @param $schema + * @param $body + * @param $type + * + * @return bool|null + * @throws NotMatchedException + */ + protected function matchDate($name, $schema, $body, $type) + { + if ($type !== 'date') { + return null; + } + + if (!(bool)strtotime($body)) { + throw new NotMatchedException("Expected '$name' to be date, but found '$body'. ", $this->structure); + } + + return true; + } + protected function matchTypes($name, $schema, $body) { if (!isset($schema['type'])) { @@ -203,6 +227,11 @@ function () use ($name, $body, $type) function () use ($name, $schema, $body, $type) { return $this->matchArray($name, $schema, $body, $type); + }, + + function () use ($name, $schema, $body, $type) + { + return $this->matchDate($name, $schema, $body, $type); } ]; diff --git a/tests/OpenApiRequestBodyTest.php b/tests/OpenApiRequestBodyTest.php index 35976ce..7e57827 100644 --- a/tests/OpenApiRequestBodyTest.php +++ b/tests/OpenApiRequestBodyTest.php @@ -2,6 +2,8 @@ namespace Test; +use ByJG\ApiTools\Exception\NotMatchedException; + class OpenApiRequestBodyTest extends OpenApiBodyTestCase { /** @@ -327,4 +329,29 @@ public function testAdditionalPropertiesWithTypeString() $this->expectExceptionMessage('Type check of additional properties not implemented'); $this->assertTrue($requestParameter->match($body)); } + + public function testMatchRequestBodyMatchesDate() + { + // Missing Request + $body = [ + "date" => "2013-02-12", + ]; + + + $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); + $this->assertTrue($requestParameter->match($body)); + } + + public function testMatchRequestBodyMatchesDateAndThrowsInvalid() + { + // Missing Request + $body = [ + "date" => "test", + ]; + + $this->expectException(NotMatchedException::class); + $this->expectExceptionMessage("Expected 'date' to be date, but found 'test'. "); + $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); + $this->assertFalse($requestParameter->match($body)); + } } diff --git a/tests/example/openapi.json b/tests/example/openapi.json index 56db757..a87b6ef 100644 --- a/tests/example/openapi.json +++ b/tests/example/openapi.json @@ -892,6 +892,45 @@ } } }, + "/store-dates": { + "post": { + "summary": "Get date", + "description": "", + "operationId": "storeDates", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "date" + ], + "properties": { + "date": { + "description": "First date", + "type": "date" + } + } + } + } + }, + "description": "Updated dates", + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + } + } + }, "/test-additional-props": { "post": { "summary": "TestAdditionalProps", From a2f17be0a56b6924701ee9913e5747ec429779b6 Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Tue, 16 Jun 2020 11:56:34 +0200 Subject: [PATCH 08/12] Use correct date format validation --- src/Base/Body.php | 54 +++++++++++---------------- src/Base/StringFormatValidator.php | 36 ++++++++++++++++++ src/OpenApi/OpenApiSchema.php | 17 +++++---- src/Swagger/SwaggerSchema.php | 14 ++++--- tests/OpenApiRequestBodyTest.php | 59 ++++++++++++++++++++++++++++-- tests/OpenApiResponseBodyTest.php | 12 +++--- tests/SwaggerRequestBodyTest.php | 4 +- tests/SwaggerResponseBodyTest.php | 12 +++--- tests/example/openapi.json | 16 ++++++-- 9 files changed, 158 insertions(+), 66 deletions(-) create mode 100644 src/Base/StringFormatValidator.php diff --git a/src/Base/Body.php b/src/Base/Body.php index 43259e4..2d49e28 100644 --- a/src/Base/Body.php +++ b/src/Base/Body.php @@ -35,6 +35,10 @@ abstract class Body * @var bool */ protected $allowNullValues; + /** + * @var StringFormatValidator + */ + private $stringFormatValidator; /** * Body constructor. @@ -44,7 +48,7 @@ abstract class Body * @param array $structure * @param bool $allowNullValues */ - public function __construct(Schema $schema, $name, $structure, $allowNullValues = false) + public function __construct(Schema $schema, $name, $structure, $allowNullValues, StringFormatValidator $stringFormatValidator) { $this->schema = $schema; $this->name = $name; @@ -53,6 +57,7 @@ public function __construct(Schema $schema, $name, $structure, $allowNullValues } $this->structure = $structure; $this->allowNullValues = $allowNullValues; + $this->stringFormatValidator = $stringFormatValidator; } /** @@ -65,12 +70,14 @@ public function __construct(Schema $schema, $name, $structure, $allowNullValues */ public static function getInstance(Schema $schema, $name, $structure, $allowNullValues = false) { + $validator = new StringFormatValidator(); + if ($schema instanceof SwaggerSchema) { - return new SwaggerResponseBody($schema, $name, $structure, $allowNullValues); + return new SwaggerResponseBody($schema, $name, $structure, $allowNullValues, $validator); } if ($schema instanceof OpenApiSchema) { - return new OpenApiResponseBody($schema, $name, $structure, $allowNullValues); + return new OpenApiResponseBody($schema, $name, $structure, $allowNullValues, $validator); } throw new GenericSwaggerException("Cannot get instance SwaggerBody or SchemaBody from " . get_class($schema)); @@ -100,6 +107,18 @@ protected function matchString($name, $schema, $body, $type) throw new NotMatchedException("Value '$body' in '$name' not matched in ENUM. ", $this->structure); } + if (isset($schema['format'])) { + if (is_string($schema['format']) === false) { + throw new NotMatchedException("Format in '$name' is not string. ", $this->structure); + } + + $isValid = $this->stringFormatValidator->validate($schema['format'], $body); + + if ($isValid === false) { + throw new NotMatchedException("Value '$body' in '$name' has invalid format ({$schema['format']}). ", $this->structure); + } + } + return true; } @@ -170,30 +189,6 @@ protected function matchArray($name, $schema, $body, $type) return true; } - /** - * Checks if the value is valid date. - * - * @param $name - * @param $schema - * @param $body - * @param $type - * - * @return bool|null - * @throws NotMatchedException - */ - protected function matchDate($name, $schema, $body, $type) - { - if ($type !== 'date') { - return null; - } - - if (!(bool)strtotime($body)) { - throw new NotMatchedException("Expected '$name' to be date, but found '$body'. ", $this->structure); - } - - return true; - } - protected function matchTypes($name, $schema, $body) { if (!isset($schema['type'])) { @@ -228,11 +223,6 @@ function () use ($name, $schema, $body, $type) { return $this->matchArray($name, $schema, $body, $type); }, - - function () use ($name, $schema, $body, $type) - { - return $this->matchDate($name, $schema, $body, $type); - } ]; foreach ($validators as $validator) { diff --git a/src/Base/StringFormatValidator.php b/src/Base/StringFormatValidator.php new file mode 100644 index 0000000..a394885 --- /dev/null +++ b/src/Base/StringFormatValidator.php @@ -0,0 +1,36 @@ +getPathDefinition($path, $method); + $hasRequestBody = isset($structure['requestBody']); + $parameters = $hasRequestBody ? $structure['requestBody'] : []; - if (!isset($structure['requestBody'])) { - return new OpenApiRequestBody($this, "$method $path", []); - } - return new OpenApiRequestBody($this, "$method $path", $structure['requestBody']); + return new OpenApiRequestBody($this, "$method $path", $parameters, false, new StringFormatValidator()); } public function setServerVariable($var, $value) diff --git a/src/Swagger/SwaggerSchema.php b/src/Swagger/SwaggerSchema.php index 99eeeab..b718500 100644 --- a/src/Swagger/SwaggerSchema.php +++ b/src/Swagger/SwaggerSchema.php @@ -2,8 +2,11 @@ namespace ByJG\ApiTools\Swagger; +use ByJG\ApiTools\Base\Body; use ByJG\ApiTools\Base\Schema; +use ByJG\ApiTools\Base\StringFormatValidator; use ByJG\ApiTools\Exception\DefinitionNotFoundException; +use ByJG\ApiTools\Exception\GenericSwaggerException; use ByJG\ApiTools\Exception\HttpMethodNotFoundException; use ByJG\ApiTools\Exception\InvalidDefinitionException; use ByJG\ApiTools\Exception\NotMatchedException; @@ -96,21 +99,22 @@ public function getDefinition($name) /** * @param $path * @param $method - * @return SwaggerRequestBody + * + * @return Body * @throws DefinitionNotFoundException * @throws HttpMethodNotFoundException * @throws InvalidDefinitionException * @throws NotMatchedException * @throws PathNotFoundException + * @throws GenericSwaggerException */ public function getRequestParameters($path, $method) { $structure = $this->getPathDefinition($path, $method); + $hasSwaggerParameters = isset($structure[self::SWAGGER_PARAMETERS]); + $parameters = $hasSwaggerParameters ? $structure[self::SWAGGER_PARAMETERS] : []; - if (!isset($structure[self::SWAGGER_PARAMETERS])) { - return new SwaggerRequestBody($this, "$method $path", []); - } - return new SwaggerRequestBody($this, "$method $path", $structure[self::SWAGGER_PARAMETERS]); + return new SwaggerRequestBody($this, "$method $path", $parameters, false, new StringFormatValidator()); } /** diff --git a/tests/OpenApiRequestBodyTest.php b/tests/OpenApiRequestBodyTest.php index 7e57827..a590ae5 100644 --- a/tests/OpenApiRequestBodyTest.php +++ b/tests/OpenApiRequestBodyTest.php @@ -22,7 +22,7 @@ public function testMatchRequestBody() "id" => "10", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -330,19 +330,70 @@ public function testAdditionalPropertiesWithTypeString() $this->assertTrue($requestParameter->match($body)); } - public function testMatchRequestBodyMatchesDate() + public function testMatchRequestBodyMatchesStringWithDateFormat() { // Missing Request $body = [ "date" => "2013-02-12", ]; + $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); + $this->assertTrue($requestParameter->match($body)); + } + + public function testMatchRequestBodyMatchesStringWithAnyFormat() + { + // Missing Request + $body = [ + "any_format" => "rock", + ]; $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); $this->assertTrue($requestParameter->match($body)); } - public function testMatchRequestBodyMatchesDateAndThrowsInvalid() + public function testMatchRequestBodyMatchesStringWithDateTimeFormat() + { + // Missing Request + $datesTimes = [ + '2000-01-01T01:00:00+1200', + '2010-10-20T17:32:28Z', + ]; + + foreach ($datesTimes as $time) { + $body = [ + "date_time" => $time, + ]; + + $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); + $this->assertTrue($requestParameter->match($body)); + } + } + + public function testMatchRequestBodyMatchesStringWithDateTimeFormatAndThrowsInvalid() + { + // Missing Request + $datesTimes = [ + '01-01T01:00:00+1200', + '0000-01-01T01:00:00+1200', + '2000-30-01T01:00:00+1200', + '2000-01-01T01:00:00+01', + ]; + + foreach ($datesTimes as $time) { + $body = [ + "date_time" => $time, + ]; + + $this->expectException(NotMatchedException::class); + $this->expectExceptionMessage("Value '{$time}' in 'date_time' has invalid format (date-time). "); + + $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); + $this->assertFalse($requestParameter->match($body)); + } + } + + public function testMatchRequestBodyMatchesStringWithDateFormatAndThrowsInvalid() { // Missing Request $body = [ @@ -350,7 +401,7 @@ public function testMatchRequestBodyMatchesDateAndThrowsInvalid() ]; $this->expectException(NotMatchedException::class); - $this->expectExceptionMessage("Expected 'date' to be date, but found 'test'. "); + $this->expectExceptionMessage("Value 'test' in 'date' has invalid format (date). "); $requestParameter = $this->openApiSchema()->getRequestParameters('/store-dates', 'post'); $this->assertFalse($requestParameter->match($body)); } diff --git a/tests/OpenApiResponseBodyTest.php b/tests/OpenApiResponseBodyTest.php index 867edf3..ba9e1e5 100644 --- a/tests/OpenApiResponseBodyTest.php +++ b/tests/OpenApiResponseBodyTest.php @@ -21,7 +21,7 @@ public function testMatchResponseBody() "id" => 10, "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -34,7 +34,7 @@ public function testMatchResponseBody() "id" => 10, "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed' ]; @@ -46,7 +46,7 @@ public function testMatchResponseBody() "id" => "10", "petId" => "50", "quantity" => "1", - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -73,7 +73,7 @@ public function testMatchResponseBodyEnumError() "id" => 10, "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'notfound', "complete" => true ]; @@ -100,7 +100,7 @@ public function testMatchResponseBodyWrongNumber() "id" => "ABC", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -127,7 +127,7 @@ public function testMatchResponseBodyMoreThanExpected() "id" => "50", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true, "more" => "value" diff --git a/tests/SwaggerRequestBodyTest.php b/tests/SwaggerRequestBodyTest.php index 33bbae2..17270c3 100644 --- a/tests/SwaggerRequestBodyTest.php +++ b/tests/SwaggerRequestBodyTest.php @@ -20,7 +20,7 @@ public function testMatchRequestBody() "id" => "10", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -67,7 +67,7 @@ public function testMatchInexistantBodyDefinition() "id" => "10", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; diff --git a/tests/SwaggerResponseBodyTest.php b/tests/SwaggerResponseBodyTest.php index cafee4a..cf61a00 100644 --- a/tests/SwaggerResponseBodyTest.php +++ b/tests/SwaggerResponseBodyTest.php @@ -21,7 +21,7 @@ public function testMatchResponseBody() "id" => 10, "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -33,7 +33,7 @@ public function testMatchResponseBody() "id" => 10, "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed' ]; $responseParameter = $schema->getResponseParameters('/v2/store/order', 'post', 200); @@ -44,7 +44,7 @@ public function testMatchResponseBody() "id" => "10", "petId" => "50", "quantity" => "1", - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -70,7 +70,7 @@ public function testMatchResponseBodyEnumError() "id" => 10, "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'notfound', "complete" => true ]; @@ -96,7 +96,7 @@ public function testMatchResponseBodyWrongNumber() "id" => "ABC", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true ]; @@ -122,7 +122,7 @@ public function testMatchResponseBodyMoreThanExpected() "id" => "50", "petId" => 50, "quantity" => 1, - "shipDate" => '2010-10-20', + "shipDate" => '2010-10-20T17:32:28Z', "status" => 'placed', "complete" => true, "more" => "value" diff --git a/tests/example/openapi.json b/tests/example/openapi.json index a87b6ef..0cda8ee 100644 --- a/tests/example/openapi.json +++ b/tests/example/openapi.json @@ -902,13 +902,21 @@ "application/json": { "schema": { "type": "object", - "required": [ - "date" - ], "properties": { "date": { "description": "First date", - "type": "date" + "type": "string", + "format": "date" + }, + "date_time": { + "description": "First date time", + "type": "string", + "format": "date-time" + }, + "any_format": { + "description": "Any format", + "type": "string", + "format": "unknown" } } } From 149ea6d7f46378aeb7a824cf8872a35a76854272 Mon Sep 17 00:00:00 2001 From: Martin Kluska Date: Tue, 16 Jun 2020 12:04:53 +0200 Subject: [PATCH 09/12] Update ReponseInterface documentation --- src/Response/ResponseInterface.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Response/ResponseInterface.php b/src/Response/ResponseInterface.php index 27551a8..bebd73a 100644 --- a/src/Response/ResponseInterface.php +++ b/src/Response/ResponseInterface.php @@ -1,11 +1,33 @@ Date: Tue, 16 Jun 2020 12:09:19 +0200 Subject: [PATCH 10/12] Check if the trait is used in PHPUnits TestCase --- src/AssertRequestAgainstSchema.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/AssertRequestAgainstSchema.php b/src/AssertRequestAgainstSchema.php index f7b3252..31d3103 100644 --- a/src/AssertRequestAgainstSchema.php +++ b/src/AssertRequestAgainstSchema.php @@ -10,6 +10,7 @@ use ByJG\ApiTools\Exception\NotMatchedException; use ByJG\ApiTools\Exception\PathNotFoundException; use ByJG\ApiTools\Exception\StatusCodeNotMatchedException; +use PHPUnit\Framework\TestCase; trait AssertRequestAgainstSchema { @@ -56,6 +57,8 @@ protected function makeRequest( $requestBody = null, $requestHeader = [] ) { + assert($this instanceof TestCase); + $this->checkSchema(); $requester = new ApiRequester(); $body = $requester @@ -91,6 +94,8 @@ protected function makeRequest( */ public function assertRequest(AbstractRequester $request) { + assert($this instanceof TestCase); + // Add own schema if nothing is passed. if (!$request->hasSchema()) { $this->checkSchema(); From d8fade83cab32faff74f9663b25612e1f41e3de3 Mon Sep 17 00:00:00 2001 From: Jakub Dibala Date: Wed, 20 Jan 2021 06:16:07 +0100 Subject: [PATCH 11/12] Do not fail if not found in path and try to match every path defined --- src/Base/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Base/Schema.php b/src/Base/Schema.php index dd59c8c..16560dd 100644 --- a/src/Base/Schema.php +++ b/src/Base/Schema.php @@ -102,7 +102,7 @@ public function getPathDefinition($path, $method) if (preg_match($pathItemPattern, $uri->getPath(), $matches)) { $pathDef = $this->jsonFile[self::SWAGGER_PATHS][$pathItem]; if (!isset($pathDef[$method])) { - throw new HttpMethodNotFoundException("The http method '$method' not found in '$path'"); + continue; } $parametersPathMethod = []; From 8f2f8d2a927b2300e6a0bb09c54f8682d64a6e14 Mon Sep 17 00:00:00 2001 From: Erik Pach Date: Thu, 4 Feb 2021 15:27:11 +0100 Subject: [PATCH 12/12] Use Guzzle v7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ded48b9..0fd8d75 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "guzzlehttp/guzzle": "6.*", + "guzzlehttp/guzzle": "7.*", "byjg/uri": "2.0.*", "php": ">=5.6", "ext-json": "*"