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: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@
"autoload-dev": {
"psr-4": {
"Cortex\\OpenApi\\Tests\\": "tests/"
}
},
"files": [
"tests/Expectations.php"
]
},
"scripts": {
"test": "pest --no-coverage",
Expand Down
2 changes: 2 additions & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ parameters:
level: 10
paths:
- src
stubFiles:
- tests/phpstan/pest-expectations.stub
tmpDir: .phpstan-cache
checkBenevolentUnionTypes: true
5 changes: 0 additions & 5 deletions phpunit.dist.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage>
<report>
<html outputDirectory="coverage"/>
</report>
</coverage>
<source>
<include>
<directory suffix=".php">./src</directory>
Expand Down
67 changes: 67 additions & 0 deletions tests/Expectations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\OpenApi\Exceptions\ValidationException;

/**
* Custom Pest expectations for OpenAPI meta-schema validation.
* Method signatures for IDE/static analysis: tests/phpstan/pest-expectations.stub
*
* @param array<string, list<string>> $expectedErrors
*/
function assertOpenApiValidationErrors(OpenApi $openApi, array $expectedErrors): void
{
try {
$openApi->validate();
test()->fail('Expected ValidationException');
} catch (ValidationException $validationException) {
foreach ($expectedErrors as $pointer => $messages) {
expect($validationException->errors())->toHaveKey($pointer);
foreach ($messages as $message) {
expect($validationException->errors()[$pointer])->toContain($message);
}
}
}
}

expect()->extend('toFailOpenApiValidation', function (array $expectedErrors): mixed {
if (! $this->value instanceof OpenApi) {
test()->fail('Expected an OpenApi instance.');
}

assertOpenApiValidationErrors($this->value, $expectedErrors);

return $this;
});

expect()->extend('toFailOpenApiValidationAt', function (string $pointer, string $message): mixed {
if (! $this->value instanceof OpenApi) {
test()->fail('Expected an OpenApi instance.');
}

assertOpenApiValidationErrors($this->value, [
$pointer => [$message],
]);

return $this;
});

expect()->extend('toPassOpenApiValidation', function (): mixed {
if (! $this->value instanceof OpenApi) {
test()->fail('Expected an OpenApi instance.');
}

try {
$this->value->validate();
} catch (ValidationException $validationException) {
test()->fail(
'Expected OpenAPI document to pass validation: ' . $validationException->getMessage(),
);
}

expect($this->value)->toBeInstanceOf(OpenApi::class);

return $this;
});
23 changes: 23 additions & 0 deletions tests/Unit/Validation/ComponentsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\JsonSchema\Schema;
use Cortex\OpenApi\Objects\Info;
use Cortex\OpenApi\Objects\Components;

covers(OpenApi::class);

it('rejects a components key containing invalid characters', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->components(
Components::create()->schema('Invalid Key!', Schema::object()),
);

expect($openApi)->toFailOpenApiValidationAt(
'/components/schemas',
'The string should match pattern: ^[a-zA-Z0-9._-]+$',
);
});
98 changes: 98 additions & 0 deletions tests/Unit/Validation/DocumentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\JsonSchema\Schema;
use Cortex\OpenApi\Objects\Tag;
use Cortex\OpenApi\Objects\Info;
use Cortex\OpenApi\Objects\PathItem;
use Cortex\OpenApi\Objects\Response;
use Cortex\OpenApi\Objects\MediaType;
use Cortex\OpenApi\Objects\Operation;
use Cortex\OpenApi\Objects\Components;
use Cortex\OpenApi\Enums\OpenApiVersion;

covers(OpenApi::class);

it('accepts a minimal valid 3.1.0 document', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->paths(
PathItem::create('/ping')->operations(
Operation::get()->responses(Response::ok()),
),
);

expect($openApi)->toPassOpenApiValidation();
});

it('accepts a minimal valid 3.1.1 document', function (): void {
$openApi = OpenApi::create(OpenApiVersion::V3_1_1)
->info(Info::create('x', '1.0.0'))
->paths(
PathItem::create('/ping')->operations(
Operation::get()->responses(Response::ok()),
),
);

expect($openApi)->toPassOpenApiValidation();
});

it('accepts a document with components, tags, and schemas', function (): void {
$openApi = OpenApi::create()
->info(Info::create('Example', '1.0.0'))
->tags(Tag::create('Users'))
->components(Components::create()->schema('User', Schema::object()->properties(Schema::string('id'))))
->paths(
PathItem::create('/users')->operations(
Operation::get()->tags('Users')->responses(
Response::ok()->content(MediaType::json(Schema::string())),
),
),
);

expect($openApi)->toPassOpenApiValidation();
});

it('rejects a document missing info', function (): void {
$openApi = OpenApi::create();

expect($openApi)->toFailOpenApiValidationAt(
'/',
'The required properties (info) are missing',
);
});

it('accepts a components-only document without paths or webhooks', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->components(Components::create()->schema('User', Schema::object()));

expect($openApi)->toPassOpenApiValidation();
});

it('accepts a webhooks-only document without paths or components', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->webhooks([
'user.created' => PathItem::create('/user.created')->operations(
Operation::post()->responses(Response::ok()),
),
]);

expect($openApi)->toPassOpenApiValidation();
});

it('rejects a document with info but no paths, components, or webhooks', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'));

expect($openApi)->toFailOpenApiValidation([
'/' => [
'The required properties (paths) are missing',
'The required properties (components) are missing',
'The required properties (webhooks) are missing',
],
]);
});
28 changes: 28 additions & 0 deletions tests/Unit/Validation/ExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\OpenApi\Objects\Info;
use Cortex\OpenApi\Objects\Example;
use Cortex\OpenApi\Objects\Components;

covers(OpenApi::class);

it('rejects an example with both value and externalValue', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->components(
Components::create()
->example('Bad', Example::create()
->value([
'id' => 1,
])
->externalValue('https://example.com/examples/user.json')),
);

expect($openApi)->toFailOpenApiValidationAt(
'/components/examples/Bad',
'The data must not match schema',
);
});
27 changes: 27 additions & 0 deletions tests/Unit/Validation/LicenseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\JsonSchema\Schema;
use Cortex\OpenApi\Objects\Info;
use Cortex\OpenApi\Objects\License;
use Cortex\OpenApi\Objects\Components;

covers(OpenApi::class);

it('rejects a license with both identifier and url', function (): void {
$openApi = OpenApi::create()
->info(
Info::create('x', '1.0.0')
->license(License::create('MIT')
->identifier('MIT')
->url('https://spdx.org/licenses/MIT.html')),
)
->components(Components::create()->schema('Empty', Schema::object()));

expect($openApi)->toFailOpenApiValidationAt(
'/info/license',
'The data must not match schema',
);
});
68 changes: 68 additions & 0 deletions tests/Unit/Validation/ParameterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\JsonSchema\Schema;
use Cortex\OpenApi\Objects\Info;
use Cortex\OpenApi\Objects\PathItem;
use Cortex\OpenApi\Objects\Response;
use Cortex\OpenApi\Objects\Operation;
use Cortex\OpenApi\Objects\Parameter;

covers(OpenApi::class);

it('accepts all four parameter locations', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->paths(
PathItem::create('/items/{id}')->operations(
Operation::get()
->parameters(
Parameter::path('id', Schema::string()),
Parameter::query('filter', Schema::string()),
Parameter::header('X-Request-Id', Schema::string()),
Parameter::cookie('session', Schema::string()),
)
->responses(Response::ok()),
),
);

expect($openApi)->toPassOpenApiValidation();
});

it('rejects a parameter with neither schema nor content', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->paths(
PathItem::create('/ping')->operations(
Operation::get()
->parameters(Parameter::query('q'))
->responses(Response::ok()),
),
);

expect($openApi)->toFailOpenApiValidation([
'/paths/~1ping/get/parameters/0' => [
'The required properties (schema) are missing',
'The required properties (content) are missing',
],
]);
});

it('rejects a path parameter where required is overridden to false', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->paths(
PathItem::create('/items/{id}')->operations(
Operation::get()
->parameters(Parameter::path('id', Schema::string())->required(false))
->responses(Response::ok()),
),
);

expect($openApi)->toFailOpenApiValidationAt(
'/paths/~1items~1%7Bid%7D/get/parameters/0/required',
'The data must match the const value',
);
});
24 changes: 24 additions & 0 deletions tests/Unit/Validation/PathsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

use Cortex\OpenApi\OpenApi;
use Cortex\OpenApi\Objects\Info;
use Cortex\OpenApi\Objects\PathItem;
use Cortex\OpenApi\Objects\Response;
use Cortex\OpenApi\Objects\Operation;

covers(OpenApi::class);

it('rejects a path key not starting with a forward slash', function (): void {
$openApi = OpenApi::create()
->info(Info::create('x', '1.0.0'))
->path('noslash', PathItem::create('noslash')->operations(
Operation::get()->responses(Response::ok()),
));

expect($openApi)->toFailOpenApiValidationAt(
'/paths',
'Unevaluated object properties not allowed: noslash',
);
});
Loading