Implements the bounded Spec 421 Entra comparable/renderable pack on the existing Coverage v2 operator surface. - Adds typed Conditional Access normalization, comparison, and render summaries - Keeps Security Defaults and other optional Entra types deferred until evidence-backed - Preserves the existing Coverage v2 surface with claim-guard and redaction hardening - Includes focused unit, feature, and browser coverage already recorded in the implementation report Validation is documented in `specs/421-entra-core-comparable-renderable-pack/implementation-report.md`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #488
70 lines
3.0 KiB
PHP
70 lines
3.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\TenantConfiguration\EntraCoverageComparator;
|
|
|
|
it('Spec421 treats volatile-only Conditional Access differences as unchanged', function (): void {
|
|
$result = app(EntraCoverageComparator::class)->compare(
|
|
'conditionalAccessPolicy',
|
|
spec421ComparablePayload(['modifiedDateTime' => '2026-06-27T10:00:00Z']),
|
|
spec421ComparablePayload(['modifiedDateTime' => '2026-06-27T11:00:00Z']),
|
|
);
|
|
|
|
expect($result['changed'])->toBeFalse()
|
|
->and($result['classification'])->toBe('unchanged')
|
|
->and(collect($result['changes'])->pluck('classification'))->toContain('ignored_volatile');
|
|
});
|
|
|
|
it('Spec421 detects material Conditional Access changes with bounded importance', function (array $after, string $field, string $importance): void {
|
|
$result = app(EntraCoverageComparator::class)->compare(
|
|
'conditionalAccessPolicy',
|
|
spec421ComparablePayload(),
|
|
spec421ComparablePayload($after),
|
|
);
|
|
$change = collect($result['changes'])->firstWhere('field', $field);
|
|
|
|
expect($result['changed'])->toBeTrue()
|
|
->and($result['classification'])->toBe('changed')
|
|
->and($change)->not->toBeNull()
|
|
->and($change['classification'])->toBe('changed')
|
|
->and($change['importance'])->toBe($importance);
|
|
})->with([
|
|
'state' => [['state' => 'disabled'], 'state', 'critical'],
|
|
'target users' => [['conditions' => ['users' => ['includeUsers' => ['All', 'group-a']]]], 'targets.users.include_users', 'important'],
|
|
'grant controls' => [['grantControls' => ['operator' => 'OR', 'builtInControls' => ['mfa', 'compliantDevice']]], 'grant_controls.built_in_controls', 'important'],
|
|
'session controls' => [['sessionControls' => ['signInFrequency' => ['value' => 4, 'type' => 'hours', 'isEnabled' => true]]], 'session_controls.signInFrequency.value', 'important'],
|
|
]);
|
|
|
|
it('Spec421 records redacted and unsupported fields as non-material diagnostics', function (): void {
|
|
$result = app(EntraCoverageComparator::class)->compare(
|
|
'conditionalAccessPolicy',
|
|
spec421ComparablePayload(),
|
|
spec421ComparablePayload(['clientSecret' => 'spec421-client-secret']),
|
|
);
|
|
|
|
expect($result['changed'])->toBeFalse()
|
|
->and(collect($result['changes'])->pluck('classification'))->toContain('redacted', 'unsupported_field')
|
|
->and(json_encode($result, JSON_THROW_ON_ERROR))->not->toContain('spec421-client-secret');
|
|
});
|
|
|
|
function spec421ComparablePayload(array $overrides = []): array
|
|
{
|
|
return array_replace_recursive([
|
|
'id' => 'cap-1',
|
|
'displayName' => 'Require MFA',
|
|
'state' => 'enabled',
|
|
'conditions' => [
|
|
'users' => ['includeUsers' => ['All']],
|
|
'applications' => ['includeApplications' => ['Office365']],
|
|
],
|
|
'grantControls' => [
|
|
'operator' => 'OR',
|
|
'builtInControls' => ['mfa'],
|
|
],
|
|
'sessionControls' => [
|
|
'signInFrequency' => ['value' => 8, 'type' => 'hours', 'isEnabled' => true],
|
|
],
|
|
], $overrides);
|
|
}
|