254 lines
12 KiB
PHP
254 lines
12 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\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('Spec422 exposes typed Exchange and Teams summaries with material compare details without provider calls', function (string $canonicalType, array $previousPayload, array $latestPayload, string $resourceType, string $expectedText, string $expectedChange): void {
|
|
[$user, $environment, $resource] = spec422FeatureEvidencePair($canonicalType, $previousPayload, $latestPayload);
|
|
app()->instance(GraphClientInterface::class, spec422FailingGraphClient());
|
|
|
|
$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)->not->toContain('raw_payload')
|
|
->and($encoded)->not->toContain('source_endpoint')
|
|
->and($encoded)->not->toContain('spec422-feature-secret')
|
|
->and($summary['compare_summary']['status'])->toBe('Material changes detected')
|
|
->and($summary['compare_summary']['changed'])->toBeTrue()
|
|
->and(collect($summary['compare_summary']['changes'])->pluck('label'))->toContain($expectedChange);
|
|
})->with([
|
|
'transport rule' => [
|
|
'transportRule',
|
|
['DisplayName' => 'Spec422 Feature Transport Rule', 'Enabled' => true, 'Actions' => ['RedirectMessageTo' => ['old-security@example.com']]],
|
|
['DisplayName' => 'Spec422 Feature Transport Rule', 'Enabled' => true, 'Actions' => ['RedirectMessageTo' => ['security@example.com']], 'clientSecret' => 'spec422-feature-secret'],
|
|
'Transport rule',
|
|
'security@example.com',
|
|
'Actions Redirect Message To',
|
|
],
|
|
'meeting policy' => [
|
|
'meetingPolicy',
|
|
['DisplayName' => 'Spec422 Feature Meeting Policy', 'AllowTranscription' => false],
|
|
['DisplayName' => 'Spec422 Feature Meeting Policy', 'AllowTranscription' => true, 'chatContent' => 'spec422-feature-secret'],
|
|
'Teams meeting policy',
|
|
'Allow Transcription: yes',
|
|
'Recording Transcription Allow Transcription',
|
|
],
|
|
]);
|
|
|
|
it('Spec422 does not render typed summaries for non-renderable latest evidence', function (): void {
|
|
[$user, $environment, $resource, $latestEvidence] = spec422FeatureEvidencePair(
|
|
'meetingPolicy',
|
|
['DisplayName' => 'Spec422 Non Renderable Meeting', 'AllowTranscription' => false],
|
|
['DisplayName' => 'Spec422 Non Renderable Meeting', 'AllowTranscription' => true],
|
|
);
|
|
$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('Spec422 returns no inspect details when the managed environment scope does not match', function (): void {
|
|
[$user, $environment, $resource] = spec422FeatureEvidencePair(
|
|
'transportRule',
|
|
['DisplayName' => 'Spec422 Scoped Rule', 'Enabled' => true],
|
|
['DisplayName' => 'Spec422 Scoped Rule', 'Enabled' => false],
|
|
);
|
|
[, $foreignEnvironment] = createMinimalUserWithTenant(role: 'owner');
|
|
|
|
expect(app(CoverageV2ReadinessReadModel::class)->inspectDetails($resource, $foreignEnvironment, $user))->toBe([]);
|
|
});
|
|
|
|
it('Spec422 requires latest evidence to belong to the same provider connection as the resource', function (): void {
|
|
[$user, $environment, $resource, $latestEvidence] = spec422FeatureEvidencePair(
|
|
'appPermissionPolicy',
|
|
['DisplayName' => 'Spec422 Provider Policy', 'BlockAppList' => []],
|
|
['DisplayName' => 'Spec422 Provider Policy', 'BlockAppList' => [['DisplayName' => 'Consumer App', 'AppId' => 'consumer-app']]],
|
|
);
|
|
$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();
|
|
});
|
|
|
|
/**
|
|
* @return array{0: mixed, 1: mixed, 2: TenantConfigurationResource, 3: TenantConfigurationResourceEvidence}
|
|
*/
|
|
function spec422FeatureEvidencePair(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 = spec422FeatureResourceType($canonicalType);
|
|
$displayName = (string) ($latestPayload['DisplayName'] ?? $latestPayload['DomainName'] ?? 'Spec422 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:spec422-feature',
|
|
'canonical_key_kind' => CanonicalKeyKind::ProviderExternalId->value,
|
|
'source_resource_id' => 'spec422-feature',
|
|
'source_display_name' => $displayName,
|
|
'source_class' => SourceClass::Tcm->value,
|
|
'source_metadata' => [
|
|
'source_contract_key' => 'spec422.synthetic.'.$canonicalType,
|
|
'source_endpoint' => '/spec422/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 = spec422FeatureRun($user, $environment, $connection, $canonicalType, minutesAgo: 5);
|
|
$latestRun = spec422FeatureRun($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' => 'spec422.synthetic.'.$canonicalType,
|
|
'source_endpoint' => '/spec422/synthetic/'.$canonicalType,
|
|
'source_version' => 'v1.0',
|
|
'raw_payload' => ['id' => 'spec422-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' => 'spec422.synthetic.'.$canonicalType,
|
|
'source_endpoint' => '/spec422/synthetic/'.$canonicalType,
|
|
'source_version' => 'v1.0',
|
|
'raw_payload' => ['id' => 'spec422-feature', 'secret' => 'spec422-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 spec422FeatureResourceType(string $canonicalType): TenantConfigurationResourceType
|
|
{
|
|
return TenantConfigurationResourceType::query()
|
|
->where('canonical_type', $canonicalType)
|
|
->where('source_class', SourceClass::Tcm->value)
|
|
->firstOrFail();
|
|
}
|
|
|
|
function spec422FeatureRun($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 spec422FailingGraphClient(): GraphClientInterface
|
|
{
|
|
return new class implements GraphClientInterface
|
|
{
|
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
|
{
|
|
throw new RuntimeException('Spec422 render path must not call provider clients.');
|
|
}
|
|
|
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
|
{
|
|
throw new RuntimeException('Spec422 render path must not call provider clients.');
|
|
}
|
|
|
|
public function getOrganization(array $options = []): GraphResponse
|
|
{
|
|
throw new RuntimeException('Spec422 render path must not call provider clients.');
|
|
}
|
|
|
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
|
{
|
|
throw new RuntimeException('Spec422 render path must not call provider clients.');
|
|
}
|
|
|
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
|
{
|
|
throw new RuntimeException('Spec422 render path must not call provider clients.');
|
|
}
|
|
|
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
|
{
|
|
throw new RuntimeException('Spec422 render path must not call provider clients.');
|
|
}
|
|
};
|
|
}
|