diff --git a/CHANGELOG.md b/CHANGELOG.md index 22efbe7..c44093d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The bare `env-kit` rate limiter. It is still registered, delegates to the same limit, and logs a warning naming `laranail-env-kit-webui.api`. Earliest removal: the next minor after 0.1. +### Fixed + +- The deprecated-name warnings fired on every resolution, so a page with N links to a bare + `env-kit.*` route name logged N lines per request, and the bare `env-kit` limiter logged once per + throttled request for the life of an Octane or queue worker. Each deprecated name now warns once + for the life of the booted application. +- The suite configured the engine through the bare `env-kit.*` keys (`path`, `backup_path`, + `audit.enabled`, `auto_backup`, `hidden_keys`, `editable_keys`, `schema`, + `limits.max_value_length`). `laranail/env-kit` now reads its configuration only at + `laranail.env-kit.*`, so against a fresh resolve every one of those overrides was ignored and 13 + tests ran against the defaults: no temp `.env`, no schema, no allowlist. The tests, and the + `docs/api.md` mention of the schema key, now use `laranail.env-kit.*`. No runtime code read the + bare keys. + ## [0.1.0] - 2026-07-11 ### Fixed diff --git a/docs/api.md b/docs/api.md index 9b644bd..9cf8841 100644 --- a/docs/api.md +++ b/docs/api.md @@ -68,7 +68,8 @@ route('laranail-env-kit-webui.keys.show', ['key' => 'APP_NAME']); > **Deprecated aliases.** The bare names these replaced — `env-kit.keys.*`, `env-kit.panel` and > the `env-kit` limiter — still work: `route('env-kit.panel')` resolves to the scoped route and -> `throttle:env-kit` applies the same limit, each logging a warning that names the replacement. +> `throttle:env-kit` applies the same limit, each logging a warning that names the replacement +> (once per name for the life of the booted application, not once per link or request). > They are removed no earlier than the next minor after 0.1. Two things do not follow the alias: > `Route::has('env-kit.panel')` and `request()->routeIs('env-kit.*')` ask the route collection > directly, so use the scoped names there. @@ -76,7 +77,7 @@ route('laranail-env-kit-webui.keys.show', ['key' => 'APP_NAME']); ## Validation & status codes Input is validated with the **headless rules** (`ValidEnvKey`, `ValidEnvValue`, -and `MatchesEnvSchema` — so a configured `env-kit.schema` is enforced over the API +and `MatchesEnvSchema` — so a configured `laranail.env-kit.schema` is enforced over the API exactly as on the CLI; it is a no-op until a schema is defined) before the engine is touched, and engine guards map to HTTP statuses: diff --git a/src/Providers/EnvKitWebUIServiceProvider.php b/src/Providers/EnvKitWebUIServiceProvider.php index 241b8e3..122542c 100644 --- a/src/Providers/EnvKitWebUIServiceProvider.php +++ b/src/Providers/EnvKitWebUIServiceProvider.php @@ -27,6 +27,16 @@ final class EnvKitWebUIServiceProvider extends PackageServiceProvider { + /** + * Deprecated names already warned about. Held for the life of the booted + * application -- one request under PHP-FPM, one worker under Octane or a + * queue worker -- so a page with N links to a bare route name logs one + * line, not N. + * + * @var array + */ + private array $warnedDeprecations = []; + public function configurePackage(Package $package): void { $package @@ -96,8 +106,8 @@ private function registerThrottle(): void * same limit and logs a warning naming the replacement. Earliest * removal: the next minor after 0.1. */ - RateLimiter::for(RegisteredNames::LEGACY_LIMITER, static function (Request $request) use ($limit): Limit { - Log::warning(sprintf( + RateLimiter::for(RegisteredNames::LEGACY_LIMITER, function (Request $request) use ($limit): Limit { + $this->warnDeprecatedOnce('limiter:' . RegisteredNames::LEGACY_LIMITER, sprintf( '[laranail/env-kit-webui] The rate limiter "%s" is deprecated; use "throttle:%s" instead.', RegisteredNames::LEGACY_LIMITER, RegisteredNames::LIMITER, @@ -127,11 +137,11 @@ private function resolveBareRouteNames(): void $previous = (fn (): mixed => $this->missingNamedRouteResolver)->call($url); URL::resolveMissingNamedRoutesUsing( - static function (string $name, mixed $parameters, ?bool $absolute) use ($previous): ?string { + function (string $name, mixed $parameters, ?bool $absolute) use ($previous): ?string { $scoped = RegisteredNames::scopedRouteFor($name); if ($scoped !== null && Route::has($scoped)) { - Log::warning(sprintf( + $this->warnDeprecatedOnce('route:' . $name, sprintf( '[laranail/env-kit-webui] The route name "%s" is deprecated; use "%s" instead.', $name, $scoped, @@ -151,6 +161,20 @@ static function (string $name, mixed $parameters, ?bool $absolute) use ($previou ); } + /** + * Log a deprecation once per name for the life of the booted application. + */ + private function warnDeprecatedOnce(string $key, string $message): void + { + if (isset($this->warnedDeprecations[$key])) { + return; + } + + $this->warnedDeprecations[$key] = true; + + Log::warning($message); + } + /** * @param list $fallbackMiddleware * @param list $prepend diff --git a/tests/Feature/LivewireTest.php b/tests/Feature/LivewireTest.php index ba065f5..00ae886 100644 --- a/tests/Feature/LivewireTest.php +++ b/tests/Feature/LivewireTest.php @@ -62,7 +62,7 @@ }); it('surfaces a guard failure inline instead of crashing', function () { - $this->bindEnv("DB_PASSWORD=secret\n", ['env-kit.auto_backup' => false]); + $this->bindEnv("DB_PASSWORD=secret\n", ['laranail.env-kit.auto_backup' => false]); Livewire::test(EnvKitPanelComponent::class) ->call('startEditing', 'DB_PASSWORD') // protected key diff --git a/tests/Feature/NamingConventionTest.php b/tests/Feature/NamingConventionTest.php index 5fda57b..2818312 100644 --- a/tests/Feature/NamingConventionTest.php +++ b/tests/Feature/NamingConventionTest.php @@ -171,3 +171,35 @@ function registeredLimiterNames(): array ->withArgs(static fn (string $message): bool => str_contains($message, RegisteredNames::LIMITER)) ->atLeast()->once(); }); + +it('warns once per deprecated route name, however many links resolve it', function (): void { + Log::spy(); + + // A page with N links to the same bare name must not log N lines. + foreach (range(1, 3) as $ignored) { + route('env-kit.panel'); + route('env-kit.keys.index'); + } + + Log::shouldHaveReceived('warning') + ->withArgs(static fn (string $message): bool => str_contains($message, '"env-kit.panel"')) + ->once(); + Log::shouldHaveReceived('warning') + ->withArgs(static fn (string $message): bool => str_contains($message, '"env-kit.keys.index"')) + ->once(); +}); + +it('warns once for the deprecated env-kit limiter, however many requests it throttles', function (): void { + $legacy = app(RateLimiter::class)->limiter(RegisteredNames::LEGACY_LIMITER); + $request = Illuminate\Http\Request::create('/'); + + Log::spy(); + + foreach (range(1, 3) as $ignored) { + $legacy($request); + } + + Log::shouldHaveReceived('warning') + ->withArgs(static fn (string $message): bool => str_contains($message, RegisteredNames::LEGACY_LIMITER)) + ->once(); +}); diff --git a/tests/Feature/PolicyTest.php b/tests/Feature/PolicyTest.php index 73413ca..b5e77cb 100644 --- a/tests/Feature/PolicyTest.php +++ b/tests/Feature/PolicyTest.php @@ -10,7 +10,7 @@ it('honours a custom hidden_keys pattern in the API (masking)', function () { $this->bindEnv("APP_NAME=Acme\nWIDGET_API=topsecret123\n"); - config(['env-kit.hidden_keys' => ['WIDGET_*']]); + config(['laranail.env-kit.hidden_keys' => ['WIDGET_*']]); $response = $this->getJson('api/v1/env-kit/keys')->assertOk(); @@ -21,7 +21,7 @@ it('returns 403 for a write outside the editable allowlist', function () { $this->bindEnv("A=1\n"); - config(['env-kit.editable_keys' => ['APP_*']]); + config(['laranail.env-kit.editable_keys' => ['APP_*']]); $this->postJson('api/v1/env-kit/keys', ['key' => 'DB_HOST', 'value' => 'x'])->assertForbidden(); diff --git a/tests/Feature/SchemaValidationTest.php b/tests/Feature/SchemaValidationTest.php index 407c942..6d6f631 100644 --- a/tests/Feature/SchemaValidationTest.php +++ b/tests/Feature/SchemaValidationTest.php @@ -12,7 +12,7 @@ it('rejects a stored value that violates the engine schema', function () { $this->bindEnv("A=1\n"); - config(['env-kit.schema' => ['PORT' => 'integer']]); + config(['laranail.env-kit.schema' => ['PORT' => 'integer']]); $this->postJson('api/v1/env-kit/keys', ['key' => 'PORT', 'value' => 'not-an-int']) ->assertStatus(422) @@ -24,7 +24,7 @@ it('validates an updated value against the schema', function () { $this->bindEnv("PORT=8080\n"); - config(['env-kit.schema' => ['PORT' => 'integer']]); + config(['laranail.env-kit.schema' => ['PORT' => 'integer']]); $this->putJson('api/v1/env-kit/keys/PORT', ['value' => 'nope']) ->assertStatus(422) diff --git a/tests/Feature/WebUiCoverageTest.php b/tests/Feature/WebUiCoverageTest.php index 148b720..ad2e98d 100644 --- a/tests/Feature/WebUiCoverageTest.php +++ b/tests/Feature/WebUiCoverageTest.php @@ -50,7 +50,7 @@ it('maps an engine validation failure (value too long) to 422', function () { $this->bindEnv("A=1\n"); - config(['laranail.env-kit-webui.enabled' => true, 'env-kit.limits.max_value_length' => 5]); + config(['laranail.env-kit-webui.enabled' => true, 'laranail.env-kit.limits.max_value_length' => 5]); $this->postJson('api/v1/env-kit/keys', ['key' => 'NEW', 'value' => 'waytoolong'])->assertStatus(422); }); diff --git a/tests/TestCase.php b/tests/TestCase.php index 828a322..c25985c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -35,7 +35,7 @@ protected function defineEnvironment($app): void // load the engine defaults explicitly so tests mirror production. $engineConfig = dirname(__DIR__) . '/vendor/laranail/env-kit/config/env-kit.php'; if (is_file($engineConfig)) { - $app['config']->set('env-kit', require $engineConfig); + $app['config']->set('laranail.env-kit', require $engineConfig); } } @@ -48,10 +48,10 @@ protected function bindEnv(string $contents): string file_put_contents($path, $contents); config([ - 'env-kit.path' => $path, - 'env-kit.backup_path' => $dir . '/backups', - 'env-kit.audit.enabled' => false, - 'env-kit.auto_backup' => false, + 'laranail.env-kit.path' => $path, + 'laranail.env-kit.backup_path' => $dir . '/backups', + 'laranail.env-kit.audit.enabled' => false, + 'laranail.env-kit.auto_backup' => false, ]); $this->app->forgetInstance(EnvKitInterface::class);