TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec421EntraCoverageLevelPromotionTest.php
ahmido 69d4ecbbd2 feat: complete spec 421 Entra comparable/renderable pack (#488)
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
2026-06-27 22:12:01 +00:00

128 lines
5.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\TenantConfiguration\GenericContentEvidenceCaptureService;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\CoverageLevel;
it('Spec421 promotes only content-backed Conditional Access evidence to renderable coverage', function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
config()->set('graph_contracts.types.conditionalAccessPolicy.volatile_fields', ['modifiedDateTime']);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$graph = spec421PromotionGraphClient();
app()->instance(GraphClientInterface::class, $graph);
$result = app(GenericContentEvidenceCaptureService::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: spec421PromotionRun($user, $environment, $connection, [
'conditionalAccessPolicy',
'securityDefaults',
'application',
'servicePrincipal',
'roleDefinition',
'administrativeUnit',
]),
canonicalTypes: [
'conditionalAccessPolicy',
'securityDefaults',
'application',
'servicePrincipal',
'roleDefinition',
'administrativeUnit',
],
);
$outcomes = collect($result['outcomes'])->keyBy('canonical_type');
expect($graph->calls)->toBe(['conditionalAccessPolicy'])
->and(TenantConfigurationResource::query()->count())->toBe(1)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(1)
->and(TenantConfigurationResourceEvidence::query()->sole()->coverage_level)->toBe(CoverageLevel::Renderable)
->and($outcomes['conditionalAccessPolicy']['outcome'])->toBe(CaptureOutcome::Captured->value)
->and($outcomes['securityDefaults']['outcome'])->toBe(CaptureOutcome::BlockedUnsupported->value)
->and($outcomes['application']['outcome'])->toBe(CaptureOutcome::BlockedUnsupported->value)
->and($outcomes['servicePrincipal']['outcome'])->toBe(CaptureOutcome::BlockedUnsupported->value)
->and($outcomes['roleDefinition']['outcome'])->toBe(CaptureOutcome::BlockedUnsupported->value)
->and($outcomes['administrativeUnit']['outcome'])->toBe(CaptureOutcome::BlockedUnsupported->value);
});
function spec421PromotionRun($user, $environment, ProviderConnection $connection, array $resourceTypes): OperationRun
{
return OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => $resourceTypes,
'required_capability' => 'evidence.manage',
],
]);
}
function spec421PromotionGraphClient(): GraphClientInterface
{
return new class implements GraphClientInterface
{
public array $calls = [];
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
$this->calls[] = $policyType;
return new GraphResponse(true, [[
'id' => 'cap-1',
'displayName' => 'Require MFA',
'state' => 'enabled',
'conditions' => ['users' => ['includeUsers' => ['All']]],
'grantControls' => ['builtInControls' => ['mfa']],
]]);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
};
}