group('admin-smoke'); beforeEach(function () { $this->hadesUser = User::factory()->create(['tier' => UserTier::HADES]); $this->workspace = Workspace::factory()->create(); $this->hadesUser->hostWorkspaces()->attach($this->workspace->id, ['is_default' => true]); }); describe('Admin Route Discovery', function () { it('discovers all admin GET routes', function () { $routes = discoverAdminRoutes(); expect($routes)->not->toBeEmpty('No admin routes found - is the app bootstrapped?'); // Log discovered routes for visibility dump('Discovered '.count($routes).' admin routes'); }); it('all admin routes respond without server errors', function () { $routes = discoverAdminRoutes(); $failures = []; foreach ($routes as $route) { $uri = $route->uri(); // Skip routes with required parameters for now if (preg_match('/\{[^}]+\}/', $uri)) { continue; } try { $response = $this->actingAs($this->hadesUser)->get('/'.$uri); } catch (Throwable $e) { $failures[] = [ 'uri' => $uri, 'status' => 500, 'error' => get_class($e).': '.$e->getMessage(), ]; continue; } // 200 = OK, 403 = permission denied (acceptable), 302 = redirect (acceptable) // 500 = server error (FAIL), 404 = route broken (FAIL for admin routes) if ($response->status() >= 500) { // Try to extract error from response $content = $response->getContent(); $errorMsg = 'Server error'; if (preg_match('/Exception:?\s*([^\n<]+)/i', $content, $m)) { $errorMsg = trim($m[1]); } elseif (preg_match('/([^<]+)<\/title>/i', $content, $m)) { $errorMsg = trim($m[1]); } $failures[] = [ 'uri' => $uri, 'status' => $response->status(), 'error' => $errorMsg, ]; } elseif ($response->status() === 404) { $failures[] = [ 'uri' => $uri, 'status' => $response->status(), 'error' => 'Route not found', ]; } } if (! empty($failures)) { $message = "Admin routes with errors:\n"; foreach ($failures as $f) { $message .= " - {$f['uri']}: {$f['status']} ({$f['error']})\n"; } $this->fail($message); } expect($failures)->toBeEmpty(); }); it('no admin routes return 500 errors', function () { $routes = discoverAdminRoutes(); foreach ($routes as $route) { $uri = $route->uri(); // Skip parameterised routes if (preg_match('/\{[^}]+\}/', $uri)) { continue; } $response = $this->actingAs($this->hadesUser)->get('/'.$uri); expect($response->status()) ->not->toBe(500, "Route /{$uri} returned 500 error"); } }); }); describe('Admin Route Security', function () { it('all admin routes require authentication', function () { $routes = discoverAdminRoutes(); foreach ($routes as $route) { $uri = $route->uri(); // Skip parameterised routes if (preg_match('/\{[^}]+\}/', $uri)) { continue; } $response = $this->get('/'.$uri); // Should redirect to login (302) or return 401/403 expect($response->status()) ->toBeIn([302, 401, 403], "Route /{$uri} is accessible without auth"); } }); it('admin-only routes deny non-Hades users', function () { $regularUser = User::factory()->create(['tier' => UserTier::FREE]); $regularUser->hostWorkspaces()->attach($this->workspace->id, ['is_default' => true]); $routes = discoverAdminRoutes(); foreach ($routes as $route) { $uri = $route->uri(); // Skip parameterised routes if (preg_match('/\{[^}]+\}/', $uri)) { continue; } // Only check routes that are truly admin-only (hub/admin/*) // Regular hub/* routes are for all authenticated users if (! str_starts_with($uri, 'hub/admin/')) { continue; } $response = $this->actingAs($regularUser)->get('/'.$uri); // Should be 403 Forbidden for admin-only routes expect($response->status()) ->toBe(403, "Route /{$uri} is accessible by non-Hades user"); } }); }); describe('Admin Route Coverage', function () { it('reports admin route statistics', function () { $routes = discoverAdminRoutes(); $total = count($routes); $withParams = 0; $withoutParams = 0; foreach ($routes as $route) { if (preg_match('/\{[^}]+\}/', $route->uri())) { $withParams++; } else { $withoutParams++; } } dump([ 'total_admin_routes' => $total, 'testable_routes' => $withoutParams, 'parameterised_routes' => $withParams, ]); expect($total)->toBeGreaterThan(0); }); }); describe('Hub Route Architecture', function () { // Note: 'admin' middleware group = routes served from admin DOMAIN (for all authenticated users) // This is separate from hub/admin/* routes which require Hades TIER it('all hub routes use admin domain routing', function () { // All hub/* routes should use either 'admin' middleware group // OR the equivalent ['web', 'admin.domain', 'auth'] stack // This ensures they're served from the admin domain with proper session handling $hubRoutes = discoverHubRoutes(); $missing = []; foreach ($hubRoutes as $route) { $uri = $route->uri(); $middleware = $route->middleware(); // Check for either 'admin' group or equivalent explicit stack $hasAdminGroup = in_array('admin', $middleware); $hasExplicitStack = in_array('admin.domain', $middleware) && in_array('auth', $middleware); if (! $hasAdminGroup && ! $hasExplicitStack) { $missing[] = $uri.' ('.implode(', ', $middleware).')'; } } if (! empty($missing)) { $message = "Hub routes missing admin domain routing:\n"; foreach ($missing as $uri) { $message .= " - {$uri}\n"; } $this->fail($message); } expect($missing)->toBeEmpty(); }); it('regular hub routes are accessible to all authenticated users', function () { // hub/* routes (excluding hub/admin/*) should work for any authenticated user // Exception: Some routes have component-level Hades checks (technical debt) $regularUser = User::factory()->create(['tier' => UserTier::FREE]); $regularUser->hostWorkspaces()->attach($this->workspace->id, ['is_default' => true]); // Routes with component-level Hades checks (should be moved to hub/admin/*) // TODO: These routes should be refactored to hub/admin/* prefix // Note: Pattern = prefix match (hub/dev matches hub/dev/logs, hub/dev/routes, etc.) $knownHadesPrefixes = [ 'hub/databases', 'hub/console', 'hub/platform', 'hub/ai-services', 'hub/prompts', 'hub/entitlements/', 'hub/deployments', 'hub/bio/shortlinks', 'hub/bio/themes', 'hub/bio/templates', 'hub/bio/analytics', 'hub/dev/', 'hub/api/dev/', 'hub/agents', 'hub/commerce', 'hub/honeypot', ]; // Convert prefix matches to full route check $isKnownHadesRoute = function (string $uri) use ($knownHadesPrefixes): bool { foreach ($knownHadesPrefixes as $prefix) { if ($uri === $prefix || str_starts_with($uri, $prefix)) { return true; } } return false; }; $hubRoutes = discoverHubRoutes(); $unexpectedDenied = []; foreach ($hubRoutes as $route) { $uri = $route->uri(); // Skip parameterised routes if (preg_match('/\{[^}]+\}/', $uri)) { continue; } // Skip hub/admin/* routes - those require Hades tier (tested separately) if (str_starts_with($uri, 'hub/admin/')) { continue; } // Skip known Hades routes (component-level checks) if ($isKnownHadesRoute($uri)) { continue; } $response = $this->actingAs($regularUser)->get('/'.$uri); // Should NOT be 403 for regular authenticated users if ($response->status() === 403) { $unexpectedDenied[] = $uri; } } if (! empty($unexpectedDenied)) { $message = "Routes denying regular users (add to knownHadesRoutes or move to hub/admin/*):\n"; foreach ($unexpectedDenied as $uri) { $message .= " - {$uri}\n"; } $this->fail($message); } expect($unexpectedDenied)->toBeEmpty(); }); it('reports hub route statistics', function () { $hubRoutes = discoverHubRoutes(); $adminOnly = 0; $userAccessible = 0; foreach ($hubRoutes as $route) { $uri = $route->uri(); if (str_starts_with($uri, 'hub/admin/')) { $adminOnly++; } else { $userAccessible++; } } dump([ 'total_hub_routes' => count($hubRoutes), 'hades_only_routes' => $adminOnly, 'user_accessible_routes' => $userAccessible, ]); expect(count($hubRoutes))->toBeGreaterThan(0); }); }); /** * Discover all admin GET routes from Laravel's route registrar. * * Uses middleware detection to find admin routes, not URL patterns. * This catches all routes with 'admin' middleware regardless of URL structure. * * @return array<Illuminate\Routing\Route> */ function discoverAdminRoutes(): array { $allRoutes = Route::getRoutes()->getRoutes(); return collect($allRoutes) ->filter(function ($route) { // Only GET routes if (! in_array('GET', $route->methods())) { return false; } // Check if route has 'admin' middleware (by name or in group) $middleware = $route->middleware(); // 'admin' is the middleware group for admin routes if (in_array('admin', $middleware)) { return true; } // Also catch routes in the admin middleware group via action $action = $route->getAction(); if (isset($action['middleware'])) { $actionMiddleware = is_array($action['middleware']) ? $action['middleware'] : [$action['middleware']]; if (in_array('admin', $actionMiddleware)) { return true; } } // Fallback: URL pattern matching for routes that may not have middleware set $uri = $route->uri(); if (str_starts_with($uri, 'hub/admin/') || str_starts_with($uri, 'admin/')) { return true; } return false; }) ->values() ->all(); } /** * Discover all hub GET routes (hub/*) from Laravel's route registrar. * * @return array<Illuminate\Routing\Route> */ function discoverHubRoutes(): array { $allRoutes = Route::getRoutes()->getRoutes(); return collect($allRoutes) ->filter(function ($route) { // Only GET routes if (! in_array('GET', $route->methods())) { return false; } // Only hub/* routes $uri = $route->uri(); return str_starts_with($uri, 'hub/'); }) ->values() ->all(); }