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)); } }