Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m7s
Implemented deterministic Baseline Result Semantics (Spec 383), introducing CompareSubjectResult and CompareEvidenceResult. Replaced generic arrays with strict Data Transfer Objects for Baseline engine output.
212 lines
9.1 KiB
PHP
212 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__.'/Support/FakeCompareStrategy.php';
|
|
|
|
use App\Jobs\CompareBaselineToTenantJob;
|
|
use App\Models\BaselineProfile;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\BaselineSnapshotItem;
|
|
use App\Models\BaselineTenantAssignment;
|
|
use App\Models\InventoryItem;
|
|
use App\Models\OperationRun;
|
|
use App\Services\Baselines\BaselineCompareService;
|
|
use App\Services\Baselines\BaselineSnapshotIdentity;
|
|
use App\Services\Intune\AuditLogger;
|
|
use App\Services\OperationRunService;
|
|
use App\Support\Baselines\BaselineCompareReasonCode;
|
|
use App\Support\Baselines\BaselineReasonCodes;
|
|
use App\Support\Baselines\Compare\CompareStrategyRegistry;
|
|
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\OperationRunType;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Tests\Feature\Baselines\Support\FailingCompareStrategy;
|
|
use Tests\Feature\Baselines\Support\FakeGovernanceSubjectTaxonomyRegistry;
|
|
|
|
it('blocks compare execution when the queued snapshot is incomplete', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->incomplete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
createInventorySyncOperationRunWithCoverage($tenant, ['deviceConfiguration' => 'succeeded']);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
|
|
],
|
|
]);
|
|
|
|
(new CompareBaselineToTenantJob($run))->handle(
|
|
app(BaselineSnapshotIdentity::class),
|
|
app(AuditLogger::class),
|
|
app(OperationRunService::class),
|
|
);
|
|
|
|
$run->refresh();
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Completed->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
|
|
->and(data_get($context, 'baseline_compare.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE)
|
|
->and(data_get($context, 'result.snapshot_id'))->toBe((int) $snapshot->getKey())
|
|
->and(data_get($run->failure_summary, '0.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE);
|
|
});
|
|
|
|
it('blocks compare execution when the queued snapshot is no longer the effective current baseline', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
|
|
]);
|
|
|
|
$historicalSnapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'captured_at' => now()->subDay(),
|
|
'completed_at' => now()->subDay(),
|
|
]);
|
|
|
|
$currentSnapshot = BaselineSnapshot::factory()->complete()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'captured_at' => now(),
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $currentSnapshot->getKey()]);
|
|
createInventorySyncOperationRunWithCoverage($tenant, ['deviceConfiguration' => 'succeeded']);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => OperationRunType::BaselineCompare->value,
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'baseline_snapshot_id' => (int) $historicalSnapshot->getKey(),
|
|
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
|
|
],
|
|
]);
|
|
|
|
(new CompareBaselineToTenantJob($run))->handle(
|
|
app(BaselineSnapshotIdentity::class),
|
|
app(AuditLogger::class),
|
|
app(OperationRunService::class),
|
|
);
|
|
|
|
$run->refresh();
|
|
$context = is_array($run->context) ? $run->context : [];
|
|
|
|
expect($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
|
|
->and(data_get($context, 'baseline_compare.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED)
|
|
->and(data_get($context, 'baseline_compare.effective_snapshot_id'))->toBe((int) $currentSnapshot->getKey())
|
|
->and(data_get($context, 'baseline_compare.latest_attempted_snapshot_id'))->toBe((int) $currentSnapshot->getKey())
|
|
->and(data_get($run->failure_summary, '0.reason_code'))->toBe(BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED);
|
|
});
|
|
|
|
it('marks compare runs as partially succeeded when strategy-owned processing fails before subject classification completes', function (): void {
|
|
Queue::fake();
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
app()->instance(GovernanceSubjectTaxonomyRegistry::class, new FakeGovernanceSubjectTaxonomyRegistry);
|
|
app()->instance(CompareStrategyRegistry::class, new CompareStrategyRegistry([
|
|
app(FailingCompareStrategy::class),
|
|
]));
|
|
|
|
$profile = BaselineProfile::factory()->active()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'scope_jsonb' => [
|
|
'version' => 2,
|
|
'entries' => [[
|
|
'domain_key' => 'entra',
|
|
'subject_class' => 'control',
|
|
'subject_type_keys' => ['conditionalAccessPolicy'],
|
|
'filters' => [],
|
|
]],
|
|
],
|
|
]);
|
|
|
|
$snapshot = BaselineSnapshot::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
'captured_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
|
|
|
BaselineTenantAssignment::factory()->create([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'baseline_profile_id' => (int) $profile->getKey(),
|
|
]);
|
|
|
|
BaselineSnapshotItem::factory()->create([
|
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
|
'subject_type' => 'control',
|
|
'subject_external_id' => 'conditional-access-policy-1',
|
|
'subject_key' => 'conditional-access-policy-1',
|
|
'policy_type' => 'conditionalAccessPolicy',
|
|
'baseline_hash' => hash('sha256', 'baseline'),
|
|
'meta_jsonb' => ['display_name' => 'Conditional Access Policy'],
|
|
]);
|
|
|
|
$inventorySyncRun = createInventorySyncOperationRunWithCoverage($tenant, ['conditionalAccessPolicy' => 'succeeded']);
|
|
|
|
InventoryItem::factory()->create([
|
|
'managed_environment_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'external_id' => 'conditional-access-policy-1',
|
|
'policy_type' => 'conditionalAccessPolicy',
|
|
'display_name' => 'Conditional Access Policy',
|
|
'meta_jsonb' => ['display_name' => 'Conditional Access Policy'],
|
|
'last_seen_operation_run_id' => (int) $inventorySyncRun->getKey(),
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
$result = app(BaselineCompareService::class)->startCompare($tenant, $user);
|
|
|
|
expect($result['ok'])->toBeTrue();
|
|
|
|
/** @var OperationRun $run */
|
|
$run = $result['run'];
|
|
|
|
(new CompareBaselineToTenantJob($run))->handle(
|
|
app(BaselineSnapshotIdentity::class),
|
|
app(AuditLogger::class),
|
|
app(OperationRunService::class),
|
|
);
|
|
|
|
$run->refresh();
|
|
|
|
expect($run->outcome)->toBe(OperationRunOutcome::PartiallySucceeded->value)
|
|
->and(data_get($run->context, 'baseline_compare.reason_code'))->toBe(BaselineCompareReasonCode::StrategyFailed->value)
|
|
->and(data_get($run->context, 'baseline_compare.strategy.execution_diagnostics.failed'))->toBeTrue()
|
|
->and(data_get($run->context, 'baseline_compare.strategy.execution_diagnostics.exception_class'))->toBe(RuntimeException::class)
|
|
->and(data_get($run->context, 'baseline_compare.strategy.state_counts'))->toBe([])
|
|
->and(data_get($run->context, 'baseline_compare.evidence_gaps.by_reason.compare_failed'))->toBe(1)
|
|
->and(data_get($run->context, 'baseline_compare.result_semantics.counts.by_reason.compare_failed'))->toBe(1);
|
|
});
|