TenantAtlas/apps/platform/tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php
ahmido e02799b383 feat: implement spec 198 monitoring page state contract (#238)
## Summary
- implement Spec 198 monitoring page-state contracts across Operations, Audit Log, Finding Exceptions Queue, Evidence Overview, Baseline Compare Landing, and Baseline Compare Matrix
- align selected-record and draft/apply behavior with query/session restoration semantics, including canonical navigation and tenant-filter normalization helpers
- add Spec 198 feature and browser coverage, update closure/spec artifacts, and refresh affected regression tests that asserted pre-contract behavior

## Verification
- focused Spec 198 feature pack passed through Sail
- Spec 198 browser smoke passed through Sail
- existing Spec 190 and Spec 194 browser smokes passed through Sail
- targeted fallout tests were updated and rerun during full-suite triage

## Notes
- Livewire v4 / Filament v5 compliant only; no legacy API reintroduction
- no provider registration changes; Laravel 11+ provider registration remains in `bootstrap/providers.php`
- no global-search behavior changed for any resource
- destructive queue decision actions remain confirmation-gated and authorization-backed
- no new Filament assets were added; existing deploy step for `php artisan filament:assets` remains unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #238
2026-04-15 21:59:42 +00:00

270 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Resources\BackupScheduleResource;
use App\Filament\Resources\BackupScheduleResource\Pages\ListBackupSchedules;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\BackupSetResource\Pages\ListBackupSets;
use App\Filament\Resources\EntraGroupResource;
use App\Filament\Resources\EntraGroupResource\Pages\ListEntraGroups;
use App\Filament\Resources\FindingResource;
use App\Filament\Resources\FindingResource\Pages\ListFindings;
use App\Filament\Resources\InventoryItemResource;
use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems;
use App\Filament\Resources\PolicyResource;
use App\Filament\Resources\PolicyResource\Pages\ListPolicies;
use App\Filament\Resources\PolicyVersionResource;
use App\Filament\Resources\PolicyVersionResource\Pages\ListPolicyVersions;
use App\Filament\Resources\RestoreRunResource;
use App\Filament\Resources\RestoreRunResource\Pages\ListRestoreRuns;
use App\Models\BackupSchedule;
use App\Models\BackupSet;
use App\Models\EntraGroup;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\RestoreRun;
use App\Models\Tenant;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
function tenantOwnedAdminSession(Tenant $tenant): array
{
return [
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
(string) $tenant->workspace_id => (int) $tenant->getKey(),
],
];
}
dataset('tenant-owned-list-pages', [
'policy list' => [
ListPolicies::class,
static fn (Tenant $tenant, string $label): Policy => Policy::factory()->for($tenant)->create([
'display_name' => $label,
]),
],
'policy-version list' => [
ListPolicyVersions::class,
static function (Tenant $tenant, string $label): PolicyVersion {
$policy = Policy::factory()->for($tenant)->create([
'display_name' => $label.' policy',
]);
return PolicyVersion::factory()->for($tenant)->for($policy)->create([
'version_number' => random_int(1, 9999),
]);
},
],
'backup-schedule list' => [
ListBackupSchedules::class,
static function (Tenant $tenant, string $label): BackupSchedule {
return BackupSchedule::create([
'tenant_id' => (int) $tenant->getKey(),
'name' => $label,
'is_enabled' => true,
'timezone' => 'UTC',
'frequency' => 'daily',
'time_of_day' => '10:00:00',
'days_of_week' => null,
'policy_types' => ['deviceConfiguration'],
'include_foundations' => true,
'retention_keep_last' => 30,
'next_run_at' => now()->addHour(),
]);
},
],
'backup-set list' => [
ListBackupSets::class,
static fn (Tenant $tenant, string $label): BackupSet => BackupSet::factory()->for($tenant)->create([
'name' => $label,
]),
],
'restore-run list' => [
ListRestoreRuns::class,
static function (Tenant $tenant, string $label): RestoreRun {
$backupSet = BackupSet::factory()->for($tenant)->create([
'name' => $label.' backup set',
]);
return RestoreRun::factory()->for($tenant)->for($backupSet)->create();
},
],
'inventory-item list' => [
ListInventoryItems::class,
static fn (Tenant $tenant, string $label): InventoryItem => InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'display_name' => $label,
]),
],
'finding list' => [
ListFindings::class,
static fn (Tenant $tenant, string $label): Finding => Finding::factory()->for($tenant)->create(),
],
'entra-group list' => [
ListEntraGroups::class,
static fn (Tenant $tenant, string $label): EntraGroup => EntraGroup::factory()->for($tenant)->create([
'display_name' => $label,
]),
],
]);
dataset('tenant-owned-detail-pages', [
'policy view' => [
PolicyResource::class,
'view',
static fn (Tenant $tenant, string $label): Policy => Policy::factory()->for($tenant)->create([
'display_name' => $label,
]),
],
'policy-version view' => [
PolicyVersionResource::class,
'view',
static function (Tenant $tenant, string $label): PolicyVersion {
$policy = Policy::factory()->for($tenant)->create([
'display_name' => $label.' policy',
]);
return PolicyVersion::factory()->for($tenant)->for($policy)->create([
'version_number' => random_int(1, 9999),
]);
},
],
'backup-set view' => [
BackupSetResource::class,
'view',
static fn (Tenant $tenant, string $label): BackupSet => BackupSet::factory()->for($tenant)->create([
'name' => $label,
]),
],
'restore-run view' => [
RestoreRunResource::class,
'view',
static function (Tenant $tenant, string $label): RestoreRun {
$backupSet = BackupSet::factory()->for($tenant)->create([
'name' => $label.' backup set',
]);
return RestoreRun::factory()->for($tenant)->for($backupSet)->create();
},
],
'finding view' => [
FindingResource::class,
'view',
static fn (Tenant $tenant, string $label): Finding => Finding::factory()->for($tenant)->create(),
],
'entra-group view' => [
EntraGroupResource::class,
'view',
static fn (Tenant $tenant, string $label): EntraGroup => EntraGroup::factory()->for($tenant)->create([
'display_name' => $label,
]),
],
'backup-schedule edit' => [
BackupScheduleResource::class,
'edit',
static function (Tenant $tenant, string $label): BackupSchedule {
return BackupSchedule::create([
'tenant_id' => (int) $tenant->getKey(),
'name' => $label,
'is_enabled' => true,
'timezone' => 'UTC',
'frequency' => 'daily',
'time_of_day' => '10:00:00',
'days_of_week' => null,
'policy_types' => ['deviceConfiguration'],
'include_foundations' => true,
'retention_keep_last' => 30,
'next_run_at' => now()->addHour(),
]);
},
],
]);
it('scopes covered tenant-owned admin lists to the remembered canonical tenant', function (string $pageClass, Closure $makeRecord): 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');
$allowed = $makeRecord($tenantA, 'Allowed record');
$blocked = $makeRecord($tenantB, 'Blocked record');
$this->actingAs($user);
Filament::setCurrentPanel('admin');
Filament::setTenant(null, true);
Filament::bootCurrentPanel();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id);
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
(string) $tenantA->workspace_id => (int) $tenantA->getKey(),
]);
Livewire::actingAs($user)->test($pageClass)
->assertCanSeeTableRecords([$allowed])
->assertCanNotSeeTableRecords([$blocked]);
})->with('tenant-owned-list-pages');
it('returns not found for covered tenant-owned admin detail pages outside the remembered canonical tenant', function (string $resourceClass, string $page, Closure $makeRecord): 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');
$allowed = $makeRecord($tenantA, 'Allowed record');
$blocked = $makeRecord($tenantB, 'Blocked record');
$this->actingAs($user);
Filament::setCurrentPanel('admin');
Filament::setTenant(null, true);
Filament::bootCurrentPanel();
$session = tenantOwnedAdminSession($tenantA);
$this->withSession($session)
->get($resourceClass::getUrl($page, ['record' => $allowed], panel: 'admin'))
->assertSuccessful();
$this->withSession($session)
->get($resourceClass::getUrl($page, ['record' => $blocked], panel: 'admin'))
->assertNotFound();
})->with('tenant-owned-detail-pages');
it('returns not found for admin inventory item detail pages outside the explicit tenant query scope', 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');
$allowed = InventoryItem::factory()->create([
'tenant_id' => (int) $tenantA->getKey(),
'display_name' => 'Allowed inventory item',
]);
$blocked = InventoryItem::factory()->create([
'tenant_id' => (int) $tenantB->getKey(),
'display_name' => 'Blocked inventory item',
]);
$session = tenantOwnedAdminSession($tenantA);
$allowedUrl = InventoryItemResource::getUrl('view', ['record' => $allowed], panel: 'admin').'?tenant='.(string) $tenantA->external_id;
$blockedUrl = InventoryItemResource::getUrl('view', ['record' => $blocked], panel: 'admin').'?tenant='.(string) $tenantA->external_id;
$this->actingAs($user)
->withSession($session)
->get($allowedUrl)
->assertSuccessful();
$this->actingAs($user)
->withSession($session)
->get($blockedUrl)
->assertNotFound();
});