diff --git a/readme.md b/readme.md index 356c6fc..1f6c6b2 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,9 @@ composer require --dev gertjuhh/symfony-openapi-validator - Call `self::assertOpenApiSchema(, );` - `schema`: path to corresponding OpenAPI yaml schema - `client`: the client used to make the request +- Or optionally use the `self::assertResponseAgainstOpenApiSchema(, );` to only validate the response + - The `operationAddress` can be passed as a third argument for this function but by default it will retrieve the + operation from the `client`. ## Example diff --git a/src/OpenApiValidator.php b/src/OpenApiValidator.php index 645f25f..c96b87a 100644 --- a/src/OpenApiValidator.php +++ b/src/OpenApiValidator.php @@ -5,6 +5,7 @@ namespace Gertjuhh\SymfonyOpenapiValidator; use League\OpenAPIValidation\PSR7\Exception\ValidationFailed; +use League\OpenAPIValidation\PSR7\OperationAddress; use League\OpenAPIValidation\PSR7\ValidatorBuilder; use League\OpenAPIValidation\Schema\Exception\SchemaMismatch; use Nyholm\Psr7\Factory\Psr17Factory; @@ -32,9 +33,27 @@ public static function assertOpenApiSchema(string $schema, KernelBrowser $client throw self::wrapValidationException($exception, 'request'); } + self::assertResponseAgainstOpenApiSchema($schema, $client, $match); + } + + public static function assertResponseAgainstOpenApiSchema( + string $schema, + KernelBrowser $client, + OperationAddress|null $operationAddress = null + ): void { + $builder = self::getValidatorBuilder($schema); + $psrFactory = self::getPsrHttpFactory(); + + if ($operationAddress === null) { + $operationAddress = new OperationAddress( + path: $client->getRequest()->getPathInfo(), + method: strtolower($client->getRequest()->getMethod()), + ); + } + try { $builder->getResponseValidator() - ->validate($match, $psrFactory->createResponse($client->getResponse())); + ->validate($operationAddress, $psrFactory->createResponse($client->getResponse())); } catch (ValidationFailed $exception) { throw self::wrapValidationException($exception, 'response'); } diff --git a/tests/openapi.yaml b/tests/openapi.yaml index 7aa4102..c2e0523 100644 --- a/tests/openapi.yaml +++ b/tests/openapi.yaml @@ -25,6 +25,36 @@ paths: schema: $ref: '#/components/schemas/NestedProperty' + /input-validation: + post: + requestBody: + content: + application/json: + schema: + type: + object + required: + - email + properties: + email: + type: string + format: email + example: john.doe@example.com + responses: + 200: + description: Ok + 422: + description: Input is invalid + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + type: string + components: schemas: HelloWorld: diff --git a/tests/unit/OpenApiValidatorTest.php b/tests/unit/OpenApiValidatorTest.php index 51ed676..94fd866 100644 --- a/tests/unit/OpenApiValidatorTest.php +++ b/tests/unit/OpenApiValidatorTest.php @@ -119,4 +119,91 @@ public function testValidatorThrowsErrorWhenNestedResponseIsInvalid(): void self::assertOpenApiSchema('tests/openapi.yaml', $browser); } + + public function testValidatorThrowsErrorWhenInputIsInvalid(): void + { + $request = Request::create( + uri: 'https://localhost/input-validation', + method: 'POST', + server: [ + 'CONTENT_TYPE' => 'application/json', + ], + content: \json_encode(['email' => 'john.doe'], \JSON_THROW_ON_ERROR), + ); + + $browser = $this->createMock(KernelBrowser::class); + $browser->expects(self::once()) + ->method('getRequest') + ->willReturn($request); + + $this->expectExceptionObject(new AssertionFailedError( + \sprintf( + '%s%s%s%s%s', + 'OpenAPI request error at email:', + "\n", + 'Body does not match schema for content-type "application/json" for Request [post /input-validation]', + "\n", + 'Value \'john.doe\' does not match format email of type string', + ) + )); + + self::assertOpenApiSchema('tests/openapi.yaml', $browser); + } + + public function testResponseValidatorWillThrowErrorWhenErrorResponseIsInvalid(): void + { + $request = Request::create( + uri: 'https://localhost/input-validation', + method: 'POST', + server: [ + 'CONTENT_TYPE' => 'application/json', + ], + content: \json_encode(['email' => 'john.doe'], \JSON_THROW_ON_ERROR), + ); + $response = new JsonResponse(data: ['output' => 'invalid'], status: 422); + + $browser = $this->createMock(KernelBrowser::class); + $browser->expects(self::exactly(2)) + ->method('getRequest') + ->willReturn($request); + $browser->expects(self::once()) + ->method('getResponse') + ->willReturn($response); + + $this->expectExceptionObject(new AssertionFailedError( + \sprintf( + '%s%s%s%s%s', + 'OpenAPI response error at message:', + "\n", + 'Body does not match schema for content-type "application/json" for Response [post /input-validation 422]', + "\n", + 'Keyword validation failed: Required property \'message\' must be present in the object', + ) + )); + + self::assertResponseAgainstOpenApiSchema('tests/openapi.yaml', $browser); + } + + public function testResponseValidatorDoesNothingWhenResponseIsValid(): void + { + $request = Request::create( + uri: 'https://localhost/input-validation', + method: 'POST', + server: [ + 'CONTENT_TYPE' => 'application/json', + ], + content: \json_encode(['email' => 'john.doe'], \JSON_THROW_ON_ERROR), + ); + $response = new JsonResponse(data: ['message' => 'invalid'], status: 422); + + $browser = $this->createMock(KernelBrowser::class); + $browser->expects(self::exactly(2)) + ->method('getRequest') + ->willReturn($request); + $browser->expects(self::once()) + ->method('getResponse') + ->willReturn($response); + + self::assertResponseAgainstOpenApiSchema('tests/openapi.yaml', $browser); + } }