readProjectFile('pages/admin/tenants/index(default).phtml'); $this->assertStringNotContainsString("can('tenants.create')", $indexTemplate); $this->assertStringContainsString('$canCreateTenants', $indexTemplate); $createTemplate = $this->readProjectFile('pages/admin/tenants/create(default).phtml'); $this->assertStringNotContainsString("can('custom_fields.manage')", $createTemplate); $this->assertStringNotContainsString("can('tenants.sso_manage')", $createTemplate); $this->assertStringContainsString('$canManageCustomFields', $createTemplate); $this->assertStringContainsString('$canManageSso', $createTemplate); } public function testAdminDepartmentsListTemplateUsesServerCapabilities(): void { $content = $this->readProjectFile('pages/admin/departments/index(default).phtml'); $this->assertStringNotContainsString("can('departments.create')", $content); $this->assertStringContainsString('$canCreateDepartments', $content); } public function testAdminRoleTemplatesUseServerCapabilities(): void { $indexTemplate = $this->readProjectFile('pages/admin/roles/index(default).phtml'); $this->assertStringNotContainsString("can('roles.create')", $indexTemplate); $this->assertStringContainsString('$canCreateRoles', $indexTemplate); $editTemplate = $this->readProjectFile('pages/admin/roles/edit(default).phtml'); $this->assertStringNotContainsString("can('roles.update')", $editTemplate); $this->assertStringNotContainsString("can('roles.delete')", $editTemplate); $this->assertStringContainsString('$canUpdateRole', $editTemplate); $this->assertStringContainsString('$canDeleteRole', $editTemplate); } public function testAdminPermissionTemplatesUseServerCapabilities(): void { $indexTemplate = $this->readProjectFile('pages/admin/permissions/index(default).phtml'); $this->assertStringNotContainsString("can('permissions.create')", $indexTemplate); $this->assertStringContainsString('$canCreatePermissions', $indexTemplate); $editTemplate = $this->readProjectFile('pages/admin/permissions/edit(default).phtml'); $this->assertStringNotContainsString("can('permissions.update')", $editTemplate); $this->assertStringNotContainsString("can('permissions.delete')", $editTemplate); $this->assertStringContainsString('$canUpdatePermission', $editTemplate); $this->assertStringContainsString('$canDeletePermission', $editTemplate); } public function testAdminSettingsTemplateUsesServerCapabilities(): void { $templateContent = $this->readProjectFile('pages/admin/settings/index(default).phtml'); $this->assertStringNotContainsString("can('settings.update')", $templateContent); $this->assertStringContainsString('$canUpdateSettings', $templateContent); } public function testAdminUserEditTemplateUsesServerCapabilities(): void { $content = $this->readProjectFile('pages/admin/users/edit(default).phtml'); $this->assertStringNotContainsString("can('users.", $content); $this->assertStringNotContainsString("can('permissions.view')", $content); $this->assertStringContainsString('$canViewSecurityArtifacts', $content); } public function testHardCutTemplatesDoNotUseLegacyCanHelper(): void { $templates = [ 'templates/partials/app-main-aside.phtml', 'templates/partials/app-main-aside-icon-bar.phtml', 'pages/admin/api-audit/index(default).phtml', 'pages/admin/system-audit/index(default).phtml', 'pages/admin/import-audit/index(default).phtml', 'pages/admin/scheduled-jobs/index(default).phtml', 'pages/admin/stats/index(default).phtml', 'pages/admin/user-lifecycle-audit/index(default).phtml', 'pages/admin/user-lifecycle-audit/view(default).phtml', 'pages/admin/users/index(default).phtml', 'pages/admin/users/create(default).phtml', 'pages/admin/tenants/edit(default).phtml', 'pages/admin/departments/edit(default).phtml', ]; foreach ($templates as $template) { $content = $this->readProjectFile($template); $this->assertStringNotContainsString("can('", $content, $template); } } public function testLayoutPartialsUseViewAuthLayoutCapabilities(): void { $defaultTemplate = $this->readProjectFile('templates/default.phtml'); $this->assertStringContainsString("\$viewAuth['layout']", $defaultTemplate); $this->assertStringNotContainsString('UiAccessService::class', $defaultTemplate); $aside = $this->readProjectFile('templates/partials/app-main-aside.phtml'); $this->assertStringContainsString("\$viewAuth['layout']", $aside); $this->assertStringContainsString('layoutHasAdminPanel(', $aside); $this->assertStringContainsString('$layoutNav', $aside); $iconBar = $this->readProjectFile('templates/partials/app-main-aside-icon-bar.phtml'); $this->assertStringContainsString("\$viewAuth['layout']", $iconBar); $this->assertStringContainsString('layoutHasAdminPanel(', $iconBar); } public function testAsideTemplateDoesNotReadRequestOrResolveServicesDirectly(): void { $aside = $this->readProjectFile('templates/partials/app-main-aside.phtml'); $this->assertStringNotContainsString('$_GET', $aside); $this->assertStringNotContainsString('$_SESSION', $aside); $this->assertStringNotContainsString('TenantAvatarService', $aside); $this->assertStringNotContainsString('app(', $aside); } public function testHardCutActionsProvideViewAuthPageCapabilities(): void { $actions = [ 'pages/admin/users/index().php', 'pages/admin/users/create().php', 'pages/admin/tenants/edit($id).php', 'pages/admin/departments/edit($id).php', 'pages/admin/stats/index().php', 'pages/admin/api-audit/index().php', 'pages/admin/system-audit/index().php', 'pages/admin/import-audit/index().php', 'pages/admin/scheduled-jobs/index().php', 'pages/admin/user-lifecycle-audit/index().php', 'pages/admin/user-lifecycle-audit/view($id).php', ]; foreach ($actions as $action) { $content = $this->readProjectFile($action); $this->assertStringContainsString("\$viewAuth['page']", $content, $action); } } #[DataProvider('adminPageAuthWiringProvider')] public function testAdminPageAuthKeysAreWiredFromActions( string $templateFile, string $actionFile, ?string $uiCapabilityMapConstant ): void { $templateContent = $this->readProjectFile($templateFile); $requiredKeys = $this->extractPageAuthKeys($templateContent); $this->assertNotEmpty($requiredKeys, $templateFile . ' must read at least one $pageAuth key.'); $actionContent = $this->readProjectFile($actionFile); $this->assertStringContainsString("\$viewAuth['page']", $actionContent, $actionFile); $wiredKeys = $this->extractViewAuthPageKeys($actionContent); if ($uiCapabilityMapConstant !== null) { $this->assertStringContainsString('->pageCapabilities(', $actionContent, $actionFile); $this->assertStringContainsString($uiCapabilityMapConstant, $actionContent, $actionFile); $wiredKeys = array_values(array_unique([ ...$wiredKeys, ...$this->extractUiCapabilityMapKeys($uiCapabilityMapConstant), ])); } $missingKeys = array_values(array_diff($requiredKeys, $wiredKeys)); $this->assertSame( [], $missingKeys, sprintf( 'Missing $pageAuth wiring in %s (from %s): %s', $templateFile, $actionFile, implode(', ', $missingKeys) ) ); } public function testTenantEditActionExposesDeleteCapabilityForTemplate(): void { $content = $this->readProjectFile('pages/admin/tenants/edit($id).php'); $this->assertStringContainsString("'can_delete_tenant' =>", $content); } public function testMapDrivenHardCutActionsUseUiCapabilityMaps(): void { $actions = [ 'pages/admin/users/index().php' => 'UiCapabilityMap::PAGE_USERS_INDEX', 'pages/admin/users/create().php' => 'UiCapabilityMap::PAGE_USERS_CREATE', 'pages/admin/stats/index().php' => 'UiCapabilityMap::PAGE_STATS_INDEX', 'pages/admin/api-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', 'pages/admin/system-audit/index().php' => 'UiCapabilityMap::PAGE_SYSTEM_AUDIT', 'pages/admin/import-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', 'pages/admin/scheduled-jobs/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', 'pages/admin/user-lifecycle-audit/index().php' => 'UiCapabilityMap::PAGE_AUDIT_PURGE', 'pages/admin/user-lifecycle-audit/view($id).php' => 'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW', ]; foreach ($actions as $action => $mapConstant) { $content = $this->readProjectFile($action); $this->assertStringContainsString('->pageCapabilities(', $content, $action); $this->assertStringContainsString($mapConstant, $content, $action); } } public function testUiAccessServiceUsesCentralLayoutMapDefinition(): void { $content = $this->readProjectFile('lib/Service/Access/UiAccessService.php'); $this->assertStringContainsString('UiCapabilityMap::LAYOUT', $content); } public function testStatsPageCapabilityMapIncludesAuditCapabilities(): void { $content = $this->readProjectFile('lib/Service/Access/UiCapabilityMap.php'); $this->assertStringContainsString("'can_view_system_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_VIEW", $content); $this->assertStringContainsString("'can_view_user_lifecycle_audit' => OperationsAuthorizationPolicy::ABILITY_ADMIN_USER_LIFECYCLE_AUDIT_VIEW", $content); } public function testStatsTemplateDefinesAuditTabAndPanel(): void { $content = $this->readProjectFile('pages/admin/stats/index(default).phtml'); $this->assertStringContainsString('data-tab="audit"', $content); $this->assertStringContainsString('data-tab-panel="audit"', $content); } /** * @return array */ public static function adminPageAuthWiringProvider(): array { return [ 'users index' => [ 'pages/admin/users/index(default).phtml', 'pages/admin/users/index().php', 'UiCapabilityMap::PAGE_USERS_INDEX', ], 'users create' => [ 'pages/admin/users/create(default).phtml', 'pages/admin/users/create().php', 'UiCapabilityMap::PAGE_USERS_CREATE', ], 'tenants edit' => [ 'pages/admin/tenants/edit(default).phtml', 'pages/admin/tenants/edit($id).php', null, ], 'departments edit' => [ 'pages/admin/departments/edit(default).phtml', 'pages/admin/departments/edit($id).php', null, ], 'stats index' => [ 'pages/admin/stats/index(default).phtml', 'pages/admin/stats/index().php', 'UiCapabilityMap::PAGE_STATS_INDEX', ], 'api audit index' => [ 'pages/admin/api-audit/index(default).phtml', 'pages/admin/api-audit/index().php', 'UiCapabilityMap::PAGE_AUDIT_PURGE', ], 'import audit index' => [ 'pages/admin/import-audit/index(default).phtml', 'pages/admin/import-audit/index().php', 'UiCapabilityMap::PAGE_AUDIT_PURGE', ], 'scheduled jobs index' => [ 'pages/admin/scheduled-jobs/index(default).phtml', 'pages/admin/scheduled-jobs/index().php', 'UiCapabilityMap::PAGE_AUDIT_PURGE', ], 'system audit index' => [ 'pages/admin/system-audit/index(default).phtml', 'pages/admin/system-audit/index().php', 'UiCapabilityMap::PAGE_SYSTEM_AUDIT', ], 'user lifecycle index' => [ 'pages/admin/user-lifecycle-audit/index(default).phtml', 'pages/admin/user-lifecycle-audit/index().php', 'UiCapabilityMap::PAGE_AUDIT_PURGE', ], 'user lifecycle view' => [ 'pages/admin/user-lifecycle-audit/view(default).phtml', 'pages/admin/user-lifecycle-audit/view($id).php', 'UiCapabilityMap::PAGE_USER_LIFECYCLE_VIEW', ], ]; } /** * @return list */ private function extractPageAuthKeys(string $content): array { preg_match_all('/\\$pageAuth\\[[\'"]([a-z_]+)[\'"]\\]/', $content, $matches); $keys = array_values(array_unique($matches[1])); sort($keys); return $keys; } /** * @return list */ private function extractViewAuthPageKeys(string $content): array { $keys = []; preg_match_all('/\\$viewAuth\\[\'page\'\\]\\s*=\\s*\\[(.*?)\\];/s', $content, $blocks); foreach ($blocks[1] as $block) { preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $block, $matches); foreach ($matches[1] as $key) { $keys[] = $key; } } $keys = array_values(array_unique($keys)); sort($keys); return $keys; } /** * @return list */ private function extractUiCapabilityMapKeys(string $mapConstant): array { $parts = explode('::', $mapConstant); $constantName = trim((string) end($parts)); $this->assertNotSame('', $constantName, 'Invalid UiCapabilityMap constant: ' . $mapConstant); $mapContent = $this->readProjectFile('lib/Service/Access/UiCapabilityMap.php'); $constantPattern = '/public const\\s+' . preg_quote($constantName, '/') . '\\s*=\\s*\\[(.*?)\\];/s'; $matched = preg_match($constantPattern, $mapContent, $constantMatch); $this->assertSame( 1, $matched, 'Could not find UiCapabilityMap constant block: ' . $mapConstant ); preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', (string) ($constantMatch[1] ?? ''), $keyMatches); $keys = array_values(array_unique($keyMatches[1])); sort($keys); return $keys; } }