TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec430ExchangePowerShellNoPromotionTest.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

196 lines
8.0 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\CoverageSourceContractResolver;
use App\Services\TenantConfiguration\GenericContentEvidenceCaptureService;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\RestoreTier;
use Illuminate\Support\Facades\File;
it('Spec430 command contracts create no OperationRun evidence provider call or customer-ready promotion', function (): void {
$beforeRuns = OperationRun::query()->count();
foreach (spec430IncludedTypes() as $canonicalType) {
app(CoverageSourceContractResolver::class)->resolve(spec430NoPromotionResourceType($canonicalType));
}
expect(OperationRun::query()->count())->toBe($beforeRuns)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec430 capture path skips included adapter contracts without provider calls or evidence rows', function (): void {
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(),
'scopes_granted' => ['Exchange.ManageAsApp'],
]);
$graph = spec430NoPromotionGraphClient();
app()->instance(GraphClientInterface::class, $graph);
$run = OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => spec430IncludedTypes(),
'required_capability' => 'evidence.manage',
],
]);
$result = app(GenericContentEvidenceCaptureService::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalTypes: spec430IncludedTypes(),
);
expect($graph->calls)->toBe([])
->and($result['summary_counts'])->toMatchArray([
'total' => 3,
'processed' => 3,
'succeeded' => 0,
'skipped' => 3,
'failed' => 0,
'errors_recorded' => 0,
])
->and($result['run_outcome'])->toBe(OperationRunOutcome::Blocked->value)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
foreach (collect($result['outcomes'])->keyBy('canonical_type') as $canonicalType => $outcome) {
expect(spec430IncludedTypes())->toContain($canonicalType)
->and($outcome['outcome'])->toBe(CaptureOutcome::BlockedMissingContract->value)
->and($outcome['reason_code'])->toBe('contract_verified_pending_capture')
->and($outcome['source_contract_key'])->toBe('exchange_powershell.'.$canonicalType);
}
});
it('Spec430 registry defaults remain internal non-evidence non-certified and non-restorable', function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
$rows = TenantConfigurationResourceType::query()
->whereIn('canonical_type', spec430IncludedTypes())
->orderBy('canonical_type')
->get();
expect($rows)->toHaveCount(3);
foreach ($rows as $row) {
expect($row->default_coverage_level)->toBe(CoverageLevel::Detected)
->and($row->default_evidence_state)->toBe(EvidenceState::NotCaptured)
->and($row->default_claim_state)->toBe(ClaimState::InternalOnly)
->and($row->allows_certified_claims)->toBeFalse()
->and($row->allows_graph_fallback_claims)->toBeFalse()
->and($row->metadata['customer_claims_allowed'])->toBeFalse()
->and($row->restore_tier)->not->toBe(RestoreTier::Restorable);
}
});
it('Spec430 introduces no UI routes navigation legacy ownership or Exchange mini-platform artifacts', function (): void {
$runtimeFiles = [
app_path('Services/TenantConfiguration/ExchangePowerShellCommandContracts.php'),
app_path('Services/TenantConfiguration/CoverageSourceContractResolver.php'),
app_path('Services/TenantConfiguration/ResourceTypeRegistry.php'),
app_path('Services/TenantConfiguration/SupportedScopeResolver.php'),
app_path('Services/TenantConfiguration/CoverageIdentityStrategyRegistry.php'),
];
$runtimeSource = collect($runtimeFiles)
->map(fn (string $file): string => file_get_contents($file) ?: '')
->implode("\n");
expect($runtimeSource)
->not->toContain('tenant_id')
->not->toContain('provider_tenant_id')
->not->toContain('entra_tenant_id')
->and(File::exists(app_path('Services/TenantConfiguration/Exchange')))->toBeFalse()
->and(File::exists(app_path('Services/Providers/ExchangeProviderAdapter.php')))->toBeFalse()
->and(glob(base_path('routes/*exchange*')) ?: [])->toBe([])
->and(glob(resource_path('views/**/*exchange*')) ?: [])->toBe([])
->and(glob(app_path('Filament/**/*ExchangePowerShell*')) ?: [])->toBe([]);
});
/**
* @return list<string>
*/
function spec430IncludedTypes(): array
{
return ['transportRule', 'remoteDomain', 'inboundConnector'];
}
function spec430NoPromotionResourceType(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);
}
function spec430NoPromotionGraphClient(): GraphClientInterface
{
return new class implements GraphClientInterface
{
/**
* @var list<array{policy_type: string, options: array<string, mixed>}>
*/
public array $calls = [];
public function listPolicies(string $policyType, array $options = []): GraphResponse
{
$this->calls[] = ['policy_type' => $policyType, 'options' => $options];
return new GraphResponse(true, [['id' => 'unexpected-provider-call']]);
}
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function getOrganization(array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function getServicePrincipalPermissions(array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
public function request(string $method, string $path, array $options = []): GraphResponse
{
return new GraphResponse(false, [], 501);
}
};
}