TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec423SecurityComplianceCoverageReadinessTest.php
Ahmed Darrazi c49acba7cd
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m20s
feat: complete spec 423 security compliance readiness pack
2026-06-30 13:57:10 +02:00

455 lines
21 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\Models\TenantConfigurationResourceType;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use App\Services\TenantConfiguration\CoverageEvidenceWriter;
use App\Services\TenantConfiguration\CoverageSourceContractDecision;
use App\Services\TenantConfiguration\CoverageV2ReadinessReadModel;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\TenantConfiguration\CanonicalKeyKind;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\IdentityState;
use App\Support\TenantConfiguration\SourceClass;
it('Spec423 exposes typed Security and Compliance summaries with readiness and compare details without provider calls', function (string $canonicalType, array $previousPayload, array $latestPayload, string $resourceType, string $expectedText, string $expectedChange): void {
[$user, $environment, $resource] = spec423FeatureEvidencePair($canonicalType, $previousPayload, $latestPayload);
app()->instance(GraphClientInterface::class, spec423FailingGraphClient());
$details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource, $environment, $user);
$summary = $details['typed_render_summary'] ?? null;
$encoded = json_encode($summary, JSON_THROW_ON_ERROR);
expect($summary)->toBeArray()
->and($summary['resource_type'])->toBe($resourceType)
->and($encoded)->toContain($expectedText)
->and($encoded)->toContain('readiness_requires_manual_review')
->and($encoded)->not->toContain('raw_payload')
->and($encoded)->not->toContain('source_endpoint')
->and($encoded)->not->toContain('spec423-feature-secret')
->and($encoded)->not->toContain('spec423-feature-content')
->and($summary['compare_summary']['status'])->toBe('Manual review required')
->and($summary['compare_summary']['changed'])->toBeTrue()
->and(collect($summary['compare_summary']['changes'])->pluck('label'))->toContain($expectedChange);
})->with([
'retention policy' => [
'retentionCompliancePolicy',
[
'DisplayName' => 'Spec423 Feature Retention Policy',
'RetentionDuration' => 5,
'RetentionDurationUnit' => 'Years',
'DispositionAction' => 'Keep',
'IncludedLocations' => ['Exchange'],
],
[
'DisplayName' => 'Spec423 Feature Retention Policy',
'RetentionDuration' => 7,
'RetentionDurationUnit' => 'Years',
'DispositionAction' => 'Delete',
'IncludedLocations' => ['Exchange'],
'clientSecret' => 'spec423-feature-secret',
],
'Retention compliance policy',
'7 Years',
'Retention Duration',
],
'label policy' => [
'labelPolicy',
[
'DisplayName' => 'Spec423 Feature Label Policy',
'PublishedLabels' => [['displayName' => 'General']],
'Mandatory' => false,
],
[
'DisplayName' => 'Spec423 Feature Label Policy',
'PublishedLabels' => [['displayName' => 'Highly Confidential']],
'Mandatory' => true,
],
'Label policy',
'Highly Confidential',
'Labeling Published Labels',
],
'dlp policy' => [
'dlpCompliancePolicy',
[
'DisplayName' => 'Spec423 Feature DLP Policy',
'Mode' => 'Audit',
'Locations' => ['Exchange'],
'Rules' => [['Name' => 'Rule', 'Actions' => ['NotifyUser']]],
],
[
'DisplayName' => 'Spec423 Feature DLP Policy',
'Mode' => 'Enforce',
'Locations' => ['Exchange'],
'Rules' => [['Name' => 'Rule', 'Actions' => ['BlockAccess'], 'DlpIncidentContent' => 'spec423-feature-content']],
],
'DLP compliance policy',
'Enforce',
'Mode',
],
]);
it('Spec423 does not render typed summaries for non-renderable latest evidence', function (): void {
[$user, $environment, $resource, $latestEvidence] = spec423FeatureEvidencePair(
'retentionCompliancePolicy',
['DisplayName' => 'Spec423 Non Renderable Retention', 'RetentionDuration' => 5],
['DisplayName' => 'Spec423 Non Renderable Retention', 'RetentionDuration' => 7],
);
$latestEvidence->forceFill(['coverage_level' => CoverageLevel::ContentBacked->value])->save();
$resource->unsetRelation('latestEvidence');
$details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource->fresh(), $environment, $user);
expect($details['typed_render_summary'] ?? null)->toBeNull();
});
it('Spec423 requires latest evidence to belong to the same provider connection as the resource', function (): void {
[$user, $environment, $resource, $latestEvidence] = spec423FeatureEvidencePair(
'labelPolicy',
['DisplayName' => 'Spec423 Provider Labels', 'Mandatory' => false],
['DisplayName' => 'Spec423 Provider Labels', 'Mandatory' => true],
);
$foreignConnection = ProviderConnection::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$latestEvidence->forceFill(['provider_connection_id' => (int) $foreignConnection->getKey()])->save();
$resource->unsetRelation('latestEvidence');
$details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource->fresh(), $environment, $user);
expect($details['typed_render_summary'] ?? null)->toBeNull();
});
it('Spec423 promotes mandatory Security and Compliance content-backed evidence rows to renderable coverage', function (string $canonicalType, array $payload): void {
app(ResourceTypeRegistry::class)->syncDefaults();
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$resourceType = spec423FeatureResourceType($canonicalType);
$resource = TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'canonical_type' => $canonicalType,
'canonical_resource_key' => $canonicalType.':provider_external_id:spec423-promotion',
'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value,
'source_resource_id' => 'spec423-promotion',
'source_display_name' => 'Spec423 promotion '.$canonicalType,
'source_class' => SourceClass::Tcm->value,
'latest_evidence_state' => EvidenceState::ContentBacked->value,
'latest_identity_state' => IdentityState::Stable->value,
'latest_claim_state' => ClaimState::InternalOnly->value,
]);
$run = OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
]);
$evidence = app(CoverageEvidenceWriter::class)->append(
resource: $resource,
resourceType: $resourceType,
providerConnection: $connection,
operationRun: $run,
decision: new CoverageSourceContractDecision(
canonicalType: $canonicalType,
outcome: CaptureOutcome::Captured,
contractKey: 'spec423.synthetic.'.$canonicalType,
sourceEndpoint: '/spec423/synthetic/'.$canonicalType,
),
rawPayload: $payload,
normalizedPayload: $payload,
payloadHash: hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)),
);
expect($evidence)->toBeInstanceOf(TenantConfigurationResourceEvidence::class)
->and($evidence->coverage_level)->toBe(CoverageLevel::Renderable)
->and($resource->fresh()->latest_evidence_state)->toBe(EvidenceState::ContentBacked);
})->with([
'retentionCompliancePolicy' => ['retentionCompliancePolicy', ['DisplayName' => 'Retention', 'RetentionDuration' => 7, 'DispositionAction' => 'Delete', 'clientSecret' => 'spec423-promotion-secret']],
'labelPolicy' => ['labelPolicy', ['DisplayName' => 'Labels', 'PublishedLabels' => [['displayName' => 'Highly Confidential']]]],
'dlpCompliancePolicy' => ['dlpCompliancePolicy', ['DisplayName' => 'DLP', 'Mode' => 'Enforce', 'Rules' => [['Name' => 'Rule', 'Actions' => ['BlockAccess']]]]],
]);
it('Spec423 keeps unknown nested material fields renderable with manual-review readiness', function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$canonicalType = 'dlpCompliancePolicy';
$resourceType = spec423FeatureResourceType($canonicalType);
$payload = [
'DisplayName' => 'Spec423 Nested DLP',
'Mode' => 'Enforce',
'Rules' => [[
'Name' => 'Nested condition rule',
'Actions' => ['BlockAccess'],
'Conditions' => ['SensitiveInfoTypes' => ['Spec423 Credit Card Detector']],
]],
];
$resource = TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'canonical_type' => $canonicalType,
'canonical_resource_key' => $canonicalType.':provider_external_id:spec423-nested',
'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value,
'source_resource_id' => 'spec423-nested',
'source_display_name' => 'Spec423 Nested DLP',
'source_class' => SourceClass::Tcm->value,
'latest_evidence_state' => EvidenceState::ContentBacked->value,
'latest_identity_state' => IdentityState::Stable->value,
'latest_claim_state' => ClaimState::InternalOnly->value,
]);
$run = OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
]);
$evidence = app(CoverageEvidenceWriter::class)->append(
resource: $resource,
resourceType: $resourceType,
providerConnection: $connection,
operationRun: $run,
decision: new CoverageSourceContractDecision(
canonicalType: $canonicalType,
outcome: CaptureOutcome::Captured,
contractKey: 'spec423.synthetic.'.$canonicalType,
sourceEndpoint: '/spec423/synthetic/'.$canonicalType,
),
rawPayload: $payload,
normalizedPayload: $payload,
payloadHash: hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR)),
);
$details = app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource->refresh(), $environment, $user);
$summary = $details['typed_render_summary'] ?? null;
$encoded = json_encode($summary, JSON_THROW_ON_ERROR);
expect($evidence->coverage_level)->toBe(CoverageLevel::Renderable)
->and($summary)->toBeArray()
->and(data_get($summary, 'readiness.state'))->toBe('readiness_requires_manual_review')
->and(data_get($summary, 'readiness.label'))->toBe('Manual review required')
->and($summary['unsupported_fields'])->toContain('Rules.0.Conditions')
->and($encoded)->not->toContain('Spec423 Credit Card Detector');
});
it('Spec423 leaves optional Security and Compliance types unpromoted without bounded evidence', function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
$resourceTypes = TenantConfigurationResourceType::query()
->whereIn('canonical_type', [
'autoSensitivityLabelPolicy',
'protectionAlert',
'complianceTag',
])
->get()
->keyBy('canonical_type');
expect($resourceTypes)->toHaveCount(3);
foreach ($resourceTypes as $resourceType) {
expect($resourceType->default_coverage_level)->toBe(CoverageLevel::Detected)
->and($resourceType->default_evidence_state)->toBe(EvidenceState::NotCaptured)
->and($resourceType->allows_certified_claims)->toBeFalse()
->and($resourceType->restore_tier->value)->toBe('not_restorable');
}
});
it('Spec423 keeps Security and Compliance support separate from restore, legal, certification, customer output, and tenant ownership', function (): void {
$paths = [
'apps/platform/app/Services/TenantConfiguration/SecurityComplianceComparablePayloadNormalizer.php',
'apps/platform/app/Services/TenantConfiguration/SecurityComplianceCoverageComparator.php',
'apps/platform/app/Services/TenantConfiguration/SecurityComplianceRenderableSummaryBuilder.php',
'apps/platform/app/Services/TenantConfiguration/SecurityComplianceReadinessEvaluator.php',
'apps/platform/app/Services/TenantConfiguration/ClaimGuard.php',
];
$content = collect($paths)
->map(fn (string $path): string => file_exists(repo_path($path)) ? (file_get_contents(repo_path($path)) ?: '') : '')
->implode("\n");
expect($content)
->not->toContain('GraphClientInterface')
->not->toContain('Http::')
->not->toContain('tenant_id')
->not->toContain('restore-ready')
->not->toContain('certification-ready')
->not->toContain('legal-ready')
->not->toContain('customer-ready')
->not->toContain('ReviewPack')
->not->toContain('namespace App\\Services\\TenantConfiguration\\SecurityCompliance');
});
/**
* @return array{0: mixed, 1: mixed, 2: TenantConfigurationResource, 3: TenantConfigurationResourceEvidence}
*/
function spec423FeatureEvidencePair(string $canonicalType, array $previousPayload, array $latestPayload): array
{
app(ResourceTypeRegistry::class)->syncDefaults();
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->withCredential()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
]);
$resourceType = spec423FeatureResourceType($canonicalType);
$displayName = (string) ($latestPayload['DisplayName'] ?? $latestPayload['displayName'] ?? 'Spec423 Feature Resource');
$resource = TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'canonical_type' => $canonicalType,
'canonical_resource_key' => $canonicalType.':provider_external_id:spec423-feature',
'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value,
'source_resource_id' => 'spec423-feature',
'source_display_name' => $displayName,
'source_class' => SourceClass::Tcm->value,
'source_metadata' => [
'source_contract_key' => 'spec423.synthetic.'.$canonicalType,
'source_endpoint' => '/spec423/synthetic/'.$canonicalType,
'source_version' => 'v1.0',
'registry_source_class' => SourceClass::Tcm->value,
'registry_support_state' => 'out_of_scope',
],
'latest_evidence_state' => EvidenceState::ContentBacked->value,
'latest_identity_state' => IdentityState::Stable->value,
'latest_claim_state' => ClaimState::InternalOnly->value,
'latest_captured_at' => now(),
]);
$previousRun = spec423FeatureRun($user, $environment, $connection, $canonicalType, minutesAgo: 5);
$latestRun = spec423FeatureRun($user, $environment, $connection, $canonicalType);
TenantConfigurationResourceEvidence::factory()->create([
'resource_id' => (int) $resource->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'operation_run_id' => (int) $previousRun->getKey(),
'source_contract_key' => 'spec423.synthetic.'.$canonicalType,
'source_endpoint' => '/spec423/synthetic/'.$canonicalType,
'source_version' => 'v1.0',
'raw_payload' => ['id' => 'spec423-feature'],
'normalized_payload' => $previousPayload,
'payload_hash' => hash('sha256', json_encode($previousPayload, JSON_THROW_ON_ERROR)),
'evidence_state' => EvidenceState::ContentBacked->value,
'coverage_level' => CoverageLevel::Comparable->value,
'capture_outcome' => CaptureOutcome::Captured->value,
'captured_at' => now()->subMinutes(5),
]);
$latestEvidence = TenantConfigurationResourceEvidence::factory()->create([
'resource_id' => (int) $resource->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'operation_run_id' => (int) $latestRun->getKey(),
'source_contract_key' => 'spec423.synthetic.'.$canonicalType,
'source_endpoint' => '/spec423/synthetic/'.$canonicalType,
'source_version' => 'v1.0',
'raw_payload' => ['id' => 'spec423-feature', 'secret' => 'spec423-feature-secret'],
'normalized_payload' => $latestPayload,
'payload_hash' => hash('sha256', json_encode($latestPayload, JSON_THROW_ON_ERROR)),
'evidence_state' => EvidenceState::ContentBacked->value,
'coverage_level' => CoverageLevel::Renderable->value,
'capture_outcome' => CaptureOutcome::Captured->value,
'captured_at' => now(),
]);
$resource->forceFill([
'latest_evidence_id' => (int) $latestEvidence->getKey(),
'latest_payload_hash' => (string) $latestEvidence->payload_hash,
])->save();
return [$user, $environment, $resource->refresh(), $latestEvidence];
}
function spec423FeatureResourceType(string $canonicalType): TenantConfigurationResourceType
{
return TenantConfigurationResourceType::query()
->where('canonical_type', $canonicalType)
->where('source_class', SourceClass::Tcm->value)
->firstOrFail();
}
function spec423FeatureRun($user, $environment, ProviderConnection $connection, string $canonicalType, int $minutesAgo = 0): OperationRun
{
$timestamp = $minutesAgo > 0 ? now()->subMinutes($minutesAgo) : now();
return OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'context' => [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => [$canonicalType],
],
'started_at' => $timestamp,
'completed_at' => $timestamp,
]);
}
function spec423FailingGraphClient(): GraphClientInterface
{
return new class implements GraphClientInterface
{
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
throw new RuntimeException('Spec423 render path must not call provider clients.');
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
throw new RuntimeException('Spec423 render path must not call provider clients.');
}
public function getOrganization(array $options = []): GraphResponse
{
throw new RuntimeException('Spec423 render path must not call provider clients.');
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
throw new RuntimeException('Spec423 render path must not call provider clients.');
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
throw new RuntimeException('Spec423 render path must not call provider clients.');
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
throw new RuntimeException('Spec423 render path must not call provider clients.');
}
};
}