mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-16 13:55:21 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
421c385fa7
commit
8d929ec09d
84
.github/workflows/openapi.yml
vendored
Normal file
84
.github/workflows/openapi.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Export OpenAPI Spec
|
||||
|
||||
# Regenerates public/openapi.json with Scramble and notifies the api-docs site
|
||||
# (api-docs.invoiceshelf.com) to rebuild. Runs on every published release — so the
|
||||
# committed spec matches released code — and can be triggered manually.
|
||||
#
|
||||
# v3 only: this lives on the 3.x branch. The api-docs site documents the v3 API.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
export:
|
||||
name: Export & publish OpenAPI document
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
extensions: bcmath, curl, dom, gd, json, libxml, mbstring, pcntl, pdo, pdo_sqlite, zip
|
||||
steps:
|
||||
- name: Checkout (release branch)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# On a release, target_commitish is the branch the tag was cut from, so
|
||||
# the spec is committed back to a branch rather than a detached tag.
|
||||
ref: ${{ github.event.release.target_commitish || github.ref_name }}
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.4
|
||||
extensions: ${{ env.extensions }}
|
||||
coverage: none
|
||||
tools: composer
|
||||
|
||||
- name: Install Composer dependencies (incl. dev — Scramble is dev-only)
|
||||
uses: ramsey/composer-install@4.0.0
|
||||
|
||||
- name: Prepare app environment
|
||||
run: |
|
||||
cp -n .env.example .env || true
|
||||
php artisan key:generate
|
||||
|
||||
- name: Migrate a throwaway SQLite database
|
||||
# Scramble introspects the schema to build response shapes, so the tables
|
||||
# must exist. Data is irrelevant — schema only.
|
||||
run: |
|
||||
mkdir -p database
|
||||
: > database/openapi.sqlite
|
||||
php artisan migrate --force --no-interaction
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: ${{ github.workspace }}/database/openapi.sqlite
|
||||
|
||||
- name: Export OpenAPI document
|
||||
run: php artisan scramble:export --path=public/openapi.json
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: ${{ github.workspace }}/database/openapi.sqlite
|
||||
|
||||
- name: Commit updated spec
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "docs: regenerate OpenAPI spec [skip ci]"
|
||||
file_pattern: public/openapi.json
|
||||
commit_user_name: "GitHub Actions"
|
||||
commit_user_email: "actions@github.com"
|
||||
|
||||
- name: Notify api-docs site to rebuild
|
||||
continue-on-error: true
|
||||
env:
|
||||
PAT: ${{ secrets.API_DOCS_DISPATCH_PAT }}
|
||||
run: |
|
||||
if [ -z "$PAT" ]; then
|
||||
echo "No API_DOCS_DISPATCH_PAT secret set; skipping rebuild notification."
|
||||
exit 0
|
||||
fi
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token $PAT" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
https://api.github.com/repos/InvoiceShelf/api-docs/dispatches \
|
||||
-d '{"event_type":"spec-updated"}'
|
||||
64
app/Providers/ScrambleServiceProvider.php
Normal file
64
app/Providers/ScrambleServiceProvider.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\Operation;
|
||||
use Dedoc\Scramble\Support\Generator\Parameter;
|
||||
use Dedoc\Scramble\Support\Generator\Schema;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
use Dedoc\Scramble\Support\Generator\Types\StringType;
|
||||
use Dedoc\Scramble\Support\RouteInfo;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
/**
|
||||
* Configures Scramble's OpenAPI generation for the InvoiceShelf API.
|
||||
*
|
||||
* Scramble is a dev-only dependency used to generate the spec in CI/local (see
|
||||
* `php artisan scramble:export`); it is absent in production (composer install
|
||||
* --no-dev). Every reference to it is therefore guarded by class_exists().
|
||||
*/
|
||||
class ScrambleServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if (! class_exists(Scramble::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Scramble::configure()
|
||||
->withDocumentTransformers($this->documentTransformer(...))
|
||||
->withOperationTransformers($this->operationTransformer(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Advertise Bearer (Sanctum personal access token) auth as the global
|
||||
* security scheme. The customer-portal guard (auth:customer) is session-based
|
||||
* and not part of the token API surface, so it is intentionally not exposed.
|
||||
*/
|
||||
protected function documentTransformer(OpenApi $openApi): void
|
||||
{
|
||||
$openApi->secure(SecurityScheme::http('bearer'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the multi-tenancy `company` header to every company-scoped operation.
|
||||
* CompanyMiddleware (alias `company`) resolves the active tenant from this
|
||||
* header, so only routes that actually run it get the parameter — auth, ping,
|
||||
* and installation endpoints stay clean.
|
||||
*/
|
||||
protected function operationTransformer(Operation $operation, RouteInfo $routeInfo): void
|
||||
{
|
||||
if (! in_array('company', $routeInfo->route->gatherMiddleware(), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operation->addParameters([
|
||||
Parameter::make('company', 'header')
|
||||
->description('ID of the company the request operates on (multi-tenancy).')
|
||||
->setSchema(Schema::fromType(new StringType))
|
||||
->required(true),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Providers\DriverRegistryProvider;
|
||||
use App\Providers\DropboxServiceProvider;
|
||||
use App\Providers\PdfServiceProvider;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use App\Providers\ScrambleServiceProvider;
|
||||
use App\Providers\ViewServiceProvider;
|
||||
use App\Support\Hashids\HashidsServiceProvider;
|
||||
|
||||
@@ -20,4 +21,5 @@ return [
|
||||
DriverRegistryProvider::class,
|
||||
AiServiceProvider::class,
|
||||
AppConfigProvider::class,
|
||||
ScrambleServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.5",
|
||||
"dedoc/scramble": "^0.13.27",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"jasonmccreary/laravel-test-assertions": "^2.9",
|
||||
"laravel/pint": "^1.13",
|
||||
|
||||
82
composer.lock
generated
82
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "56df267c2bb3603a8093fb53d6cf137c",
|
||||
"content-hash": "0330848b7c96620b5ee47fcb585f719c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -9531,6 +9531,86 @@
|
||||
],
|
||||
"time": "2024-11-12T16:29:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dedoc/scramble",
|
||||
"version": "v0.13.27",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dedoc/scramble.git",
|
||||
"reference": "5b067b1168b4092ee10b320469a09823610f48b1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dedoc/scramble/zipball/5b067b1168b4092ee10b320469a09823610f48b1",
|
||||
"reference": "5b067b1168b4092ee10b320469a09823610f48b1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
|
||||
"myclabs/deep-copy": "^1.12",
|
||||
"nikic/php-parser": "^5.0",
|
||||
"php": "^8.1",
|
||||
"phpstan/phpdoc-parser": "^1.0|^2.0",
|
||||
"spatie/laravel-package-tools": "^1.9.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^3.3",
|
||||
"laravel/pint": "^v1.1.0",
|
||||
"nunomaduro/collision": "^7.0|^8.0",
|
||||
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
|
||||
"pestphp/pest": "^2.34|^3.7|^4.4",
|
||||
"pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^10.5|^11.5.3|^12.5.12",
|
||||
"spatie/laravel-permission": "^6.10|^7.2",
|
||||
"spatie/pest-plugin-snapshots": "^2.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Dedoc\\Scramble\\ScrambleServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dedoc\\Scramble\\": "src",
|
||||
"Dedoc\\Scramble\\Database\\Factories\\": "database/factories"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Roman Lytvynenko",
|
||||
"email": "litvinenko95@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Automatic generation of API documentation for Laravel applications.",
|
||||
"homepage": "https://github.com/dedoc/scramble",
|
||||
"keywords": [
|
||||
"documentation",
|
||||
"laravel",
|
||||
"openapi"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dedoc/scramble/issues",
|
||||
"source": "https://github.com/dedoc/scramble/tree/v0.13.27"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/romalytvynenko",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-12T12:41:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
"version": "1.1.6",
|
||||
|
||||
187
config/scramble.php
Normal file
187
config/scramble.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||
|
||||
return [
|
||||
/*
|
||||
* Which routes to document. String or array form; use Scramble::routes() for custom selection.
|
||||
*
|
||||
* 'api_path' => [
|
||||
* 'include' => 'api',
|
||||
* 'exclude' => ['api/internal'],
|
||||
* ],
|
||||
*
|
||||
* Without *, patterns match path segments (api matches api and api/users, not apiary).
|
||||
* With *, Str::is is used (e.g. api/v*).
|
||||
*
|
||||
* One static include → default server is /{include} and paths are stripped (/users).
|
||||
* Multiple includes or wildcards → server defaults to / and paths stay full (/api/users).
|
||||
* Override with `servers`, or use Scramble::registerApi() for separate bases.
|
||||
*/
|
||||
'api_path' => 'api/v1',
|
||||
|
||||
/*
|
||||
* Your API domain. By default, app domain is used. This is also a part of the default API routes
|
||||
* matcher, so when implementing your own, make sure you use this config if needed.
|
||||
*/
|
||||
'api_domain' => null,
|
||||
|
||||
/*
|
||||
* The path where your OpenAPI specification will be exported (relative to the
|
||||
* project root). Committed to the repo by CI so the api-docs.invoiceshelf.com
|
||||
* build can fetch it; also servable directly from this instance at /openapi.json.
|
||||
*/
|
||||
'export_path' => 'public/openapi.json',
|
||||
|
||||
/*
|
||||
* Cache configuration for the generated OpenAPI document.
|
||||
*
|
||||
* Use `scramble:cache` to warm the cache and `scramble:clear` to invalidate it.
|
||||
*/
|
||||
'cache' => [
|
||||
'key' => 'scramble.openapi',
|
||||
'store' => 'file',
|
||||
],
|
||||
|
||||
'info' => [
|
||||
/*
|
||||
* API version. Defaults to the value in the repo's version.md so the
|
||||
* generated spec self-labels (e.g. 3.0.0-alpha.1 on v3.0, 2.x on master).
|
||||
*/
|
||||
'version' => env('API_VERSION', is_file(base_path('version.md'))
|
||||
? trim((string) file_get_contents(base_path('version.md')))
|
||||
: '0.0.1'),
|
||||
|
||||
/*
|
||||
* Description rendered on the home page of the API documentation (`/docs/api`).
|
||||
*/
|
||||
'description' => 'REST API for InvoiceShelf — open-source invoicing & expense tracking. '
|
||||
.'Authenticate with a Sanctum personal access token (Authorization: Bearer <token>). '
|
||||
.'Company-scoped endpoints additionally require a `company` header carrying the company ID.',
|
||||
],
|
||||
|
||||
'ui' => [
|
||||
'title' => 'InvoiceShelf API',
|
||||
],
|
||||
|
||||
'renderer' => 'elements',
|
||||
|
||||
'renderers' => [
|
||||
/*
|
||||
* Stoplight Elements config options: https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options
|
||||
*/
|
||||
'elements' => [
|
||||
'view' => 'scramble::docs',
|
||||
'theme' => 'light',
|
||||
'hideTryIt' => false,
|
||||
'hideSchemas' => false,
|
||||
'logo' => '',
|
||||
'tryItCredentialsPolicy' => 'include',
|
||||
'layout' => 'responsive',
|
||||
'router' => 'hash',
|
||||
],
|
||||
/*
|
||||
* Scalar API reference config options: https://scalar.com/products/api-references/configuration
|
||||
*/
|
||||
'scalar' => [
|
||||
'view' => 'scramble::scalar',
|
||||
'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference',
|
||||
'theme' => 'laravel',
|
||||
'proxyUrl' => 'https://proxy.scalar.com',
|
||||
'darkMode' => false,
|
||||
'showDeveloperTools' => 'never',
|
||||
'agent' => ['disabled' => true],
|
||||
'credentials' => 'include',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* The list of servers of the API. By default, when `null`, server URL will be created from
|
||||
* `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you
|
||||
* will need to specify the local server URL manually (if needed).
|
||||
*
|
||||
* Example of non-default config (final URLs are generated using Laravel `url` helper):
|
||||
*
|
||||
* ```php
|
||||
* 'servers' => [
|
||||
* 'Live' => 'api',
|
||||
* 'Prod' => 'https://scramble.dedoc.co/api',
|
||||
* ],
|
||||
* ```
|
||||
*/
|
||||
'servers' => [
|
||||
// InvoiceShelf is self-hosted: every instance has its own host. A clean
|
||||
// placeholder is documented here instead of baking in the build-time
|
||||
// APP_URL (which would leak a dev/CI hostname into the public spec).
|
||||
// Readers replace it with their own instance URL.
|
||||
'Your InvoiceShelf instance' => 'https://your-instance.example.com/api/v1',
|
||||
],
|
||||
|
||||
/**
|
||||
* Determines how Scramble stores the descriptions of enum cases.
|
||||
* Available options:
|
||||
* - 'description' – Case descriptions are stored as the enum schema's description using table formatting.
|
||||
* - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension.
|
||||
*
|
||||
* @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions
|
||||
* - false - Case descriptions are ignored.
|
||||
*/
|
||||
'enum_cases_description_strategy' => 'description',
|
||||
|
||||
/**
|
||||
* Determines how Scramble stores the names of enum cases.
|
||||
* Available options:
|
||||
* - 'names' – Case names are stored in the `x-enumNames` enum schema extension.
|
||||
* - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension.
|
||||
* - false - Case names are not stored.
|
||||
*/
|
||||
'enum_cases_names_strategy' => false,
|
||||
|
||||
/**
|
||||
* When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated
|
||||
* OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until
|
||||
* OpenAPI 3.2 is released and query string structure can be described properly.
|
||||
*
|
||||
* For example, this nested validation rule describes the object with `bar` property:
|
||||
* `['foo.bar' => ['required', 'int']]`.
|
||||
*
|
||||
* When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so:
|
||||
* `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`.
|
||||
*
|
||||
* When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so:
|
||||
* `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`.
|
||||
*/
|
||||
'flatten_deep_query_parameters' => true,
|
||||
|
||||
'middleware' => [
|
||||
'web',
|
||||
RestrictedDocsAccess::class,
|
||||
],
|
||||
|
||||
'extensions' => [],
|
||||
|
||||
/*
|
||||
* Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route
|
||||
* middleware.
|
||||
*
|
||||
* Disabled by default. Uncomment the line below to enable `MiddlewareAuthSecurityStrategy`.
|
||||
* When at least one documented route uses middleware matching the configured patterns (by default
|
||||
* `auth` and `auth:*`), bearer auth is applied globally. Routes without matching middleware are
|
||||
* marked as public (`security: []`).
|
||||
*
|
||||
* Set to `null` explicitly to disable. If you already configure security manually via
|
||||
* `afterOpenApiGenerated` / `extendOpenApi`, keep this disabled to avoid duplicate schemes.
|
||||
*
|
||||
* Customize with a class-string or [class, options]:
|
||||
*
|
||||
* 'security_strategy' => [
|
||||
* \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
|
||||
* [
|
||||
* 'middleware' => ['auth', 'auth:*'],
|
||||
* 'scheme' => \Dedoc\Scramble\Support\Generator\SecurityScheme::http('bearer'),
|
||||
* ],
|
||||
* ],
|
||||
*/
|
||||
// 'security_strategy' => \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class,
|
||||
'security_strategy' => null,
|
||||
];
|
||||
31244
public/openapi.json
Normal file
31244
public/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
87
tests/Feature/OpenApiDocumentationTest.php
Normal file
87
tests/Feature/OpenApiDocumentationTest.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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();
|
||||
});
|
||||
Reference in New Issue
Block a user