352 lines
15 KiB
PHP
352 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\Finding;
|
|
use App\Support\Baselines\BaselineCompareReasonCode;
|
|
use App\Support\ManagedEnvironmentLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
it('renders no baseline assigned as the first product process flow blocker', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Which baseline drift requires action?')
|
|
->assertSee('Baseline not assigned')
|
|
->assertSee('This environment does not have an assigned baseline.')
|
|
->assertSee('Open baseline profiles')
|
|
->assertSee('Compare readiness flow')
|
|
->assertSee('Available inputs')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('raw diff')
|
|
->assertDontSee('raw payload')
|
|
->assertDontSee('provider response')
|
|
->assertDontSee('stack trace');
|
|
|
|
$content = $component->html();
|
|
|
|
spec336AssertFlowStep($content, 'Baseline assigned', 'Missing', true);
|
|
spec336AssertFlowStep($content, 'Baseline snapshot', 'Unavailable', false);
|
|
spec336AssertFlowStep($content, 'Environment snapshot', 'Available', false);
|
|
spec336AssertFlowStep($content, 'Compare run', 'Unavailable', false);
|
|
spec336AssertFlowStep($content, 'Decision output', 'Unavailable', false);
|
|
|
|
expect(substr_count($content, 'data-testid="baseline-compare-readiness-step"'))->toBe(5)
|
|
->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(3)
|
|
->and($content)->not->toContain('data-testid="baseline-compare-diagnostics" open');
|
|
});
|
|
|
|
it('renders baseline assigned with snapshot missing as a blocked compare flow', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
]);
|
|
|
|
BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Baseline snapshot required')
|
|
->assertSee('A baseline is assigned, but no usable baseline snapshot is available.')
|
|
->assertSee('Compare cannot run until baseline snapshot input exists.')
|
|
->assertSee('Open baseline profile')
|
|
->assertSee('No usable baseline snapshot input is linked.')
|
|
->assertDontSee('Drift findings available')
|
|
->assertDontSee('No drift detected')
|
|
->assertDontSee('raw diff');
|
|
|
|
$content = $component->html();
|
|
|
|
spec336AssertFlowStep($content, 'Baseline assigned', 'Complete', false);
|
|
spec336AssertFlowStep($content, 'Baseline snapshot', 'Missing', true);
|
|
spec336AssertFlowStep($content, 'Compare run', 'Unavailable', false);
|
|
spec336AssertFlowStep($content, 'Decision output', 'Unavailable', false);
|
|
spec336AssertInputState($content, 'Assigned baseline', 'Available');
|
|
spec336AssertInputState($content, 'Baseline snapshot', 'Missing');
|
|
spec336AssertInputState($content, 'Drift findings', 'Unavailable');
|
|
});
|
|
|
|
it('renders compare required with the capability-aware primary action', function (): void {
|
|
[$owner, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$readonly] = createUserWithTenant(tenant: $environment, role: 'readonly', workspaceRole: 'readonly');
|
|
seedActiveBaselineForTenant($environment);
|
|
createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']);
|
|
|
|
$this->actingAs($owner);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Compare run required')
|
|
->assertSee('Required inputs exist, but no compare run has been created for the current state.')
|
|
->assertSee('Compare now')
|
|
->assertActionEnabled('compareNow')
|
|
->assertDontSee('Ready to Compare');
|
|
|
|
$content = $component->html();
|
|
|
|
spec336AssertFlowStep($content, 'Baseline assigned', 'Complete', false);
|
|
spec336AssertFlowStep($content, 'Baseline snapshot', 'Available', false);
|
|
spec336AssertFlowStep($content, 'Compare run', 'Required', true);
|
|
spec336AssertFlowStep($content, 'Decision output', 'Required', false);
|
|
spec336AssertInputState($content, 'OperationRun proof', 'Unavailable');
|
|
|
|
baselineCompareLandingLivewire($environment, user: $readonly)
|
|
->assertSee('Compare unavailable')
|
|
->assertActionDisabled('compareNow');
|
|
});
|
|
|
|
it('renders in-progress and failed compare proof without claiming a decision output', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$profile, $snapshot] = seedActiveBaselineForTenant($environment);
|
|
|
|
$runningRun = seedBaselineCompareRun(
|
|
$environment,
|
|
$profile,
|
|
$snapshot,
|
|
[],
|
|
OperationRunStatus::Running->value,
|
|
OperationRunOutcome::Pending->value,
|
|
now(),
|
|
);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$runningComponent = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Compare in progress')
|
|
->assertSee('View operation progress')
|
|
->assertSet('operationRunId', (int) $runningRun->getKey());
|
|
|
|
$runningContent = $runningComponent->html();
|
|
spec336AssertFlowStep($runningContent, 'Compare run', 'In progress', true);
|
|
spec336AssertFlowStep($runningContent, 'Decision output', 'Unavailable', false);
|
|
spec336AssertInputState($runningContent, 'OperationRun proof', 'Available');
|
|
|
|
$failedRun = seedBaselineCompareRun(
|
|
$environment,
|
|
$profile,
|
|
$snapshot,
|
|
[],
|
|
OperationRunStatus::Completed->value,
|
|
OperationRunOutcome::Failed->value,
|
|
now()->addMinute(),
|
|
);
|
|
|
|
$failedComponent = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Compare failed')
|
|
->assertSee('Review compare failure')
|
|
->assertSet('operationRunId', (int) $failedRun->getKey());
|
|
|
|
$failedContent = $failedComponent->html();
|
|
spec336AssertFlowStep($failedContent, 'Compare run', 'Failed', true);
|
|
spec336AssertFlowStep($failedContent, 'Decision output', 'Unavailable', false);
|
|
spec336AssertInputState($failedContent, 'OperationRun proof', 'Available');
|
|
});
|
|
|
|
it('renders drift findings and zero-drift outcomes without broad health or evidence claims', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$profile, $snapshot] = seedActiveBaselineForTenant($environment);
|
|
|
|
$run = seedBaselineCompareRun($environment, $profile, $snapshot, [
|
|
'reason_code' => BaselineCompareReasonCode::OverdueFindingsRemain->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
]);
|
|
|
|
Finding::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'scope_key' => 'baseline_profile:'.$profile->getKey(),
|
|
'severity' => Finding::SEVERITY_HIGH,
|
|
'status' => Finding::STATUS_NEW,
|
|
'source' => OperationRunType::BaselineCompare->value,
|
|
'baseline_operation_run_id' => (int) $run->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$driftComponent = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Drift findings available')
|
|
->assertSee('Drift requires review before a decision is recorded.')
|
|
->assertSee('Review drift findings')
|
|
->assertSee('Evidence unavailable - Operation proof available')
|
|
->assertDontSee('environment is healthy')
|
|
->assertDontSee('customer-safe')
|
|
->assertDontSee('compliant');
|
|
|
|
$driftContent = $driftComponent->html();
|
|
spec336AssertFlowStep($driftContent, 'Compare run', 'Available', false);
|
|
spec336AssertFlowStep($driftContent, 'Decision output', 'Available', false);
|
|
spec336AssertInputState($driftContent, 'Drift findings', 'Available');
|
|
|
|
$quietEnvironment = createUserWithTenant(tenant: null, user: $user, role: 'owner', workspaceRole: 'manager')[1];
|
|
[$quietProfile, $quietSnapshot] = seedActiveBaselineForTenant($quietEnvironment);
|
|
seedBaselineCompareRun($quietEnvironment, $quietProfile, $quietSnapshot, [
|
|
'reason_code' => BaselineCompareReasonCode::NoDriftDetected->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
]);
|
|
|
|
setAdminPanelContext($quietEnvironment);
|
|
|
|
$quietComponent = baselineCompareLandingLivewire($quietEnvironment)
|
|
->assertSee('No drift detected')
|
|
->assertSee('No governance action is required from this compare result within available compare coverage.')
|
|
->assertSee('Review evidence')
|
|
->assertSee('Evidence unavailable - Operation proof available')
|
|
->assertDontSee('environment is healthy')
|
|
->assertDontSee('customer-safe')
|
|
->assertDontSee('compliant');
|
|
|
|
spec336AssertFlowStep($quietComponent->html(), 'Decision output', 'Available', false);
|
|
});
|
|
|
|
it('keeps compare result, OperationRun proof, and evidence unavailable states separated', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$profile, $snapshot] = seedActiveBaselineForTenant($environment);
|
|
|
|
$run = seedBaselineCompareRun($environment, $profile, $snapshot, [
|
|
'reason_code' => BaselineCompareReasonCode::EvidenceCaptureIncomplete->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
'evidence_gaps' => [
|
|
'count' => 1,
|
|
'by_reason' => [
|
|
'inventory_record_missing' => 1,
|
|
],
|
|
],
|
|
], OperationRunStatus::Completed->value, OperationRunOutcome::PartiallySucceeded->value);
|
|
|
|
Finding::factory()->create([
|
|
'managed_environment_id' => (int) $environment->getKey(),
|
|
'workspace_id' => (int) $environment->workspace_id,
|
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
|
'scope_key' => 'baseline_profile:'.$profile->getKey(),
|
|
'severity' => Finding::SEVERITY_MEDIUM,
|
|
'status' => Finding::STATUS_NEW,
|
|
'source' => OperationRunType::BaselineCompare->value,
|
|
'baseline_operation_run_id' => (int) $run->getKey(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Drift findings available')
|
|
->assertSee('OperationRun proof')
|
|
->assertSee('Evidence unavailable - Evidence gaps need review')
|
|
->assertSee('Compare result exists, but evidence output is not available. Evidence gaps need review.')
|
|
->assertSee('Diagnostics - Collapsed')
|
|
->assertDontSee('customer-safe')
|
|
->assertDontSee('raw diff');
|
|
|
|
$content = $component->html();
|
|
spec336AssertFlowStep($content, 'Compare run', 'Available', false);
|
|
spec336AssertFlowStep($content, 'Decision output', 'Needs review', true);
|
|
spec336AssertInputState($content, 'OperationRun proof', 'Available');
|
|
spec336AssertInputState($content, 'Evidence path', 'Needs review');
|
|
expect($content)->not->toContain('data-testid="baseline-compare-diagnostics" open');
|
|
});
|
|
|
|
it('preserves environment-owned routing and workspace isolation', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
$hiddenEnvironment = createUserWithTenant(user: $user, role: 'owner', workspaceRole: 'manager')[1];
|
|
[$hiddenProfile, $hiddenSnapshot] = seedActiveBaselineForTenant($hiddenEnvironment);
|
|
|
|
seedBaselineCompareRun($hiddenEnvironment, $hiddenProfile, $hiddenSnapshot, [
|
|
'reason_code' => BaselineCompareReasonCode::OverdueFindingsRemain->value,
|
|
'coverage' => [
|
|
'effective_types' => ['deviceConfiguration'],
|
|
'covered_types' => ['deviceConfiguration'],
|
|
'uncovered_types' => [],
|
|
'proof' => true,
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
|
->get(ManagedEnvironmentLinks::baselineCompareUrl($environment))
|
|
->assertOk()
|
|
->assertSeeText('Which baseline drift requires action?')
|
|
->assertDontSeeText((string) $hiddenProfile->name)
|
|
->assertDontSeeText('MANAGED_ENVIRONMENT');
|
|
|
|
$this->actingAs($user)
|
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
|
->get('/admin/baseline-compare-landing?tenant_id='.(int) $environment->getKey())
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('renders invalid assigned baseline scope as needs-review instead of running compare', function (): void {
|
|
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
|
[$profile] = seedActiveBaselineForTenant($environment);
|
|
|
|
DB::table('baseline_tenant_assignments')
|
|
->where('managed_environment_id', (int) $environment->getKey())
|
|
->where('baseline_profile_id', (int) $profile->getKey())
|
|
->update([
|
|
'override_scope_jsonb' => json_encode(['version' => 2, 'entries' => []], JSON_THROW_ON_ERROR),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$this->actingAs($user);
|
|
setAdminPanelContext($environment);
|
|
|
|
$component = baselineCompareLandingLivewire($environment)
|
|
->assertSee('Baseline scope requires review')
|
|
->assertSee('A baseline is assigned, but its scope cannot be used safely for compare.')
|
|
->assertSee('Open baseline profile')
|
|
->assertActionDisabled('compareNow');
|
|
|
|
$content = $component->html();
|
|
spec336AssertFlowStep($content, 'Baseline assigned', 'Needs review', true);
|
|
spec336AssertFlowStep($content, 'Compare run', 'Unavailable', false);
|
|
spec336AssertFlowStep($content, 'Decision output', 'Unavailable', false);
|
|
});
|
|
|
|
function spec336AssertFlowStep(string $content, string $label, string $state, bool $currentBlocker): void
|
|
{
|
|
$blocker = $currentBlocker ? 'true' : 'false';
|
|
|
|
expect($content)->toMatch(
|
|
'/data-step-label="'.preg_quote($label, '/').'"[\s\S]*?data-step-state="'.preg_quote($state, '/').'"[\s\S]*?data-step-current-blocker="'.$blocker.'"[\s\S]*?>\s*'.preg_quote($state, '/').'\s*</'
|
|
);
|
|
}
|
|
|
|
function spec336AssertInputState(string $content, string $label, string $state): void
|
|
{
|
|
expect($content)->toMatch(
|
|
'/data-input-label="'.preg_quote($label, '/').'"[\s\S]*?>\s*'.preg_quote($state, '/').'\s*</'
|
|
);
|
|
}
|