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

217 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Support\Baselines\BaselineScope;
use App\Support\Baselines\Compare\CompareOrchestrationContext;
use App\Support\Baselines\Compare\CompareStrategy;
use App\Support\Baselines\Compare\CompareStrategyCapability;
use App\Support\Baselines\Compare\CompareStrategyKey;
use App\Support\Baselines\Compare\CompareStrategyRegistry;
use App\Support\Governance\GovernanceDomainKey;
use App\Support\Governance\GovernanceSubjectClass;
it('selects a single compatible strategy family for a canonical scope entry', function (): void {
$registry = new CompareStrategyRegistry([
compareStrategyStub(
key: 'intune_policy',
capabilities: [
new CompareStrategyCapability(
strategyKey: CompareStrategyKey::intunePolicy(),
domainKeys: [GovernanceDomainKey::Intune->value, GovernanceDomainKey::PlatformFoundation->value],
subjectClasses: [
GovernanceSubjectClass::Policy->value,
GovernanceSubjectClass::ConfigurationResource->value,
],
),
],
),
]);
$scope = BaselineScope::fromJsonb([
'version' => 2,
'entries' => [
[
'domain_key' => GovernanceDomainKey::Intune->value,
'subject_class' => GovernanceSubjectClass::Policy->value,
'subject_type_keys' => ['deviceConfiguration'],
'filters' => [],
],
[
'domain_key' => GovernanceDomainKey::PlatformFoundation->value,
'subject_class' => GovernanceSubjectClass::ConfigurationResource->value,
'subject_type_keys' => ['assignmentFilter'],
'filters' => [],
],
],
]);
$selection = $registry->select($scope);
expect($selection->isSupported())->toBeTrue()
->and($selection->strategyKey?->value)->toBe('intune_policy')
->and($selection->matchedScopeEntries)->toHaveCount(2)
->and($selection->rejectedScopeEntries)->toBe([]);
});
it('rejects canonical scope entries when no strategy supports them', function (): void {
$registry = new CompareStrategyRegistry([
compareStrategyStub(
key: 'intune_policy',
capabilities: [
new CompareStrategyCapability(
strategyKey: CompareStrategyKey::intunePolicy(),
domainKeys: [GovernanceDomainKey::Intune->value],
subjectClasses: [GovernanceSubjectClass::Policy->value],
),
],
),
]);
$scope = new BaselineScope(
entries: [[
'domain_key' => GovernanceDomainKey::Entra->value,
'subject_class' => GovernanceSubjectClass::Control->value,
'subject_type_keys' => ['conditionalAccessPolicy'],
'filters' => [],
]],
version: 2,
);
$selection = $registry->select($scope);
expect($selection->isUnsupported())->toBeTrue()
->and($selection->strategyKey)->toBeNull()
->and($selection->matchedScopeEntries)->toBe([])
->and($selection->rejectedScopeEntries)->toHaveCount(1);
});
it('marks scope as mixed when multiple strategy families are required', function (): void {
$registry = new CompareStrategyRegistry([
compareStrategyStub(
key: 'intune_policy',
capabilities: [
new CompareStrategyCapability(
strategyKey: CompareStrategyKey::intunePolicy(),
domainKeys: [GovernanceDomainKey::Intune->value],
subjectClasses: [GovernanceSubjectClass::Policy->value],
),
],
),
compareStrategyStub(
key: 'future_control',
capabilities: [
new CompareStrategyCapability(
strategyKey: CompareStrategyKey::from('future_control'),
domainKeys: [GovernanceDomainKey::Entra->value],
subjectClasses: [GovernanceSubjectClass::Control->value],
),
],
),
]);
$scope = new BaselineScope(
entries: [
[
'domain_key' => GovernanceDomainKey::Intune->value,
'subject_class' => GovernanceSubjectClass::Policy->value,
'subject_type_keys' => ['deviceConfiguration'],
'filters' => [],
],
[
'domain_key' => GovernanceDomainKey::Entra->value,
'subject_class' => GovernanceSubjectClass::Control->value,
'subject_type_keys' => ['conditionalAccessPolicy'],
'filters' => [],
],
],
version: 2,
);
$selection = $registry->select($scope);
expect($selection->isMixed())->toBeTrue()
->and($selection->strategyKey)->toBeNull()
->and($selection->matchedScopeEntries)->toHaveCount(2)
->and($selection->diagnostics['matched_strategy_keys'] ?? [])->toEqual(['future_control', 'intune_policy']);
});
it('supports deterministic future-domain selection without implicit intune fallback', function (): void {
$registry = new CompareStrategyRegistry([
compareStrategyStub(
key: 'future_control',
capabilities: [
new CompareStrategyCapability(
strategyKey: CompareStrategyKey::from('future_control'),
domainKeys: [GovernanceDomainKey::Entra->value],
subjectClasses: [GovernanceSubjectClass::Control->value],
),
],
),
]);
$scope = new BaselineScope(
entries: [[
'domain_key' => GovernanceDomainKey::Entra->value,
'subject_class' => GovernanceSubjectClass::Control->value,
'subject_type_keys' => ['conditionalAccessPolicy'],
'filters' => [],
]],
version: 2,
);
$selection = $registry->select($scope);
expect($selection->isSupported())->toBeTrue()
->and($selection->strategyKey?->value)->toBe('future_control')
->and($registry->resolve('future_control'))->toBeInstanceOf(CompareStrategy::class);
});
it('throws when resolving an unknown strategy key', function (): void {
$registry = new CompareStrategyRegistry([]);
expect(fn (): CompareStrategy => $registry->resolve('missing_strategy'))
->toThrow(InvalidArgumentException::class, 'Unknown compare strategy');
});
/**
* @param list<CompareStrategyCapability> $capabilities
*/
function compareStrategyStub(string $key, array $capabilities): CompareStrategy
{
return new class($key, $capabilities) implements CompareStrategy
{
/**
* @param list<CompareStrategyCapability> $capabilities
*/
public function __construct(
private readonly string $keyValue,
private readonly array $capabilities,
) {}
public function key(): CompareStrategyKey
{
return CompareStrategyKey::from($this->keyValue);
}
public function capabilities(): array
{
return $this->capabilities;
}
public function compare(
CompareOrchestrationContext $context,
Tenant $tenant,
array $baselineItems,
array $currentItems,
array $resolvedCurrentEvidence,
array $severityMapping,
): array {
return [
'subject_results' => [],
'diagnostics' => [],
];
}
};
}