TenantAtlas/apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php
ahmido eca19819d1 feat: add workspace baseline compare matrix (#221)
## 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
2026-04-11 10:20:25 +00:00

273 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Concerns;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineSnapshotItem;
use App\Models\BaselineTenantAssignment;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineProfileStatus;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Tests\Feature\Baselines\Support\BaselineSubjectResolutionFixtures;
trait BuildsBaselineCompareMatrixFixtures
{
/**
* @return array{
* user: User,
* workspace: Workspace,
* profile: BaselineProfile,
* snapshot: BaselineSnapshot,
* visibleTenant: Tenant,
* visibleTenantTwo: Tenant,
* hiddenTenant: Tenant,
* subjects: array<string, BaselineSnapshotItem>
* }
*/
protected function makeBaselineCompareMatrixFixture(
string $viewerRole = 'owner',
?string $workspaceRole = null,
): array {
[$user, $visibleTenant] = createUserWithTenant(role: $viewerRole, workspaceRole: $workspaceRole ?? $viewerRole);
$workspace = Workspace::query()->findOrFail((int) $visibleTenant->workspace_id);
$profile = BaselineProfile::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => BaselineProfileStatus::Active->value,
'capture_mode' => BaselineCaptureMode::Opportunistic->value,
'name' => 'Visible-set baseline',
'scope_jsonb' => [
'policy_types' => ['deviceConfiguration', 'compliancePolicy'],
'foundation_types' => [],
],
]);
$snapshot = BaselineSnapshot::factory()->complete()->create([
'workspace_id' => (int) $workspace->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
'captured_at' => now()->subHours(2),
'completed_at' => now()->subHours(2),
]);
$profile->forceFill([
'active_snapshot_id' => (int) $snapshot->getKey(),
])->save();
$visibleTenantTwo = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Northwind',
]);
$hiddenTenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Hidden Fabrikam',
]);
$user->tenants()->syncWithoutDetaching([
(int) $visibleTenantTwo->getKey() => ['role' => 'owner'],
]);
WorkspaceMembership::query()->updateOrCreate([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
], [
'role' => $workspaceRole ?? $viewerRole,
]);
$this->assignTenantToBaselineProfile($profile, $visibleTenant);
$this->assignTenantToBaselineProfile($profile, $visibleTenantTwo);
$this->assignTenantToBaselineProfile($profile, $hiddenTenant);
$subjects = [
'wifi-corp-profile' => $this->makeBaselineCompareMatrixSubject(
$snapshot,
'deviceConfiguration',
'wifi-corp-profile',
'WiFi Corp Profile',
'dc:wifi-corp-profile',
),
'windows-compliance' => $this->makeBaselineCompareMatrixSubject(
$snapshot,
'compliancePolicy',
'windows-compliance',
'Windows Compliance',
'cp:windows-compliance',
),
];
return [
'user' => $user,
'workspace' => $workspace,
'profile' => $profile,
'snapshot' => $snapshot,
'visibleTenant' => $visibleTenant,
'visibleTenantTwo' => $visibleTenantTwo,
'hiddenTenant' => $hiddenTenant,
'subjects' => $subjects,
];
}
protected function makeBaselineCompareMatrixSubject(
BaselineSnapshot $snapshot,
string $policyType,
string $subjectKey,
string $displayName,
?string $subjectExternalId = null,
): BaselineSnapshotItem {
return BaselineSnapshotItem::factory()->create([
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'policy_type' => $policyType,
'subject_key' => $subjectKey,
'subject_external_id' => $subjectExternalId ?? $policyType.':'.$subjectKey,
'meta_jsonb' => ['display_name' => $displayName],
]);
}
protected function assignTenantToBaselineProfile(BaselineProfile $profile, Tenant $tenant): BaselineTenantAssignment
{
return BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $profile->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
'tenant_id' => (int) $tenant->getKey(),
]);
}
/**
* @param array<string, mixed> $contextOverrides
* @param array<string, mixed> $attributes
*/
protected function makeBaselineCompareMatrixRun(
Tenant $tenant,
BaselineProfile $profile,
BaselineSnapshot $snapshot,
array $contextOverrides = [],
array $attributes = [],
): OperationRun {
$defaults = [
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'type' => OperationRunType::BaselineCompare->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'initiator_name' => 'Spec190 Matrix',
'summary_counts' => [
'matched_items' => 1,
'different_items' => 0,
'missing_items' => 0,
'unexpected_items' => 0,
],
'context' => array_replace_recursive([
'baseline_profile_id' => (int) $profile->getKey(),
'baseline_snapshot_id' => (int) $snapshot->getKey(),
'baseline_compare' => [
'reason_code' => null,
'subjects_total' => 2,
'fidelity' => 'content',
'coverage' => [
'proof' => true,
'effective_types' => ['deviceConfiguration', 'compliancePolicy'],
'covered_types' => ['deviceConfiguration', 'compliancePolicy'],
'uncovered_types' => [],
],
'evidence_gaps' => [
'count' => 0,
'by_reason' => [],
'subjects' => [],
],
],
], $contextOverrides),
'started_at' => now()->subMinutes(5),
'completed_at' => now()->subMinute(),
];
return OperationRun::factory()->create(array_replace_recursive($defaults, $attributes));
}
/**
* @param array<string, mixed> $overrides
*/
protected function makeBaselineCompareMatrixFinding(
Tenant $tenant,
BaselineProfile $profile,
OperationRun $run,
string $subjectKey,
array $overrides = [],
): Finding {
$defaults = [
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'finding_type' => Finding::FINDING_TYPE_DRIFT,
'source' => 'baseline.compare',
'scope_key' => 'baseline_profile:'.(int) $profile->getKey(),
'baseline_operation_run_id' => (int) $run->getKey(),
'current_operation_run_id' => (int) $run->getKey(),
'subject_type' => 'policy',
'subject_external_id' => 'subject:'.$subjectKey,
'severity' => Finding::SEVERITY_HIGH,
'status' => Finding::STATUS_NEW,
'evidence_jsonb' => [
'subject_key' => $subjectKey,
'change_type' => 'different_version',
],
];
return Finding::factory()->create(array_replace_recursive($defaults, $overrides));
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
protected function baselineCompareMatrixGap(string $policyType, string $subjectKey, array $overrides = []): array
{
return BaselineSubjectResolutionFixtures::structuredGap(array_replace([
'policy_type' => $policyType,
'subject_key' => $subjectKey,
], $overrides));
}
protected function setAdminWorkspaceContext(User $user, Workspace|int $workspace, ?Tenant $rememberedTenant = null): array
{
$workspaceId = $workspace instanceof Workspace ? (int) $workspace->getKey() : (int) $workspace;
$session = [
WorkspaceContext::SESSION_KEY => $workspaceId,
];
if ($rememberedTenant instanceof Tenant) {
$session[WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY] = [
(string) $workspaceId => (int) $rememberedTenant->getKey(),
];
}
$this->actingAs($user)->withSession($session);
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
if ($rememberedTenant instanceof Tenant) {
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
(string) $workspaceId => (int) $rememberedTenant->getKey(),
]);
}
Filament::setCurrentPanel('admin');
Filament::setTenant(null, true);
Filament::bootCurrentPanel();
return $session;
}
}