Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ How the response is looked up:
first, and the optional third argument of `verify()` skips the matching and uses the given template.
* **Status code**: the exact code, then its range (`2XX`), then `default`.
* **Media type**: the response's `Content-Type`, including `+json` types such as `application/problem+json`.
Without a match it falls back to `application/json`. Only JSON bodies are validated.
Without a match it falls back to `application/json`, so a JSON body labelled `text/html` is still validated.

Bodies are validated with [opis/json-schema](https://github.com/opis/json-schema), which supports JSON Schema
2020-12, the dialect of OpenAPI 3.1. OpenAPI 3.0 documents are translated first: `nullable` and the boolean form of
Expand Down Expand Up @@ -75,7 +75,8 @@ The body is validated against that schema as written:
* `required` properties and types are checked.
* Formats that opis knows, such as `date-time` and `email`, are checked.
* Properties the schema does not declare are allowed, unless it sets `additionalProperties: false`.
* A body that is not valid JSON is validated as `null`.
* A body that is not valid JSON is validated as `null`, so it fails. With a `Content-Type` that is not JSON it is
skipped instead.

### Guzzle and PSR-18 clients
Verifying on the client side needs no framework. It works against a running server, a mock
Expand Down
21 changes: 19 additions & 2 deletions src/OpenApiVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ public function verify(RequestInterface $request, ResponseInterface $response, ?
/**
* Verify a response body for the given request method, path and status code.
*
* @return bool `true` if the body has been validated, `false` if not (no matching schema)
* A body that is not JSON is not validated if its content type is not JSON either.
*
* @return bool `true` if the body has been validated, `false` if not (no matching schema, or not JSON)
*
* @throws OpenApiSchemaMismatchException
*/
Expand All @@ -77,11 +79,26 @@ public function verifyResponseBody(string $method, string $path, int $statusCode
return false;
}

if ([] !== ($errors = $this->schemaValidator->validate(json_decode($body), $schema, $this->specificationLoader->getOpenApiVersion()))) {
$data = json_decode($body);
if (null === $data && JSON_ERROR_NONE !== json_last_error() && !$this->claimsJson($contentType)) {
return false;
}

if ([] !== ($errors = $this->schemaValidator->validate($data, $schema, $this->specificationLoader->getOpenApiVersion()))) {
throw (new OpenApiSchemaMismatchException(sprintf('Schema mismatch: %s[%s]:%s', $method, $statusCode, $path)))
->setErrors($errors);
}

return true;
}

/**
* Whether the content type is JSON, or missing, in which case the body is taken to be JSON.
*/
private function claimsJson(?string $contentType): bool
{
$type = strtolower(trim(explode(';', (string) $contentType)[0]));

return '' === $type || 'application/json' === $type || str_ends_with($type, '+json');
}
}
4 changes: 4 additions & 0 deletions tests/OpenApiVerifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public static function responses(): iterable
yield 'json suffix media type mismatch' => ['/problems', 200, '{}', 'application/problem+json', null];
yield 'non-json media type skipped' => ['/problems', 200, 'hello', 'text/plain', false];
yield 'unmatched content type falls back to json' => ['/users/42', 200, '{"id":42}', 'text/html; charset=UTF-8', true];
yield 'unmatched content type falls back to json, mismatch' => ['/users/42', 200, '{"name":"joe"}', 'text/html', null];
yield 'unmatched content type, body not json, skipped' => ['/users/42', 200, '<p>hi</p>', 'application/xml', false];
yield 'json content type, body not json, mismatch' => ['/users/42', 200, '<p>hi</p>', 'application/json', null];
yield 'no content type, body not json, mismatch' => ['/users/42', 200, '<p>hi</p>', null, null];
}

#[Test]
Expand Down
Loading