TenantAtlas/apps/platform/tests/Unit/Baselines/CompareSubjectResultContractTest.php
2026-04-13 23:09:11 +02:00

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']);
}
});