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