## 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
217 lines
7.6 KiB
PHP
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' => [],
|
|
];
|
|
}
|
|
};
|
|
} |