## Summary - add the Spec 322 artifact set for the browser no-drift regression guard - add Feature navigation guards for admin surface scope, environment CTA URLs, and legacy alias rejection - add Browser smoke coverage for workspace hubs, environment-owned surfaces, workspace-owned analysis surfaces, and alerts/audit flows - add the Spec 322 browser support harness used by the new smoke coverage ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php tests/Feature/Navigation/Spec322LegacyQueryAliasGuardTest.php tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php --compact` - `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec322WorkspaceHubNoDriftSmokeTest.php tests/Browser/Spec322EnvironmentOwnedSurfaceSmokeTest.php tests/Browser/Spec322WorkspaceOwnedAnalysisSmokeTest.php tests/Browser/Spec322AlertsAuditNoDriftSmokeTest.php --compact` - `cd apps/platform && ./vendor/bin/sail pint --dirty` - `git diff --check` ## Notes - a broader filtered regression run still reports existing Baseline Compare feature-test failures outside this diff Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #379
268 lines
9.7 KiB
PHP
268 lines
9.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage;
|
|
use App\Filament\Pages\Monitoring\Operations;
|
|
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\AlertDestination;
|
|
use App\Models\AlertRule;
|
|
use App\Models\AuditLog as AuditLogModel;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Livewire\Livewire;
|
|
|
|
it('legacy_environment_query_aliases_do_not_create_filter_or_shell_state', function (): void {
|
|
[$user, $environmentA, $environmentB, $records] = spec322LegacyAliasFixture();
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext();
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
|
|
|
|
$legacyQueries = [
|
|
'tenant' => ['tenant' => (string) $environmentA->getKey()],
|
|
'tenant_id' => ['tenant_id' => (int) $environmentA->getKey()],
|
|
'managed_environment_id' => ['managed_environment_id' => (int) $environmentA->getKey()],
|
|
'environment' => ['environment' => (string) $environmentA->getRouteKey()],
|
|
'tenant_scope' => ['tenant_scope' => 'environment'],
|
|
'tableFilters' => [
|
|
'tableFilters' => [
|
|
'managed_environment_id' => ['value' => (string) $environmentA->getKey()],
|
|
],
|
|
],
|
|
];
|
|
|
|
foreach ($legacyQueries as $query) {
|
|
Livewire::withQueryParams($query)
|
|
->actingAs($user)
|
|
->test(Operations::class)
|
|
->assertSet('tableFilters.managed_environment_id.value', null)
|
|
->assertDontSee('Environment filter:')
|
|
->assertCanSeeTableRecords([$records['runA'], $records['runB']]);
|
|
|
|
Livewire::withQueryParams($query)
|
|
->actingAs($user)
|
|
->test(ListAlertDeliveries::class)
|
|
->assertSet('tableFilters.managed_environment_id.value', null)
|
|
->assertDontSee('Environment filter:')
|
|
->assertCanSeeTableRecords([$records['deliveryA'], $records['deliveryB']]);
|
|
|
|
Livewire::withQueryParams($query)
|
|
->actingAs($user)
|
|
->test(AuditLogPage::class)
|
|
->assertSet('tableFilters.managed_environment_id.value', null)
|
|
->assertDontSee('Environment filter:')
|
|
->assertCanSeeTableRecords([$records['auditA'], $records['auditB'], $records['auditWorkspace']]);
|
|
}
|
|
});
|
|
|
|
it('has_no_active_legacy_tenant_panel_routes', function (): void {
|
|
$legacyRouteUris = collect(Route::getRoutes())
|
|
->map(fn ($route): string => ltrim((string) $route->uri(), '/'))
|
|
->filter(fn (string $uri): bool => preg_match('#^admin/t(?:/|$)#', $uri) === 1)
|
|
->values();
|
|
|
|
$registeredProviders = require base_path('bootstrap/providers.php');
|
|
$tenantPanelProviders = collect($registeredProviders)
|
|
->filter(fn (string $provider): bool => str_contains($provider, 'TenantPanelProvider'))
|
|
->values();
|
|
|
|
expect($legacyRouteUris)->toBeEmpty()
|
|
->and($tenantPanelProviders)->toBeEmpty()
|
|
->and(file_exists(app_path('Providers/Filament/TenantPanelProvider.php')))->toBeFalse()
|
|
->and(file_exists(app_path('Filament/Providers/TenantPanelProvider.php')))->toBeFalse()
|
|
->and(Filament::getPanel('tenant'))->toBeNull();
|
|
|
|
$this->get('/admin/t/example')->assertNotFound();
|
|
});
|
|
|
|
it('allows_tenant_terms_only_in_provider_boundary_contexts', function (): void {
|
|
$files = spec322LegacyGuardFiles([
|
|
base_path('app/Support/Navigation'),
|
|
base_path('app/Filament/Pages/Monitoring/Operations.php'),
|
|
base_path('app/Filament/Pages/Monitoring/FindingExceptionsQueue.php'),
|
|
base_path('app/Filament/Pages/Governance/GovernanceInbox.php'),
|
|
base_path('app/Filament/Pages/Governance/DecisionRegister.php'),
|
|
base_path('app/Filament/Pages/Monitoring/EvidenceOverview.php'),
|
|
base_path('app/Filament/Pages/Reviews/ReviewRegister.php'),
|
|
base_path('app/Filament/Pages/Reviews/CustomerReviewWorkspace.php'),
|
|
base_path('app/Filament/Resources/ProviderConnectionResource.php'),
|
|
base_path('app/Filament/Resources/ProviderConnectionResource/Pages/ListProviderConnections.php'),
|
|
base_path('routes/web.php'),
|
|
]);
|
|
|
|
$hits = spec322LegacyPatternHits($files, [
|
|
'/\btenantPrefilterUrl\s*\(/',
|
|
'/\bCanonicalAdminTenantFilterState\b/',
|
|
'/\bWorkspaceScopedTenantRoutes\b/',
|
|
'/\bTenantPageCategory\b/',
|
|
'/\bEnsureFilamentTenantSelected\b/',
|
|
'/'.'ensure-filament-'.'tenant-selected'.'/',
|
|
'/\blastTenantId\s*\(/',
|
|
'/\brememberedTenant\s*\(/',
|
|
'/\brememberTenantContext\s*\(/',
|
|
'/\bLAST_TENANT_IDS_SESSION_KEY\b/',
|
|
'/\bTenantBound\b/',
|
|
'/\bTenantScopedEvidence\b/',
|
|
]);
|
|
|
|
expect($hits)->toBeEmpty("Retired Tenant platform-context terms remain:\n".implode("\n", $hits));
|
|
});
|
|
|
|
/**
|
|
* @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: ManagedEnvironment, 3: array<string, mixed>}
|
|
*/
|
|
function spec322LegacyAliasFixture(): array
|
|
{
|
|
$environmentA = ManagedEnvironment::factory()->active()->create([
|
|
'name' => 'Spec322 Alias Environment A',
|
|
'external_id' => 'spec322-alias-environment-a',
|
|
]);
|
|
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner');
|
|
|
|
$environmentB = ManagedEnvironment::factory()->active()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'name' => 'Spec322 Alias Environment B',
|
|
'external_id' => 'spec322-alias-environment-b',
|
|
]);
|
|
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
|
|
|
|
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
|
|
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
|
|
|
|
$rule = AlertRule::factory()->create(['workspace_id' => (int) $environmentA->workspace_id]);
|
|
$destination = AlertDestination::factory()->create(['workspace_id' => (int) $environmentA->workspace_id]);
|
|
|
|
$deliveryA = AlertDelivery::factory()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'managed_environment_id' => (int) $environmentA->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'status' => AlertDelivery::STATUS_FAILED,
|
|
]);
|
|
$deliveryB = AlertDelivery::factory()->create([
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
'managed_environment_id' => (int) $environmentB->getKey(),
|
|
'alert_rule_id' => (int) $rule->getKey(),
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'status' => AlertDelivery::STATUS_SENT,
|
|
]);
|
|
|
|
$auditA = spec322LegacyAuditRecord($environmentA, 'Spec322 alias audit A');
|
|
$auditB = spec322LegacyAuditRecord($environmentB, 'Spec322 alias audit B');
|
|
$auditWorkspace = spec322LegacyAuditRecord(null, 'Spec322 workspace audit', [
|
|
'workspace_id' => (int) $environmentA->workspace_id,
|
|
]);
|
|
|
|
return [$user, $environmentA, $environmentB, compact(
|
|
'runA',
|
|
'runB',
|
|
'deliveryA',
|
|
'deliveryB',
|
|
'auditA',
|
|
'auditB',
|
|
'auditWorkspace',
|
|
)];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
function spec322LegacyAuditRecord(?ManagedEnvironment $environment, string $summary, array $attributes = []): AuditLogModel
|
|
{
|
|
$workspaceId = array_key_exists('workspace_id', $attributes)
|
|
? (int) $attributes['workspace_id']
|
|
: (int) ($environment?->workspace_id);
|
|
|
|
return AuditLogModel::query()->create(array_merge([
|
|
'workspace_id' => $workspaceId,
|
|
'managed_environment_id' => $environment?->getKey(),
|
|
'actor_email' => 'spec322@example.test',
|
|
'actor_name' => 'Spec322 Operator',
|
|
'action' => 'operation.completed',
|
|
'status' => 'success',
|
|
'resource_type' => 'operation_run',
|
|
'resource_id' => '322',
|
|
'summary' => $summary,
|
|
'metadata' => [],
|
|
'recorded_at' => now(),
|
|
], $attributes));
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $roots
|
|
* @return list<string>
|
|
*/
|
|
function spec322LegacyGuardFiles(array $roots): array
|
|
{
|
|
$files = [];
|
|
|
|
foreach ($roots as $root) {
|
|
if (is_file($root)) {
|
|
$files[] = $root;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (! is_dir($root)) {
|
|
continue;
|
|
}
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if (! $file instanceof SplFileInfo || ! $file->isFile()) {
|
|
continue;
|
|
}
|
|
|
|
if (! in_array($file->getExtension(), ['php', 'md'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$files[] = $file->getPathname();
|
|
}
|
|
}
|
|
|
|
sort($files);
|
|
|
|
return array_values(array_unique($files));
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $files
|
|
* @param list<string> $patterns
|
|
* @return list<string>
|
|
*/
|
|
function spec322LegacyPatternHits(array $files, array $patterns): array
|
|
{
|
|
$hits = [];
|
|
|
|
foreach ($files as $path) {
|
|
$contents = file_get_contents($path);
|
|
|
|
if (! is_string($contents)) {
|
|
continue;
|
|
}
|
|
|
|
$lines = preg_split('/\R/', $contents) ?: [];
|
|
|
|
foreach ($patterns as $pattern) {
|
|
foreach ($lines as $lineNumber => $line) {
|
|
if (preg_match($pattern, $line) !== 1) {
|
|
continue;
|
|
}
|
|
|
|
$hits[] = str_replace(repo_path().'/', '', $path).':'.($lineNumber + 1).' -> '.trim($line);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $hits;
|
|
}
|