161 lines
6.8 KiB
PHP
161 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Tests\Support\OpsUx\SourceFileScanner;
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
function operationRunLinkContractIncludePaths(): array
|
|
{
|
|
$root = SourceFileScanner::projectRoot();
|
|
|
|
return [
|
|
'tenant_recent_operations_summary' => $root.'/app/Filament/Widgets/Tenant/RecentOperationsSummary.php',
|
|
'inventory_coverage' => $root.'/app/Filament/Pages/InventoryCoverage.php',
|
|
'inventory_item_resource' => $root.'/app/Filament/Resources/InventoryItemResource.php',
|
|
'review_pack_resource' => $root.'/app/Filament/Resources/ReviewPackResource.php',
|
|
'tenantless_operation_run_viewer' => $root.'/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php',
|
|
'related_navigation_resolver' => $root.'/app/Support/Navigation/RelatedNavigationResolver.php',
|
|
'system_directory_tenant' => $root.'/app/Filament/System/Pages/Directory/ViewTenant.php',
|
|
'system_directory_workspace' => $root.'/app/Filament/System/Pages/Directory/ViewWorkspace.php',
|
|
'system_ops_runs' => $root.'/app/Filament/System/Pages/Ops/Runs.php',
|
|
'system_ops_view_run' => $root.'/app/Filament/System/Pages/Ops/ViewRun.php',
|
|
'admin_panel_provider' => $root.'/app/Providers/Filament/AdminPanelProvider.php',
|
|
'tenant_panel_provider' => $root.'/app/Providers/Filament/TenantPanelProvider.php',
|
|
'ensure_filament_tenant_selected' => $root.'/app/Support/Middleware/EnsureFilamentTenantSelected.php',
|
|
'clear_tenant_context_controller' => $root.'/app/Http/Controllers/ClearTenantContextController.php',
|
|
'operation_run_url_delegate' => $root.'/app/Support/OpsUx/OperationRunUrl.php',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
function operationRunLinkContractAllowlist(): array
|
|
{
|
|
$paths = operationRunLinkContractIncludePaths();
|
|
|
|
return [
|
|
$paths['admin_panel_provider'] => 'Admin panel navigation is bootstrapping infrastructure and intentionally links to the canonical collection route before request-scoped navigation context exists.',
|
|
$paths['tenant_panel_provider'] => 'Tenant panel navigation is bootstrapping infrastructure and intentionally links to the canonical collection route before tenant-specific helper context is owned by the source surface.',
|
|
$paths['ensure_filament_tenant_selected'] => 'Tenant-selection middleware owns redirect/navigation fallback infrastructure and must not fabricate source-surface navigation context.',
|
|
$paths['clear_tenant_context_controller'] => 'Clear-tenant redirects preserve an explicit redirect contract and cannot depend on UI helper context.',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $paths
|
|
* @param array<string, string> $allowlist
|
|
* @return list<array{file: string, line: int, snippet: string, expectedHelper: string, reason: string}>
|
|
*/
|
|
function operationRunLinkContractViolations(array $paths, array $allowlist = []): array
|
|
{
|
|
$patterns = [
|
|
[
|
|
'pattern' => '/route\(\s*[\'"]admin\.operations\.index[\'"]/',
|
|
'expectedHelper' => 'OperationRunLinks::index(...)',
|
|
'reason' => 'Raw admin operations collection route assembly bypasses the canonical admin link helper.',
|
|
],
|
|
[
|
|
'pattern' => '/route\(\s*[\'"]admin\.operations\.view[\'"]/',
|
|
'expectedHelper' => 'OperationRunLinks::view(...) or OperationRunLinks::tenantlessView(...)',
|
|
'reason' => 'Raw admin operation detail route assembly bypasses the canonical admin link helper.',
|
|
],
|
|
[
|
|
'pattern' => '/[\'"]\/system\/ops\/runs(?:\/[^\'"]*)?[\'"]/',
|
|
'expectedHelper' => 'SystemOperationRunLinks::index() or SystemOperationRunLinks::view(...)',
|
|
'reason' => 'Direct system operations path assembly bypasses the canonical system link helper.',
|
|
],
|
|
[
|
|
'pattern' => '/\b(?:Runs|ViewRun)::getUrl\(/',
|
|
'expectedHelper' => 'SystemOperationRunLinks::index() or SystemOperationRunLinks::view(...)',
|
|
'reason' => 'Direct system operations page URL generation belongs behind the canonical system link helper.',
|
|
],
|
|
];
|
|
|
|
$violations = [];
|
|
|
|
foreach ($paths as $path) {
|
|
if (array_key_exists($path, $allowlist)) {
|
|
continue;
|
|
}
|
|
|
|
$source = SourceFileScanner::read($path);
|
|
$lines = preg_split('/\R/', $source) ?: [];
|
|
|
|
foreach ($lines as $index => $line) {
|
|
foreach ($patterns as $pattern) {
|
|
if (preg_match($pattern['pattern'], $line) !== 1) {
|
|
continue;
|
|
}
|
|
|
|
$violations[] = [
|
|
'file' => SourceFileScanner::relativePath($path),
|
|
'line' => $index + 1,
|
|
'snippet' => SourceFileScanner::snippet($source, $index + 1),
|
|
'expectedHelper' => $pattern['expectedHelper'],
|
|
'reason' => $pattern['reason'],
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $violations;
|
|
}
|
|
|
|
it('keeps covered operation run link producers on canonical helper families', function (): void {
|
|
$paths = operationRunLinkContractIncludePaths();
|
|
$allowlist = operationRunLinkContractAllowlist();
|
|
|
|
$violations = operationRunLinkContractViolations($paths, $allowlist);
|
|
|
|
expect($violations)->toBeEmpty();
|
|
})->group('surface-guard');
|
|
|
|
it('keeps the operation run link exception boundary explicit and infrastructure-owned', function (): void {
|
|
$allowlist = operationRunLinkContractAllowlist();
|
|
|
|
expect(array_keys($allowlist))->toHaveCount(4);
|
|
|
|
foreach ($allowlist as $reason) {
|
|
expect($reason)
|
|
->not->toBe('')
|
|
->not->toContain('convenience');
|
|
}
|
|
|
|
foreach (array_keys($allowlist) as $path) {
|
|
expect(SourceFileScanner::read($path))->toContain("route('admin.operations.index')");
|
|
}
|
|
})->group('surface-guard');
|
|
|
|
it('reports actionable file and snippet output for a representative raw bypass', function (): void {
|
|
$probePath = storage_path('framework/testing/OperationRunLinkContractProbe.php');
|
|
|
|
if (! is_dir(dirname($probePath))) {
|
|
mkdir(dirname($probePath), 0777, true);
|
|
}
|
|
|
|
file_put_contents($probePath, <<<'PHP'
|
|
<?php
|
|
|
|
return route('admin.operations.view', ['run' => 123]);
|
|
PHP);
|
|
|
|
try {
|
|
$violations = operationRunLinkContractViolations([
|
|
'probe' => $probePath,
|
|
]);
|
|
} finally {
|
|
@unlink($probePath);
|
|
}
|
|
|
|
expect($violations)->toHaveCount(1)
|
|
->and($violations[0]['file'])->toContain('OperationRunLinkContractProbe.php')
|
|
->and($violations[0]['line'])->toBe(3)
|
|
->and($violations[0]['snippet'])->toContain("route('admin.operations.view'")
|
|
->and($violations[0]['expectedHelper'])->toContain('OperationRunLinks::view')
|
|
->and($violations[0]['reason'])->toContain('bypasses the canonical admin link helper');
|
|
})->group('surface-guard');
|