132 lines
4.1 KiB
PHP
132 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests;
|
|
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
/**
|
|
* Compares an actual JSON response against what `public/openapi.yaml` says
|
|
* it should look like.
|
|
*
|
|
* Shared between the public and the admin contract tests because the
|
|
* question is the same one twice: does the shape the document promises match
|
|
* the shape the API sends. What differs between the two is everything
|
|
* *around* the question — how a caller authenticates, what a listing is
|
|
* allowed to leave out — not the comparison itself.
|
|
*/
|
|
trait VerifiesOpenApiContract
|
|
{
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
protected array $spec;
|
|
|
|
protected function loadOpenApiSpec(): void
|
|
{
|
|
$this->spec = (array) Yaml::parseFile(public_path('openapi.yaml'));
|
|
}
|
|
|
|
/**
|
|
* The field names a schema promises, one level deep — enough for a flat
|
|
* comparison against `array_keys()` on a decoded JSON object.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
protected function documentedKeys(array $schema): array
|
|
{
|
|
return array_keys($this->resolve($schema)['properties'] ?? []);
|
|
}
|
|
|
|
/**
|
|
* The documented fields that must always be present.
|
|
*
|
|
* Anything the specification marks `nullable` is left out of this half of
|
|
* the check — that is the vocabulary this document already uses for a
|
|
* field that may not be there, whether because the value itself can be
|
|
* null or because it only appears down one particular route. The other
|
|
* direction stays strict regardless: whatever is actually sent has to be
|
|
* described, nullable or not.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
protected function alwaysPresent(array $schema): array
|
|
{
|
|
$resolved = $this->resolve($schema);
|
|
|
|
return array_keys(array_filter(
|
|
(array) ($resolved['properties'] ?? []),
|
|
fn (array $property) => ($property['nullable'] ?? false) !== true,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* The schema of one record inside a `{"data": ...}` envelope, whichever
|
|
* way the endpoint wraps it — a bare object for a detail endpoint, or the
|
|
* `items` of an array for a listing.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function recordSchema(string $path, string $method = 'get', int $status = 200): array
|
|
{
|
|
$schema = $this->resolve(
|
|
$this->spec['paths'][$path][$method]['responses'][$status]['content']['application/json']['schema'] ?? [],
|
|
);
|
|
|
|
$data = $this->resolve($schema['properties']['data'] ?? []);
|
|
|
|
if (($data['type'] ?? null) === 'array') {
|
|
$data = $this->resolve($data['items'] ?? []);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Everything documented for one path's response, `data` included — for
|
|
* comparing against the envelope as a whole rather than only the record
|
|
* inside it.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function responseSchema(string $path, string $method = 'get', int $status = 200): array
|
|
{
|
|
return $this->resolve(
|
|
$this->spec['paths'][$path][$method]['responses'][$status]['content']['application/json']['schema'] ?? [],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Follow `$ref` and flatten `allOf`, which is how the specification says
|
|
* "this, plus a couple of fields" without repeating a schema.
|
|
*
|
|
* @param array<string, mixed> $schema
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function resolve(array $schema): array
|
|
{
|
|
if (isset($schema['$ref'])) {
|
|
$name = basename((string) $schema['$ref']);
|
|
|
|
return $this->resolve((array) ($this->spec['components']['schemas'][$name] ?? []));
|
|
}
|
|
|
|
if (isset($schema['allOf'])) {
|
|
$merged = ['type' => 'object', 'properties' => []];
|
|
|
|
foreach ((array) $schema['allOf'] as $part) {
|
|
$resolved = $this->resolve((array) $part);
|
|
$merged['properties'] = [
|
|
...$merged['properties'],
|
|
...($resolved['properties'] ?? []),
|
|
];
|
|
}
|
|
|
|
return [...$schema, ...$merged];
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
}
|