Files
InvoiceShelf/tests/Feature/OpenApiDocumentationTest.php
Darko Gjorgjijoski 8d929ec09d feat(api): generate OpenAPI spec with Scramble for api-docs.invoiceshelf.com (#685)
Auto-generate an OpenAPI 3.1 spec from the v1 API's FormRequests and Resources
(no annotations) for publishing at api-docs.invoiceshelf.com as a static
Swagger UI site.

- config/scramble.php: scope to api/v1, version from version.md, clean
  placeholder server, export to public/openapi.json
- ScrambleServiceProvider: advertise Bearer (Sanctum) auth; add the required
  `company` tenancy header only to routes using the `company` middleware
- OpenApiDocumentationTest: assert spec shape, auth scheme, company-header gating
- .github/workflows/openapi.yml: export + commit spec on release, notify the
  api-docs site to rebuild
- public/openapi.json: generated seed spec (184 paths)
- dedoc/scramble added as a dev-only dependency

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:51:54 +02:00

88 lines
2.8 KiB
PHP

<?php
use Dedoc\Scramble\Generator;
use Dedoc\Scramble\Scramble;
// Generating the full ~180-route document via static inference is memory-heavy;
// the test runner's default 128M limit is not enough (the CLI export runs with a
// higher CLI limit). Raise it for this file only.
ini_set('memory_limit', '1024M');
/**
* Build the OpenAPI document in-process (the same call `scramble:export` makes).
* Memoized so the ~180-route inference only runs once across this file's tests —
* the document is deterministic (derived from routes/models, not seeded data).
*/
function openApiDocument(): array
{
static $document;
return $document ??= app(Generator::class)(Scramble::getGeneratorConfig('default'));
}
/** @return list<string> "method path" for every operation carrying a `company` header */
function companyHeaderOperations(array $document): array
{
$matches = [];
foreach ($document['paths'] as $path => $operations) {
foreach ($operations as $method => $operation) {
if (! is_array($operation)) {
continue;
}
foreach ($operation['parameters'] ?? [] as $parameter) {
if (($parameter['name'] ?? null) === 'company' && ($parameter['in'] ?? null) === 'header') {
$matches[] = "{$method} {$path}";
}
}
}
}
return $matches;
}
it('generates a valid OpenAPI document for the v1 API', function () {
$document = openApiDocument();
expect($document['openapi'] ?? '')->toStartWith('3.')
->and($document['info']['title'])->toBe('InvoiceShelf API')
->and($document['info']['version'])->toBe(trim((string) file_get_contents(base_path('version.md'))))
->and($document['paths'])->not->toBeEmpty();
});
it('advertises bearer token authentication', function () {
$document = openApiDocument();
$scheme = collect($document['components']['securitySchemes'] ?? [])
->firstWhere('scheme', 'bearer');
expect($scheme)->not->toBeNull()
->and($scheme['type'])->toBe('http');
});
it('adds the company header to company-scoped routes', function () {
expect(companyHeaderOperations(openApiDocument()))->not->toBeEmpty();
});
it('does not add the company header to unauthenticated bootstrap routes', function () {
$document = openApiDocument();
// /auth/login runs before any tenant is resolved, so it must not require the header.
$loginOperations = $document['paths']['/auth/login'] ?? [];
$hasCompanyHeader = false;
foreach ($loginOperations as $operation) {
if (! is_array($operation)) {
continue;
}
foreach ($operation['parameters'] ?? [] as $parameter) {
if (($parameter['name'] ?? null) === 'company') {
$hasCompanyHeader = true;
}
}
}
expect($hasCompanyHeader)->toBeFalse();
});