TenantAtlas/apps/platform/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php
ahmido d8e331e92f Spec 207: implement shared test fixture slimming (#240)
## Summary
- implement the canonical shared fixture profile model with minimal, standard, and full semantics plus temporary legacy alias resolution
- slim default factory behavior for operation runs, backup sets, provider connections, and provider credentials while keeping explicit heavy opt-in states
- migrate the first console, navigation, RBAC, and drift caller packs to explicit lean helpers and wire lane comparison reporting into the existing Spec 206 seams
- reconcile spec 207 docs, contracts, quickstart guidance, and task tracking with the implemented behavior

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/CreateUserWithTenantProfilesTest.php tests/Unit/Factories/TenantFactoryTest.php tests/Unit/Factories/OperationRunFactoryTest.php tests/Unit/Factories/BackupSetFactoryTest.php tests/Unit/Factories/ProviderConnectionFactoryTest.php tests/Unit/Factories/ProviderCredentialFactoryTest.php tests/Feature/Guards/FixtureCostProfilesGuardTest.php tests/Feature/Guards/FixtureLaneImpactBudgetTest.php tests/Feature/Guards/TestLaneArtifactsContractTest.php tests/Feature/Console/ReconcileOperationRunsCommandTest.php tests/Feature/Console/ReconcileBackupScheduleOperationRunsCommandTest.php tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php tests/Feature/BaselineDriftEngine/FindingFidelityTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `./scripts/platform-test-lane fast-feedback`
- `./scripts/platform-test-lane confidence`
- `./scripts/platform-test-report fast-feedback`
- `./scripts/platform-test-report confidence`

## Lane outcome
- `fast-feedback`: 136.400761s vs 176.73623s baseline, status `improved`
- `confidence`: 394.5669s vs 394.383441s baseline, status `stable`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #240
2026-04-16 17:29:25 +00:00

287 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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}")
->assertOk()
->assertSee('/admin/provider-connections?tenant_id='.$tenant->external_id, 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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(tenant: $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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
]);
$this->followingRedirects()
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}/provider-connections")
->assertOk();
$this->followingRedirects()
->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] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
$this->followingRedirects()
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}/provider-connections")
->assertOk();
$this->followingRedirects()
->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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(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] = createMinimalUserWithTenant(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);
});