TenantAtlas/apps/platform/tests/Unit/Support/TenantConfiguration/Spec426ExchangeTeamsCanonicalIdentityTest.php
ahmido f7d06621a0 feat: implement Exchange Teams evidence identity readiness (#493)
Automated PR for spec 426 exchange teams core evidence identity readiness. Includes service changes and coverage/requirement/spec updates from commit fb4dc20c.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #493
2026-07-03 11:43:11 +00:00

112 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\TenantConfigurationResourceType;
use App\Services\TenantConfiguration\CanonicalIdentityResolver;
use App\Services\TenantConfiguration\CoverageIdentityStrategyRegistry;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\TenantConfiguration\CanonicalKeyKind;
use App\Support\TenantConfiguration\IdentityState;
it('Spec426 registers stable identity strategies for the four selected Exchange and Teams types', function (): void {
$strategies = app(CoverageIdentityStrategyRegistry::class)->strategies();
foreach (['transportRule', 'acceptedDomain', 'appPermissionPolicy', 'meetingPolicy'] as $canonicalType) {
$strategy = $strategies[$canonicalType] ?? null;
expect($strategy)->toBeArray()
->and($strategy['strategy_identifier'])->toStartWith('tcm.')
->and($strategy['requires_provider_connection_scope'])->toBeTrue()
->and($strategy['allows_derived_identity'])->toBeFalse()
->and($strategy['allows_experimental_identity'])->toBeFalse()
->and($strategy['derived_claims_allowed'])->toBeFalse();
$identityFields = [
...$strategy['preferred_identity_fields'],
...$strategy['fallback_identity_fields'],
];
expect(array_intersect($identityFields, ['displayName', 'DisplayName', 'name', 'Name', 'Identity']))
->toBe([], "{$canonicalType} must not treat display-only or name-like Identity fields as stable identity.");
}
});
it('Spec426 resolves stable source-backed identity from provider or natural keys', function (string $canonicalType, array $payload, CanonicalKeyKind $expectedKeyKind): void {
$result = app(CanonicalIdentityResolver::class)->resolve(
spec426IdentityResourceType($canonicalType),
$payload,
['source_contract_key' => $canonicalType, 'source_version' => 'v1.0'],
);
expect($result->identityState)->toBe(IdentityState::Stable)
->and($result->keyKind)->toBe($expectedKeyKind)
->and($result->sourceResourceId)->not->toStartWith('missing:');
})->with([
'transport rule guid' => ['transportRule', ['Guid' => 'b0f47b86-0875-46ba-8753-37d19bb0789a', 'Identity' => 'Transport Rule 1', 'DisplayName' => 'Mail flow rule'], CanonicalKeyKind::TcmResourceIdentifier],
'accepted domain natural key' => ['acceptedDomain', ['DomainName' => 'contoso.com', 'DisplayName' => 'Contoso'], CanonicalKeyKind::ProviderExternalId],
'app permission policy id' => ['appPermissionPolicy', ['policyId' => 'app-policy-global', 'Identity' => 'Global', 'DisplayName' => 'Global app policy'], CanonicalKeyKind::TcmResourceIdentifier],
'meeting policy id' => ['meetingPolicy', ['policyId' => 'meeting-policy-global', 'Identity' => 'Global', 'DisplayName' => 'Global meeting policy'], CanonicalKeyKind::TcmResourceIdentifier],
]);
it('Spec426 prefers stable provider IDs over name-like Identity values', function (string $canonicalType, array $payload, string $expectedField, string $expectedValue): void {
$result = app(CanonicalIdentityResolver::class)->resolve(
spec426IdentityResourceType($canonicalType),
$payload,
['source_contract_key' => $canonicalType, 'source_version' => 'v1.0'],
);
expect($result->identityState)->toBe(IdentityState::Stable)
->and($result->sourceIdentity['values']['field'])->toBe($expectedField)
->and($result->sourceIdentity['values']['value'])->toBe($expectedValue)
->and($result->sourceResourceId)->toBe($expectedValue);
})->with([
'transport rule guid before Identity' => ['transportRule', ['Identity' => 'Transport Rule 1', 'Guid' => 'b0f47b86-0875-46ba-8753-37d19bb0789a'], 'Guid', 'b0f47b86-0875-46ba-8753-37d19bb0789a'],
'app permission policy policyId before Identity' => ['appPermissionPolicy', ['Identity' => 'Global', 'policyId' => 'app-policy-global'], 'policyId', 'app-policy-global'],
'meeting policy policyId before Identity' => ['meetingPolicy', ['Identity' => 'Global', 'policyId' => 'meeting-policy-global'], 'policyId', 'meeting-policy-global'],
]);
it('Spec426 blocks display-only payloads from stable readiness identity', function (string $canonicalType): void {
$result = app(CanonicalIdentityResolver::class)->resolve(
spec426IdentityResourceType($canonicalType),
['DisplayName' => 'Shared display name', 'name' => 'Shared display name'],
['source_contract_key' => $canonicalType, 'source_version' => 'v1.0'],
);
expect($result->identityState)->toBe(IdentityState::MissingExternalId)
->and($result->sourceResourceId)->toStartWith('missing:')
->and($result->canonicalResourceKey)->not->toContain('Shared display name');
})->with([
'transportRule',
'acceptedDomain',
'appPermissionPolicy',
'meetingPolicy',
]);
it('Spec426 blocks Identity-only payloads from stable readiness identity', function (string $canonicalType): void {
$result = app(CanonicalIdentityResolver::class)->resolve(
spec426IdentityResourceType($canonicalType),
['Identity' => 'Global', 'DisplayName' => 'Global'],
['source_contract_key' => $canonicalType, 'source_version' => 'v1.0'],
);
expect($result->identityState)->toBe(IdentityState::MissingExternalId)
->and($result->sourceResourceId)->toStartWith('missing:')
->and($result->canonicalResourceKey)->not->toContain('Global');
})->with([
'transportRule',
'acceptedDomain',
'appPermissionPolicy',
'meetingPolicy',
]);
function spec426IdentityResourceType(string $canonicalType): TenantConfigurationResourceType
{
$definition = collect(ResourceTypeRegistry::defaultDefinitions())
->firstWhere('canonical_type', $canonicalType);
expect($definition)->not->toBeNull("Missing default resource type definition for {$canonicalType}.");
return new TenantConfigurationResourceType($definition);
}