## Summary - add a workspace-scoped baseline compare matrix page under baseline profiles - derive matrix tenant summaries, subject rows, cell states, freshness, and trust from existing snapshots, compare runs, and findings - add confirmation-gated `Compare assigned tenants` actions on the baseline detail and matrix surfaces without introducing a workspace umbrella run - preserve matrix navigation context into tenant compare and finding drilldowns and add centralized matrix badge semantics - include spec, plan, data model, contracts, quickstart, tasks, and focused feature/browser coverage for Spec 190 ## Verification - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/BaselineCompareMatrixBadgesTest.php tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Baselines/BaselineComparePerformanceGuardTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Rbac/BaselineCompareMatrixAuthorizationTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/NoDiagnosticWarningBadgesTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - completed an integrated-browser smoke flow locally for matrix render, differ filter, finding drilldown round-trip, and `Compare assigned tenants` confirmation/action Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #221
255 lines
9.5 KiB
PHP
255 lines
9.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Tenant;
|
|
use App\Support\Baselines\BaselineCompareMatrixBuilder;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\Feature\Concerns\BuildsBaselineCompareMatrixFixtures;
|
|
|
|
uses(RefreshDatabase::class, BuildsBaselineCompareMatrixFixtures::class);
|
|
|
|
it('builds visible-set-only tenant and subject summaries from assigned baseline truth', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$visibleRun = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$visibleRunTwo = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$hiddenRun = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['hiddenTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$visibleRunTwo,
|
|
'wifi-corp-profile',
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['hiddenTenant'],
|
|
$fixture['profile'],
|
|
$hiddenRun,
|
|
'wifi-corp-profile',
|
|
['severity' => 'critical'],
|
|
);
|
|
|
|
$matrix = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user']);
|
|
|
|
$wifiRow = collect($matrix['rows'])->first(
|
|
static fn (array $row): bool => ($row['subject']['subjectKey'] ?? null) === 'wifi-corp-profile',
|
|
);
|
|
|
|
expect($matrix['reference']['assignedTenantCount'])->toBe(3)
|
|
->and($matrix['reference']['visibleTenantCount'])->toBe(2)
|
|
->and(collect($matrix['tenantSummaries'])->pluck('tenantName')->all())->toEqualCanonicalizing([
|
|
(string) $fixture['visibleTenant']->name,
|
|
(string) $fixture['visibleTenantTwo']->name,
|
|
])
|
|
->and($wifiRow)->not->toBeNull()
|
|
->and($wifiRow['subject']['deviationBreadth'])->toBe(1)
|
|
->and(count($wifiRow['cells']))->toBe(2);
|
|
});
|
|
|
|
it('derives matrix cell precedence from compare freshness, evidence gaps, findings, and uncovered policy types', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$matchTenant = $fixture['visibleTenant'];
|
|
$differTenant = $fixture['visibleTenantTwo'];
|
|
|
|
$missingTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Missing',
|
|
]);
|
|
$ambiguousTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Ambiguous',
|
|
]);
|
|
$notComparedTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Uncovered',
|
|
]);
|
|
$staleTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Contoso Stale',
|
|
]);
|
|
|
|
$fixture['user']->tenants()->syncWithoutDetaching([
|
|
(int) $missingTenant->getKey() => ['role' => 'owner'],
|
|
(int) $ambiguousTenant->getKey() => ['role' => 'owner'],
|
|
(int) $notComparedTenant->getKey() => ['role' => 'owner'],
|
|
(int) $staleTenant->getKey() => ['role' => 'owner'],
|
|
]);
|
|
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $missingTenant);
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $ambiguousTenant);
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $notComparedTenant);
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $staleTenant);
|
|
|
|
$this->makeBaselineCompareMatrixRun($matchTenant, $fixture['profile'], $fixture['snapshot']);
|
|
|
|
$differRun = $this->makeBaselineCompareMatrixRun($differTenant, $fixture['profile'], $fixture['snapshot']);
|
|
$this->makeBaselineCompareMatrixFinding($differTenant, $fixture['profile'], $differRun, 'wifi-corp-profile', [
|
|
'evidence_jsonb' => [
|
|
'subject_key' => 'wifi-corp-profile',
|
|
'change_type' => 'different_version',
|
|
],
|
|
]);
|
|
|
|
$missingRun = $this->makeBaselineCompareMatrixRun($missingTenant, $fixture['profile'], $fixture['snapshot']);
|
|
$this->makeBaselineCompareMatrixFinding($missingTenant, $fixture['profile'], $missingRun, 'wifi-corp-profile', [
|
|
'evidence_jsonb' => [
|
|
'subject_key' => 'wifi-corp-profile',
|
|
'change_type' => 'missing_policy',
|
|
],
|
|
]);
|
|
|
|
$ambiguousRun = $this->makeBaselineCompareMatrixRun(
|
|
$ambiguousTenant,
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[
|
|
'baseline_compare' => [
|
|
'evidence_gaps' => [
|
|
'count' => 1,
|
|
'by_reason' => ['ambiguous_match' => 1],
|
|
'subjects' => [
|
|
$this->baselineCompareMatrixGap('deviceConfiguration', 'wifi-corp-profile', [
|
|
'reason_code' => 'ambiguous_match',
|
|
'resolution_outcome' => 'ambiguous_match',
|
|
]),
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
$this->makeBaselineCompareMatrixFinding($ambiguousTenant, $fixture['profile'], $ambiguousRun, 'wifi-corp-profile', [
|
|
'evidence_jsonb' => [
|
|
'subject_key' => 'wifi-corp-profile',
|
|
'change_type' => 'missing_policy',
|
|
],
|
|
]);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$notComparedTenant,
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[
|
|
'baseline_compare' => [
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => [],
|
|
'uncovered_types' => ['deviceConfiguration'],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$staleTenant,
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[],
|
|
[
|
|
'completed_at' => $fixture['snapshot']->captured_at->copy()->subDay(),
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $fixture['profile']->getKey(),
|
|
'baseline_snapshot_id' => (int) $fixture['snapshot']->getKey() - 1,
|
|
'baseline_compare' => [
|
|
'coverage' => [
|
|
'proof' => true,
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 0,
|
|
'by_reason' => [],
|
|
'subjects' => [],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$matrix = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user']);
|
|
|
|
$wifiRow = collect($matrix['rows'])->first(
|
|
static fn (array $row): bool => ($row['subject']['subjectKey'] ?? null) === 'wifi-corp-profile',
|
|
);
|
|
|
|
$statesByTenant = collect($wifiRow['cells'] ?? [])
|
|
->mapWithKeys(static fn (array $cell): array => [(int) $cell['tenantId'] => (string) $cell['state']])
|
|
->all();
|
|
|
|
expect($statesByTenant[(int) $matchTenant->getKey()] ?? null)->toBe('match')
|
|
->and($statesByTenant[(int) $differTenant->getKey()] ?? null)->toBe('differ')
|
|
->and($statesByTenant[(int) $missingTenant->getKey()] ?? null)->toBe('missing')
|
|
->and($statesByTenant[(int) $ambiguousTenant->getKey()] ?? null)->toBe('ambiguous')
|
|
->and($statesByTenant[(int) $notComparedTenant->getKey()] ?? null)->toBe('not_compared')
|
|
->and($statesByTenant[(int) $staleTenant->getKey()] ?? null)->toBe('stale_result');
|
|
});
|
|
|
|
it('applies policy-type, state, severity, and subject-focus filters honestly', function (): void {
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$visibleRun = $this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixRun(
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['profile'],
|
|
$fixture['snapshot'],
|
|
[],
|
|
[
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
],
|
|
);
|
|
|
|
$this->makeBaselineCompareMatrixFinding(
|
|
$fixture['visibleTenant'],
|
|
$fixture['profile'],
|
|
$visibleRun,
|
|
'wifi-corp-profile',
|
|
['severity' => 'critical'],
|
|
);
|
|
|
|
$deviceOnly = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user'], [
|
|
'policyTypes' => ['deviceConfiguration'],
|
|
]);
|
|
|
|
$driftOnly = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user'], [
|
|
'states' => ['differ'],
|
|
'severities' => ['critical'],
|
|
]);
|
|
|
|
$subjectFocus = app(BaselineCompareMatrixBuilder::class)->build($fixture['profile'], $fixture['user'], [
|
|
'focusedSubjectKey' => 'wifi-corp-profile',
|
|
]);
|
|
|
|
expect(count($deviceOnly['rows']))->toBe(1)
|
|
->and($deviceOnly['rows'][0]['subject']['policyType'])->toBe('deviceConfiguration')
|
|
->and(count($driftOnly['rows']))->toBe(1)
|
|
->and($driftOnly['rows'][0]['subject']['subjectKey'])->toBe('wifi-corp-profile')
|
|
->and(count($subjectFocus['rows']))->toBe(1)
|
|
->and($subjectFocus['rows'][0]['subject']['subjectKey'])->toBe('wifi-corp-profile');
|
|
});
|