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>
406 lines
13 KiB
PHP
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();
|
|
}
|