TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec432ExchangePowerShellProductionRunnerGateTest.php
ahmido f4e342121a feat: add Exchange PowerShell production runner gate (#499)
Spec 432: Exchange PowerShell production runner boundary and runtime gate. Validation: php artisan test --filter=Spec432 --compact; ./vendor/bin/pint --dirty --test --format agent; git diff --check.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #499
2026-07-07 18:34:18 +00:00

728 lines
31 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentMembership;
use App\Models\ManagedEnvironmentPermission;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\TenantConfiguration\DisabledExchangePowerShellCommandRunner;
use App\Services\TenantConfiguration\ExchangePowerShellCommandRunner;
use App\Services\TenantConfiguration\ExchangePowerShellCredentialReferenceResolver;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationGate;
use App\Services\TenantConfiguration\ExchangePowerShellPermissionEvidenceEvaluator;
use App\Services\TenantConfiguration\ExchangePowerShellProcessExecutor;
use App\Services\TenantConfiguration\ExchangePowerShellProcessResult;
use App\Services\TenantConfiguration\ExchangePowerShellProductionRunner;
use App\Services\TenantConfiguration\FakeExchangePowerShellProcessExecutor;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Providers\ProviderCredentialKind;
use App\Support\Providers\ProviderCredentialSource;
use App\Support\Providers\ProviderVerificationStatus;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
beforeEach(function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => false,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => false,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => [],
'tenantpilot.exchange_powershell.runtime.allowed_environments' => [app()->environment()],
'tenantpilot.exchange_powershell.runtime.module_check_enabled' => true,
]);
});
it('Spec432 keeps the disabled runner as the default binding and production flag default false', function (): void {
$runner = app(ExchangePowerShellCommandRunner::class);
expect(config(ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH))->toBeFalse()
->and($runner)->toBeInstanceOf(DisabledExchangePowerShellCommandRunner::class);
});
it('Spec432 does not select the production runner behind the production flag alone', function (): void {
config([ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true]);
$this->app->forgetInstance(ExchangePowerShellCommandRunner::class);
expect(app(ExchangePowerShellCommandRunner::class))->toBeInstanceOf(DisabledExchangePowerShellCommandRunner::class);
});
it('Spec432 selects the production runner only when invocation and production gates are enabled', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
$this->app->forgetInstance(ExchangePowerShellCommandRunner::class);
expect(app(ExchangePowerShellCommandRunner::class))->toBeInstanceOf(ExchangePowerShellProductionRunner::class);
});
it('Spec432 invocation feature flag blocks production mode even when production runner flag is true', function (): void {
config([
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
$executor = spec432FakeExecutor();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
expect($executor->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('execution_blocked_feature_disabled')
->and(data_get($run->context, 'provider_identity_resolution_mode'))->toBe('production_runner_reference_gate');
});
it('Spec432 production runner flag false blocks production mode before process execution', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
$executor = spec432FakeExecutor();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'remoteDomain',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
expect($executor->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('execution_blocked_production_runner_disabled')
->and(data_get($run->context, 'production_runner_gate.enabled'))->toBeFalse();
});
it('Spec432 production mode blocks without a supported credential reference and does not resolve credential material', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
$executor = spec432FakeExecutor();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
expect($executor->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('credential_blocked_missing_reference')
->and(data_get($run->context, 'credential_policy.credential_material_resolved_by_invocation_gate'))->toBeFalse()
->and(data_get($run->context, 'runner_result.credential_state'))->toBe('missing');
});
it('Spec432 blocks secret-based app credentials without persisting credential payload or raw output tokens', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, ProviderCredentialKind::ClientSecret->value, [
'payload' => [
'client_id' => 'spec432-client',
'client_secret' => 'spec432-secret-material',
],
]);
$executor = spec432FakeExecutor();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
$persisted = json_encode([
'context' => $run->context,
'failure_summary' => $run->failure_summary,
'summary_counts' => $run->summary_counts,
], JSON_THROW_ON_ERROR);
expect($executor->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('credential_blocked_secret_based_app')
->and(data_get($run->context, 'runner_result.credential_state'))->toBe('secret_based_app_unsupported')
->and($persisted)
->not->toContain('spec432-secret-material')
->not->toContain('client_secret')
->not->toContain('access_token')
->not->toContain('authorization_header')
->not->toContain('cookie');
});
it('Spec432 admin consent alone still blocks without verified Exchange permission evidence', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment, seedExchangePowerShellPermission: false);
$executor = spec432FakeExecutor();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
expect($executor->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'provider_capability.provider_capability_key'))->toBe('exchange_powershell_invoke')
->and(data_get($run->context, 'provider_capability.missing_requirement_keys'))->toContain('provider.exchange_powershell_invocation');
});
it('Spec432 verified permission evidence only advances to the next credential gate by default', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, ProviderCredentialKind::Certificate->value);
$executor = spec432FakeExecutor();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
expect($executor->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('credential_blocked_certificate_unsupported')
->and(data_get($run->context, 'runner_result.credential_state'))->toBe('certificate_unsupported');
});
it('Spec432 credential reference resolver covers fail-closed credential states without payload exposure', function (
string $kind,
array $overrides,
string $expectedFailureCode,
string $expectedCredentialState,
): void {
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, $kind, [
'payload' => [
'private_key' => 'spec432-private-key-material',
'client_secret' => 'spec432-client-secret-material',
'federated_token' => 'spec432-federated-token-material',
],
...$overrides,
]);
$result = app(ExchangePowerShellCredentialReferenceResolver::class)->resolve($connection);
$resultJson = json_encode($result, JSON_THROW_ON_ERROR);
expect($result->allowed)->toBeFalse()
->and($result->failureCode)->toBe($expectedFailureCode)
->and($result->context['credential_state'])->toBe($expectedCredentialState)
->and($resultJson)
->not->toContain('spec432-private-key-material')
->not->toContain('spec432-client-secret-material')
->not->toContain('spec432-federated-token-material')
->not->toContain('client_secret');
})->with([
'certificate missing metadata' => [
ProviderCredentialKind::Certificate->value,
['last_rotated_at' => null],
'credential_blocked_certificate_missing',
'certificate_missing',
],
'certificate expired' => [
ProviderCredentialKind::Certificate->value,
['expires_at' => now()->subDay()],
'credential_blocked_certificate_expired',
'certificate_expired',
],
'certificate unsupported' => [
ProviderCredentialKind::Certificate->value,
[],
'credential_blocked_certificate_unsupported',
'certificate_unsupported',
],
'federated missing metadata' => [
ProviderCredentialKind::Federated->value,
['last_rotated_at' => null],
'credential_blocked_federated_missing',
'federated_missing',
],
'federated unsupported' => [
ProviderCredentialKind::Federated->value,
[],
'credential_blocked_federated_unsupported',
'federated_unsupported',
],
'managed identity unsupported' => [
'managed_identity',
[],
'credential_blocked_managed_identity_unsupported',
'managed_identity_unsupported',
],
'unknown kind' => [
'unknown_kind',
[],
'credential_blocked_unknown_kind',
'unknown_kind',
],
]);
it('Spec432 permission evidence evaluator fails closed for unvalidated stale and wrong-scope evidence', function (
array $detailsOverrides,
array $rowOverrides,
string $expectedFailureCode,
string $expectedEvidenceState,
): void {
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment, seedExchangePowerShellPermission: false);
ManagedEnvironmentPermission::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'permission_key' => 'Exchange.ManageAsApp',
'status' => 'granted',
'details' => [
'source' => 'spec432-test',
'workspace_id' => (int) $connection->workspace_id,
'managed_environment_id' => (int) $connection->managed_environment_id,
'provider' => (string) $connection->provider,
'provider_connection_id' => (int) $connection->getKey(),
...$detailsOverrides,
],
'last_checked_at' => now(),
...$rowOverrides,
]);
$result = app(ExchangePowerShellPermissionEvidenceEvaluator::class)->evaluate($environment, $connection);
expect($result->allowed)->toBeFalse()
->and($result->failureCode)->toBe($expectedFailureCode)
->and($result->context['permission_evidence_state'])->toBe($expectedEvidenceState);
})->with([
'unvalidated' => [
[],
['status' => 'missing'],
'permission_evidence_blocked_unvalidated',
'unvalidated',
],
'stale' => [
[],
['last_checked_at' => now()->subDays(31)],
'permission_evidence_blocked_stale',
'stale',
],
'wrong workspace' => [
['workspace_id' => 999999],
[],
'permission_evidence_blocked_wrong_workspace',
'wrong_workspace',
],
'wrong environment' => [
['managed_environment_id' => 999999],
[],
'permission_evidence_blocked_wrong_environment',
'wrong_environment',
],
'unsupported provider' => [
['provider' => 'other'],
[],
'permission_evidence_blocked_unsupported_provider',
'unsupported_provider',
],
'wrong provider connection' => [
['provider_connection_id' => 999999],
[],
'permission_evidence_blocked_wrong_provider_connection',
'wrong_provider_connection',
],
]);
it('Spec432 supported reference kind plus verified evidence can complete through fake process without evidence promotion', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, ProviderCredentialKind::Certificate->value, [
'payload' => [
'private_key' => 'spec432-private-key-material',
'certificate_password' => 'spec432-certificate-password',
],
]);
$executor = spec432FakeExecutor([
ExchangePowerShellProcessResult::completed(0, "7.4.0\n"),
ExchangePowerShellProcessResult::completed(0, "ExchangeOnlineManagement\n"),
ExchangePowerShellProcessResult::completed(0, '[{"id":"rule-1","password":"raw-output-secret"}]', durationMs: 14),
]);
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
$persisted = json_encode([
'context' => $run->context,
'failure_summary' => $run->failure_summary,
'summary_counts' => $run->summary_counts,
], JSON_THROW_ON_ERROR);
expect($executor->calls)->toHaveCount(3)
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
->and($run->summary_counts)->toMatchArray([
'total' => 1,
'processed' => 1,
'succeeded' => 1,
'failed' => 0,
'skipped' => 0,
'items' => 1,
])
->and(data_get($run->context, 'runner_result.runner_mode'))->toBe('production')
->and(data_get($run->context, 'runner_result.runtime_state'))->toBe('ready')
->and(data_get($run->context, 'runner_result.credential_state'))->toBe('certificate_supported')
->and(data_get($run->context, 'runner_result.permission_evidence_state'))->toBe('verified')
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0)
->and($persisted)
->not->toContain('raw-output-secret')
->not->toContain('spec432-private-key-material')
->not->toContain('spec432-certificate-password')
->not->toContain('raw_shell_stdout')
->not->toContain('raw_shell_stderr');
$workspaceLock = Cache::lock('exchange-powershell:workspace:'.(int) $environment->workspace_id, 10);
$providerLock = Cache::lock('exchange-powershell:provider:'.(int) $connection->getKey(), 10);
expect($workspaceLock->get())->toBeTrue()
->and($providerLock->get())->toBeTrue();
$providerLock->release();
$workspaceLock->release();
});
it('Spec432 concurrency lock blocks conflicting production execution before process command execution and releases owned locks', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, ProviderCredentialKind::Certificate->value);
$executor = spec432FakeExecutor([
ExchangePowerShellProcessResult::completed(0, "7.4.0\n"),
ExchangePowerShellProcessResult::completed(0, "ExchangeOnlineManagement\n"),
]);
$workspaceLock = Cache::lock('exchange-powershell:workspace:'.(int) $environment->workspace_id, 300);
expect($workspaceLock->get())->toBeTrue();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
expect($executor->calls)->toHaveCount(2)
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('execution_blocked_concurrency_workspace')
->and(data_get($run->context, 'runner_result.concurrency_state'))->toBe('workspace_busy');
$workspaceLock->release();
});
it('Spec432 provider concurrency lock blocks conflicting production execution and releases the workspace lock', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, ProviderCredentialKind::Certificate->value);
$executor = spec432FakeExecutor([
ExchangePowerShellProcessResult::completed(0, "7.4.0\n"),
ExchangePowerShellProcessResult::completed(0, "ExchangeOnlineManagement\n"),
]);
$providerLock = Cache::lock('exchange-powershell:provider:'.(int) $connection->getKey(), 300);
$workspaceProbe = Cache::lock('exchange-powershell:workspace:'.(int) $environment->workspace_id, 10);
expect($providerLock->get())->toBeTrue();
try {
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
$workspaceReleased = $workspaceProbe->get();
} finally {
if (isset($workspaceReleased) && $workspaceReleased) {
$workspaceProbe->release();
}
$providerLock->release();
}
expect($executor->calls)->toHaveCount(2)
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('execution_blocked_concurrency_provider')
->and(data_get($run->context, 'runner_result.concurrency_state'))->toBe('provider_busy')
->and($workspaceReleased)->toBeTrue();
});
it('Spec432 releases production locks after timeout and executor exception paths', function (
ExchangePowerShellProcessResult $processResult,
string $expectedFailureCode,
): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec432ProviderConnection($environment);
spec432Credential($connection, ProviderCredentialKind::Certificate->value);
spec432FakeExecutor([
ExchangePowerShellProcessResult::completed(0, "7.4.0\n"),
ExchangePowerShellProcessResult::completed(0, "ExchangeOnlineManagement\n"),
$processResult,
]);
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
);
$workspaceLock = Cache::lock('exchange-powershell:workspace:'.(int) $environment->workspace_id, 10);
$providerLock = Cache::lock('exchange-powershell:provider:'.(int) $connection->getKey(), 10);
expect($run->outcome)->toBe(OperationRunOutcome::Failed->value)
->and(data_get($run->context, 'failure_code'))->toBe($expectedFailureCode)
->and($workspaceLock->get())->toBeTrue()
->and($providerLock->get())->toBeTrue();
$providerLock->release();
$workspaceLock->release();
})->with([
'timeout' => [ExchangePowerShellProcessResult::timedOut(60000), 'execution_failed_timeout'],
'executor exception result' => [ExchangePowerShellProcessResult::exception('RuntimeException'), 'execution_failed_unexpected_exception'],
]);
it('Spec432 preserves provider scope and RBAC boundaries before production execution', function (): void {
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
]);
[$readonlyUser, $environment] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
$connection = spec432ProviderConnection($environment);
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $readonlyUser,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
))->toThrow(AuthorizationException::class)
->and(OperationRun::query()->count())->toBe(0);
[$owner, $ownerEnvironment] = createMinimalUserWithTenant(role: 'owner');
$otherEnvironment = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $ownerEnvironment->workspace_id,
]);
$otherConnection = spec432ProviderConnection($otherEnvironment);
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $ownerEnvironment,
providerConnection: $otherConnection,
actor: $owner,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
))->toThrow(NotFoundHttpException::class);
$outsiderEnvironment = ManagedEnvironment::factory()->create();
$outsiderConnection = spec432ProviderConnection($outsiderEnvironment);
ManagedEnvironmentMembership::query()
->where('managed_environment_id', (int) $outsiderEnvironment->getKey())
->where('user_id', (int) $owner->getKey())
->delete();
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $outsiderEnvironment,
providerConnection: $outsiderConnection,
actor: $owner,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
))->toThrow(NotFoundHttpException::class);
});
it('Spec432 introduces no UI routes jobs migrations tenant id or trigger surface', function (): void {
$runtimeFiles = [
app_path('Services/TenantConfiguration/ExchangePowerShellProductionRunner.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellRuntimeReadinessChecker.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellCredentialReferenceResolver.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellPermissionEvidenceEvaluator.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellProcessCommandBuilder.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellOutputGuard.php'),
];
$runtimeSource = collect($runtimeFiles)
->map(fn (string $file): string => file_get_contents($file) ?: '')
->implode("\n");
expect($runtimeSource)
->not->toContain('tenant_id')
->not->toContain('Start-')
->not->toContain('Set-')
->not->toContain('New-')
->not->toContain('Remove-')
->and(glob(app_path('Filament/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(base_path('routes/*exchange*')) ?: [])->toBe([])
->and(glob(resource_path('views/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*exchange_powershell*')) ?: [])->toBe([])
->and(File::exists(app_path('Jobs/TenantConfiguration/InvokeExchangePowerShell.php')))->toBeFalse();
});
function spec432FakeExecutor(array $results = []): FakeExchangePowerShellProcessExecutor
{
$executor = new FakeExchangePowerShellProcessExecutor(results: $results);
app()->instance(ExchangePowerShellProcessExecutor::class, $executor);
app()->forgetInstance(ExchangePowerShellCommandRunner::class);
return $executor;
}
/**
* @param array<string, mixed> $overrides
*/
function spec432ProviderConnection(
ManagedEnvironment $environment,
array $overrides = [],
bool $seedExchangePowerShellPermission = true,
): ProviderConnection {
$connection = ProviderConnection::factory()
->dedicated()
->consentGranted()
->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'entra_tenant_id' => $environment->providerTenantContext(),
'is_default' => true,
'verification_status' => ProviderVerificationStatus::Healthy->value,
'last_health_check_at' => now(),
...$overrides,
]);
if ($seedExchangePowerShellPermission) {
spec432SeedExchangePowerShellPermission($environment, $connection);
}
return $connection;
}
function spec432SeedExchangePowerShellPermission(ManagedEnvironment $environment, ProviderConnection $connection, string $status = 'granted'): void
{
ManagedEnvironmentPermission::query()->updateOrCreate(
[
'managed_environment_id' => (int) $environment->getKey(),
'permission_key' => 'Exchange.ManageAsApp',
'workspace_id' => (int) $environment->workspace_id,
],
[
'status' => $status,
'details' => [
'source' => 'spec432-test',
'workspace_id' => (int) $connection->workspace_id,
'managed_environment_id' => (int) $connection->managed_environment_id,
'provider' => (string) $connection->provider,
'provider_connection_id' => (int) $connection->getKey(),
],
'last_checked_at' => now(),
],
);
}
/**
* @param array<string, mixed> $overrides
*/
function spec432Credential(ProviderConnection $connection, string $kind, array $overrides = []): ProviderCredential
{
$storedKind = in_array($kind, ProviderCredentialKind::values(), true)
? $kind
: ProviderCredentialKind::ClientSecret->value;
$credential = ProviderCredential::factory()->create([
'provider_connection_id' => (int) $connection->getKey(),
'type' => $storedKind,
'credential_kind' => $storedKind,
'source' => ProviderCredentialSource::DedicatedManual->value,
'last_rotated_at' => now(),
'expires_at' => now()->addYear(),
...$overrides,
]);
if ($storedKind !== $kind) {
DB::table('provider_credentials')
->where('id', (int) $credential->getKey())
->update([
'type' => $kind,
'credential_kind' => $kind,
]);
}
return $credential;
}