## Summary - implement Spec 177 inventory coverage truth across resolver, badges, KPIs, coverage page, and operation run detail surfaces - add repo-native spec artifacts for the feature under `specs/177-inventory-coverage-truth` - add unit, feature, and browser coverage for truth derivation, continuity, and inventory item filter/pagination smoke paths ## Testing - `vendor/bin/sail bin pint --dirty --format agent` - focused Spec 177 browser smoke file passed with 2 tests / 57 assertions - extended inventory-focused test pack passed with 52 tests / 434 assertions Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #208
269 lines
9.5 KiB
PHP
269 lines
9.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Pages\InventoryCoverage;
|
|
use App\Filament\Resources\InventoryItemResource;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
pest()->browser()->timeout(15_000);
|
|
|
|
function inventoryItemListLivewireScript(string $body): string
|
|
{
|
|
return <<<JS
|
|
const component = window.Livewire?.all().find((entry) => {
|
|
if (entry.name === 'App\\Filament\\Resources\\InventoryItemResource\\Pages\\ListInventoryItems') {
|
|
return true;
|
|
}
|
|
|
|
const state = entry.canonical ?? {};
|
|
|
|
return Object.prototype.hasOwnProperty.call(state, 'tableFilters')
|
|
&& Object.prototype.hasOwnProperty.call(state, 'tableDeferredFilters')
|
|
&& Object.prototype.hasOwnProperty.call(state, 'tableRecordsPerPage')
|
|
&& Object.prototype.hasOwnProperty.call(state, 'tableSearch');
|
|
});
|
|
|
|
if (! component) {
|
|
throw new Error('ListInventoryItems Livewire component not found.');
|
|
}
|
|
|
|
{$body}
|
|
JS;
|
|
}
|
|
|
|
function seedSpec177InventoryCoverageTruthFixtures(Tenant $tenant): OperationRun
|
|
{
|
|
foreach (range(1, 130) as $index) {
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => sprintf('Browser Inventory %02d', $index),
|
|
'policy_type' => 'deviceConfiguration',
|
|
'external_id' => sprintf('browser-inventory-%02d', $index),
|
|
'platform' => 'windows',
|
|
'last_seen_at' => now()->subMinutes($index),
|
|
]);
|
|
}
|
|
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => 'Browser Conditional Access',
|
|
'policy_type' => 'conditionalAccessPolicy',
|
|
'external_id' => 'browser-conditional-access',
|
|
'platform' => 'windows',
|
|
'last_seen_at' => now()->subMinutes(31),
|
|
]);
|
|
|
|
return createInventorySyncOperationRunWithCoverage(
|
|
$tenant,
|
|
[
|
|
'conditionalAccessPolicy' => 'succeeded',
|
|
'deviceConfiguration' => 'failed',
|
|
'roleScopeTag' => 'skipped',
|
|
],
|
|
['roleScopeTag'],
|
|
[
|
|
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
|
|
'completed_at' => now()->subMinute(),
|
|
],
|
|
);
|
|
}
|
|
|
|
function seedSpec177InventoryItemFilterPaginationFixtures(Tenant $tenant): void
|
|
{
|
|
foreach (range(1, 45) as $index) {
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => sprintf('Windows Fresh Device %02d', $index),
|
|
'policy_type' => 'deviceConfiguration',
|
|
'external_id' => sprintf('windows-fresh-device-%02d', $index),
|
|
'platform' => 'windows',
|
|
'last_seen_at' => now()->subMinutes($index),
|
|
]);
|
|
}
|
|
|
|
foreach (range(1, 3) as $index) {
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => sprintf('Mac Fresh Device %02d', $index),
|
|
'policy_type' => 'deviceConfiguration',
|
|
'external_id' => sprintf('mac-fresh-device-%02d', $index),
|
|
'platform' => 'macOS',
|
|
'last_seen_at' => now()->subMinutes(45 + $index),
|
|
]);
|
|
}
|
|
|
|
foreach (range(1, 3) as $index) {
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => sprintf('Conditional Access Fresh %02d', $index),
|
|
'policy_type' => 'conditionalAccessPolicy',
|
|
'external_id' => sprintf('conditional-access-fresh-%02d', $index),
|
|
'platform' => 'windows',
|
|
'last_seen_at' => now()->subMinutes(48 + $index),
|
|
]);
|
|
}
|
|
|
|
foreach (range(46, 55) as $index) {
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => sprintf('Windows Fresh Device %02d', $index),
|
|
'policy_type' => 'deviceConfiguration',
|
|
'external_id' => sprintf('windows-fresh-device-%02d', $index),
|
|
'platform' => 'windows',
|
|
'last_seen_at' => now()->subMinutes(6 + $index),
|
|
]);
|
|
}
|
|
|
|
foreach (range(1, 3) as $index) {
|
|
InventoryItem::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'display_name' => sprintf('Windows Stale Device %02d', $index),
|
|
'policy_type' => 'deviceConfiguration',
|
|
'external_id' => sprintf('windows-stale-device-%02d', $index),
|
|
'platform' => 'windows',
|
|
'last_seen_at' => now()->subDays(3)->subMinutes($index),
|
|
]);
|
|
}
|
|
}
|
|
|
|
it('smokes inventory coverage truth surfaces with filters, pagination, and run drill-through', function (): void {
|
|
$tenant = Tenant::factory()->create([
|
|
'name' => 'Spec177 Browser Tenant',
|
|
'external_id' => 'spec177-browser-tenant',
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
$run = seedSpec177InventoryCoverageTruthFixtures($tenant);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
|
]);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
$searchPage = visit(InventoryItemResource::getUrl('index', tenant: $tenant));
|
|
|
|
$searchPage
|
|
->waitForText('Inventory Items')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Covered types')
|
|
->assertSee('Need follow-up')
|
|
->assertSee('Coverage basis')
|
|
->assertSee('Open basis run')
|
|
->assertSee('Run Inventory Sync')
|
|
->assertSee('Browser Inventory 01')
|
|
->assertDontSee('Browser Inventory 130')
|
|
->fill('main input[placeholder="Search"]', 'Browser Inventory 01')
|
|
->waitForText('Browser Inventory 01')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Browser Inventory 01')
|
|
->assertDontSee('Browser Inventory 02');
|
|
|
|
$page = visit(InventoryCoverage::getUrl(tenant: $tenant));
|
|
|
|
$page
|
|
->waitForText('Tenant coverage truth')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Latest coverage-bearing sync completed')
|
|
->assertSee('Open basis run')
|
|
->assertSee('Open inventory items')
|
|
->fill('input[placeholder="Search by type or label"]', 'Conditional Access')
|
|
->waitForText('Conditional Access')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Conditional Access')
|
|
->assertScript("document.querySelector('input[placeholder=\"Search by type or label\"]')?.value === 'Conditional Access'", true)
|
|
->click('Open basis run')
|
|
->waitForText('Operation #'.(int) $run->getKey())
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()])
|
|
->assertSee('Inventory sync coverage')
|
|
->assertSee('Need follow-up');
|
|
|
|
$page->script(<<<'JS'
|
|
history.back();
|
|
JS);
|
|
|
|
$page
|
|
->waitForText('Tenant coverage truth')
|
|
->assertNoJavaScriptErrors()
|
|
->click('Open inventory items')
|
|
->waitForText('Inventory Items')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Browser Inventory 01');
|
|
});
|
|
|
|
it('smokes inventory item pagination with stable filter combinations', function (): void {
|
|
$tenant = Tenant::factory()->create([
|
|
'name' => 'Spec177 Browser Filter Tenant',
|
|
'external_id' => 'spec177-browser-filter-tenant',
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
|
|
|
seedSpec177InventoryItemFilterPaginationFixtures($tenant);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
|
]);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
$page = visit(InventoryItemResource::getUrl('index', tenant: $tenant));
|
|
|
|
$page
|
|
->waitForText('Inventory Items')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Windows Fresh Device 01')
|
|
->assertDontSee('Windows Fresh Device 30')
|
|
->assertDontSee('Mac Fresh Device 01')
|
|
->wait(1);
|
|
|
|
$page->script(inventoryItemListLivewireScript(<<<'JS'
|
|
component.$wire.set('tableRecordsPerPage', 50);
|
|
JS));
|
|
|
|
$page
|
|
->wait(1)
|
|
->waitForText('Mac Fresh Device 01')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Mac Fresh Device 01')
|
|
->assertSee('Conditional Access Fresh 01')
|
|
->assertDontSee('Windows Fresh Device 46');
|
|
|
|
$page->script(inventoryItemListLivewireScript(<<<'JS'
|
|
component.$wire.set('tableDeferredFilters.policy_type.value', 'deviceConfiguration');
|
|
component.$wire.set('tableDeferredFilters.platform.value', 'windows');
|
|
component.$wire.set('tableDeferredFilters.stale.value', '0');
|
|
component.$wire.call('applyTableFilters');
|
|
JS));
|
|
|
|
$page
|
|
->wait(1)
|
|
->waitForText('Active filters')
|
|
->waitForText('Windows Fresh Device 46')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Policy type: Device Configuration')
|
|
->assertSee('Platform: Windows')
|
|
->assertSee('Freshness: Fresh')
|
|
->assertSee('Windows Fresh Device 46')
|
|
->assertDontSee('Mac Fresh Device 01')
|
|
->assertDontSee('Conditional Access Fresh 01')
|
|
->assertDontSee('Windows Stale Device 01');
|
|
|
|
$page->script(inventoryItemListLivewireScript(<<<'JS'
|
|
component.$wire.call('gotoPage', 2);
|
|
JS));
|
|
|
|
$page
|
|
->wait(1)
|
|
->waitForText('Windows Fresh Device 51')
|
|
->assertNoJavaScriptErrors()
|
|
->assertSee('Windows Fresh Device 51')
|
|
->assertDontSee('Windows Fresh Device 01')
|
|
->assertDontSee('Mac Fresh Device 01');
|
|
});
|