fix(release): gate the /release route on the flag (spec gating triple)

Register the /release route only when config('clusev.release_controls') is
true — the first of the spec's three gating layers (route + component mount +
sidebar). The route file is re-included per request when uncached, so the flag
is honoured; mount() abort_unless(...) still covers the cached-route case.

The gating test re-evaluates routes/web.php after toggling the flag (the file
is read once at boot, so a runtime config()->set() alone cannot register the
route), mirroring a fresh request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-22 23:48:17 +02:00
parent 4b68b8dc04
commit 76c1d46d12
2 changed files with 27 additions and 9 deletions

View File

@ -135,11 +135,13 @@ Route::middleware('auth')->group(function () {
Route::get('/help', Help\Index::class)->name('help'); Route::get('/help', Help\Index::class)->name('help');
Route::get('/wireguard', Wireguard\Index::class)->name('wireguard'); Route::get('/wireguard', Wireguard\Index::class)->name('wireguard');
// Dev-only Release page. The route is always registered (routes/web.php is evaluated once at // Dev-only Release page — first of the gating triple (spec): the route is registered ONLY when
// boot, so a boot-time config flag could not be toggled per-request), but the component's // the flag is on, so with it off /release does not exist (uncached route file is re-included
// mount() abort_unless(config('clusev.release_controls')) is the real gate: with the flag off // per request, so the flag is honoured). Release\Index::mount() abort_unless(...) is the second
// every hit 404s, exactly as if the route did not exist. The sidebar item is flag-gated too. // guard (covers a cached route file); the sidebar item is flag-gated too (third).
Route::get('/release', Release\Index::class)->name('release'); if (config('clusev.release_controls')) {
Route::get('/release', Release\Index::class)->name('release');
}
Route::get('/wg-status.json', fn (\App\Services\WgStatus $wg) => response()->json($wg->read()))->name('wg.status'); Route::get('/wg-status.json', fn (\App\Services\WgStatus $wg) => response()->json($wg->read()))->name('wg.status');

View File

@ -4,6 +4,8 @@ namespace Tests\Feature;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\RouteCollection;
use Illuminate\Support\Facades\Route;
use Tests\TestCase; use Tests\TestCase;
class ReleaseGatingTest extends TestCase class ReleaseGatingTest extends TestCase
@ -16,27 +18,41 @@ class ReleaseGatingTest extends TestCase
$this->actingAs(User::factory()->create(['must_change_password' => false])); $this->actingAs(User::factory()->create(['must_change_password' => false]));
} }
/**
* Set the release flag and re-evaluate routes/web.php so the conditional `/release`
* registration reflects it. The route file is read once at boot (flag default off), so a
* runtime config()->set() alone cannot register the route re-loading the file mirrors a
* fresh request that re-includes routes/web.php with the flag now set.
*/
private function withReleaseFlag(bool $on): void
{
config()->set('clusev.release_controls', $on);
Route::setRoutes(new RouteCollection);
require base_path('routes/web.php');
$this->app['router']->getRoutes()->refreshNameLookups();
}
public function test_release_route_is_404_when_the_flag_is_off(): void public function test_release_route_is_404_when_the_flag_is_off(): void
{ {
config()->set('clusev.release_controls', false); $this->withReleaseFlag(false);
$this->get('/release')->assertNotFound(); $this->get('/release')->assertNotFound();
} }
public function test_release_route_loads_when_the_flag_is_on(): void public function test_release_route_loads_when_the_flag_is_on(): void
{ {
config()->set('clusev.release_controls', true); $this->withReleaseFlag(true);
$this->get('/release')->assertOk(); $this->get('/release')->assertOk();
} }
public function test_sidebar_hides_the_release_item_when_the_flag_is_off(): void public function test_sidebar_hides_the_release_item_when_the_flag_is_off(): void
{ {
config()->set('clusev.release_controls', false); $this->withReleaseFlag(false);
$this->get('/audit')->assertOk()->assertDontSee(__('release.nav')); $this->get('/audit')->assertOk()->assertDontSee(__('release.nav'));
} }
public function test_sidebar_shows_the_release_item_when_the_flag_is_on(): void public function test_sidebar_shows_the_release_item_when_the_flag_is_on(): void
{ {
config()->set('clusev.release_controls', true); $this->withReleaseFlag(true);
$this->get('/audit')->assertOk()->assertSee(__('release.nav')); $this->get('/audit')->assertOk()->assertSee(__('release.nav'));
} }
} }