## Summary - extract baseline compare orchestration behind an explicit strategy contract and registry - preserve the current Intune compare path through a dedicated `IntuneCompareStrategy` - harden compare launch and review surfaces for mixed, unsupported, incomplete, and strategy-failure truth - add Spec 203 artifacts, focused regression coverage, and future-domain strategy proof tests ## Testing - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Baselines/CompareStrategyRegistryTest.php tests/Unit/Baselines/CompareSubjectResultContractTest.php tests/Feature/Baselines/BaselineCompareStrategySelectionTest.php tests/Feature/Baselines/BaselineComparePreconditionsTest.php tests/Feature/Baselines/BaselineCompareExecutionGuardTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Notes - no new Filament panel/provider registration changes - no global-search resource changes - no new asset registration or deployment step changes Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #233
122 lines
5.2 KiB
PHP
122 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Support\Baselines\Compare\CompareFindingCandidate;
|
|
use App\Support\Baselines\Compare\CompareState;
|
|
use App\Support\Baselines\Compare\CompareSubjectIdentity;
|
|
use App\Support\Baselines\Compare\CompareSubjectProjection;
|
|
use App\Support\Baselines\Compare\CompareSubjectResult;
|
|
|
|
it('serializes compare subject results with structured finding and diagnostics payloads', function (): void {
|
|
$result = new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity(
|
|
domainKey: 'entra',
|
|
subjectClass: 'control',
|
|
subjectTypeKey: 'conditionalAccessPolicy',
|
|
externalSubjectId: 'cap-1',
|
|
subjectKey: 'cap-1',
|
|
),
|
|
projection: new CompareSubjectProjection(
|
|
platformSubjectClass: 'control',
|
|
domainKey: 'entra',
|
|
subjectTypeKey: 'conditionalAccessPolicy',
|
|
operatorLabel: 'Conditional Access Policy',
|
|
summaryKind: 'control_snapshot',
|
|
additionalLabels: ['family' => 'identity'],
|
|
),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'available',
|
|
compareState: CompareState::Drift,
|
|
trustLevel: 'limited_confidence',
|
|
evidenceQuality: 'meta',
|
|
severityRecommendation: 'medium',
|
|
findingCandidate: new CompareFindingCandidate(
|
|
changeType: 'different_version',
|
|
severity: 'medium',
|
|
fingerprintBasis: [
|
|
'policy_type' => 'conditionalAccessPolicy',
|
|
'subject_key' => 'cap-1',
|
|
'change_type' => 'different_version',
|
|
],
|
|
evidencePayload: [
|
|
'summary' => ['kind' => 'control_snapshot'],
|
|
],
|
|
),
|
|
diagnostics: [
|
|
'strategy_key' => 'future_control',
|
|
],
|
|
);
|
|
|
|
$payload = $result->toArray();
|
|
|
|
expect($result->hasFindingCandidate())->toBeTrue()
|
|
->and($result->isGapState())->toBeFalse()
|
|
->and($payload['compare_state'])->toBe(CompareState::Drift->value)
|
|
->and($payload['baseline_availability'])->toBe('available')
|
|
->and($payload['current_state_availability'])->toBe('available')
|
|
->and($payload['projection']['platform_subject_class'])->toBe('control')
|
|
->and($payload['projection']['summary_kind'])->toBe('control_snapshot')
|
|
->and($payload['finding_candidate']['change_type'])->toBe('different_version')
|
|
->and($payload['finding_candidate']['severity'])->toBe('medium')
|
|
->and($payload['diagnostics']['strategy_key'])->toBe('future_control');
|
|
});
|
|
|
|
it('requires a finding candidate for drift results', function (): void {
|
|
expect(fn (): CompareSubjectResult => new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity('intune', 'policy', 'deviceConfiguration', 'policy-1', 'policy-1'),
|
|
projection: new CompareSubjectProjection('policy', 'intune', 'deviceConfiguration', 'Policy 1'),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'missing',
|
|
compareState: CompareState::Drift,
|
|
trustLevel: 'limited_confidence',
|
|
evidenceQuality: 'meta',
|
|
))->toThrow(InvalidArgumentException::class, 'require a finding candidate');
|
|
});
|
|
|
|
it('rejects non-drift results that still try to write findings', function (): void {
|
|
expect(fn (): CompareSubjectResult => new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity('entra', 'control', 'conditionalAccessPolicy', 'cap-1', 'cap-1'),
|
|
projection: new CompareSubjectProjection('control', 'entra', 'conditionalAccessPolicy', 'CAP 1'),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'unknown',
|
|
compareState: CompareState::Incomplete,
|
|
trustLevel: 'unusable',
|
|
evidenceQuality: 'missing',
|
|
findingCandidate: new CompareFindingCandidate(
|
|
changeType: 'different_version',
|
|
severity: 'high',
|
|
fingerprintBasis: ['subject_key' => 'cap-1'],
|
|
evidencePayload: [],
|
|
),
|
|
))->toThrow(InvalidArgumentException::class, 'Only drift compare subject results');
|
|
});
|
|
|
|
it('treats unsupported, incomplete, ambiguous, and failed states as gap states', function (): void {
|
|
$states = [
|
|
CompareState::Unsupported,
|
|
CompareState::Incomplete,
|
|
CompareState::Ambiguous,
|
|
CompareState::Failed,
|
|
];
|
|
|
|
foreach ($states as $state) {
|
|
$result = new CompareSubjectResult(
|
|
subjectIdentity: new CompareSubjectIdentity('entra', 'control', 'conditionalAccessPolicy', 'cap-1', 'cap-1'),
|
|
projection: new CompareSubjectProjection('control', 'entra', 'conditionalAccessPolicy', 'CAP 1'),
|
|
baselineAvailability: 'available',
|
|
currentStateAvailability: 'unknown',
|
|
compareState: $state,
|
|
trustLevel: 'unusable',
|
|
evidenceQuality: 'missing',
|
|
diagnostics: [
|
|
'reason_code' => 'strategy_failed',
|
|
'gap_record' => ['reason_code' => 'strategy_failed'],
|
|
],
|
|
);
|
|
|
|
expect($result->isGapState())->toBeTrue()
|
|
->and($result->gapReasonCode())->toBe('strategy_failed')
|
|
->and($result->gapRecord())->toBe(['reason_code' => 'strategy_failed']);
|
|
}
|
|
}); |