Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.0 KiB
PHP
116 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api;
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* The browsable API documentation.
|
|
*
|
|
* The specification was always there as a file; what this covers is that it is
|
|
* readable, that it stays in step with the routes it claims to describe, and
|
|
* that nothing about it depends on somebody else's CDN.
|
|
*/
|
|
class ApiDocsTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
#[Test]
|
|
public function the_docs_page_opens_without_signing_in(): void
|
|
{
|
|
// The people who need it are writing the public frontend and have no
|
|
// reason to hold a panel account.
|
|
$this->get('/docs/api')
|
|
->assertOk()
|
|
->assertSee('swagger', escape: false);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_points_at_the_installation_being_looked_at(): void
|
|
{
|
|
$html = (string) $this->get('/docs/api')->assertOk()->getContent();
|
|
|
|
// Not the canonical URL from the configuration: the same page is
|
|
// opened on a laptop, on staging and in production, and "try it out"
|
|
// has to reach whichever one it is.
|
|
$this->assertStringContainsString(json_encode(url('/api')), $html);
|
|
$this->assertStringContainsString(json_encode(asset('openapi.yaml')), $html);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_carries_its_own_assets(): void
|
|
{
|
|
$html = (string) $this->get('/docs/api')->assertOk()->getContent();
|
|
|
|
// The docs have to work on a laptop with no internet, and a script tag
|
|
// pointing at somebody else's server is a dependency this application
|
|
// does not otherwise have.
|
|
$this->assertStringNotContainsString('cdn.', $html);
|
|
$this->assertStringNotContainsString('unpkg', $html);
|
|
|
|
foreach (['swagger-ui.css', 'swagger-ui-bundle.js', 'swagger-ui-standalone-preset.js'] as $asset) {
|
|
$this->assertFileExists(public_path("vendor/swagger-ui/{$asset}"));
|
|
$this->assertStringContainsString("vendor/swagger-ui/{$asset}", $html);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function the_specification_parses(): void
|
|
{
|
|
$spec = Yaml::parseFile(public_path('openapi.yaml'));
|
|
|
|
$this->assertIsArray($spec);
|
|
$this->assertArrayHasKey('paths', $spec);
|
|
$this->assertNotEmpty($spec['paths']);
|
|
}
|
|
|
|
/**
|
|
* Every path the specification advertises exists as a route.
|
|
*
|
|
* Documentation that describes an endpoint nobody implemented is worse
|
|
* than none: it is believed.
|
|
*/
|
|
#[Test]
|
|
public function every_documented_endpoint_is_routed(): void
|
|
{
|
|
$spec = Yaml::parseFile(public_path('openapi.yaml'));
|
|
|
|
$routes = collect(app('router')->getRoutes()->getRoutes())
|
|
->flatMap(fn ($route) => collect($route->methods())
|
|
->map(fn (string $method) => strtolower($method).' '.$route->uri()))
|
|
->all();
|
|
|
|
$missing = [];
|
|
|
|
foreach ($spec['paths'] as $path => $operations) {
|
|
// The specification is served under an `/api` base URL, and route
|
|
// parameters are named independently on either side.
|
|
$uri = 'api'.preg_replace('/\{[^}]+\}/', '{param}', $path);
|
|
|
|
// A path item may also carry `parameters`, `summary` and friends;
|
|
// only the HTTP methods describe an endpoint.
|
|
$methods = array_intersect(
|
|
array_keys($operations),
|
|
['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace'],
|
|
);
|
|
|
|
foreach ($methods as $method) {
|
|
$normalised = collect($routes)
|
|
->map(fn (string $route) => preg_replace('/\{[^}]+\}/', '{param}', $route))
|
|
->contains(strtolower($method).' '.$uri);
|
|
|
|
if (! $normalised) {
|
|
$missing[] = strtoupper($method)." {$path}";
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->assertSame([], $missing, "Documented but not routed:\n".implode("\n", $missing));
|
|
}
|
|
}
|