lthn.io/app/Core/Tests/Feature/AdminRouteSmokeTest.php
Claude 41a90cbff8
feat: lthn.io API serving live chain data
Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.

Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 17:17:42 +01:00

406 lines
13 KiB
PHP

<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
/**
* Admin Route Smoke Tests
*
* Discovery-based tests that automatically find and verify all admin routes.
* No manual route lists to maintain - new routes are tested automatically.
*/
use Core\Tenant\Enums\UserTier;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Facades\Route;
uses()->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>([^<]+)<\/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();
}