TenantAtlas/tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php
ahmido cc93329672 feat: canonical tenant context resolution (#164)
## Summary
- introduce a canonical admin tenant filter-state helper and route all in-scope workspace-admin tenant resolution through `OperateHubShell::activeEntitledTenant()`
- align operations monitoring, operation-run deep links, Entra group admin list/view/search behavior, and shared context-bar rendering with the documented scope contract
- add the Spec 135 design artifacts, architecture note, focused guardrail coverage, and non-regression tests for filter persistence, direct-record access, and global search safety

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsKpiHeaderTenantContextTest.php tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php tests/Feature/Spec085/OperationsIndexHeaderTest.php tests/Feature/Spec085/RunDetailBackAffordanceTest.php tests/Feature/Filament/OperationRunListFiltersTest.php tests/Feature/Filament/EntraGroupAdminScopeTest.php tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php tests/Feature/DirectoryGroups/BrowseGroupsTest.php tests/Feature/Filament/EntraGroupEnterpriseDetailPageTest.php tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php tests/Feature/Filament/EntraGroupResolvedReferencePresentationTest.php tests/Feature/Guards/AdminTenantResolverGuardTest.php tests/Feature/OpsUx/OperateHubShellTest.php tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/TableStatePersistenceTest.php tests/Feature/Filament/TenantScopingTest.php tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php tests/Unit/Support/References/CapabilityAwareReferenceResolverTest.php`

## Notes
- Filament v5 remains on Livewire v4.0+ compliant surfaces only.
- No provider registration changes were needed; Laravel 12 provider registration remains in `bootstrap/providers.php`.
- Entra group global search remains enabled and is now scoped to the canonical admin tenant contract.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #164
2026-03-11 21:24:28 +00:00

231 lines
7.9 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Monitoring\Operations;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Route;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('serves /admin/operations without tenant context (workspace-wide)', function (): void {
$tenantA = Tenant::factory()->create();
[$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner');
$tenantB = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantA->workspace_id,
]);
$user->tenants()->syncWithoutDetaching([
$tenantB->getKey() => ['role' => 'owner'],
]);
$runA = OperationRun::factory()->create([
'tenant_id' => (int) $tenantA->getKey(),
'workspace_id' => (int) $tenantA->workspace_id,
'type' => 'policy.sync',
'initiator_name' => 'TenantA',
]);
$runB = OperationRun::factory()->create([
'tenant_id' => (int) $tenantB->getKey(),
'workspace_id' => (int) $tenantB->workspace_id,
'type' => 'inventory_sync',
'initiator_name' => 'TenantB',
]);
Filament::setTenant(null, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id])
->get('/admin/operations')
->assertOk()
->assertSee('Policy sync')
->assertSee('Inventory sync')
->assertSee('TenantA')
->assertSee('TenantB');
});
it('serves /admin/operations/{run} with and without tenant context', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
$run = OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'policy.sync',
]);
Filament::setTenant(null, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run')
->assertDontSee('/admin/t/'.((int) $tenant->getKey()).'/operations/r/'.((int) $run->getKey()));
Filament::setTenant($tenant, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run');
});
it('returns not found for operation detail when the active tenant context does not match the run tenant', function (): void {
$tenantA = Tenant::factory()->create();
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
$tenantB = Tenant::factory()->create([
'workspace_id' => (int) $tenantA->workspace_id,
]);
createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner');
$runB = OperationRun::factory()->create([
'tenant_id' => (int) $tenantB->getKey(),
'workspace_id' => (int) $tenantB->workspace_id,
'type' => 'inventory_sync',
]);
Filament::setTenant($tenantA, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $runB->getKey()]))
->assertNotFound();
});
it('defaults the tenant filter from tenant context and can be cleared', function (): void {
$tenantA = Tenant::factory()->create();
[$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner');
$tenantB = Tenant::factory()->create([
'status' => 'active',
'workspace_id' => (int) $tenantA->workspace_id,
]);
$user->tenants()->syncWithoutDetaching([
$tenantB->getKey() => ['role' => 'owner'],
]);
$runA = OperationRun::factory()->create([
'tenant_id' => (int) $tenantA->getKey(),
'workspace_id' => (int) $tenantA->workspace_id,
'type' => 'policy.sync',
'initiator_name' => 'TenantA',
]);
$runB = OperationRun::factory()->create([
'tenant_id' => (int) $tenantB->getKey(),
'workspace_id' => (int) $tenantB->workspace_id,
'type' => 'inventory_sync',
'initiator_name' => 'TenantB',
]);
Filament::setTenant($tenantA, true);
$this->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
]);
session([
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
]);
$component = Livewire::actingAs($user)
->test(Operations::class)
->assertSee('TenantA')
->assertDontSee('TenantB')
->assertSet('tableFilters.tenant_id.value', (string) $tenantA->getKey());
$component
->callAction('operate_hub_show_all_tenants')
->assertSet('tableFilters.tenant_id.value', null)
->assertRedirect('/admin/operations');
Filament::setTenant(null, true);
Livewire::actingAs($user)
->test(Operations::class)
->assertSee('TenantA')
->assertSee('TenantB');
});
it('shows an explicit back-link when canonical context is present on the operations index', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
Filament::setTenant($tenant, true);
$context = new CanonicalNavigationContext(
sourceSurface: 'backup_set.detail_section',
canonicalRouteName: 'admin.operations.index',
tenantId: (int) $tenant->getKey(),
backLinkLabel: 'Back to backup set',
backLinkUrl: '/admin/tenant/backup-sets/1',
);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(OperationRunLinks::index($tenant, $context))
->assertOk()
->assertSee('Back to backup set')
->assertSee('/admin/tenant/backup-sets/1', false);
});
it('keeps the canonical back-link action after Livewire hydration on the operations index', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
Filament::setTenant($tenant, true);
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
Livewire::withQueryParams([
'nav' => [
'source_surface' => 'finding.list_row',
'canonical_route_name' => 'admin.operations.index',
'tenant_id' => (int) $tenant->getKey(),
'back_label' => 'Back to findings',
'back_url' => '/admin/findings?tenant='.$tenant->external_id,
],
])
->actingAs($user)
->test(Operations::class)
->assertActionVisible('operate_hub_back_to_origin_operations');
});
it('does not register legacy operation resource routes', function (): void {
expect(Route::has('filament.admin.resources.operations.index'))->toBeFalse();
expect(Route::has('filament.admin.resources.operations.view'))->toBeFalse();
});
it('has reserved Monitoring placeholder pages for Alerts and Audit Log', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
Filament::setTenant(null, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->followingRedirects()
->get('/admin/alerts')
->assertOk();
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get('/admin/audit-log')
->assertOk();
});