Skip to content

Commit

Permalink
Merge pull request #25 from PcComponentes/feature/add-request-validat…
Browse files Browse the repository at this point in the history
…ion-using-openapi

Feature/Add request validation using openapi
  • Loading branch information
ladbsoft authored Oct 31, 2023
2 parents 0002fc2 + 0b5806b commit 4c0abe9
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 16 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,18 @@ Configuration:
```

## ResponseValidatorOpenApiContext
Check if http requests are documented in your openapi file:
```gherkin
Then the request should be valid according to OpenApi "docs/openapi.yml" with path "/your/openapi/path/"
```
Check if http responses are documented in your openapi file:
```gherkin
Then the response should be valid according to OpenApi "docs/openapi.yml" with path "/your/openapi/path/"
```
Additionally, you can check both the request and response with:
```gherkin
Then the request and response should be valid according to OpenApi "docs/openapi.yml" with path "/your/openapi/path/"
```
Configuration:
```yaml
- PcComponentes\OpenApiMessagingContext\Behat\ResponseValidatorOpenApiContext:
Expand Down
67 changes: 60 additions & 7 deletions src/Behat/ResponseValidatorOpenApiContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function theJsonResponseShouldBeValidAccordingToOpenApiSchema($dumpPath,
$path = \realpath($this->rootPath . '/' . $dumpPath);
$this->checkSchemaFile($path);

$responseJson = $this->extractContent();
$responseJson = $this->extractResponseContent();

$allSpec = $this->cacheAdapter->get(
\md5($path),
Expand All @@ -52,6 +52,45 @@ function (ItemInterface $item) use ($path) {
}
}

/** @Then the request should be valid according to OpenApi :dumpPath with path :openApiPath */
public function theRequestShouldBeValidAccordingToOpenApiWithPath(string $dumpPath, string $openApiPath): void
{
$path = \realpath($this->rootPath . '/' . $dumpPath);
$this->checkSchemaFile($path);

$method = \strtolower($this->extractMethod());
$contentType = $this->extractRequestContentType();

$requestJson = $this->extractRequestContent();

$allSpec = $this->cacheAdapter->get(
\md5($path),
function (ItemInterface $item) use ($path) {
$item->expiresAfter(null);

$allSpec = Yaml::parse(file_get_contents($path));

return $this->getDataExternalReferences($allSpec, $path);
},
);

$schemaSpec = (new OpenApiSchemaParser($allSpec))->fromRequestBody(
$openApiPath,
$method,
$contentType,
);

$validator = new JsonValidator(
$requestJson,
new JsonSchema(\json_decode(\json_encode($schemaSpec), false)),
);
$validation = $validator->validate();

if ($validation->hasError()) {
throw new JsonValidationException($validation->errorMessage());
}
}

/** @Then the response should be valid according to OpenApi :dumpPath with path :openApiPath */
public function theResponseShouldBeValidAccordingToOpenApiWithPath(string $dumpPath, string $openApiPath): void
{
Expand All @@ -60,9 +99,9 @@ public function theResponseShouldBeValidAccordingToOpenApiWithPath(string $dumpP

$statusCode = $this->extractStatusCode();
$method = \strtolower($this->extractMethod());
$contentType = $this->contentType();
$contentType = $this->responseContentType();

$responseJson = $this->extractContent();
$responseJson = $this->extractResponseContent();

$allSpec = $this->cacheAdapter->get(
\md5($path),
Expand Down Expand Up @@ -93,21 +132,35 @@ function (ItemInterface $item) use ($path) {
}
}

/** @Then the request and response should be valid according to OpenApi :dumpPath with path :openApiPath */
public function theRequestAndResponseShouldBeValidAccordingToOpenApiWithPath(
string $dumpPath,
string $openApiPath,
): void {
$this->theRequestShouldBeValidAccordingToOpenApiWithPath($dumpPath, $openApiPath);

$this->theResponseShouldBeValidAccordingToOpenApiWithPath($dumpPath, $openApiPath);
}

abstract protected function extractMethod(): string;

abstract protected function extractContentType(): ?string;
abstract protected function extractRequestContentType(): ?string;

abstract protected function extractResponseContentType(): ?string;

abstract protected function extractStatusCode(): int;

abstract protected function extractContent(): string;
abstract protected function extractRequestContent(): string;

abstract protected function extractResponseContent(): string;

private function contentType(): string
private function responseContentType(): string
{
if (self::HTTP_NO_CONTENT_CODE === $this->extractStatusCode()) {
return '';
}

$contentType = $this->extractContentType();
$contentType = $this->extractResponseContentType();

if (null === $contentType) {
throw new \RuntimeException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Behat\MinkExtension\Context\MinkContext;
use PcComponentes\OpenApiMessagingContext\Behat\ResponseValidatorOpenApiContext;

/**
* @deprecated No longer used. Open an issue to let us know if you do, and kindly implement extractRequestContentType() and extractRequestContent()
*/
final class MinkResponseValidatorOpenApiContext extends ResponseValidatorOpenApiContext
{
private const CONTENT_TYPE_RESPONSE_HEADER_KEY = 'content-type';
Expand All @@ -26,7 +29,12 @@ protected function extractMethod(): string
return $requestClient->getHistory()->current()->getMethod();
}

protected function extractContentType(): ?string
protected function extractRequestContentType(): ?string
{
throw new \LogicException('Not implemented');
}

protected function extractResponseContentType(): ?string
{
return $this->minkContext->getSession()->getResponseHeader(self::CONTENT_TYPE_RESPONSE_HEADER_KEY);
}
Expand All @@ -36,7 +44,12 @@ protected function extractStatusCode(): int
return $this->minkContext->getSession()->getStatusCode();
}

protected function extractContent(): string
protected function extractRequestContent(): string
{
throw new \LogicException('Not implemented');
}

protected function extractResponseContent(): string
{
return $this->minkContext->getSession()->getPage()->getContent();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ protected function extractMethod(): string
return $this->requestHistory->getLastRequest()->getMethod();
}

protected function extractContentType(): ?string
protected function extractRequestContentType(): ?string
{
$request = $this->requestHistory->getLastRequest();

return $request->getMimeType($request->getContentType());
}

protected function extractResponseContentType(): ?string
{
return $this->requestHistory->getLastResponse()->headers->get(self::CONTENT_TYPE_RESPONSE_HEADER_KEY);
}
Expand All @@ -31,7 +38,12 @@ protected function extractStatusCode(): int
return $this->requestHistory->getLastResponse()->getStatusCode();
}

protected function extractContent(): string
protected function extractRequestContent(): string
{
return $this->requestHistory->getLastRequest()->getContent();
}

protected function extractResponseContent(): string
{
return $this->requestHistory->getLastResponse()->getContent();
}
Expand Down
47 changes: 42 additions & 5 deletions src/OpenApi/OpenApiSchemaParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ public function parse($name): array
return $this->extractData($schemaSpec);
}

public function fromRequestBody(string $path, string $method, string $contentType): array
{
$rootPaths = $this->originalContent['paths'];
$this->assertPathRoot($path, $rootPaths);
$pathRoot = $rootPaths[$path];

$this->assertMethodRoot($path, $method, $pathRoot);
$methodRoot = $pathRoot[$method];

if (false === \array_key_exists('requestBody', $methodRoot)) {
return [];
}

$requestBodyRoot = $methodRoot['requestBody'];
$this->assertRequestContentTypeRoot($path, $method, $contentType, $requestBodyRoot);

return $this->extractData($requestBodyRoot['content'][$contentType]['schema']);
}

public function fromResponse(string $path, string $method, int $statusCode, string $contentType): array
{
$rootPaths = $this->originalContent['paths'];
Expand All @@ -36,7 +55,7 @@ public function fromResponse(string $path, string $method, int $statusCode, stri
return [];
}

$this->assertContentTypeRoot($path, $method, $statusCode, $contentType, $statusCodeRoot);
$this->assertResponseContentTypeRoot($path, $method, $statusCode, $contentType, $statusCodeRoot);

return $this->extractData($statusCodeRoot['content'][$contentType]['schema']);
}
Expand Down Expand Up @@ -105,16 +124,34 @@ private function assertMethodRoot(string $path, string $method, $pathRoot): void
private function assertStatusCodeRoot(string $path, string $method, int $statusCode, $methodRoot): void
{
if (false === \array_key_exists('responses', $methodRoot) || false === \array_key_exists(
$statusCode,
$methodRoot['responses'],
)) {
$statusCode,
$methodRoot['responses'],
)) {
throw new \InvalidArgumentException(
\sprintf('%s response not found on %s path with %s method', $statusCode, $path, $method),
);
}
}

private function assertContentTypeRoot(
private function assertRequestContentTypeRoot(
string $path,
string $method,
string $contentType,
$statusCodeRoot,
): void {
if (false === \array_key_exists($contentType, $statusCodeRoot['content'])) {
throw new \InvalidArgumentException(
\sprintf(
'%s content-type not found on %s path with %s method',
$contentType,
$path,
$method,
),
);
}
}

private function assertResponseContentTypeRoot(
string $path,
string $method,
int $statusCode,
Expand Down

0 comments on commit 4c0abe9

Please sign in to comment.