Files
solidtime/tests/Unit/ApiDocs/ApiDocsExportTest.php
Constantin Graf ed2f19f58b Fix API docs generation with Scramble 0.13
Scramble 0.13 moved the collected resource type of resource collections
to a different template slot, which broke the custom paginated resource
collection extension and the API docs export. Scramble now infers
paginated responses natively, so the extension, the marker interface and
the @return annotations that overrode the inference are removed.

Scramble 0.13 also documents closure routes, so the API fallback routes
are moved to a controller that is excluded from the docs.

Add a test that exports the API docs.
2026-10-08 12:38:03 +02:00

114 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Unit\ApiDocs;
use App\Http\Controllers\Api\FallbackController;
use Dedoc\Scramble\Infer\Context;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use PHPUnit\Framework\Attributes\CoversClass;
use Tests\TestCase;
#[CoversClass(FallbackController::class)]
class ApiDocsExportTest extends TestCase
{
private string $path;
protected function setUp(): void
{
parent::setUp();
// Scramble keeps its inference context in a static property, which would otherwise outlive the refreshed application
Context::reset();
$this->path = storage_path('framework/testing/api-docs-'.uniqid().'.json');
File::ensureDirectoryExists(dirname($this->path));
}
protected function tearDown(): void
{
File::delete($this->path);
parent::tearDown();
}
/**
* @return array<string, mixed>
*/
private function exportApiDocs(): array
{
$exitCode = $this->withoutMockingConsoleOutput()->artisan('scramble:export', [
'--path' => $this->path,
]);
$this->assertSame(Command::SUCCESS, $exitCode);
$this->assertFileExists($this->path);
return json_decode(File::get($this->path), true, flags: JSON_THROW_ON_ERROR);
}
/**
* @param array<string, mixed> $docs
* @param array<string, mixed> $schema
* @return array<string, mixed>
*/
private function resolveSchema(array $docs, array $schema): array
{
if (isset($schema['$ref'])) {
$name = str_replace('#/components/schemas/', '', $schema['$ref']);
return $this->resolveSchema($docs, $docs['components']['schemas'][$name]);
}
return $schema;
}
public function test_api_docs_can_be_exported(): void
{
// Act
$docs = $this->exportApiDocs();
// Assert
$this->assertArrayHasKey('paths', $docs);
$this->assertNotEmpty($docs['paths']);
}
public function test_paginated_endpoints_are_documented_with_pagination(): void
{
// Act
$docs = $this->exportApiDocs();
// Assert
$tagsSchema = $docs['paths']['/v1/organizations/{organization}/tags']['get']['responses']['200']['content']['application/json']['schema'];
$this->assertSame(['data', 'links', 'meta'], $tagsSchema['required']);
$tagsData = $this->resolveSchema($docs, $tagsSchema['properties']['data']);
$this->assertSame('array', $tagsData['type']);
$this->assertSame('#/components/schemas/TagResource', $tagsData['items']['$ref']);
$timeEntriesSchema = $docs['paths']['/v1/organizations/{organization}/time-entries']['get']['responses']['200']['content']['application/json']['schema'];
$this->assertSame(['data', 'meta'], $timeEntriesSchema['required']);
$this->assertSame(['total'], $timeEntriesSchema['properties']['meta']['required']);
}
public function test_fallback_routes_are_excluded_from_api_docs(): void
{
// Act
$docs = $this->exportApiDocs();
// Assert
$this->assertArrayNotHasKey('/', $docs['paths']);
$this->assertArrayNotHasKey('/{fallbackPlaceholder}', $docs['paths']);
}
public function test_fallback_routes_return_not_found(): void
{
// Act
$rootResponse = $this->getJson('/api');
$unknownResponse = $this->getJson('/api/does-not-exist');
// Assert
$rootResponse->assertNotFound();
$rootResponse->assertJsonPath('message', 'API resource not found');
$unknownResponse->assertNotFound();
$unknownResponse->assertJsonPath('message', 'API resource not found');
}
}