uri(), '_boost')) { $boostRoutes[$route->uri().' ['.implode('|', $route->methods()).']'] = $route->gatherMiddleware(); } } // 모집단 가드 — `_boost` 라우트가 하나도 안 잡히면 아래 foreach 는 공허참이 된다. $this->assertNotEmpty( $boostRoutes, '_boost 라우트가 하나도 등록되지 않았습니다 — 이 테스트가 공허하게 통과하고 있습니다 ' .'(bootstrap/app.php 의 devtools 그룹 등록을 확인하세요).' ); foreach ($boostRoutes as $label => $middleware) { $this->assertContains( self::GATE_ALIAS, $middleware, "{$label} 에 ".self::GATE_ALIAS.' 가 붙어 있지 않습니다. 게이트 없는 DevTools 라우트는 ' .'production 에서도 미인증 접근을 허용하며, 예외·로그를 남기지 않아 정상 응답이 ' .'유일한 증상입니다. 게이트는 개별 라우트가 아니라 bootstrap/app.php 의 devtools ' .'래퍼(Route::middleware([\'api\', \'debug.gate\'])) 가 단일 지점에서 부여합니다.' ); } } /** * 알려진 8개 엔드포인트가 실제로 등록되어 있어야 한다. * * 위 계약은 "등록된 것 전부" 를 보므로, 라우트가 통째로 사라져도 통과한다. 8개 URI 를 * 명시해 두어 삭제·오타로 인한 소실을 함께 잡는다. GET 4종은 SPA catch-all shadow 때문에 * 행위 테스트로 등록 여부를 구분할 수 없어서, 이 단언이 유일한 통로다. */ public function test_known_devtools_endpoints_are_registered(): void { $expected = [ '_boost/browser-logs' => 'POST', '_boost/g7-debug/dump-state' => 'POST', '_boost/g7-debug/log' => 'POST', '_boost/g7-debug/state' => 'GET', '_boost/g7-debug/actions' => 'GET', '_boost/g7-debug/cache' => 'GET', '_boost/g7-debug/change-detection' => 'GET', '_boost/g7-debug/clear' => 'DELETE', ]; $registered = []; foreach (RouteFacade::getRoutes() as $route) { foreach ($route->methods() as $method) { $registered[$route->uri()][] = $method; } } foreach ($expected as $uri => $method) { $this->assertArrayHasKey($uri, $registered, "DevTools 라우트가 등록되지 않았습니다: {$uri}"); $this->assertContains( $method, $registered[$uri], "{$uri} 에 {$method} 메서드가 등록되지 않았습니다." ); } } /** * `routes/devtools.php` 는 개별 게이트를 다시 들이지 않아야 한다. * * 주석·문자열은 걷어내고 실행 코드만 본다 — 이 파일의 헤더 docblock 은 "여기에 * `DebugGate::isEnabled()` 를 적지 말라" 고 **설명하기 위해** 그 심볼을 언급하므로, * 원문 그대로 검사하면 정상 상태가 위반으로 잡힌다. */ public function test_devtools_route_file_has_no_inline_debug_gate(): void { $source = file_get_contents(base_path('routes/devtools.php')); $this->assertIsString($source); $code = $this->stripCommentsAndStrings($source); $this->assertStringNotContainsString( 'DebugGate::isEnabled', $code, 'routes/devtools.php 에 개별 디버그 게이트가 다시 들어왔습니다. 게이트는 ' .'bootstrap/app.php 의 devtools 그룹 미들웨어(debug.gate) 가 단일 지점에서 담당합니다 ' .'— 핸들러마다 적으면 새 라우트에서 빠뜨리게 되고, 그 라우트만 조용히 열립니다.' ); } /** * 소스에서 주석과 문자열 리터럴 본문을 제거합니다. * * 실행되는 코드만 판정 대상이다. 어휘 분석으로 토큰을 걷어낸다. * * @param string $source 대상 소스 * @return string 주석·문자열이 제거된 소스 */ private function stripCommentsAndStrings(string $source): string { $stripped = ''; foreach (token_get_all($source) as $token) { if (is_array($token)) { if (in_array($token[0], [T_COMMENT, T_DOC_COMMENT, T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE, T_INLINE_HTML], true)) { continue; } $stripped .= $token[1]; continue; } $stripped .= $token; } return $stripped; } }