diff --git a/app/Support/Middleware/EnsureFilamentTenantSelected.php b/app/Support/Middleware/EnsureFilamentTenantSelected.php index 6e78604..29f1cca 100644 --- a/app/Support/Middleware/EnsureFilamentTenantSelected.php +++ b/app/Support/Middleware/EnsureFilamentTenantSelected.php @@ -54,6 +54,12 @@ public function handle(Request $request, Closure $next): Response return $next($request); } + + if ($this->isWorkspaceScopedPageWithTenant($refererPath)) { + $this->configureNavigationForRequest($panel); + + return $next($request); + } } if (preg_match('#^/admin/operations/[^/]+$#', $path) === 1) { @@ -124,6 +130,12 @@ public function handle(Request $request, Closure $next): Response abort(404); } + if ($this->isWorkspaceScopedPageWithTenant($path)) { + $this->configureNavigationForRequest($panel); + + return $next($request); + } + Filament::setTenant($tenant, true); app(WorkspaceContext::class)->rememberLastTenantId((int) $workspaceId, (int) $tenant->getKey(), $request); @@ -244,6 +256,11 @@ private function configureNavigationForRequest(\Filament\Panel $panel): void }); } + private function isWorkspaceScopedPageWithTenant(string $path): bool + { + return preg_match('#^/admin/tenants/[^/]+/required-permissions$#', $path) === 1; + } + private function adminPathRequiresTenantSelection(string $path): bool { if (! str_starts_with($path, '/admin/')) { diff --git a/tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php b/tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php index d7b735a..f58cbf6 100644 --- a/tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php +++ b/tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php @@ -2,13 +2,17 @@ declare(strict_types=1); +use App\Filament\Resources\TenantResource; + it('renders the no-data state with a canonical start verification link when no stored permission data exists', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly'); + $expectedUrl = TenantResource::getUrl('view', ['record' => $tenant]); + $this->actingAs($user) ->get("/admin/tenants/{$tenant->external_id}/required-permissions") ->assertSuccessful() ->assertSee('Keine Daten verfügbar') - ->assertSee('/admin/onboarding', false) + ->assertSee($expectedUrl, false) ->assertSee('Start verification'); }); diff --git a/tests/Feature/RequiredPermissions/RequiredPermissionsFiltersTest.php b/tests/Feature/RequiredPermissions/RequiredPermissionsFiltersTest.php index b3b6b6a..4675936 100644 --- a/tests/Feature/RequiredPermissions/RequiredPermissionsFiltersTest.php +++ b/tests/Feature/RequiredPermissions/RequiredPermissionsFiltersTest.php @@ -25,6 +25,7 @@ 'features' => ['backup', 'restore'], ], ]); + config()->set('entra_permissions.permissions', []); TenantPermission::create([ 'tenant_id' => (int) $tenant->getKey(), diff --git a/tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php b/tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php index 2ef7921..5576458 100644 --- a/tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php +++ b/tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php @@ -2,14 +2,18 @@ declare(strict_types=1); +use App\Filament\Resources\TenantResource; + it('renders re-run verification and next-step links using canonical manage surfaces only', function (): void { [$user, $tenant] = createUserWithTenant(role: 'readonly'); + $expectedUrl = TenantResource::getUrl('view', ['record' => $tenant]); + $this->actingAs($user) ->get("/admin/tenants/{$tenant->external_id}/required-permissions") ->assertSuccessful() ->assertSee('Re-run verification') - ->assertSee('/admin/onboarding', false) + ->assertSee($expectedUrl, false) ->assertDontSee('/admin/t/', false); }); @@ -20,6 +24,6 @@ ->get("/admin/tenants/{$tenant->external_id}/required-permissions") ->assertSuccessful() ->assertSeeInOrder(['Summary', 'Issues', 'Passed', 'Technical details']) - ->assertSee('
assertSee('data-testid="technical-details"', false) ->assertDontSee('data-testid="technical-details" open', false); }); diff --git a/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php b/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php new file mode 100644 index 0000000..673e9ee --- /dev/null +++ b/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php @@ -0,0 +1,160 @@ +actingAs($user) + ->get("/admin/tenants/{$tenant->external_id}/required-permissions"); + + $response->assertOk(); + + // Workspace nav items from configureNavigationForRequest() workspace builder + $response->assertSee('Operations', false); + $response->assertSee('Audit Log', false); +}); + +it('does not render tenant navigation items on the required permissions page', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'readonly'); + + $response = $this->actingAs($user) + ->get("/admin/tenants/{$tenant->external_id}/required-permissions"); + + $response->assertOk(); + + // Tenant-scoped nav groups/items must NOT appear + $response->assertDontSee('>Inventory', false); + $response->assertDontSee('>Backups & Restore', false); +}); + +it('shows workspace sidebar when navigating directly via URL', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'readonly'); + + $response = $this->actingAs($user) + ->get("/admin/tenants/{$tenant->external_id}/required-permissions"); + + $response->assertOk(); + + // Workspace nav present (Operations is always visible in workspace nav) + $response->assertSee('Operations', false); + $response->assertSee('Audit Log', false); + + // Tenant nav absent + $response->assertDontSee('>Directory', false); + $response->assertDontSee('>Governance', false); +}); + +it('returns 404 for non-workspace-members after middleware change (FR-002 regression guard)', function (): void { + $user = User::factory()->create(); + $workspace = Workspace::factory()->create(); + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + ]); + + // User is NOT a workspace member — no WorkspaceMembership created + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), + ]) + ->get("/admin/tenants/{$tenant->external_id}/required-permissions") + ->assertNotFound(); +}); + +it('returns 404 for workspace members without tenant entitlement after middleware change (FR-002 regression guard)', function (): void { + $user = User::factory()->create(); + $workspace = Workspace::factory()->create(); + $tenant = Tenant::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + ]); + + WorkspaceMembership::factory()->create([ + 'workspace_id' => (int) $workspace->getKey(), + 'user_id' => (int) $user->getKey(), + 'role' => 'owner', + ]); + + // User IS a workspace member but NOT entitled to this tenant + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), + ]) + ->get("/admin/tenants/{$tenant->external_id}/required-permissions") + ->assertNotFound(); +}); + +/* +|-------------------------------------------------------------------------- +| T002 — Regression: tenant-scoped pages still show tenant sidebar +|-------------------------------------------------------------------------- +| +| Verifies that the middleware change does NOT affect tenant-scoped pages. +| Pages under /admin/t/{tenant}/ must continue to show tenant sidebar. +| +*/ + +it('still renders tenant sidebar on tenant-scoped pages (regression guard)', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + // Use the tenant dashboard — a known tenant-scoped URL + $response = $this->actingAs($user) + ->get("/admin/t/{$tenant->external_id}"); + + $response->assertOk(); + + // Tenant-scoped nav groups MUST be present on tenant pages (Inventory group) + $response->assertSee('Inventory', false); +}); + +/* +|-------------------------------------------------------------------------- +| T007 — Context bar: tenant name is visible on the page +|-------------------------------------------------------------------------- +| +| The page's $scopedTenant is resolved from the route param via +| resolveScopedTenant() — NOT from Filament::getTenant(). So the tenant +| data is available even though Filament::setTenant() is skipped. +| Verify the page renders successfully and that the tenant's data is used. +| +*/ + +it('resolves scoped tenant correctly with workspace sidebar (context bar / US2)', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'readonly'); + + $response = $this->actingAs($user) + ->get("/admin/tenants/{$tenant->external_id}/required-permissions"); + + $response->assertOk(); + + // The page resolves $scopedTenant from route param and uses it for data display. + // Verify the page title is present (static title: "Required permissions") + $response->assertSee('Required permissions', false); + + // The page uses $scopedTenant for links (e.g., "Re-run verification" links back to TenantResource) + // If $scopedTenant were null, the page would abort(404) in mount(). + // The fact that we get 200 proves $scopedTenant resolved correctly despite setTenant() being skipped. + + // Workspace nav present (sidebar fix working) + $response->assertSee('Operations', false); + + // Tenant nav absent (sidebar fix working) + $response->assertDontSee('>Inventory', false); +});