493 lines
19 KiB
PHP
493 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPermission;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Verification\VerificationReportWriter;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
pest()->browser()->timeout(10_000);
|
|
|
|
it('keeps stale verification warnings and the selected provider connection stable after refresh', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => '30303030-3030-3030-3030-303030303030',
|
|
'name' => 'Stale Verification Tenant',
|
|
'status' => Tenant::STATUS_ONBOARDING,
|
|
]);
|
|
$user = User::factory()->create(['name' => 'Verification Owner']);
|
|
|
|
createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$verifiedConnection = ProviderConnection::factory()->platform()->consentGranted()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Previously verified connection',
|
|
'is_default' => true,
|
|
'status' => 'connected',
|
|
]);
|
|
|
|
$selectedConnection = ProviderConnection::factory()->platform()->consentGranted()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'dummy',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Current selected connection',
|
|
'is_default' => false,
|
|
'status' => 'connected',
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $verifiedConnection->getKey(),
|
|
'target_scope' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
],
|
|
],
|
|
]);
|
|
|
|
$draft = createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $tenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'verify',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'tenant_name' => (string) $tenant->name,
|
|
'provider_connection_id' => (int) $selectedConnection->getKey(),
|
|
'verification_operation_run_id' => (int) $run->getKey(),
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
|
]);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$visibleSelectValue = <<<'JS'
|
|
(() => {
|
|
const select = [...document.querySelectorAll('select')].find((element) => {
|
|
const style = window.getComputedStyle(element);
|
|
|
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
});
|
|
|
|
return select?.value ?? null;
|
|
})()
|
|
JS;
|
|
|
|
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
|
|
|
|
$page
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->assertSee('Verify access')
|
|
->assertSee('Status: Needs attention')
|
|
->assertSee('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.')
|
|
->assertSee('Start verification')
|
|
->refresh()
|
|
->waitForText('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.')
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->assertSee('Status: Needs attention')
|
|
->assertSee('Start verification')
|
|
->click('Provider connection')
|
|
->assertScript($visibleSelectValue, (string) $selectedConnection->getKey());
|
|
});
|
|
|
|
it('preserves bootstrap revisit state and blocked activation guards after refresh', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$tenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => '40404040-4040-4040-4040-404040404040',
|
|
'name' => 'Blocked Review Tenant',
|
|
'status' => Tenant::STATUS_ONBOARDING,
|
|
]);
|
|
$user = User::factory()->create(['name' => 'Review Owner']);
|
|
|
|
createUserWithTenant(
|
|
tenant: $tenant,
|
|
user: $user,
|
|
role: 'owner',
|
|
workspaceRole: 'owner',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Blocked review connection',
|
|
'is_default' => true,
|
|
'status' => 'connected',
|
|
]);
|
|
|
|
$verificationRun = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'target_scope' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'entra_tenant_name' => (string) $tenant->name,
|
|
],
|
|
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
|
|
[
|
|
'key' => 'permissions.admin_consent',
|
|
'title' => 'Required application permissions',
|
|
'status' => 'fail',
|
|
'severity' => 'critical',
|
|
'blocking' => true,
|
|
'reason_code' => 'permission_denied',
|
|
'message' => 'Missing required Graph permissions.',
|
|
'evidence' => [],
|
|
'next_steps' => [],
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
|
|
$bootstrapRun = createInventorySyncOperationRun($tenant, [
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'status' => 'success',
|
|
]);
|
|
|
|
$draft = createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $tenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'complete',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'tenant_name' => (string) $tenant->name,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'verification_operation_run_id' => (int) $verificationRun->getKey(),
|
|
'bootstrap_operation_types' => ['inventory_sync'],
|
|
'bootstrap_operation_runs' => [
|
|
'inventory_sync' => (int) $bootstrapRun->getKey(),
|
|
],
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
|
]);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
|
|
|
|
$activateButtonIsDisabled = <<<'JS'
|
|
(() => {
|
|
const button = [...document.querySelectorAll('button')].find((element) => element.textContent?.includes('Activate tenant'));
|
|
|
|
return button?.disabled ?? null;
|
|
})()
|
|
JS;
|
|
|
|
$openBootstrapStep = <<<'JS'
|
|
(() => {
|
|
const button = [...document.querySelectorAll('button')].find((element) => element.textContent?.includes('Bootstrap'));
|
|
|
|
if (! button) {
|
|
return false;
|
|
}
|
|
|
|
button.click();
|
|
|
|
return true;
|
|
})()
|
|
JS;
|
|
|
|
$page
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->assertSee('Complete')
|
|
->assertSee('Override blocked verification')
|
|
->assertSee('Blocked — 0/1 checks passed')
|
|
->assertSee('Started - 1 operation run(s) started')
|
|
->assertScript($activateButtonIsDisabled, true)
|
|
->refresh()
|
|
->waitForText('Override blocked verification')
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->assertSee('Blocked — 0/1 checks passed')
|
|
->assertSee('Started - 1 operation run(s) started')
|
|
->assertScript($activateButtonIsDisabled, true)
|
|
->assertScript($openBootstrapStep, true)
|
|
->assertSee('Started 1 bootstrap run(s).');
|
|
});
|
|
|
|
it('opens the full-page permissions deep dive in a new tab without replacing onboarding', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(
|
|
role: 'owner',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$workspace = $tenant->workspace()->firstOrFail();
|
|
|
|
$configured = array_merge(
|
|
config('intune_permissions.permissions', []),
|
|
config('entra_permissions.permissions', []),
|
|
);
|
|
|
|
$permissionKeys = array_values(array_filter(array_map(static function (mixed $permission): ?string {
|
|
if (! is_array($permission)) {
|
|
return null;
|
|
}
|
|
|
|
$key = $permission['key'] ?? null;
|
|
|
|
return is_string($key) && trim($key) !== '' ? trim($key) : null;
|
|
}, $configured)));
|
|
|
|
if ($permissionKeys === []) {
|
|
test()->markTestSkipped('No configured required permissions found.');
|
|
}
|
|
|
|
$missingKey = $permissionKeys[0];
|
|
|
|
foreach (array_slice($permissionKeys, 1) as $key) {
|
|
TenantPermission::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'permission_key' => $key,
|
|
'status' => 'granted',
|
|
'details' => ['source' => 'db'],
|
|
'last_checked_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Browser assist connection',
|
|
'is_default' => true,
|
|
'status' => 'connected',
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Blocked->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'target_scope' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'entra_tenant_name' => (string) $tenant->name,
|
|
],
|
|
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
|
|
[
|
|
'key' => 'permissions.admin_consent',
|
|
'title' => 'Required application permissions',
|
|
'status' => 'fail',
|
|
'severity' => 'critical',
|
|
'blocking' => true,
|
|
'reason_code' => 'provider_permission_missing',
|
|
'message' => "Missing required application permission: {$missingKey}",
|
|
'evidence' => [],
|
|
'next_steps' => [],
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
|
|
$draft = createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $tenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'verify',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'tenant_name' => (string) $tenant->name,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'verification_operation_run_id' => (int) $run->getKey(),
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
|
]);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
|
|
|
|
$page
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->click('Verify access')
|
|
->waitForText('View required permissions')
|
|
->click('View required permissions')
|
|
->waitForText('Open full page');
|
|
|
|
$page->script(<<<'JS'
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: {
|
|
writeText: async () => Promise.resolve(),
|
|
},
|
|
});
|
|
|
|
document.querySelector('[data-testid="verification-assist-copy-application"]')?.click();
|
|
JS);
|
|
|
|
$page
|
|
->waitForText('Copied')
|
|
->assertAttribute('[data-testid="verification-assist-full-page"]', 'target', '_blank')
|
|
->assertAttribute('[data-testid="verification-assist-full-page"]', 'rel', 'noopener noreferrer')
|
|
->click('Open full page')
|
|
->wait(1)
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->assertSee('Open full page')
|
|
->click('Close')
|
|
->click('Provider connection')
|
|
->assertSee('Select an existing connection or create a new one.');
|
|
});
|
|
|
|
it('opens the permissions assist from report remediation steps without leaving onboarding', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(
|
|
role: 'owner',
|
|
ensureDefaultMicrosoftProviderConnection: false,
|
|
);
|
|
|
|
$workspace = $tenant->workspace()->firstOrFail();
|
|
|
|
$configured = array_merge(
|
|
config('intune_permissions.permissions', []),
|
|
config('entra_permissions.permissions', []),
|
|
);
|
|
|
|
$permissionKeys = array_values(array_filter(array_map(static function (mixed $permission): ?string {
|
|
if (! is_array($permission)) {
|
|
return null;
|
|
}
|
|
|
|
$key = $permission['key'] ?? null;
|
|
|
|
return is_string($key) && trim($key) !== '' ? trim($key) : null;
|
|
}, $configured)));
|
|
|
|
if ($permissionKeys === []) {
|
|
test()->markTestSkipped('No configured required permissions found.');
|
|
}
|
|
|
|
foreach (array_slice($permissionKeys, 1) as $key) {
|
|
TenantPermission::query()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'permission_key' => $key,
|
|
'status' => 'granted',
|
|
'details' => ['source' => 'db'],
|
|
'last_checked_at' => now(),
|
|
]);
|
|
}
|
|
|
|
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'display_name' => 'Browser next-step connection',
|
|
'is_default' => true,
|
|
'status' => 'connected',
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Blocked->value,
|
|
'context' => [
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'target_scope' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'entra_tenant_name' => (string) $tenant->name,
|
|
],
|
|
'verification_report' => VerificationReportWriter::build('provider.connection.check', [
|
|
[
|
|
'key' => 'provider.connection.preflight',
|
|
'title' => 'Provider connection preflight',
|
|
'status' => 'fail',
|
|
'severity' => 'critical',
|
|
'blocking' => true,
|
|
'reason_code' => 'provider_permission_missing',
|
|
'message' => 'Provider connection requires admin consent before use.',
|
|
'evidence' => [],
|
|
'next_steps' => [
|
|
[
|
|
'label' => 'Grant admin consent',
|
|
'url' => 'https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/grant-admin-consent',
|
|
],
|
|
[
|
|
'label' => 'Review platform connection',
|
|
'url' => route('filament.admin.resources.provider-connections.edit', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()]),
|
|
],
|
|
],
|
|
],
|
|
]),
|
|
],
|
|
]);
|
|
|
|
$draft = createOnboardingDraft([
|
|
'workspace' => $workspace,
|
|
'tenant' => $tenant,
|
|
'started_by' => $user,
|
|
'updated_by' => $user,
|
|
'current_step' => 'verify',
|
|
'state' => [
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
'tenant_name' => (string) $tenant->name,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'verification_operation_run_id' => (int) $run->getKey(),
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)->withSession([
|
|
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
|
]);
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]))
|
|
->assertNoJavaScriptErrors()
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->click('Verify access')
|
|
->waitForText('Grant admin consent')
|
|
->click('Grant admin consent')
|
|
->waitForText('Required permissions assist')
|
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
|
->assertSee('Open full page')
|
|
->assertSee('Review platform connection');
|
|
});
|