actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get('/admin/tenants') ->assertOk(); }); it('returns 404 for non-members on the workspace-managed tenants index', function (): void { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get('/admin/tenants') ->assertNotFound(); }); it('allows workspace members to open the workspace-managed tenant view route', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}") ->assertOk(); }); it('exposes a provider connections link from the workspace-managed tenant view page', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}") ->assertOk() ->assertSee("/admin/tenants/{$tenant->external_id}/provider-connections", false); }); it('returns 404 for non-members on the workspace-managed tenant view route', function (): void { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}") ->assertNotFound(); }); it('exposes memberships management under workspace scope', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}/memberships") ->assertOk(); }); it('requires tenant entitlement for the contracted tenant operational routes', function (): void { $workspace = Workspace::factory()->create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'external_id' => '11111111-1111-1111-1111-111111111111', 'tenant_id' => '11111111-1111-1111-1111-111111111111', ]); [$entitledUser] = createUserWithTenant($tenant, role: 'readonly'); $nonEntitledUser = User::factory()->create(); WorkspaceMembership::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'user_id' => (int) $nonEntitledUser->getKey(), 'role' => 'owner', ]); $this->actingAs($entitledUser) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get("/admin/t/{$tenant->external_id}") ->assertOk(); $this->actingAs($entitledUser) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get("/admin/t/{$tenant->external_id}/diagnostics") ->assertOk(); $this->actingAs($nonEntitledUser) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get("/admin/t/{$tenant->external_id}") ->assertNotFound(); $this->actingAs($nonEntitledUser) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get("/admin/t/{$tenant->external_id}/diagnostics") ->assertNotFound(); }); it('keeps tenant panel route shape canonical and rejects duplicated /t prefixes', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/t/{$tenant->external_id}/diagnostics") ->assertOk(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/t/t/{$tenant->external_id}/diagnostics") ->assertNotFound(); }); it('removes tenant-scoped management routes', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/t/{$tenant->external_id}/provider-connections") ->assertNotFound(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/t/{$tenant->external_id}/required-permissions") ->assertNotFound(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/t/{$tenant->external_id}/memberships") ->assertNotFound(); }); it('serves provider connection management under workspace-managed tenant routes only', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $connection = ProviderConnection::factory()->create([ 'workspace_id' => (int) $tenant->workspace_id, 'tenant_id' => (int) $tenant->getKey(), ]); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}/provider-connections") ->assertOk(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}/provider-connections/{$connection->getKey()}/edit") ->assertOk(); }); it('returns 403 for workspace members missing mutation capability on provider connections', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly', workspaceRole: 'readonly'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}/provider-connections") ->assertOk(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}/provider-connections/create") ->assertForbidden(); }); it('writes canonical membership audit entries for membership mutations', function (): void { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $member = User::factory()->create(); /** @var TenantMembershipManager $manager */ $manager = app(TenantMembershipManager::class); $membership = $manager->addMember( tenant: $tenant, actor: $owner, member: $member, role: 'readonly', source: 'manual', ); $manager->changeRole( tenant: $tenant, actor: $owner, membership: $membership, newRole: 'operator', ); $manager->removeMember( tenant: $tenant, actor: $owner, membership: $membership, ); $actions = AuditLog::query() ->where('tenant_id', (int) $tenant->getKey()) ->whereIn('action', [ AuditActionId::TenantMembershipAdd->value, AuditActionId::TenantMembershipRoleChange->value, AuditActionId::TenantMembershipRemove->value, ]) ->pluck('action') ->all(); expect($actions)->toContain(AuditActionId::TenantMembershipAdd->value); expect($actions)->toContain(AuditActionId::TenantMembershipRoleChange->value); expect($actions)->toContain(AuditActionId::TenantMembershipRemove->value); }); it('keeps workspace navigation entries after panel split', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get('/admin/tenants') ->assertOk() ->assertSee('Tenants') ->assertSee('Operations') ->assertSee('Alerts') ->assertSee('Audit Log'); }); it('does not expose tenant-management resources in tenant panel registration or navigation URLs', function (): void { $tenantPanelResources = Filament::getPanel('tenant')->getResources(); expect($tenantPanelResources)->not->toContain(TenantResource::class); expect($tenantPanelResources)->not->toContain(ProviderConnectionResource::class); [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/t/{$tenant->external_id}") ->assertOk() ->assertDontSee("/admin/t/{$tenant->external_id}/provider-connections", false) ->assertDontSee("/admin/t/{$tenant->external_id}/tenants", false); }); it('keeps global search scoped to workspace-managed tenant resources only', function (): void { [$workspaceUser, $tenant] = createUserWithTenant(role: 'owner'); Filament::setCurrentPanel('admin'); Filament::setTenant(null, true); $this->actingAs($workspaceUser); $results = TenantResource::getGlobalSearchResults((string) $tenant->name); expect($results->count())->toBeGreaterThan(0); $nonMember = User::factory()->create(); Filament::setCurrentPanel('admin'); Filament::setTenant(null, true); $this->actingAs($nonMember); $nonMemberResults = TenantResource::getGlobalSearchResults((string) $tenant->name); expect($nonMemberResults)->toHaveCount(0); });