283 lines
10 KiB
PHP
283 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\ProviderConnectionResource;
|
|
use App\Filament\Resources\TenantResource;
|
|
use App\Models\AuditLog;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Services\Auth\TenantMembershipManager;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Filament\Facades\Filament;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Http::preventStrayRequests();
|
|
});
|
|
|
|
it('allows workspace members to open the workspace-managed tenants index', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$this->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);
|
|
});
|