TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec422ExchangeTeamsComparableRenderableTest.php
ahmido 13d363c8b8 feat: complete spec 422 exchange teams comparable renderable pack (#489)
## Summary

This PR completes spec 422 exchange teams comparable renderable pack with comparable diffing, renderable summary builders, and comprehensive test updates.

## Commit
- 4c1e14c6 feat: complete spec 422 exchange teams comparable renderable pack

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #489
2026-06-30 04:20:13 +00:00

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.');
}
};
}