Skip to content

Commit

Permalink
Add output for inner exceptions thrown with NotEnoughValidSchemas (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
NanoSector authored Jan 31, 2024
1 parent 23a7da7 commit 6b19bd0
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 4 deletions.
36 changes: 32 additions & 4 deletions src/OpenApiValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use League\OpenAPIValidation\Schema\Exception\NotEnoughValidSchemas;
use League\OpenAPIValidation\Schema\Exception\SchemaMismatch;
use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\AssertionFailedError;
Expand All @@ -33,13 +34,13 @@ public static function assertOpenApiSchema(string $schema, KernelBrowser $client
throw self::wrapValidationException($exception, 'request');
}

self::assertResponseAgainstOpenApiSchema($schema, $client, $match);
self::assertResponseAgainstOpenApiSchema($schema, $client, $match);
}

public static function assertResponseAgainstOpenApiSchema(
string $schema,
KernelBrowser $client,
OperationAddress|null $operationAddress = null
OperationAddress | null $operationAddress = null,
): void {
$builder = self::getValidatorBuilder($schema);
$psrFactory = self::getPsrHttpFactory();
Expand All @@ -59,6 +60,20 @@ public static function assertResponseAgainstOpenApiSchema(
}
}

private static function extractPathFromException(\Throwable $exception): string | null
{
if ($exception instanceof SchemaMismatch
&& ($breadcrumb = $exception->dataBreadCrumb())
// league/openapi-psr7-validator can return an array with a single null value
// when the breadcrumb's first compoundIndex is null; filter this out here
&& !empty($chain = array_filter($breadcrumb->buildChain()))
) {
return \implode('.', $chain);
}

return null;
}

private static function getPsrHttpFactory(): PsrHttpFactory
{
if (null === self::$psrHttpFactory) {
Expand Down Expand Up @@ -86,8 +101,21 @@ private static function wrapValidationException(\Throwable $exception, string $s
while (null !== ($exception = $exception->getPrevious())) {
$message[] = $exception->getMessage();

if (!$at && $exception instanceof SchemaMismatch && $breadcrumb = $exception->dataBreadCrumb()) {
$at = \implode('.', $breadcrumb->buildChain());
if (!$at) {
$at = self::extractPathFromException($exception);
}

if ($exception instanceof NotEnoughValidSchemas) {
foreach ($exception->innerExceptions() as $option => $innerException) {
$innerAt = self::extractPathFromException($innerException);

$message[] = sprintf(
'==> Schema %d: %s%s',
((int)$option) + 1,
$innerException->getMessage(),
$innerAt ? sprintf(' (at %s)', $innerAt) : '',
);
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions tests/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ paths:
schema:
$ref: '#/components/schemas/HelloWorld'

/match-oneof:
get:
responses:
200:
description: Hello world request
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/HelloWorld'
- $ref: '#/components/schemas/NestedProperty'

/nested-property:
get:
responses:
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/OpenApiValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,40 @@ public function testValidatorThrowsErrorWhenResponseIsInvalid(): void
self::assertOpenApiSchema('tests/openapi.yaml', $browser);
}

public function testValidatorThrowsErrorContainingInnerExceptionsWhenResponseFailsOneOfValidation(): void
{
$request = Request::create(uri: 'https://localhost/match-oneof', server: [
'HTTP_X_REQUESTED_WITH',
'XMLHttpRequest',
]);
$response = new JsonResponse(['nested' => new \stdClass()], headers: ['content-type' => 'application/json']);

$browser = $this->createMock(KernelBrowser::class);
$browser->expects(self::once())
->method('getRequest')
->willReturn($request);
$browser->expects(self::once())
->method('getResponse')
->willReturn($response);

$this->expectExceptionObject(
new AssertionFailedError(
\implode(
"\n",
[
'OpenAPI response error:',
'Body does not match schema for content-type "application/json" for Response [get /match-oneof 200]',
'Keyword validation failed: Data must match exactly one schema, but matched none',
'==> Schema 1: Keyword validation failed: Required property \'hello\' must be present in the object (at hello)',
'==> Schema 2: Keyword validation failed: Required property \'property\' must be present in the object (at nested.property)',
],
),
)
);

self::assertOpenApiSchema('tests/openapi.yaml', $browser);
}

public function testValidatorThrowsErrorWhenNestedResponseIsInvalid(): void
{
$request = Request::create(uri: 'https://localhost/nested-property', server: [
Expand Down

0 comments on commit 6b19bd0

Please sign in to comment.