Skip to content

Commit

Permalink
Implement validating only the response (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
melvinversluijs authored Jan 30, 2024
1 parent e04cde4 commit 23a7da7
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 1 deletion.
3 changes: 3 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ composer require --dev gertjuhh/symfony-openapi-validator
- Call `self::assertOpenApiSchema(<schema>, <client>);`
- `schema`: path to corresponding OpenAPI yaml schema
- `client`: the client used to make the request
- Or optionally use the `self::assertResponseAgainstOpenApiSchema(<schema>, <client>);` 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

Expand Down
21 changes: 20 additions & 1 deletion src/OpenApiValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}
Expand Down
30 changes: 30 additions & 0 deletions tests/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
87 changes: 87 additions & 0 deletions tests/unit/OpenApiValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit 23a7da7

Please sign in to comment.