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
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@
"fakerphp/faker": "^1.9",
"firebase/php-jwt": "^6.0.0",
"guzzlehttp/promises": "^1.4.0",
"illuminate/collections": "8.x-dev",
"illuminate/collections": "^8.0",
"monolog/monolog": "^1.25.1 || ^2.8.0 || ^3.0.0",
"myclabs/deep-copy": "^1.11",
"nesbot/carbon": "^2.48",
"nyholm/psr7": "1.*",
"league/openapi-psr7-validator": "^0.18",
"openclassrooms/openapi-psr7-validator": "dev-master",
"opis/json-schema": "^2.3",
"php-http/httplug": "^2.2",
"phpdocumentor/reflection-docblock": "^5.3",
Expand Down Expand Up @@ -101,6 +103,10 @@
{
"type": "vcs",
"url": "https://github.com/sidux/collections"
},
{
"type": "vcs",
"url": "https://github.com/OpenClassrooms/openapi-psr7-validator"
}
],
"scripts": {
Expand Down
86 changes: 86 additions & 0 deletions config/phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
parameters:
ignoreErrors:
-
message: "#^PHPDoc tag @var with type Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\), APITester\\\\Definition\\\\Security\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Definition/Api.php

-
message: "#^PHPDoc tag @var with type Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\), string\\> is not subtype of type Illuminate\\\\Support\\\\Collection\\<int, mixed\\>\\.$#"
count: 1
path: ../src/Definition/Api.php

-
message: "#^Cannot cast APITester\\\\Definition\\\\Tag to string\\.$#"
count: 1
path: ../src/Definition/Collection/Operations.php

-
message: "#^PHPDoc tag @var with type APITester\\\\Definition\\\\Example\\\\OperationExample is not subtype of type \\$this\\(APITester\\\\Definition\\\\Example\\\\OperationExample\\)\\.$#"
count: 2
path: ../src/Definition/Example/OperationExample.php

-
message: "#^Cannot cast APITester\\\\Definition\\\\Example\\\\OperationExample\\|null to string\\.$#"
count: 1
path: ../src/Definition/Operation.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/Error400Preparator.php

-
message: "#^Parameter \\#1 \\$items of method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),APITester\\\\Definition\\\\Scope\\>\\:\\:intersect\\(\\) expects Illuminate\\\\Contracts\\\\Support\\\\Arrayable\\<\\(int\\|string\\), APITester\\\\Definition\\\\Scope\\>\\|iterable\\<\\(int\\|string\\), APITester\\\\Definition\\\\Scope\\>, array\\<string\\> given\\.$#"
count: 1
path: ../src/Preparator/Error403Preparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/Error404Preparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/Error405Preparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/Error406Preparator.php

-
message: "#^Parameter \\#1 \\$items of method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),APITester\\\\Definition\\\\Response\\>\\:\\:compare\\(\\) expects iterable\\<\\(int\\|string\\), APITester\\\\Definition\\\\Response\\>, array\\<string\\> given\\.$#"
count: 1
path: ../src/Preparator/Error406Preparator.php

-
message: "#^Parameter \\#1 \\$items of method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),APITester\\\\Definition\\\\Response\\>\\:\\:intersect\\(\\) expects Illuminate\\\\Contracts\\\\Support\\\\Arrayable\\<\\(int\\|string\\), APITester\\\\Definition\\\\Response\\>\\|iterable\\<\\(int\\|string\\), APITester\\\\Definition\\\\Response\\>, array\\<string\\> given\\.$#"
count: 1
path: ../src/Preparator/Error406Preparator.php

-
message: "#^Method APITester\\\\Preparator\\\\ExamplesPreparator\\:\\:prepareTestCases\\(\\) should return iterable\\<APITester\\\\Test\\\\TestCase\\> but returns APITester\\\\Definition\\\\Collection\\\\OperationExamples\\.$#"
count: 1
path: ../src/Preparator/ExamplesPreparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/ExamplesPreparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/PaginationErrorPreparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/RandomPreparator.php

-
message: "#^PHPDoc tag @var with type iterable\\<\\(int\\|string\\), APITester\\\\Test\\\\TestCase\\> is not subtype of type APITester\\\\Definition\\\\Collection\\\\Operations\\.$#"
count: 1
path: ../src/Preparator/SecurityErrorPreparator.php
2 changes: 1 addition & 1 deletion config/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ includes:
- ../vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon
- ../vendor/spaze/phpstan-disallowed-calls/disallowed-insecure-calls.neon
- ../vendor/spaze/phpstan-disallowed-calls/disallowed-loose-calls.neon

- phpstan-baseline.neon
parameters:
treatPhpDocTypesAsCertain: false
bootstrapFiles:
Expand Down
14 changes: 14 additions & 0 deletions src/Definition/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,27 @@ final class Api

private Tags $tags;

private ?ApiSpecification $specification = null;

public function __construct()
{
$this->operations = new Operations();
$this->servers = new Servers();
$this->tags = new Tags();
}

public function getSpecification(): ?ApiSpecification
{
return $this->specification;
}

public function setSpecification(ApiSpecification $specification): self
{
$this->specification = $specification;

return $this;
}

public static function create(): self
{
return new self();
Expand Down
10 changes: 10 additions & 0 deletions src/Definition/ApiSpecification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace APITester\Definition;

interface ApiSpecification
{
public function getDocument(): mixed;
}
4 changes: 3 additions & 1 deletion src/Definition/Loader/OpenApiDefinitionLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use APITester\Definition\Example\ResponseExample;
use APITester\Definition\Loader\Exception\DefinitionLoadingException;
use APITester\Definition\Loader\Exception\InvalidExampleException;
use APITester\Definition\OpenApiSpecification;
use APITester\Definition\Operation;
use APITester\Definition\Parameter;
use APITester\Definition\Response;
Expand Down Expand Up @@ -57,13 +58,13 @@ public function __construct(?LoggerInterface $logger = null)

public function load(string $filePath, string $format = self::FORMAT_YAML, array $filters = []): Api
{
$api = Api::create();
if (!\in_array($format, self::FORMATS, true)) {
throw new \InvalidArgumentException('Invalid format ' . $format);
}
try {
/** @var OpenApi $openApi */
$openApi = Reader::readFromYamlFile($filePath);
$api = Api::create();
} catch (\Exception $e) {
throw new DefinitionLoadingException("Could not load {$filePath}", $e);
}
Expand All @@ -81,6 +82,7 @@ public function load(string $filePath, string $format = self::FORMAT_YAML, array
)
->setServers($this->getServers($openApi->servers))
->setTags($this->getTags($openApi->tags))
->setSpecification(new OpenApiSpecification($openApi))
;
}

Expand Down
20 changes: 20 additions & 0 deletions src/Definition/OpenApiSpecification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace APITester\Definition;

use cebe\openapi\spec\OpenApi;

final class OpenApiSpecification implements ApiSpecification
{
public function __construct(
private OpenApi $document
) {
}

public function getDocument(): OpenApi
{
return $this->document;
}
}
1 change: 1 addition & 0 deletions src/Test/Suite.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ private function prepareTestCases(): void
$testCase->setLogger($this->logger);
$testCase->setBeforeCallbacks($this->beforeTestCaseCallbacks);
$testCase->setAfterCallbacks($this->afterTestCaseCallbacks);
$testCase->setSpecification($this->api->getSpecification());
$allTests->add($testCase);
}
} catch (PreparatorLoadingException $e) {
Expand Down
94 changes: 82 additions & 12 deletions src/Test/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace APITester\Test;

use APITester\Definition\ApiSpecification;
use APITester\Definition\Body;
use APITester\Definition\Example\OperationExample;
use APITester\Definition\Example\ResponseExample;
use APITester\Definition\OpenApiSpecification;
use APITester\Requester\Requester;
use APITester\Requester\SymfonyKernelRequester;
use APITester\Test\Exception\InvalidResponseSchemaException;
Expand All @@ -20,6 +22,10 @@
use Carbon\Carbon;
use cebe\openapi\spec\Schema;
use Nyholm\Psr7\Stream;
use OpenClassrooms\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use OpenClassrooms\OpenAPIValidation\PSR7\OperationAddress;
use OpenClassrooms\OpenAPIValidation\PSR7\ResponseValidator;
use OpenClassrooms\OpenAPIValidation\Schema\Exception\SchemaMismatch;
use Opis\JsonSchema\Errors\ErrorFormatter;
use Opis\JsonSchema\Validator;
use PHPUnit\Framework\ExpectationFailedException;
Expand Down Expand Up @@ -74,9 +80,7 @@ final class TestCase implements \JsonSerializable, Filterable

private string $preparator;

private Validator $validator;

private ErrorFormatter $errorFormatter;
private ?ApiSpecification $specification = null;

/**
* @param array<int, string> $excludedFields
Expand All @@ -95,8 +99,6 @@ public function __construct(
$this->operation = $nameParts[1] ?? null;
$this->request = $operationExample->getPsrRequest();
$this->schemaValidation = $schemaValidation;
$this->validator = new Validator();
$this->errorFormatter = new ErrorFormatter();
}

/**
Expand All @@ -113,6 +115,11 @@ public function setSchemaValidation(bool $schemaValidation): void
$this->schemaValidation = $schemaValidation;
}

public function setSpecification(?ApiSpecification $specification = null): void
{
$this->specification = $specification;
}

/**
* @throws ClientExceptionInterface
* @throws ExceptionInterface
Expand Down Expand Up @@ -270,6 +277,52 @@ public function jsonSerialize(): array
];
}

/**
* @throws InvalidResponseSchemaException
*/
public function validateOpenApiSchema(): void
{
if (!$this->specification instanceof OpenApiSpecification) {
throw new InvalidResponseSchemaException(
'Unexpected error during schema validation: OpenApi document missing',
0,
);
}

$operationAddress = new OperationAddress(
$this->operationExample->getPath(),
mb_strtolower($this->operationExample->getMethod())
);

try {
$validator = new ResponseValidator($this->specification->getDocument());
$validator->validate($operationAddress, $this->response);
} catch (ValidationFailed $e) {
$exception = $e->getPrevious();

if ($exception instanceof SchemaMismatch) {
$breadcrumb = implode(
'.',
$exception->dataBreadCrumb() !== null ? $exception->dataBreadCrumb()
->buildChain() : []
);
$message = sprintf('Invalid field: %s. %s', $breadcrumb, $exception->getMessage());
}
throw new InvalidResponseSchemaException(
$message ??
sprintf('Response schema validation failed: %s', $exception?->getMessage()),
0,
$e
);
} catch (\Throwable $e) {
throw new InvalidResponseSchemaException(
'Unexpected error during schema validation: ' . $e->getMessage(),
0,
$e
);
}
}

/**
* @throws ExceptionInterface
*/
Expand Down Expand Up @@ -321,6 +374,24 @@ public function test(): void
}
}

/**
* @throws InvalidResponseSchemaException
*/
private function checkSchemaResponse(): void
{
if (!$this->schemaValidation) {
return;
}

if ($this->specification instanceof OpenApiSpecification) {
$this->validateOpenApiSchema();

return;
}

$this->validateSchema();
}

private function getSchemaResponseForStatusCode(int $statusCode): ?Schema
{
if ($this->operationExample->getParent() === null) {
Expand All @@ -339,12 +410,8 @@ private function getSchemaResponseForStatusCode(int $statusCode): ?Schema
/**
* @throws InvalidResponseSchemaException
*/
private function checkSchemaResponse(): void
private function validateSchema(): void
{
if (!$this->schemaValidation) {
return;
}

$schema = $this->getSchemaResponseForStatusCode($this->response->getStatusCode());

if ($schema === null) {
Expand All @@ -354,11 +421,14 @@ private function checkSchemaResponse(): void
$data = json_decode((string) ResponseExample::fromPsrResponse($this->response)->getContent());
$schemaData = (object) $schema->getSerializableData();

$result = $this->validator->validate($data, $schemaData);
$validator = new Validator();
$errorFormatter = new ErrorFormatter();

$result = $validator->validate($data, $schemaData);

if (!$result->isValid()) {
if ($result->error() !== null) {
$errorDescription = (string) json_encode($this->errorFormatter->format($result->error()));
$errorDescription = (string) json_encode($errorFormatter->format($result->error()));
$this->logger->error($errorDescription);
}
throw new InvalidResponseSchemaException();
Expand Down
2 changes: 2 additions & 0 deletions tests/Fixtures/FixturesLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ final class FixturesLocation
public const CONFIG_OPENAPI = __DIR__ . '/Config/api-tester.yaml';

public const CONFIG_EXAMPLES_EXTENSION = __DIR__ . '/Examples/petstore/examples.new.yml';

public const OPEN_API_WITH_EXAMPLES = __DIR__ . '/OpenAPI/openapi-with-examples.yaml';
}
Loading