TenantAtlas/tests/Feature/Filament/InventoryCoverageTableTest.php
ahmido 3971c315d8 feat: add inventory coverage interactive table (#151)
## Summary
- replace the static Inventory Coverage HTML tables with a Filament native searchable, sortable, filterable table on the existing tenant page
- normalize supported policy types and foundations into one runtime dataset while preserving centralized badge semantics and the documented read-only action-surface exemption
- add the full spec kit artifact set for feature 124 and focused Pest coverage for rendering, search, sort, filters, empty state, and regression-sensitive page copy

## Testing
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryCoverageTableTest.php tests/Feature/Filament/InventoryPagesTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php`

## Filament Notes
- Livewire v4.0+ compliance: yes, this uses Filament v5 table APIs on the existing page and does not introduce any Livewire v3 patterns
- Provider registration: unchanged; Laravel 11+ provider registration remains in `bootstrap/providers.php`
- Globally searchable resources: none changed in this feature; no Resource global-search behavior was added or modified
- Destructive actions: none; the page remains read-only and only exposes a non-destructive clear-filters empty-state action
- Asset strategy: no new panel or shared assets were added, so no `filament:assets` deployment change is required for this feature
- Testing plan delivered: focused Filament/Pest coverage for the page table surface plus existing page-load regressions

## Follow-up
- Manual dark-mode and badge-regression QA from task `T018` is still pending and should be completed before merge if that check remains mandatory in your review flow.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #151
2026-03-08 18:33:00 +00:00

216 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\InventoryCoverage;
use App\Models\Tenant;
use App\Models\User;
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\TagBadgeCatalog;
use App\Support\Badges\TagBadgeDomain;
use Filament\Facades\Filament;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
function inventoryCoverageRecordKey(string $segment, string $type): string
{
return "{$segment}:{$type}";
}
function inventoryCoverageComponent(User $user, Tenant $tenant): Testable
{
$tenant->makeCurrent();
Filament::setTenant($tenant, true);
test()->actingAs($user);
return Livewire::actingAs($user)->test(InventoryCoverage::class);
}
function removeInventoryCoverageRestoreMetadata(): void
{
config()->set(
'tenantpilot.supported_policy_types',
collect(config('tenantpilot.supported_policy_types', []))
->map(function (array $row): array {
unset($row['restore']);
return $row;
})
->all(),
);
config()->set(
'tenantpilot.foundation_types',
collect(config('tenantpilot.foundation_types', []))
->map(function (array $row): array {
unset($row['restore']);
return $row;
})
->all(),
);
}
it('renders searchable coverage rows for policy and foundation metadata', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
$deviceConfigurationKey = inventoryCoverageRecordKey('policy', 'deviceConfiguration');
$scopeTagKey = inventoryCoverageRecordKey('foundation', 'roleScopeTag');
inventoryCoverageComponent($user, $tenant)
->assertOk()
->assertTableColumnExists('type')
->assertTableColumnExists('label')
->assertTableColumnExists('category')
->assertTableColumnExists('dependencies')
->assertCountTableRecords(
count(config('tenantpilot.supported_policy_types', [])) + count(config('tenantpilot.foundation_types', [])),
)
->assertCanSeeTableRecords([$conditionalAccessKey, $scopeTagKey])
->searchTable('conditional')
->assertCanSeeTableRecords([$conditionalAccessKey])
->assertCanNotSeeTableRecords([$deviceConfigurationKey, $scopeTagKey])
->searchTable('Scope Tag')
->assertCanSeeTableRecords([$scopeTagKey])
->assertCanNotSeeTableRecords([$conditionalAccessKey])
->searchTable(null)
->assertCanSeeTableRecords([$conditionalAccessKey, $deviceConfigurationKey, $scopeTagKey]);
});
it('sorts coverage rows by type and label deterministically', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$appProtectionKey = inventoryCoverageRecordKey('policy', 'appProtectionPolicy');
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
$deviceComplianceKey = inventoryCoverageRecordKey('policy', 'deviceCompliancePolicy');
$adminTemplatesKey = inventoryCoverageRecordKey('policy', 'groupPolicyConfiguration');
$appConfigDeviceKey = inventoryCoverageRecordKey('policy', 'managedDeviceAppConfiguration');
$appConfigMamKey = inventoryCoverageRecordKey('policy', 'mamAppConfiguration');
inventoryCoverageComponent($user, $tenant)
->sortTable('type')
->assertCanSeeTableRecords([$appProtectionKey, $conditionalAccessKey, $deviceComplianceKey], inOrder: true)
->sortTable('label')
->assertCanSeeTableRecords([$adminTemplatesKey, $appConfigDeviceKey, $appConfigMamKey], inOrder: true);
});
it('filters coverage rows by category and restore mode when restore metadata exists', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$assignmentFilterKey = inventoryCoverageRecordKey('foundation', 'assignmentFilter');
$scopeTagKey = inventoryCoverageRecordKey('foundation', 'roleScopeTag');
$deviceConfigurationKey = inventoryCoverageRecordKey('policy', 'deviceConfiguration');
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
$securityBaselineKey = inventoryCoverageRecordKey('policy', 'securityBaselinePolicy');
inventoryCoverageComponent($user, $tenant)
->assertTableFilterExists('category')
->assertTableFilterExists('restore')
->filterTable('category', 'Foundations')
->assertCanSeeTableRecords([$assignmentFilterKey, $scopeTagKey])
->assertCanNotSeeTableRecords([$deviceConfigurationKey, $conditionalAccessKey])
->removeTableFilters()
->filterTable('restore', 'preview-only')
->assertCanSeeTableRecords([$conditionalAccessKey, $securityBaselineKey])
->assertCanNotSeeTableRecords([$deviceConfigurationKey, $assignmentFilterKey]);
});
it('omits the restore filter when the runtime dataset has no restore metadata', function (): void {
removeInventoryCoverageRestoreMetadata();
[$user, $tenant] = createUserWithTenant(role: 'owner');
$component = inventoryCoverageComponent($user, $tenant)
->assertTableFilterExists('category');
expect($component->instance()->getTable()->getFilter('restore'))->toBeNull();
});
it('shows a single clear-filters empty state action and can reset back to a populated dataset', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
inventoryCoverageComponent($user, $tenant)
->assertTableEmptyStateActionsExistInOrder(['clear_filters'])
->searchTable('no-such-coverage-entry')
->assertCountTableRecords(0)
->assertSee('No coverage entries match this view')
->assertSee('Clear filters')
->searchTable(null)
->assertCountTableRecords(
count(config('tenantpilot.supported_policy_types', [])) + count(config('tenantpilot.foundation_types', [])),
)
->assertCanSeeTableRecords([$conditionalAccessKey]);
});
it('preserves badge semantics and dependency indicators in the interactive table columns', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$conditionalAccessKey = inventoryCoverageRecordKey('policy', 'conditionalAccessPolicy');
$deviceConfigurationKey = inventoryCoverageRecordKey('policy', 'deviceConfiguration');
$assignmentFilterKey = inventoryCoverageRecordKey('foundation', 'assignmentFilter');
$typeSpec = TagBadgeCatalog::spec(TagBadgeDomain::PolicyType, 'conditionalAccessPolicy');
$categorySpec = TagBadgeCatalog::spec(TagBadgeDomain::PolicyCategory, 'Conditional Access');
$restoreSpec = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, 'preview-only');
$riskSpec = BadgeCatalog::spec(BadgeDomain::PolicyRisk, 'high');
inventoryCoverageComponent($user, $tenant)
->assertTableColumnFormattedStateSet('label', $typeSpec->label, $conditionalAccessKey)
->assertTableColumnFormattedStateSet('category', $categorySpec->label, $conditionalAccessKey)
->assertTableColumnFormattedStateSet('restore', $restoreSpec->label, $conditionalAccessKey)
->assertTableColumnFormattedStateSet('risk', $riskSpec->label, $conditionalAccessKey)
->assertTableColumnFormattedStateSet('segment', 'Policy', $conditionalAccessKey)
->assertTableColumnFormattedStateSet('segment', 'Foundation', $assignmentFilterKey)
->assertTableColumnStateSet('dependencies', true, $deviceConfigurationKey)
->assertTableColumnStateSet('dependencies', false, $assignmentFilterKey)
->assertTableColumnExists('label', function (TextColumn $column) use ($typeSpec): bool {
$state = $column->getState();
return $column->isBadge()
&& $column->getColor($state) === $typeSpec->color
&& $column->getIcon($state) === $typeSpec->icon;
}, $conditionalAccessKey)
->assertTableColumnExists('category', function (TextColumn $column) use ($categorySpec): bool {
$state = $column->getState();
return $column->isBadge()
&& $column->getColor($state) === $categorySpec->color
&& $column->getIcon($state) === $categorySpec->icon;
}, $conditionalAccessKey)
->assertTableColumnExists('restore', function (TextColumn $column) use ($restoreSpec): bool {
$state = $column->getState();
return $column->isBadge()
&& $column->getColor($state) === $restoreSpec->color
&& $column->getIcon($state) === $restoreSpec->icon;
}, $conditionalAccessKey)
->assertTableColumnExists('risk', function (TextColumn $column) use ($riskSpec): bool {
$state = $column->getState();
return $column->isBadge()
&& $column->getColor($state) === $riskSpec->color
&& $column->getIcon($state) === $riskSpec->icon;
}, $conditionalAccessKey)
->assertTableColumnExists('dependencies', function (IconColumn $column): bool {
$state = $column->getState();
return $state === true
&& $column->getColor($state) === 'success'
&& (string) $column->getIcon($state) === 'heroicon-m-check-circle';
}, $deviceConfigurationKey)
->assertTableColumnExists('dependencies', function (IconColumn $column): bool {
$state = $column->getState();
return $state === false
&& $column->getColor($state) === 'gray'
&& (string) $column->getIcon($state) === 'heroicon-m-minus-circle';
}, $assignmentFilterKey);
});