|
|
|
|
@ -2,89 +2,86 @@
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
use App\Filament\Pages\TenantDashboard;
|
|
|
|
|
use App\Models\AuditLog;
|
|
|
|
|
use App\Models\OperationRun;
|
|
|
|
|
use App\Models\ProviderConnection;
|
|
|
|
|
use App\Models\ProviderCredential;
|
|
|
|
|
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
use App\Models\TenantOnboardingSession;
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
use App\Models\Workspace;
|
|
|
|
|
use App\Models\WorkspaceMembership;
|
|
|
|
|
use App\Services\Auth\TenantMembershipManager;
|
|
|
|
|
use App\Services\Auth\WorkspaceRoleCapabilityMap;
|
|
|
|
|
use App\Support\Auth\Capabilities;
|
|
|
|
|
use Illuminate\Support\Facades\Bus;
|
|
|
|
|
use Illuminate\Support\Facades\Gate;
|
|
|
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
|
use Livewire\Livewire;
|
|
|
|
|
|
|
|
|
|
it('returns 404 for non-members when starting onboarding', function (): void {
|
|
|
|
|
it('returns 404 for non-members when starting onboarding with a selected workspace', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user)
|
|
|
|
|
->get("/admin/w/{$workspace->getKey()}/managed-tenants/onboarding")
|
|
|
|
|
->get('/admin/onboarding')
|
|
|
|
|
->assertNotFound();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns 403 for workspace members without onboarding capability', function (): void {
|
|
|
|
|
it('allows workspace members without onboarding capability to view the wizard but forbids execution', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'role' => 'readonly',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user)
|
|
|
|
|
->get("/admin/w/{$workspace->getKey()}/managed-tenants/onboarding")
|
|
|
|
|
->assertForbidden();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders onboarding wizard for workspace owners', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user)
|
|
|
|
|
->get("/admin/w/{$workspace->getKey()}/managed-tenants/onboarding")
|
|
|
|
|
->get('/admin/onboarding')
|
|
|
|
|
->assertSuccessful();
|
|
|
|
|
|
|
|
|
|
Livewire::actingAs($user)
|
|
|
|
|
->test(ManagedTenantOnboardingWizard::class)
|
|
|
|
|
->call('identifyManagedTenant', [
|
|
|
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
|
|
|
'environment' => 'prod',
|
|
|
|
|
'name' => 'Acme',
|
|
|
|
|
])
|
|
|
|
|
->assertStatus(403);
|
|
|
|
|
|
|
|
|
|
expect(Tenant::query()->count())->toBe(0);
|
|
|
|
|
expect(TenantOnboardingSession::query()->count())->toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows owners to identify a managed tenant and creates a pending tenant + session', function (): void {
|
|
|
|
|
it('renders onboarding wizard for workspace owners and allows identifying a managed tenant', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'user_id' => (int) $user->getKey(),
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user);
|
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
|
$this->actingAs($user)
|
|
|
|
|
->get('/admin/onboarding')
|
|
|
|
|
->assertSuccessful();
|
|
|
|
|
|
|
|
|
|
$tenantGuid = '11111111-1111-1111-1111-111111111111';
|
|
|
|
|
$entraTenantId = '22222222-2222-2222-2222-222222222222';
|
|
|
|
|
|
|
|
|
|
Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace])
|
|
|
|
|
->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
|
|
|
|
|
Livewire::actingAs($user)
|
|
|
|
|
->test(ManagedTenantOnboardingWizard::class)
|
|
|
|
|
->call('identifyManagedTenant', [
|
|
|
|
|
'entra_tenant_id' => $entraTenantId,
|
|
|
|
|
'environment' => 'prod',
|
|
|
|
|
'name' => 'Acme',
|
|
|
|
|
'primary_domain' => 'acme.example',
|
|
|
|
|
'notes' => 'Initial onboarding',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
|
|
|
|
|
$tenant = Tenant::query()->where('tenant_id', $entraTenantId)->firstOrFail();
|
|
|
|
|
|
|
|
|
|
expect((int) $tenant->workspace_id)->toBe((int) $workspace->getKey());
|
|
|
|
|
expect($tenant->status)->toBe('pending');
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('managed_tenant_onboarding_sessions', [
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
|
|
|
'current_step' => 'identify',
|
|
|
|
|
]);
|
|
|
|
|
expect($tenant->status)->toBe(Tenant::STATUS_ONBOARDING);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('tenant_memberships', [
|
|
|
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
|
|
|
@ -92,134 +89,86 @@
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
(int) \App\Models\TenantMembership::query()
|
|
|
|
|
->where('tenant_id', $tenant->getKey())
|
|
|
|
|
->where('role', 'owner')
|
|
|
|
|
->count()
|
|
|
|
|
)->toBe(1);
|
|
|
|
|
$this->assertDatabaseHas('managed_tenant_onboarding_sessions', [
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
|
|
|
'entra_tenant_id' => $entraTenantId,
|
|
|
|
|
'current_step' => 'identify',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('upgrades the initiating user to owner if they already have a lower tenant role', function (): void {
|
|
|
|
|
it('is idempotent when identifying the same Entra tenant ID twice', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'user_id' => (int) $user->getKey(),
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$tenantGuid = '66666666-6666-6666-6666-666666666666';
|
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'tenant_id' => $tenantGuid,
|
|
|
|
|
$entraTenantId = '33333333-3333-3333-3333-333333333333';
|
|
|
|
|
|
|
|
|
|
$component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class);
|
|
|
|
|
|
|
|
|
|
$component->call('identifyManagedTenant', [
|
|
|
|
|
'entra_tenant_id' => $entraTenantId,
|
|
|
|
|
'environment' => 'prod',
|
|
|
|
|
'name' => 'Acme',
|
|
|
|
|
'status' => 'pending',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
\App\Models\TenantMembership::query()->create([
|
|
|
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
|
|
|
'user_id' => (int) $user->getKey(),
|
|
|
|
|
'role' => 'readonly',
|
|
|
|
|
'source' => 'manual',
|
|
|
|
|
'created_by_user_id' => (int) $user->getKey(),
|
|
|
|
|
$component->call('identifyManagedTenant', [
|
|
|
|
|
'entra_tenant_id' => $entraTenantId,
|
|
|
|
|
'environment' => 'prod',
|
|
|
|
|
'name' => 'Acme',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user);
|
|
|
|
|
|
|
|
|
|
Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace])
|
|
|
|
|
->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
|
|
|
|
|
|
|
|
|
|
$membership = \App\Models\TenantMembership::query()
|
|
|
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
|
|
|
->where('user_id', (int) $user->getKey())
|
|
|
|
|
->firstOrFail();
|
|
|
|
|
|
|
|
|
|
expect($membership->role)->toBe('owner');
|
|
|
|
|
|
|
|
|
|
expect(\App\Models\TenantMembership::query()
|
|
|
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
|
|
|
->where('user_id', (int) $user->getKey())
|
|
|
|
|
expect(Tenant::query()->where('tenant_id', $entraTenantId)->count())->toBe(1);
|
|
|
|
|
expect(TenantOnboardingSession::query()
|
|
|
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
|
|
|
->where('entra_tenant_id', $entraTenantId)
|
|
|
|
|
->whereNull('completed_at')
|
|
|
|
|
->count())->toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('writes audit logs for onboarding start and resume', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
it('returns 404 and does not create anything when entra_tenant_id exists in another workspace', function (): void {
|
|
|
|
|
$entraTenantId = '44444444-4444-4444-4444-444444444444';
|
|
|
|
|
|
|
|
|
|
$workspaceA = Workspace::factory()->create();
|
|
|
|
|
$workspaceB = Workspace::factory()->create();
|
|
|
|
|
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'workspace_id' => (int) $workspaceA->getKey(),
|
|
|
|
|
'user_id' => (int) $user->getKey(),
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user);
|
|
|
|
|
|
|
|
|
|
$tenantGuid = '44444444-4444-4444-4444-444444444444';
|
|
|
|
|
|
|
|
|
|
$component = Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace]);
|
|
|
|
|
|
|
|
|
|
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
|
|
|
|
|
$component->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'actor_id' => (int) $user->getKey(),
|
|
|
|
|
'action' => 'managed_tenant_onboarding.start',
|
|
|
|
|
'resource_type' => 'tenant',
|
|
|
|
|
'resource_id' => (string) $tenant->getKey(),
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
|
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
|
|
|
'actor_id' => (int) $user->getKey(),
|
|
|
|
|
'action' => 'managed_tenant_onboarding.resume',
|
|
|
|
|
'resource_type' => 'tenant',
|
|
|
|
|
'resource_id' => (string) $tenant->getKey(),
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(AuditLog::query()
|
|
|
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
|
|
|
->where('resource_type', 'tenant')
|
|
|
|
|
->where('resource_id', (string) $tenant->getKey())
|
|
|
|
|
->whereIn('action', ['managed_tenant_onboarding.start', 'managed_tenant_onboarding.resume'])
|
|
|
|
|
->count())->toBeGreaterThanOrEqual(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('blocks demoting or removing the last remaining tenant owner', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'workspace_id' => (int) $workspaceB->getKey(),
|
|
|
|
|
'user_id' => (int) $user->getKey(),
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user);
|
|
|
|
|
Tenant::factory()->create([
|
|
|
|
|
'workspace_id' => (int) $workspaceA->getKey(),
|
|
|
|
|
'tenant_id' => $entraTenantId,
|
|
|
|
|
'status' => Tenant::STATUS_ACTIVE,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$tenantGuid = '55555555-5555-5555-5555-555555555555';
|
|
|
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspaceB->getKey());
|
|
|
|
|
|
|
|
|
|
Livewire::test(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class, ['workspace' => $workspace])
|
|
|
|
|
->call('identifyManagedTenant', ['tenant_id' => $tenantGuid, 'name' => 'Acme']);
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::query()->where('tenant_id', $tenantGuid)->firstOrFail();
|
|
|
|
|
$membership = \App\Models\TenantMembership::query()
|
|
|
|
|
->where('tenant_id', $tenant->getKey())
|
|
|
|
|
->where('user_id', $user->getKey())
|
|
|
|
|
->firstOrFail();
|
|
|
|
|
|
|
|
|
|
expect(fn () => app(TenantMembershipManager::class)->changeRole($tenant, $user, $membership, 'manager'))
|
|
|
|
|
->toThrow(DomainException::class, 'You cannot demote the last remaining owner.');
|
|
|
|
|
|
|
|
|
|
expect(fn () => app(TenantMembershipManager::class)->removeMember($tenant, $user, $membership))
|
|
|
|
|
->toThrow(DomainException::class, 'You cannot remove the last remaining owner.');
|
|
|
|
|
Livewire::actingAs($user)
|
|
|
|
|
->test(ManagedTenantOnboardingWizard::class)
|
|
|
|
|
->call('identifyManagedTenant', [
|
|
|
|
|
'entra_tenant_id' => $entraTenantId,
|
|
|
|
|
'environment' => 'prod',
|
|
|
|
|
'name' => 'Other Workspace',
|
|
|
|
|
])
|
|
|
|
|
->assertStatus(404);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns 404 for legacy onboarding entry points', function (): void {
|
|
|
|
|
@ -232,15 +181,19 @@
|
|
|
|
|
$this->get('/admin/new')->assertNotFound();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('is idempotent when identifying the same managed tenant twice', function (): void {
|
|
|
|
|
$workspace = Workspace::factory()->create();
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Legacy onboarding suite (deprecated)
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
| The remainder of this file previously contained an end-to-end onboarding
|
|
|
|
|
| suite that relied on deprecated routes and pre-enterprise state semantics.
|
|
|
|
|
| Spec 073 replaces it with focused coverage under tests/Feature/Onboarding
|
|
|
|
|
| and tests/Feature/Rbac.
|
|
|
|
|
|
|
|
|
|
|
| Keeping the legacy assertions around (commented) is intentional to avoid
|
|
|
|
|
| reintroducing removed routes or old semantics.
|
|
|
|
|
|
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
|
|
|
'workspace_id' => $workspace->getKey(),
|
|
|
|
|
'user_id' => $user->getKey(),
|
|
|
|
|
'role' => 'owner',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->actingAs($user);
|
|
|
|
|
|
|
|
|
|
@ -872,3 +825,5 @@
|
|
|
|
|
'current_step' => 'identify',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|