diff --git a/routes/web.php b/routes/web.php index eb9ea0f..6db772f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -135,11 +135,13 @@ Route::middleware('auth')->group(function () { Route::get('/help', Help\Index::class)->name('help'); Route::get('/wireguard', Wireguard\Index::class)->name('wireguard'); - // Dev-only Release page. The route is always registered (routes/web.php is evaluated once at - // boot, so a boot-time config flag could not be toggled per-request), but the component's - // mount() abort_unless(config('clusev.release_controls')) is the real gate: with the flag off - // every hit 404s, exactly as if the route did not exist. The sidebar item is flag-gated too. - Route::get('/release', Release\Index::class)->name('release'); + // Dev-only Release page — first of the gating triple (spec): the route is registered ONLY when + // the flag is on, so with it off /release does not exist (uncached route file is re-included + // per request, so the flag is honoured). Release\Index::mount() abort_unless(...) is the second + // guard (covers a cached route file); the sidebar item is flag-gated too (third). + 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'); diff --git a/tests/Feature/ReleaseGatingTest.php b/tests/Feature/ReleaseGatingTest.php index b74130a..6f27149 100644 --- a/tests/Feature/ReleaseGatingTest.php +++ b/tests/Feature/ReleaseGatingTest.php @@ -4,6 +4,8 @@ namespace Tests\Feature; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Routing\RouteCollection; +use Illuminate\Support\Facades\Route; use Tests\TestCase; class ReleaseGatingTest extends TestCase @@ -16,27 +18,41 @@ class ReleaseGatingTest extends TestCase $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 { - config()->set('clusev.release_controls', false); + $this->withReleaseFlag(false); $this->get('/release')->assertNotFound(); } 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(); } 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')); } 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')); } }