TenantAtlas/apps/platform/tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellMetadataTest.php
ahmido 9b58a5696d feat: add Exchange PowerShell adapter contract slice 1 (#497)
Spec 430: Exchange PowerShell adapter contract slice 1. Validation was not rerun during PR creation handoff; branch contains implementation report and tests.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #497
2026-07-05 12:08:16 +00:00

109 lines
6.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\TenantConfigurationResourceType;
use App\Services\TenantConfiguration\CanonicalIdentityResolver;
use App\Services\TenantConfiguration\ClaimGuard;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\IdentityState;
it('Spec430 permission metadata remains pending runtime validation and does not widen provider readiness claims', function (string $canonicalType): void {
$contract = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType);
$permissions = $contract['permission_model'];
expect($permissions['status'])->toBe('runtime_validation_pending')
->and($permissions['known_permission_names'])->toBe(['Exchange.ManageAsApp'])
->and($permissions['least_privilege_runtime_validated'])->toBeFalse()
->and($permissions['exchange_rbac_runtime_validated'])->toBeFalse()
->and($permissions['admin_consent_required'])->toBeTrue()
->and($permissions['permission_failure_modes'])->toContain('permission_denied')
->and($permissions['permission_failure_modes'])->toContain('command_unavailable')
->and($permissions['permission_failure_modes'])->toContain('adapter_unavailable');
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
it('Spec430 response-shape metadata distinguishes empty denied unavailable malformed and unexpected shapes', function (string $canonicalType): void {
$shape = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType)['response_shape'];
expect($shape['raw_payload_shape'])->toBe('exchange_powershell_structured_collection')
->and($shape['collection_item_path'])->toBe('items')
->and($shape['empty_collection_meaning'])->toBe('valid_empty_collection')
->and($shape['permission_denied_response_meaning'])->toBe('permission_denied')
->and($shape['command_unavailable_response_meaning'])->toBe('command_unavailable')
->and($shape['adapter_unavailable_response_meaning'])->toBe('adapter_unavailable')
->and($shape['malformed_response_meaning'])->toBe('malformed_response')
->and($shape['unexpected_object_shape_meaning'])->toBe('unexpected_object_shape')
->and($shape['response_shape_safety'])->toBe('fail_closed_until_empty_denied_unavailable_malformed_and_unexpected_shapes_are_distinct');
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
it('Spec430 display-name-only payloads do not produce stable identity for included types', function (string $canonicalType): void {
$result = app(CanonicalIdentityResolver::class)->resolve(
spec430MetadataResourceType($canonicalType),
['DisplayName' => 'Display-only object', 'Name' => 'Display-only object'],
['source_contract_key' => 'exchange_powershell.'.$canonicalType, 'source_version' => ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION],
);
expect($result->identityState)->toBe(IdentityState::MissingExternalId)
->and($result->sourceResourceId)->toStartWith('missing:')
->and($result->canonicalResourceKey)->not->toContain('Display-only object');
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
it('Spec430 redaction metadata forbids secrets raw shell output content and protected configuration exposure', function (string $canonicalType): void {
$rules = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType)['redaction_rules'];
$json = json_encode($rules, JSON_THROW_ON_ERROR);
expect($rules['raw_payload_default_visible'])->toBeFalse()
->and($rules['permission_context_default_visible'])->toBeFalse()
->and($rules['forbidden_default_output'])->toContain('tokens')
->and($rules['forbidden_default_output'])->toContain('secrets')
->and($rules['forbidden_default_output'])->toContain('authorization_header')
->and($rules['forbidden_default_output'])->toContain('cookies')
->and($rules['forbidden_default_output'])->toContain('certificate_private_material')
->and($rules['forbidden_default_output'])->toContain('passwords')
->and($rules['forbidden_default_output'])->toContain('raw_transcripts')
->and($rules['forbidden_default_output'])->toContain('raw_shell_stdout')
->and($rules['forbidden_default_output'])->toContain('raw_shell_stderr')
->and($rules['forbidden_default_output'])->toContain('mail_body_content')
->and($rules['forbidden_default_output'])->toContain('mailbox_content')
->and($rules['forbidden_default_output'])->toContain('file_content')
->and($rules['forbidden_default_output'])->toContain('teams_transcript_content')
->and($json)->not->toContain('client-secret-value')
->and($json)->not->toContain('authorization-token-value');
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
it('Spec430 future execution scope metadata preserves workspace managed-environment and provider-connection handoff', function (string $canonicalType): void {
$scope = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType)['future_execution_scope'];
expect($scope)->toMatchArray([
'requires_workspace_id' => true,
'requires_managed_environment_id' => true,
'requires_provider_connection_id' => true,
'provider_native_scope_identifiers_are_metadata_only' => true,
]);
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
it('Spec430 claim guard allows only the exact internal adapter-contract statement', function (): void {
$guard = app(ClaimGuard::class);
expect($guard->evaluateStatement(ExchangePowerShellCommandContracts::ALLOWED_INTERNAL_CLAIM, internalOperatorOnly: true))
->toBe(ClaimState::InternalOnly)
->and($guard->evaluateStatement(ExchangePowerShellCommandContracts::ALLOWED_INTERNAL_CLAIM, internalOperatorOnly: false))
->toBe(ClaimState::ClaimBlocked)
->and($guard->evaluateStatement('Exchange PowerShell adapter contracts are customer ready', internalOperatorOnly: true))
->toBe(ClaimState::ClaimBlocked)
->and($guard->evaluateStatement('Exchange PowerShell coverage is certified', internalOperatorOnly: true))
->toBe(ClaimState::ClaimBlocked);
});
function spec430MetadataResourceType(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);
}