TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php
2026-07-07 15:54:34 +02:00

616 lines
27 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\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Models\User;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\Providers\ProviderOperationStartGate;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContract;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
use App\Services\TenantConfiguration\ExchangePowerShellCommandRunner;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationContext;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationGate;
use App\Services\TenantConfiguration\FakeExchangePowerShellCommandRunner;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Providers\ProviderVerificationStatus;
use App\Support\Verification\ManagedEnvironmentPermissionCheckClusters;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\File;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
beforeEach(function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => false]);
});
it('Spec431 fake runner invocation creates a completed count-only OperationRun before runner execution', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->calls)->toHaveCount(1)
->and($runner->calls[0]['operation_run_id'])->toBe((int) $run->getKey())
->and($run->type)->toBe(ExchangePowerShellInvocationGate::OPERATION_TYPE)
->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, 'provider_connection_id'))->toBe((int) $connection->getKey())
->and(data_get($run->context, 'provider_identity_resolution_mode'))->toBe('fake_runner_bypass')
->and(data_get($run->context, 'source_contract.source_contract_state'))->toBe('contract_verified_pending_capture')
->and(data_get($run->context, 'source_contract.provider_calls_allowed'))->toBeFalse()
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
$contextJson = json_encode($run->context, JSON_THROW_ON_ERROR);
expect($contextJson)
->not->toContain('raw_provider_payload')
->not->toContain('raw_shell_stdout')
->not->toContain('client_secret')
->not->toContain('access_token');
});
it('Spec431 default feature gate blocks before fake runner execution', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'remoteDomain',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->calls)->toBe([])
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('execution_blocked_feature_disabled')
->and(data_get($run->context, 'feature_gate.enabled'))->toBeFalse();
});
it('Spec431 denies in-scope users missing provider-run capability as forbidden without OperationRun', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
))->toThrow(AuthorizationException::class);
expect($runner->calls)->toBe([])
->and(OperationRun::query()->count())->toBe(0);
});
it('Spec431 hides environments outside workspace membership without OperationRun', function (): void {
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$outsider = User::factory()->create();
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $outsider,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
))->toThrow(NotFoundHttpException::class);
expect($runner->calls)->toBe([])
->and(OperationRun::query()->count())->toBe(0);
});
it('Spec431 hides environments outside explicit managed-environment entitlement scope', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$allowedEnvironment = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
]);
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
ManagedEnvironmentMembership::query()
->where('managed_environment_id', (int) $environment->getKey())
->where('user_id', (int) $user->getKey())
->delete();
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedEnvironment->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
'source' => 'manual',
]);
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
))->toThrow(NotFoundHttpException::class);
expect($runner->calls)->toBe([])
->and(OperationRun::query()->count())->toBe(0);
});
it('Spec431 rejects provider connections outside the environment scope before runner execution', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$otherEnvironment = ManagedEnvironment::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
]);
$connection = spec431ProviderConnection($otherEnvironment);
$runner = spec431FakeRunner();
expect(fn () => app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
))->toThrow(NotFoundHttpException::class);
expect($runner->calls)->toBe([])
->and(OperationRun::query()->count())->toBe(0);
});
it('Spec431 provider readiness blocks missing admin consent before runner execution', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment, [
'consent_status' => ProviderConsentStatus::Required->value,
'consent_granted_at' => null,
]);
$runner = spec431FakeRunner();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'reason_code'))->toBe(ProviderReasonCodes::ProviderConsentMissing);
});
it('Spec431 provider readiness blocks missing Exchange permission evidence before fake runner execution', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment, seedExchangePowerShellPermission: false);
$runner = spec431FakeRunner();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->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.status'))->toBe('unknown')
->and(data_get($run->context, 'provider_capability.missing_requirement_keys'))->toContain('provider.exchange_powershell_invocation');
});
it('Spec431 non-fake mode requires existing provider identity resolution before runner execution', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_DISABLED,
);
expect($runner->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'reason_code'))->toBeIn([
ProviderReasonCodes::DedicatedCredentialMissing,
ProviderReasonCodes::ProviderCredentialMissing,
]);
});
it('Spec431 rejects invalid contracts and raw mutation commands without runner execution', function (string $canonicalType, ?string $commandName, string $expectedFailure): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: $canonicalType,
commandName: $commandName,
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe($expectedFailure);
$contextJson = json_encode($run->context, JSON_THROW_ON_ERROR);
if (is_string($commandName) && str_starts_with($commandName, 'Set-')) {
expect($contextJson)->not->toContain($commandName);
}
if (str_contains($canonicalType, 'client_secret')) {
expect($contextJson)->not->toContain($canonicalType);
}
})->with([
'unsupported target' => ['acceptedDomain', null, 'source_contract_missing'],
'unsafe target' => ['client_secret_super_token', null, 'source_contract_missing'],
'mutation command' => ['transportRule', 'Set-TransportRule', 'command_contract_rejected'],
'wrong allowlisted command' => ['transportRule', 'Get-RemoteDomain', 'command_contract_rejected'],
]);
it('Spec431 rejects unknown command parameters before runner execution', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner();
$secretParameterName = 'client_secret_super_token';
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'transportRule',
parameters: [$secretParameterName => 'unexpected'],
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->calls)->toBe([])
->and($run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($run->context, 'failure_code'))->toBe('command_contract_rejected')
->and(data_get($run->context, 'command_validation.reason_code'))->toBe('unknown_parameter_rejected')
->and(data_get($run->context, 'command_validation.rejected_parameter_present'))->toBeTrue()
->and(data_get($run->context, 'command_validation.rejected_parameter'))->toBeNull()
->and(json_encode($run->context, JSON_THROW_ON_ERROR))
->not->toContain($secretParameterName)
->not->toContain('unexpected');
});
it('Spec431 malformed fake-runner output fails safely without raw output promotion', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner(FakeExchangePowerShellCommandRunner::SCENARIO_MALFORMED_TEXT);
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'inboundConnector',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
expect($runner->calls)->toHaveCount(1)
->and($run->outcome)->toBe(OperationRunOutcome::Failed->value)
->and(data_get($run->context, 'failure_code'))->toBe('execution_failed_shape_unsafe')
->and(data_get($run->context, 'response_shape.safe'))->toBeFalse()
->and(json_encode($run->context, JSON_THROW_ON_ERROR))->not->toContain('raw transcript rejected');
});
it('Spec431 fake-runner scenarios persist only sanitized lifecycle outcomes', function (
string $scenario,
string $expectedOutcome,
?string $expectedFailureCode,
?string $expectedReasonCode,
array $expectedSummaryCounts,
): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$runner = spec431FakeRunner($scenario);
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
tenant: $environment,
providerConnection: $connection,
actor: $user,
canonicalType: 'remoteDomain',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
);
$failureSummary = is_array($run->failure_summary) ? $run->failure_summary : [];
$reasonCode = data_get($run->context, 'reason_code') ?? data_get($failureSummary, '0.reason_code');
$persistedJson = json_encode([
'context' => $run->context,
'failure_summary' => $failureSummary,
'summary_counts' => $run->summary_counts,
], JSON_THROW_ON_ERROR);
expect($runner->calls)->toHaveCount(1)
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe($expectedOutcome)
->and($run->summary_counts)->toMatchArray($expectedSummaryCounts)
->and(array_keys($run->summary_counts ?? []))->each->toBeIn(['total', 'processed', 'succeeded', 'failed', 'skipped', 'items'])
->and(data_get($run->context, 'failure_code'))->toBe($expectedFailureCode)
->and($reasonCode)->toBe($expectedReasonCode)
->and($persistedJson)
->not->toContain('raw transcript rejected')
->not->toContain('Simulated runner exception')
->not->toContain('access_token')
->not->toContain('client_secret')
->not->toContain('refresh_token')
->not->toContain('password=');
})->with([
'empty success' => [
FakeExchangePowerShellCommandRunner::SCENARIO_EMPTY_SUCCESS,
OperationRunOutcome::Succeeded->value,
null,
null,
['total' => 0, 'processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 0, 'items' => 0],
],
'authentication failure' => [
FakeExchangePowerShellCommandRunner::SCENARIO_AUTH_FAILURE,
OperationRunOutcome::Failed->value,
'execution_failed_authentication',
ProviderReasonCodes::ProviderAuthFailed,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
'authorization failure' => [
FakeExchangePowerShellCommandRunner::SCENARIO_AUTHORIZATION_FAILURE,
OperationRunOutcome::Failed->value,
'execution_failed_authorization',
ProviderReasonCodes::ProviderPermissionDenied,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
'timeout' => [
FakeExchangePowerShellCommandRunner::SCENARIO_TIMEOUT,
OperationRunOutcome::Failed->value,
'execution_failed_timeout',
ProviderReasonCodes::NetworkUnreachable,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
'throttled' => [
FakeExchangePowerShellCommandRunner::SCENARIO_THROTTLED,
OperationRunOutcome::Failed->value,
'execution_failed_throttled',
ProviderReasonCodes::RateLimited,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
'module unavailable' => [
FakeExchangePowerShellCommandRunner::SCENARIO_MODULE_UNAVAILABLE,
OperationRunOutcome::Blocked->value,
'execution_blocked_module_unavailable',
ProviderReasonCodes::ProviderBindingUnsupported,
['total' => 0, 'processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 0, 'items' => 0],
],
'command unavailable' => [
FakeExchangePowerShellCommandRunner::SCENARIO_COMMAND_UNAVAILABLE,
OperationRunOutcome::Blocked->value,
'execution_blocked_command_unavailable',
ProviderReasonCodes::ProviderBindingUnsupported,
['total' => 0, 'processed' => 0, 'succeeded' => 0, 'failed' => 0, 'skipped' => 0, 'items' => 0],
],
'malformed scalar' => [
FakeExchangePowerShellCommandRunner::SCENARIO_MALFORMED_SCALAR,
OperationRunOutcome::Failed->value,
'execution_failed_shape_unsafe',
ProviderReasonCodes::ProviderConnectionInvalid,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
'malformed text' => [
FakeExchangePowerShellCommandRunner::SCENARIO_MALFORMED_TEXT,
OperationRunOutcome::Failed->value,
'execution_failed_shape_unsafe',
ProviderReasonCodes::ProviderConnectionInvalid,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
'unexpected exception' => [
FakeExchangePowerShellCommandRunner::SCENARIO_UNEXPECTED_EXCEPTION,
OperationRunOutcome::Failed->value,
'execution_failed_unexpected_exception',
ProviderReasonCodes::UnknownError,
['total' => 1, 'processed' => 1, 'succeeded' => 0, 'failed' => 1, 'skipped' => 0, 'items' => 0],
],
]);
it('Spec431 direct provider start-gate invocation is blocked as a bypass path', function (): void {
config([ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true]);
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec431ProviderConnection($environment);
$dispatched = 0;
$result = app(ProviderOperationStartGate::class)->start(
tenant: $environment,
connection: $connection,
operationType: ExchangePowerShellInvocationGate::OPERATION_TYPE,
dispatcher: function () use (&$dispatched): void {
$dispatched++;
},
extraContext: [
'invocation_gate' => 'exchange_powershell_invocation_gate',
'parameter_names' => ['client_secret_super_token'],
],
);
expect($dispatched)->toBe(0)
->and($result->status)->toBe('blocked')
->and($result->run->outcome)->toBe(OperationRunOutcome::Blocked->value)
->and(data_get($result->run->context, 'reason_code_extension'))->toBe('ext.exchange_powershell_invocation_gate_required')
->and(json_encode($result->run->context, JSON_THROW_ON_ERROR))->not->toContain('client_secret_super_token')
->and(file_get_contents(app_path('Providers/AppServiceProvider.php')) ?: '')->not->toContain('Closure::bind')
->and(file_get_contents(app_path('Services/Providers/ProviderOperationStartGate.php')) ?: '')->not->toContain('startTrusted');
});
it('Spec431 direct runner calls reject raw command strings arrays and unverified parameters by type boundary', function (): void {
$runner = new FakeExchangePowerShellCommandRunner;
$context = new ExchangePowerShellInvocationContext(
operationRunId: 431,
workspaceId: 1,
managedEnvironmentId: 1,
providerConnectionId: 1,
canonicalType: 'transportRule',
commandName: 'Get-TransportRule',
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_FAKE,
redactionPolicy: ExchangePowerShellInvocationGate::REDACTION_POLICY,
sourceContractState: 'contract_verified_pending_capture',
);
$contracts = app(ExchangePowerShellCommandContracts::class);
$contract = $contracts->contractForCanonicalType('transportRule');
expect($contract)->toBeArray();
$verifiedContract = ExchangePowerShellCommandContract::fromVerifiedArray($contract, $contracts, []);
$mutatedContract = [
...$contract,
'allowed_parameters' => ['client_secret_super_token'],
];
expect(fn () => $runner->run('Get-TransportRule', $context))->toThrow(TypeError::class)
->and(fn () => $runner->run(['command_name' => 'Get-TransportRule'], $context))->toThrow(TypeError::class)
->and(fn () => ExchangePowerShellCommandContract::fromVerifiedArray(
$contract,
$contracts,
['client_secret_super_token' => 'unexpected'],
))->toThrow(InvalidArgumentException::class)
->and(fn () => ExchangePowerShellCommandContract::fromVerifiedArray(
$mutatedContract,
$contracts,
['client_secret_super_token' => 'unexpected'],
))->toThrow(InvalidArgumentException::class);
$runner->run($verifiedContract, $context);
expect($runner->calls[0]['parameter_names'])->toBe([]);
});
it('Spec431 introduces no rendered UI surface routes jobs migrations or evidence promotion', function (): void {
$runtimeFiles = [
app_path('Services/TenantConfiguration/ExchangePowerShellInvocationGate.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellCommandRunner.php'),
app_path('Services/TenantConfiguration/FakeExchangePowerShellCommandRunner.php'),
app_path('Services/TenantConfiguration/DisabledExchangePowerShellCommandRunner.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')
->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()
->and(ManagedEnvironmentPermissionCheckClusters::definitionForKey('provider.exchange_powershell_invocation'))->toBeNull();
});
/**
* @param array<string, mixed> $overrides
*/
function spec431ProviderConnection(
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) {
spec431SeedExchangePowerShellPermission($environment, $connection);
}
return $connection;
}
function spec431SeedExchangePowerShellPermission(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' => 'spec431-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(),
],
);
}
function spec431FakeRunner(string $scenario = FakeExchangePowerShellCommandRunner::SCENARIO_SUCCESS): FakeExchangePowerShellCommandRunner
{
$runner = new FakeExchangePowerShellCommandRunner($scenario);
app()->instance(ExchangePowerShellCommandRunner::class, $runner);
return $runner;
}