Initial commit: Pireon Admin — headless admin panel and public API
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>
This commit is contained in:
115
tests/Feature/Api/ApiDocsTest.php
Normal file
115
tests/Feature/Api/ApiDocsTest.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user