diff --git a/phpunit.xml b/phpunit.xml index 4c36339..17cc638 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,5 +8,64 @@ tests modules/*/tests + + tests/Architecture + + + tests/Architecture/AppContainerIsolationContractTest.php + tests/Architecture/ContainerFactoryContractTest.php + tests/Architecture/CoreStarterkitContractTest.php + tests/Architecture/CoreTemplateIsolationTest.php + tests/Architecture/DatabaseUpdatesContractTest.php + tests/Architecture/FrontendTelemetryContractTest.php + tests/Architecture/I18nKeyCompletenessContractTest.php + tests/Architecture/ModuleRegistryContractTest.php + tests/Architecture/ModuleStructureContractTest.php + tests/Architecture/RepositoryInterfaceContractTest.php + tests/Architecture/StatusTaxonomyContractTest.php + + + tests/Architecture/AuthzAdminMasterDataContractTest.php + tests/Architecture/AuthzAdminSettingsContractTest.php + tests/Architecture/AuthzAdminTenantsContractTest.php + tests/Architecture/AuthzAdminUsersContractTest.php + tests/Architecture/ApiResourceContractTest.php + tests/Architecture/AuthzApiBootstrapContractTest.php + tests/Architecture/FileStorageContractTest.php + tests/Architecture/PostEndpointCsrfContractTest.php + tests/Architecture/PostRedirectGetContractTest.php + tests/Architecture/PublicPageContractTest.php + tests/Architecture/SecurityCryptoContractTest.php + tests/Architecture/SecurityLoggingContractTest.php + + + tests/Architecture/AsideNavigationContractTest.php + tests/Architecture/AuthHelpLinksContractTest.php + tests/Architecture/AuthLoginAccessibilityContractTest.php + tests/Architecture/AuthzUiActionContractTest.php + tests/Architecture/AuthzUiLayoutContractTest.php + tests/Architecture/AuthzUiTemplateContractTest.php + tests/Architecture/DetailActionPolicyContractTest.php + tests/Architecture/DetailPageContractTest.php + tests/Architecture/DetailValidationSummaryContractTest.php + tests/Architecture/FilterDrawerContractTest.php + tests/Architecture/FrontendComponentRuntimeContractTest.php + tests/Architecture/ListActionContractTest.php + tests/Architecture/ListDataEndpointContractTest.php + tests/Architecture/ListTemplateContractTest.php + tests/Architecture/ListTitlebarContractTest.php + tests/Architecture/ListUiSharedPartialsContractTest.php + + + tests/Architecture/ModuleRegistryContractTest.php + tests/Architecture/ModuleStructureContractTest.php + tests/Architecture/NoAddressBookHardcodingTest.php + tests/Architecture/NoBookmarksHardcodingTest.php + + + tests/Architecture/ApiSchemaContractTest.php + tests/Architecture/AgentSystemContractTest.php + tests/Architecture/CodexSkillsContractTest.php + diff --git a/tests/Architecture/ApiResourceContractTest.php b/tests/Architecture/ApiResourceContractTest.php new file mode 100644 index 0000000..0181203 --- /dev/null +++ b/tests/Architecture/ApiResourceContractTest.php @@ -0,0 +1,58 @@ +readProjectFile('pages/api/v1/tenants/show($id).php'); + $this->assertStringContainsString("'region' => \$tenant['region'] ?? ''", $tenantShowContent); + $this->assertStringContainsString("'vat_id' => \$tenant['vat_id'] ?? ''", $tenantShowContent); + $this->assertStringContainsString("'support_email' => \$tenant['support_email'] ?? ''", $tenantShowContent); + $this->assertStringContainsString("'billing_email' => \$tenant['billing_email'] ?? ''", $tenantShowContent); + $this->assertStringContainsString("'primary_color_use_default' => \$primaryColor === ''", $tenantShowContent); + $this->assertStringContainsString("'allow_user_theme_mode' => \$allowUserThemeMode", $tenantShowContent); + } + + public function testApiUserWriteEndpointsUseUuidAssignmentInput(): void + { + $usersIndex = $this->readProjectFile('pages/api/v1/users/index().php'); + $this->assertStringContainsString('UserApiWriteInputMapper::class', $usersIndex); + $this->assertStringContainsString('->normalize($input)', $usersIndex); + + $usersShow = $this->readProjectFile('pages/api/v1/users/show($id).php'); + $this->assertStringContainsString('UserApiWriteInputMapper::class', $usersShow); + $this->assertStringContainsString('->normalize($input)', $usersShow); + + $mapper = $this->readProjectFile('lib/Service/User/UserApiWriteInputMapper.php'); + $this->assertStringContainsString("'tenant_ids' => 'use_tenant_uuids'", $mapper); + $this->assertStringContainsString("'role_ids' => 'use_role_uuids'", $mapper); + $this->assertStringContainsString("'department_ids' => 'use_department_uuids'", $mapper); + $this->assertStringContainsString("'primary_tenant_id' => 'use_primary_tenant_uuid'", $mapper); + } + + public function testApiDepartmentEndpointsUseTenantUuidAndNoTenantIdExposure(): void + { + $departmentIndex = $this->readProjectFile('pages/api/v1/departments/index().php'); + $this->assertStringContainsString("ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_id' => ['use_tenant_uuid']]));", $departmentIndex); + $this->assertStringContainsString("if (array_key_exists('tenant_uuid', \$input)) {", $departmentIndex); + + $departmentShow = $this->readProjectFile('pages/api/v1/departments/show($id).php'); + $this->assertStringContainsString("'tenant_uuid' => \$tenantUuid", $departmentShow); + $this->assertStringNotContainsString("'tenant_id' => \$department['tenant_id'] ?? null", $departmentShow); + $this->assertStringContainsString("'cost_center' => \$department['cost_center'] ?? ''", $departmentShow); + } + + public function testApiRoleShowIncludesPermissionKeys(): void + { + $roleShow = $this->readProjectFile('pages/api/v1/roles/show($id).php'); + $this->assertStringContainsString('RolePermissionRepository::class', $roleShow); + $this->assertStringContainsString('listPermissionKeysByRoleIds([$roleId])', $roleShow); + $this->assertStringContainsString("'permission_keys' => \$permissionKeys", $roleShow); + } +} diff --git a/tests/Architecture/ApiSchemaContractTest.php b/tests/Architecture/ApiSchemaContractTest.php new file mode 100644 index 0000000..3728eb7 --- /dev/null +++ b/tests/Architecture/ApiSchemaContractTest.php @@ -0,0 +1,66 @@ +readProjectFile('docs/openapi.yaml'); + $this->assertMatchesRegularExpression('/\/auth\/login:\s+post:.*?security:\s*\[\]/s', $content); + $this->assertStringContainsString('no Authorization header required', $content); + } + + public function testOpenApiIncludesNewCriticalFrontendEndpoints(): void + { + $content = $this->readProjectFile('docs/openapi.yaml'); + $this->assertStringContainsString('/permissions:', $content); + $this->assertStringContainsString('/me/password:', $content); + $this->assertStringContainsString('/me/avatar:', $content); + $this->assertStringContainsString('/users/avatar/{uuid}:', $content); + $this->assertStringContainsString('/tenants/avatar/{uuid}:', $content); + $this->assertStringContainsString('/settings:', $content); + $this->assertStringContainsString('/settings/public:', $content); + } + + public function testOpenApiUsesUuidBasedMasterDataContracts(): void + { + $content = $this->readProjectFile('docs/openapi.yaml'); + $this->assertStringContainsString('required: [description, tenant_uuid]', $content); + $this->assertStringContainsString('tenant_uuids:', $content); + $this->assertStringContainsString('primary_tenant_uuid:', $content); + $this->assertStringContainsString('role_uuids:', $content); + $this->assertStringContainsString('department_uuids:', $content); + $this->assertStringContainsString('permission_keys:', $content); + $this->assertStringContainsString('tenant_uuid:', $content); + } + + public function testTenantShowOpenApiStaysInSyncWithResponseContract(): void + { + $openApiContent = $this->readProjectFile('docs/openapi.yaml'); + $this->assertStringContainsString('TenantShowResponse:', $openApiContent); + $this->assertStringContainsString('primary_color_use_default:', $openApiContent); + $this->assertStringContainsString('allow_user_theme_mode:', $openApiContent); + $this->assertStringContainsString('support_email:', $openApiContent); + } + + public function testApiErrorEnvelopeIsCentralizedAndRequestIdAware(): void + { + $apiResponse = $this->readProjectFile('lib/Http/ApiResponse.php'); + $this->assertStringContainsString("'ok' => false", $apiResponse); + $this->assertStringContainsString("'error_code' => \$normalizedErrorCode", $apiResponse); + $this->assertStringContainsString("'details' => \$normalizedDetails", $apiResponse); + $this->assertStringContainsString("header('X-Request-Id: ' . \$requestId);", $apiResponse); + $this->assertStringNotContainsString("'error' => \$normalizedErrorCode", $apiResponse); + $this->assertStringNotContainsString("\$body['errors']", $apiResponse); + + $openApi = $this->readProjectFile('docs/openapi.yaml'); + $this->assertStringContainsString('required: [ok, request_id, error_code, details]', $openApi); + $this->assertStringNotContainsString('Legacy alias of `error_code`', $openApi); + $this->assertStringNotContainsString('Legacy alias of `details.errors`', $openApi); + } +} diff --git a/tests/Architecture/AuthLoginFrontendCleanupContractTest.php b/tests/Architecture/AuthLoginFrontendCleanupContractTest.php deleted file mode 100644 index 22abb15..0000000 --- a/tests/Architecture/AuthLoginFrontendCleanupContractTest.php +++ /dev/null @@ -1,34 +0,0 @@ -readProjectFile('web/css/pages/app-login.css'); - - $this->assertStringNotContainsString('.back_to_login', $content); - } - - public function testLoginPasswordToggleUsesDuplicateBindingGuard(): void - { - $content = $this->readProjectFile('web/js/components/app-password-toggle.js'); - - $this->assertStringContainsString("input.dataset.passwordToggleBound === '1'", $content); - $this->assertStringContainsString("input.dataset.passwordToggleBound = '1';", $content); - $this->assertStringContainsString("toggle.dataset.passwordToggleBound = '1';", $content); - } - - public function testLoginPasswordHintsUsesDuplicateBindingGuard(): void - { - $content = $this->readProjectFile('web/js/components/app-password-hints.js'); - - $this->assertStringContainsString("container.dataset.passwordHintsBound === '1'", $content); - $this->assertStringContainsString("container.dataset.passwordHintsBound = '1';", $content); - } -} diff --git a/tests/Architecture/AuthRegisterSecurityContractTest.php b/tests/Architecture/AuthRegisterSecurityContractTest.php deleted file mode 100644 index c9afbb1..0000000 --- a/tests/Architecture/AuthRegisterSecurityContractTest.php +++ /dev/null @@ -1,23 +0,0 @@ -readProjectFile('pages/auth/register().php'); - - $this->assertStringContainsString("if (\$request->isMethod('POST')) {", $content); - $this->assertStringContainsString("if (!Session::checkCsrfToken()) {", $content); - $this->assertStringContainsString("t('Form expired, please try again')", $content); - $this->assertMatchesRegularExpression( - '/if \(\$request->isMethod\(\'POST\'\)\) \{\s*if \(!Session::checkCsrfToken\(\)\) \{/s', - $content - ); - } -} diff --git a/tests/Architecture/AuthzAdminContractTest.php b/tests/Architecture/AuthzAdminContractTest.php deleted file mode 100644 index 3944aab..0000000 --- a/tests/Architecture/AuthzAdminContractTest.php +++ /dev/null @@ -1,281 +0,0 @@ -readProjectFile('pages/admin/users/data().php'); - $this->assertStringContainsString('Guard::requireLogin();', $content); - $this->assertStringContainsString('Guard::requireAbilityOrForbidden(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW);', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $content); - } - - - public function testUsersExportActionRequiresUsersViewPermission(): void - { - $content = $this->readProjectFile('pages/admin/users/export().php'); - $this->assertStringContainsString('Guard::requireLogin();', $content); - $this->assertStringContainsString('AuthorizationService::class', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $content); - } - - - public function testUsersActivateActionRequiresUsersUpdatePermission(): void - { - $content = $this->readProjectFile('pages/admin/users/activate($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE', $content); - } - - - public function testUsersDeactivateActionRequiresUsersUpdatePermission(): void - { - $content = $this->readProjectFile('pages/admin/users/deactivate($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DEACTIVATE', $content); - } - - - public function testUsersBulkActionPermissionMappingIsConsistent(): void - { - $content = $this->readProjectFile('pages/admin/users/bulk($action).php'); - $this->assertStringContainsString('AuthorizationService::class', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_BULK', $content); - $this->assertStringContainsString("'bulk_action' => \$action", $content); - } - - - public function testAdditionalAdminUserActionsUseCentralPolicy(): void - { - $indexContent = $this->readProjectFile('pages/admin/users/index().php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $indexContent); - - $createContent = $this->readProjectFile('pages/admin/users/create().php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE', $createContent); - - $deleteContent = $this->readProjectFile('pages/admin/users/delete($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DELETE', $deleteContent); - - $accessPdfContent = $this->readProjectFile('pages/admin/users/access-pdf($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF', $accessPdfContent); - - $accessPdfBulkContent = $this->readProjectFile('pages/admin/users/access-pdf-bulk().php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK', $accessPdfBulkContent); - - $tokenCreateContent = $this->readProjectFile('pages/admin/users/api-token-create($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE', $tokenCreateContent); - - $tokenRevokeContent = $this->readProjectFile('pages/admin/users/api-token-revoke($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE', $tokenRevokeContent); - - $sendAccessContent = $this->readProjectFile('pages/admin/users/send-access($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_SEND_ACCESS', $sendAccessContent); - - $forgetTokensContent = $this->readProjectFile('pages/admin/users/forget-tokens($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_FORGET_TOKENS', $forgetTokensContent); - } - - - public function testAdminUserEditUsesContextAndSubmitPolicies(): void - { - $content = $this->readProjectFile('pages/admin/users/edit($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT', $content); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT', $content); - } - - - public function testAdminUserAvatarEndpointsUseCentralPolicies(): void - { - $uploadContent = $this->readProjectFile('pages/admin/users/avatar($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_UPLOAD', $uploadContent); - - $deleteContent = $this->readProjectFile('pages/admin/users/avatar-delete($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_DELETE', $deleteContent); - - $viewContent = $this->readProjectFile('pages/admin/users/avatar-file().php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW', $viewContent); - } - - - public function testAdminTenantEditAndCustomFieldActionsUseCentralPolicies(): void - { - $indexContent = $this->readProjectFile('pages/admin/tenants/index().php'); - $this->assertStringContainsString('AuthorizationService::class', $indexContent); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $indexContent); - - $dataContent = $this->readProjectFile('pages/admin/tenants/data().php'); - $this->assertStringContainsString('Guard::requireAbilityDecisionOrForbidden(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW);', $dataContent); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $dataContent); - - $createContent = $this->readProjectFile('pages/admin/tenants/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE', $createContent); - - $editContent = $this->readProjectFile('pages/admin/tenants/edit($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $editContent); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT', $editContent); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT', $editContent); - - $customFieldCreate = $this->readProjectFile('pages/admin/tenants/custom-field-create($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE', $customFieldCreate); - - $customFieldUpdate = $this->readProjectFile('pages/admin/tenants/custom-field-update($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE', $customFieldUpdate); - - $customFieldDelete = $this->readProjectFile('pages/admin/tenants/custom-field-delete($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE', $customFieldDelete); - } - - - public function testAdminTenantDeleteUsesCentralPolicy(): void - { - $deleteContent = $this->readProjectFile('pages/admin/tenants/delete($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $deleteContent); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_DELETE', $deleteContent); - } - - - public function testAdminTenantMediaEndpointsUseCentralPolicies(): void - { - $avatarView = $this->readProjectFile('pages/admin/tenants/avatar-file().php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_AVATAR_VIEW', $avatarView); - - $avatarUpload = $this->readProjectFile('pages/admin/tenants/avatar($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $avatarUpload); - - $avatarDelete = $this->readProjectFile('pages/admin/tenants/avatar-delete($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $avatarDelete); - - $faviconUpload = $this->readProjectFile('pages/admin/tenants/favicon($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $faviconUpload); - - $faviconDelete = $this->readProjectFile('pages/admin/tenants/favicon-delete($id).php'); - $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $faviconDelete); - } - - - public function testAdminDepartmentsEditAndDeleteUseCentralPolicies(): void - { - $indexContent = $this->readProjectFile('pages/admin/departments/index().php'); - $this->assertStringContainsString('AuthorizationService::class', $indexContent); - $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $indexContent); - - $dataContent = $this->readProjectFile('pages/admin/departments/data().php'); - $this->assertStringContainsString('Guard::requireAbilityDecisionOrForbidden(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW);', $dataContent); - $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $dataContent); - - $createContent = $this->readProjectFile('pages/admin/departments/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); - $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE', $createContent); - - $editContent = $this->readProjectFile('pages/admin/departments/edit($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $editContent); - $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT', $editContent); - $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT', $editContent); - - $deleteContent = $this->readProjectFile('pages/admin/departments/delete($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $deleteContent); - $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_DELETE', $deleteContent); - } - - - public function testAdminRolesActionsUseCentralPolicies(): void - { - $indexContent = $this->readProjectFile('pages/admin/roles/index().php'); - $this->assertStringContainsString('AuthorizationService::class', $indexContent); - $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $indexContent); - - $dataContent = $this->readProjectFile('pages/admin/roles/data().php'); - $this->assertStringContainsString('Guard::requireAbilityOrForbidden(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW);', $dataContent); - $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $dataContent); - - $createContent = $this->readProjectFile('pages/admin/roles/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); - $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE', $createContent); - - $editContent = $this->readProjectFile('pages/admin/roles/edit($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $editContent); - $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_CONTEXT', $editContent); - $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_SUBMIT', $editContent); - - $deleteContent = $this->readProjectFile('pages/admin/roles/delete($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $deleteContent); - $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_DELETE', $deleteContent); - } - - - public function testAdminPermissionsActionsUseCentralPolicies(): void - { - $indexContent = $this->readProjectFile('pages/admin/permissions/index().php'); - $this->assertStringContainsString('AuthorizationService::class', $indexContent); - $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $indexContent); - - $dataContent = $this->readProjectFile('pages/admin/permissions/data().php'); - $this->assertStringContainsString('Guard::requireAbilityOrForbidden(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW);', $dataContent); - $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $dataContent); - - $createContent = $this->readProjectFile('pages/admin/permissions/create().php'); - $this->assertStringContainsString('AuthorizationService::class', $createContent); - $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE', $createContent); - - $editContent = $this->readProjectFile('pages/admin/permissions/edit($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $editContent); - $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT', $editContent); - $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT', $editContent); - - $deleteContent = $this->readProjectFile('pages/admin/permissions/delete($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $deleteContent); - $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_DELETE', $deleteContent); - } - - - public function testAdminSettingsActionsUseCentralPolicies(): void - { - $indexContent = $this->readProjectFile('pages/admin/settings/index().php'); - $this->assertStringContainsString('AuthorizationService::class', $indexContent); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW', $indexContent); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE', $indexContent); - - $logoUpload = $this->readProjectFile('pages/admin/settings/logo().php'); - $this->assertStringContainsString('AuthorizationService::class', $logoUpload); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $logoUpload); - - $logoDelete = $this->readProjectFile('pages/admin/settings/logo-delete().php'); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $logoDelete); - - $faviconUpload = $this->readProjectFile('pages/admin/settings/favicon().php'); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $faviconUpload); - - $faviconDelete = $this->readProjectFile('pages/admin/settings/favicon-delete().php'); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $faviconDelete); - - $revokeApiTokens = $this->readProjectFile('pages/admin/settings/revoke-api-tokens().php'); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE', $revokeApiTokens); - - $expireRememberTokens = $this->readProjectFile('pages/admin/settings/expire-remember-tokens().php'); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE', $expireRememberTokens); - - $runUserLifecycle = $this->readProjectFile('pages/admin/settings/run-user-lifecycle().php'); - $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_USER_LIFECYCLE_RUN', $runUserLifecycle); - } - - - public function testScheduledJobEditUsesAbilityChecksAndNoPermissionHelper(): void - { - $content = $this->readProjectFile('pages/admin/scheduled-jobs/edit($id).php'); - $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW', $content); - $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_MANAGE', $content); - $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_RUN_NOW', $content); - $this->assertStringNotContainsString("can('jobs.manage')", $content); - $this->assertStringNotContainsString("can('jobs.run_now')", $content); - } - - -} diff --git a/tests/Architecture/AuthzAdminMasterDataContractTest.php b/tests/Architecture/AuthzAdminMasterDataContractTest.php new file mode 100644 index 0000000..796e910 --- /dev/null +++ b/tests/Architecture/AuthzAdminMasterDataContractTest.php @@ -0,0 +1,82 @@ +readProjectFile('pages/admin/departments/index().php'); + $this->assertStringContainsString('AuthorizationService::class', $indexContent); + $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $indexContent); + + $dataContent = $this->readProjectFile('pages/admin/departments/data().php'); + $this->assertStringContainsString('Guard::requireAbilityDecisionOrForbidden(DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW);', $dataContent); + $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $dataContent); + + $createContent = $this->readProjectFile('pages/admin/departments/create().php'); + $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE', $createContent); + + $editContent = $this->readProjectFile('pages/admin/departments/edit($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $editContent); + $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_CONTEXT', $editContent); + $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_EDIT_SUBMIT', $editContent); + + $deleteContent = $this->readProjectFile('pages/admin/departments/delete($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $deleteContent); + $this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_DELETE', $deleteContent); + } + + public function testAdminRolesActionsUseCentralPolicies(): void + { + $indexContent = $this->readProjectFile('pages/admin/roles/index().php'); + $this->assertStringContainsString('AuthorizationService::class', $indexContent); + $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $indexContent); + + $dataContent = $this->readProjectFile('pages/admin/roles/data().php'); + $this->assertStringContainsString('Guard::requireAbilityOrForbidden(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW);', $dataContent); + $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $dataContent); + + $createContent = $this->readProjectFile('pages/admin/roles/create().php'); + $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE', $createContent); + + $editContent = $this->readProjectFile('pages/admin/roles/edit($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $editContent); + $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_CONTEXT', $editContent); + $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_SUBMIT', $editContent); + + $deleteContent = $this->readProjectFile('pages/admin/roles/delete($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $deleteContent); + $this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_DELETE', $deleteContent); + } + + public function testAdminPermissionsActionsUseCentralPolicies(): void + { + $indexContent = $this->readProjectFile('pages/admin/permissions/index().php'); + $this->assertStringContainsString('AuthorizationService::class', $indexContent); + $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $indexContent); + + $dataContent = $this->readProjectFile('pages/admin/permissions/data().php'); + $this->assertStringContainsString('Guard::requireAbilityOrForbidden(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW);', $dataContent); + $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $dataContent); + + $createContent = $this->readProjectFile('pages/admin/permissions/create().php'); + $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE', $createContent); + + $editContent = $this->readProjectFile('pages/admin/permissions/edit($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $editContent); + $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT', $editContent); + $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT', $editContent); + + $deleteContent = $this->readProjectFile('pages/admin/permissions/delete($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $deleteContent); + $this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_DELETE', $deleteContent); + } +} diff --git a/tests/Architecture/AuthzAdminSettingsContractTest.php b/tests/Architecture/AuthzAdminSettingsContractTest.php new file mode 100644 index 0000000..23cf405 --- /dev/null +++ b/tests/Architecture/AuthzAdminSettingsContractTest.php @@ -0,0 +1,50 @@ +readProjectFile('pages/admin/settings/index().php'); + $this->assertStringContainsString('AuthorizationService::class', $indexContent); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW', $indexContent); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE', $indexContent); + + $logoUpload = $this->readProjectFile('pages/admin/settings/logo().php'); + $this->assertStringContainsString('AuthorizationService::class', $logoUpload); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $logoUpload); + + $logoDelete = $this->readProjectFile('pages/admin/settings/logo-delete().php'); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $logoDelete); + + $faviconUpload = $this->readProjectFile('pages/admin/settings/favicon().php'); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $faviconUpload); + + $faviconDelete = $this->readProjectFile('pages/admin/settings/favicon-delete().php'); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_BRANDING_UPDATE', $faviconDelete); + + $revokeApiTokens = $this->readProjectFile('pages/admin/settings/revoke-api-tokens().php'); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE', $revokeApiTokens); + + $expireRememberTokens = $this->readProjectFile('pages/admin/settings/expire-remember-tokens().php'); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_TOKENS_REVOKE', $expireRememberTokens); + + $runUserLifecycle = $this->readProjectFile('pages/admin/settings/run-user-lifecycle().php'); + $this->assertStringContainsString('SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_USER_LIFECYCLE_RUN', $runUserLifecycle); + } + + public function testScheduledJobEditUsesAbilityChecksAndNoPermissionHelper(): void + { + $content = $this->readProjectFile('pages/admin/scheduled-jobs/edit($id).php'); + $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW', $content); + $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_MANAGE', $content); + $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_RUN_NOW', $content); + $this->assertStringNotContainsString("can('jobs.manage')", $content); + $this->assertStringNotContainsString("can('jobs.run_now')", $content); + } +} diff --git a/tests/Architecture/AuthzAdminTenantsContractTest.php b/tests/Architecture/AuthzAdminTenantsContractTest.php new file mode 100644 index 0000000..36e8523 --- /dev/null +++ b/tests/Architecture/AuthzAdminTenantsContractTest.php @@ -0,0 +1,64 @@ +readProjectFile('pages/admin/tenants/index().php'); + $this->assertStringContainsString('AuthorizationService::class', $indexContent); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $indexContent); + + $dataContent = $this->readProjectFile('pages/admin/tenants/data().php'); + $this->assertStringContainsString('Guard::requireAbilityDecisionOrForbidden(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW);', $dataContent); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $dataContent); + + $createContent = $this->readProjectFile('pages/admin/tenants/create().php'); + $this->assertStringContainsString('AuthorizationService::class', $createContent); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE', $createContent); + + $editContent = $this->readProjectFile('pages/admin/tenants/edit($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $editContent); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT', $editContent); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT', $editContent); + + $customFieldCreate = $this->readProjectFile('pages/admin/tenants/custom-field-create($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE', $customFieldCreate); + + $customFieldUpdate = $this->readProjectFile('pages/admin/tenants/custom-field-update($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE', $customFieldUpdate); + + $customFieldDelete = $this->readProjectFile('pages/admin/tenants/custom-field-delete($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE', $customFieldDelete); + } + + public function testAdminTenantDeleteUsesCentralPolicy(): void + { + $deleteContent = $this->readProjectFile('pages/admin/tenants/delete($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $deleteContent); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_DELETE', $deleteContent); + } + + public function testAdminTenantMediaEndpointsUseCentralPolicies(): void + { + $avatarView = $this->readProjectFile('pages/admin/tenants/avatar-file().php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_AVATAR_VIEW', $avatarView); + + $avatarUpload = $this->readProjectFile('pages/admin/tenants/avatar($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $avatarUpload); + + $avatarDelete = $this->readProjectFile('pages/admin/tenants/avatar-delete($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $avatarDelete); + + $faviconUpload = $this->readProjectFile('pages/admin/tenants/favicon($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $faviconUpload); + + $faviconDelete = $this->readProjectFile('pages/admin/tenants/favicon-delete($id).php'); + $this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE', $faviconDelete); + } +} diff --git a/tests/Architecture/AuthzAdminUsersContractTest.php b/tests/Architecture/AuthzAdminUsersContractTest.php new file mode 100644 index 0000000..573e905 --- /dev/null +++ b/tests/Architecture/AuthzAdminUsersContractTest.php @@ -0,0 +1,98 @@ +readProjectFile('pages/admin/users/data().php'); + $this->assertStringContainsString('Guard::requireLogin();', $content); + $this->assertStringContainsString('Guard::requireAbilityOrForbidden(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW);', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $content); + } + + public function testUsersExportActionRequiresUsersViewPermission(): void + { + $content = $this->readProjectFile('pages/admin/users/export().php'); + $this->assertStringContainsString('Guard::requireLogin();', $content); + $this->assertStringContainsString('AuthorizationService::class', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $content); + } + + public function testUsersActivateActionRequiresUsersUpdatePermission(): void + { + $content = $this->readProjectFile('pages/admin/users/activate($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE', $content); + } + + public function testUsersDeactivateActionRequiresUsersUpdatePermission(): void + { + $content = $this->readProjectFile('pages/admin/users/deactivate($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DEACTIVATE', $content); + } + + public function testUsersBulkActionPermissionMappingIsConsistent(): void + { + $content = $this->readProjectFile('pages/admin/users/bulk($action).php'); + $this->assertStringContainsString('AuthorizationService::class', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_BULK', $content); + $this->assertStringContainsString("'bulk_action' => \$action", $content); + } + + public function testAdditionalAdminUserActionsUseCentralPolicy(): void + { + $indexContent = $this->readProjectFile('pages/admin/users/index().php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $indexContent); + + $createContent = $this->readProjectFile('pages/admin/users/create().php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE', $createContent); + + $deleteContent = $this->readProjectFile('pages/admin/users/delete($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DELETE', $deleteContent); + + $accessPdfContent = $this->readProjectFile('pages/admin/users/access-pdf($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF', $accessPdfContent); + + $accessPdfBulkContent = $this->readProjectFile('pages/admin/users/access-pdf-bulk().php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK', $accessPdfBulkContent); + + $tokenCreateContent = $this->readProjectFile('pages/admin/users/api-token-create($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE', $tokenCreateContent); + + $tokenRevokeContent = $this->readProjectFile('pages/admin/users/api-token-revoke($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE', $tokenRevokeContent); + + $sendAccessContent = $this->readProjectFile('pages/admin/users/send-access($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_SEND_ACCESS', $sendAccessContent); + + $forgetTokensContent = $this->readProjectFile('pages/admin/users/forget-tokens($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_FORGET_TOKENS', $forgetTokensContent); + } + + public function testAdminUserEditUsesContextAndSubmitPolicies(): void + { + $content = $this->readProjectFile('pages/admin/users/edit($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT', $content); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT', $content); + } + + public function testAdminUserAvatarEndpointsUseCentralPolicies(): void + { + $uploadContent = $this->readProjectFile('pages/admin/users/avatar($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_UPLOAD', $uploadContent); + + $deleteContent = $this->readProjectFile('pages/admin/users/avatar-delete($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_DELETE', $deleteContent); + + $viewContent = $this->readProjectFile('pages/admin/users/avatar-file().php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW', $viewContent); + } +} diff --git a/tests/Architecture/AuthzApiBootstrapContractTest.php b/tests/Architecture/AuthzApiBootstrapContractTest.php new file mode 100644 index 0000000..589f260 --- /dev/null +++ b/tests/Architecture/AuthzApiBootstrapContractTest.php @@ -0,0 +1,76 @@ +readProjectFile('pages/api/v1/auth/login().php'); + $this->assertStringContainsString('ApiBootstrap::init(false);', $content); + } + + public function testApiBootstrapSupportsOptionalAuthRequirement(): void + { + $content = $this->readProjectFile('lib/Http/ApiBootstrap.php'); + $this->assertStringContainsString('public static function init(bool $requireAuth = true): void', $content); + $this->assertStringContainsString('if ($requireAuth) {', $content); + $this->assertStringContainsString('$authenticated = ApiAuth::authenticate();', $content); + $this->assertStringContainsString('ApiResponse::unauthorized();', $content); + } + + public function testApiUsersEndpointsUseCentralUserPolicy(): void + { + $indexContent = $this->readProjectFile('pages/api/v1/users/index().php'); + $this->assertStringContainsString('AuthorizationService::class', $indexContent); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_GET', $indexContent); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST', $indexContent); + + $showContent = $this->readProjectFile('pages/api/v1/users/show($id).php'); + $this->assertStringContainsString('AuthorizationService::class', $showContent); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET', $showContent); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_UPDATE', $showContent); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_DELETE', $showContent); + } + + public function testApiPermissionAndSettingsEndpointsHaveExpectedGuards(): void + { + $permissionContent = $this->readProjectFile('pages/api/v1/permissions/index().php'); + $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_PERMISSIONS_VIEW);', $permissionContent); + $this->assertStringContainsString("foreach ((\$result['rows'] ?? []) as \$row)", $permissionContent); + + $settingsContent = $this->readProjectFile('pages/api/v1/settings/index().php'); + $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_SETTINGS_VIEW);', $settingsContent); + $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_SETTINGS_UPDATE);', $settingsContent); + } + + public function testApiMeEndpointsUseSelfServicePermissions(): void + { + $meContent = $this->readProjectFile('pages/api/v1/me/index().php'); + $this->assertStringContainsString('$method === \'PUT\' || $method === \'PATCH\'', $meContent); + $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE', $meContent); + $this->assertStringContainsString('ApiResponse::validationFromFormErrors(', $meContent); + + $passwordContent = $this->readProjectFile('pages/api/v1/me/password().php'); + $this->assertStringContainsString("ApiResponse::requireMethod('POST');", $passwordContent); + $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE);', $passwordContent); + + $avatarContent = $this->readProjectFile('pages/api/v1/me/avatar().php'); + $this->assertStringContainsString("define('MINTY_ALLOW_OUTPUT', true);", $avatarContent); + $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE', $avatarContent); + } + + public function testApiAvatarEndpointsUseExpectedAuthorizationModel(): void + { + $userAvatarContent = $this->readProjectFile('pages/api/v1/users/avatar($id).php'); + $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET', $userAvatarContent); + + $tenantAvatarContent = $this->readProjectFile('pages/api/v1/tenants/avatar($id).php'); + $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_API_TENANTS_VIEW', $tenantAvatarContent); + $this->assertStringContainsString("ApiAuth::requireResourceAccess('tenants', \$tenantId);", $tenantAvatarContent); + } +} diff --git a/tests/Architecture/AuthzApiContractTest.php b/tests/Architecture/AuthzApiContractTest.php deleted file mode 100644 index 11c1c74..0000000 --- a/tests/Architecture/AuthzApiContractTest.php +++ /dev/null @@ -1,192 +0,0 @@ -readProjectFile('pages/api/v1/auth/login().php'); - $this->assertStringContainsString('ApiBootstrap::init(false);', $content); - } - - - public function testApiBootstrapSupportsOptionalAuthRequirement(): void - { - $content = $this->readProjectFile('lib/Http/ApiBootstrap.php'); - $this->assertStringContainsString('public static function init(bool $requireAuth = true): void', $content); - $this->assertStringContainsString('if ($requireAuth) {', $content); - $this->assertStringContainsString('$authenticated = ApiAuth::authenticate();', $content); - $this->assertStringContainsString('ApiResponse::unauthorized();', $content); - } - - - public function testApiUsersEndpointsUseCentralUserPolicy(): void - { - $indexContent = $this->readProjectFile('pages/api/v1/users/index().php'); - $this->assertStringContainsString('AuthorizationService::class', $indexContent); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_GET', $indexContent); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST', $indexContent); - - $showContent = $this->readProjectFile('pages/api/v1/users/show($id).php'); - $this->assertStringContainsString('AuthorizationService::class', $showContent); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET', $showContent); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_UPDATE', $showContent); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_DELETE', $showContent); - } - - - public function testApiPermissionAndSettingsEndpointsHaveExpectedGuards(): void - { - $permissionContent = $this->readProjectFile('pages/api/v1/permissions/index().php'); - $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_PERMISSIONS_VIEW);', $permissionContent); - $this->assertStringContainsString("foreach ((\$result['rows'] ?? []) as \$row)", $permissionContent); - - $settingsContent = $this->readProjectFile('pages/api/v1/settings/index().php'); - $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_SETTINGS_VIEW);', $settingsContent); - $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_SETTINGS_UPDATE);', $settingsContent); - } - - - public function testApiMeEndpointsUseSelfServicePermissions(): void - { - $meContent = $this->readProjectFile('pages/api/v1/me/index().php'); - $this->assertStringContainsString('$method === \'PUT\' || $method === \'PATCH\'', $meContent); - $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE', $meContent); - $this->assertStringContainsString('ApiResponse::validationFromFormErrors(', $meContent); - - $passwordContent = $this->readProjectFile('pages/api/v1/me/password().php'); - $this->assertStringContainsString("ApiResponse::requireMethod('POST');", $passwordContent); - $this->assertStringContainsString('ApiResponse::requireAbility(\\MintyPHP\\Service\\Access\\OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE);', $passwordContent); - - $avatarContent = $this->readProjectFile('pages/api/v1/me/avatar().php'); - $this->assertStringContainsString("define('MINTY_ALLOW_OUTPUT', true);", $avatarContent); - $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_API_ME_SELF_UPDATE', $avatarContent); - } - - - public function testApiAvatarEndpointsUseExpectedAuthorizationModel(): void - { - $userAvatarContent = $this->readProjectFile('pages/api/v1/users/avatar($id).php'); - $this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET', $userAvatarContent); - - $tenantAvatarContent = $this->readProjectFile('pages/api/v1/tenants/avatar($id).php'); - $this->assertStringContainsString('OperationsAuthorizationPolicy::ABILITY_API_TENANTS_VIEW', $tenantAvatarContent); - $this->assertStringContainsString("ApiAuth::requireResourceAccess('tenants', \$tenantId);", $tenantAvatarContent); - } - - - public function testApiTenantShowExposesFullTenantMasterData(): void - { - $tenantShowContent = $this->readProjectFile('pages/api/v1/tenants/show($id).php'); - $this->assertStringContainsString("'region' => \$tenant['region'] ?? ''", $tenantShowContent); - $this->assertStringContainsString("'vat_id' => \$tenant['vat_id'] ?? ''", $tenantShowContent); - $this->assertStringContainsString("'support_email' => \$tenant['support_email'] ?? ''", $tenantShowContent); - $this->assertStringContainsString("'billing_email' => \$tenant['billing_email'] ?? ''", $tenantShowContent); - $this->assertStringContainsString("'primary_color_use_default' => \$primaryColor === ''", $tenantShowContent); - $this->assertStringContainsString("'allow_user_theme_mode' => \$allowUserThemeMode", $tenantShowContent); - - $openApiContent = $this->readProjectFile('docs/openapi.yaml'); - $this->assertStringContainsString('TenantShowResponse:', $openApiContent); - $this->assertStringContainsString('primary_color_use_default:', $openApiContent); - $this->assertStringContainsString('allow_user_theme_mode:', $openApiContent); - $this->assertStringContainsString('support_email:', $openApiContent); - } - - - public function testApiUserWriteEndpointsUseUuidAssignmentInput(): void - { - $usersIndex = $this->readProjectFile('pages/api/v1/users/index().php'); - $this->assertStringContainsString('UserApiWriteInputMapper::class', $usersIndex); - $this->assertStringContainsString('->normalize($input)', $usersIndex); - - $usersShow = $this->readProjectFile('pages/api/v1/users/show($id).php'); - $this->assertStringContainsString('UserApiWriteInputMapper::class', $usersShow); - $this->assertStringContainsString('->normalize($input)', $usersShow); - - $mapper = $this->readProjectFile('lib/Service/User/UserApiWriteInputMapper.php'); - $this->assertStringContainsString("'tenant_ids' => 'use_tenant_uuids'", $mapper); - $this->assertStringContainsString("'role_ids' => 'use_role_uuids'", $mapper); - $this->assertStringContainsString("'department_ids' => 'use_department_uuids'", $mapper); - $this->assertStringContainsString("'primary_tenant_id' => 'use_primary_tenant_uuid'", $mapper); - } - - - public function testApiDepartmentEndpointsUseTenantUuidAndNoTenantIdExposure(): void - { - $departmentIndex = $this->readProjectFile('pages/api/v1/departments/index().php'); - $this->assertStringContainsString("ApiResponse::validationFromFormErrors(formErrorsFrom(['tenant_id' => ['use_tenant_uuid']]));", $departmentIndex); - $this->assertStringContainsString("if (array_key_exists('tenant_uuid', \$input)) {", $departmentIndex); - - $departmentShow = $this->readProjectFile('pages/api/v1/departments/show($id).php'); - $this->assertStringContainsString("'tenant_uuid' => \$tenantUuid", $departmentShow); - $this->assertStringNotContainsString("'tenant_id' => \$department['tenant_id'] ?? null", $departmentShow); - $this->assertStringContainsString("'cost_center' => \$department['cost_center'] ?? ''", $departmentShow); - } - - - public function testApiRoleShowIncludesPermissionKeys(): void - { - $roleShow = $this->readProjectFile('pages/api/v1/roles/show($id).php'); - $this->assertStringContainsString('RolePermissionRepository::class', $roleShow); - $this->assertStringContainsString('listPermissionKeysByRoleIds([$roleId])', $roleShow); - $this->assertStringContainsString("'permission_keys' => \$permissionKeys", $roleShow); - } - - - public function testOpenApiMarksLoginAsPublic(): void - { - $content = $this->readProjectFile('docs/openapi.yaml'); - $this->assertMatchesRegularExpression('/\/auth\/login:\s+post:.*?security:\s*\[\]/s', $content); - $this->assertStringContainsString('no Authorization header required', $content); - } - - - public function testOpenApiIncludesNewCriticalFrontendEndpoints(): void - { - $content = $this->readProjectFile('docs/openapi.yaml'); - $this->assertStringContainsString('/permissions:', $content); - $this->assertStringContainsString('/me/password:', $content); - $this->assertStringContainsString('/me/avatar:', $content); - $this->assertStringContainsString('/users/avatar/{uuid}:', $content); - $this->assertStringContainsString('/tenants/avatar/{uuid}:', $content); - $this->assertStringContainsString('/settings:', $content); - $this->assertStringContainsString('/settings/public:', $content); - } - - - public function testOpenApiUsesUuidBasedMasterDataContracts(): void - { - $content = $this->readProjectFile('docs/openapi.yaml'); - $this->assertStringContainsString('required: [description, tenant_uuid]', $content); - $this->assertStringContainsString('tenant_uuids:', $content); - $this->assertStringContainsString('primary_tenant_uuid:', $content); - $this->assertStringContainsString('role_uuids:', $content); - $this->assertStringContainsString('department_uuids:', $content); - $this->assertStringContainsString('permission_keys:', $content); - $this->assertStringContainsString('tenant_uuid:', $content); - } - - - public function testApiErrorEnvelopeIsCentralizedAndRequestIdAware(): void - { - $apiResponse = $this->readProjectFile('lib/Http/ApiResponse.php'); - $this->assertStringContainsString("'ok' => false", $apiResponse); - $this->assertStringContainsString("'error_code' => \$normalizedErrorCode", $apiResponse); - $this->assertStringContainsString("'details' => \$normalizedDetails", $apiResponse); - $this->assertStringContainsString("header('X-Request-Id: ' . \$requestId);", $apiResponse); - $this->assertStringNotContainsString("'error' => \$normalizedErrorCode", $apiResponse); - $this->assertStringNotContainsString("\$body['errors']", $apiResponse); - - $openApi = $this->readProjectFile('docs/openapi.yaml'); - $this->assertStringContainsString('required: [ok, request_id, error_code, details]', $openApi); - $this->assertStringNotContainsString('Legacy alias of `error_code`', $openApi); - $this->assertStringNotContainsString('Legacy alias of `details.errors`', $openApi); - } - - -} diff --git a/tests/Architecture/AuthzUiActionContractTest.php b/tests/Architecture/AuthzUiActionContractTest.php new file mode 100644 index 0000000..0a31a0b --- /dev/null +++ b/tests/Architecture/AuthzUiActionContractTest.php @@ -0,0 +1,97 @@ +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); + } + } +} diff --git a/tests/Architecture/AuthzUiContractSupport.php b/tests/Architecture/AuthzUiContractSupport.php new file mode 100644 index 0000000..f11d649 --- /dev/null +++ b/tests/Architecture/AuthzUiContractSupport.php @@ -0,0 +1,124 @@ + + */ + 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; + } +} diff --git a/tests/Architecture/AuthzUiContractTest.php b/tests/Architecture/AuthzUiContractTest.php deleted file mode 100644 index bda85b9..0000000 --- a/tests/Architecture/AuthzUiContractTest.php +++ /dev/null @@ -1,357 +0,0 @@ -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; - } - - -} diff --git a/tests/Architecture/AuthzUiLayoutContractTest.php b/tests/Architecture/AuthzUiLayoutContractTest.php new file mode 100644 index 0000000..f7cf3a0 --- /dev/null +++ b/tests/Architecture/AuthzUiLayoutContractTest.php @@ -0,0 +1,49 @@ +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 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); + } +} diff --git a/tests/Architecture/AuthzUiTemplateContractTest.php b/tests/Architecture/AuthzUiTemplateContractTest.php new file mode 100644 index 0000000..056dca4 --- /dev/null +++ b/tests/Architecture/AuthzUiTemplateContractTest.php @@ -0,0 +1,102 @@ +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 testStatsTemplateDefinesAuditTabAndPanel(): void + { + $content = $this->readProjectFile('pages/admin/stats/index(default).phtml'); + $this->assertStringContainsString('data-tab="audit"', $content); + $this->assertStringContainsString('data-tab-panel="audit"', $content); + } +} diff --git a/tests/Architecture/ListActionContractTest.php b/tests/Architecture/ListActionContractTest.php new file mode 100644 index 0000000..1f71b10 --- /dev/null +++ b/tests/Architecture/ListActionContractTest.php @@ -0,0 +1,21 @@ +listIndexActions() as $file) { + $content = $this->readProjectFile($file); + $this->assertStringContainsString('$searchToolbarFilterSchema', $content, $file); + $this->assertStringContainsString('$drawerToolbarFilterSchema', $content, $file); + $this->assertStringContainsString('$filterChipMeta', $content, $file); + } + } +} diff --git a/tests/Architecture/ListContractFiles.php b/tests/Architecture/ListContractFiles.php new file mode 100644 index 0000000..11d7942 --- /dev/null +++ b/tests/Architecture/ListContractFiles.php @@ -0,0 +1,160 @@ +readProjectFile($templateFile); + $matched = preg_match( + "/assetVersion\\('((?:modules\\/[^\\/]+\\/)?js\\/pages\\/[^']+\\.js)'\\)/", + $template, + $captures + ); + $this->assertSame(1, $matched, $templateFile . ' must reference a page module via assetVersion().'); + + $assetPath = ltrim($captures[1], '/'); + $webPath = 'web/' . $assetPath; + $root = $this->projectRootPath(); + + if (is_file($root . '/' . $webPath)) { + return $this->readProjectFile($webPath); + } + + if (str_starts_with($assetPath, 'modules/')) { + $sourcePath = preg_replace('#^modules/([^/]+)/#', 'modules/$1/web/', $assetPath, 1); + $this->assertIsString($sourcePath, 'Could not resolve module asset path for ' . $templateFile); + + return $this->readProjectFile($sourcePath); + } + + return $this->readProjectFile($webPath); + } + + private function usesSharedListFiltersPartial(string $content): bool + { + return str_contains($content, "require templatePath('partials/app-list-filters.phtml');"); + } + + /** + * @return list + */ + private function hardCutGridEndpointFiles(): array + { + return [ + 'modules/addressbook/pages/address-book/data().php', + 'pages/search/data().php', + 'pages/admin/users/data().php', + 'pages/admin/tenants/data().php', + 'pages/admin/departments/data().php', + 'pages/admin/roles/data().php', + 'pages/admin/permissions/data().php', + 'pages/admin/mail-log/data().php', + 'pages/admin/scheduled-jobs/data().php', + 'pages/admin/scheduled-jobs/runs-data($id).php', + 'pages/admin/api-audit/data().php', + 'pages/admin/import-audit/data().php', + 'pages/admin/user-lifecycle-audit/data().php', + 'pages/admin/system-audit/data().php', + ]; + } + + /** + * @return array + */ + private function hardCutGridEndpointSchemaFiles(): array + { + return [ + 'modules/addressbook/pages/address-book/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/search/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/users/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/tenants/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/departments/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/roles/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/permissions/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/mail-log/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/scheduled-jobs/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/scheduled-jobs/runs-data($id).php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/runs-filter-schema.php'", + 'pages/admin/api-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/import-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/user-lifecycle-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + 'pages/admin/system-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", + ]; + } + + /** + * @return list + */ + private function listIndexTemplates(): array + { + return [ + 'modules/addressbook/pages/address-book/index(default).phtml', + 'pages/search/index(default).phtml', + 'pages/admin/users/index(default).phtml', + 'pages/admin/tenants/index(default).phtml', + 'pages/admin/departments/index(default).phtml', + 'pages/admin/roles/index(default).phtml', + 'pages/admin/permissions/index(default).phtml', + 'pages/admin/mail-log/index(default).phtml', + 'pages/admin/scheduled-jobs/index(default).phtml', + 'pages/admin/api-audit/index(default).phtml', + 'pages/admin/import-audit/index(default).phtml', + 'pages/admin/user-lifecycle-audit/index(default).phtml', + 'pages/admin/system-audit/index(default).phtml', + ]; + } + + /** + * @return list + */ + private function listIndexActions(): array + { + return [ + 'modules/addressbook/pages/address-book/index().php', + 'pages/search/index().php', + 'pages/admin/users/index().php', + 'pages/admin/tenants/index().php', + 'pages/admin/departments/index().php', + 'pages/admin/roles/index().php', + 'pages/admin/permissions/index().php', + 'pages/admin/mail-log/index().php', + 'pages/admin/scheduled-jobs/index().php', + 'pages/admin/api-audit/index().php', + 'pages/admin/import-audit/index().php', + 'pages/admin/user-lifecycle-audit/index().php', + 'pages/admin/system-audit/index().php', + ]; + } + + /** + * @return list + */ + private function drawerListTemplates(): array + { + return [ + 'modules/addressbook/pages/address-book/index(default).phtml', + 'pages/admin/users/index(default).phtml', + 'pages/admin/tenants/index(default).phtml', + 'pages/admin/departments/index(default).phtml', + 'pages/admin/roles/index(default).phtml', + 'pages/admin/permissions/index(default).phtml', + 'pages/admin/mail-log/index(default).phtml', + 'pages/admin/scheduled-jobs/index(default).phtml', + 'pages/admin/api-audit/index(default).phtml', + 'pages/admin/import-audit/index(default).phtml', + 'pages/admin/user-lifecycle-audit/index(default).phtml', + 'pages/admin/system-audit/index(default).phtml', + ]; + } + + /** + * @return list + */ + private function searchOnlyTemplates(): array + { + return [ + 'pages/search/index(default).phtml', + ]; + } +} diff --git a/tests/Architecture/ListDataEndpointContractTest.php b/tests/Architecture/ListDataEndpointContractTest.php new file mode 100644 index 0000000..b76c9b5 --- /dev/null +++ b/tests/Architecture/ListDataEndpointContractTest.php @@ -0,0 +1,106 @@ +hardCutGridEndpointFiles(); + $this->assertCount(14, $files, 'Unexpected number of hard-cut grid data endpoints.'); + $this->assertNotContains('pages/admin/search/data().php', $files); + + foreach ($files as $file) { + $content = $this->readProjectFile($file); + $this->assertStringContainsString('gridRequireGetRequest();', $content, $file); + $this->assertStringNotContainsString("strtoupper((string) (requestInput()->method())) !== 'GET'", $content, $file); + } + } + + public function testDataEndpointsDoNotInlineQueryAllIndexParsing(): void + { + foreach ($this->hardCutGridEndpointFiles() as $file) { + $content = $this->readProjectFile($file); + $this->assertStringNotContainsString("requestInput()->queryAll()['", $content, $file); + $this->assertStringContainsString('gridParseFiltersFromSchemaFile', $content, $file); + } + } + + public function testDataEndpointsDoNotUseLegacySingleIdAliases(): void + { + foreach ($this->hardCutGridEndpointFiles() as $file) { + $content = $this->readProjectFile($file); + $this->assertSame( + 0, + preg_match('/\?\?\s*\([^\n]*\[[\"\"][a-z_]+_id[\"\"]\]/', $content), + $file + ); + } + } + + public function testAuditRepositoriesDoNotUseLegacySingleIdAliasFallbacks(): void + { + $files = [ + 'lib/Repository/Audit/ApiAuditLogRepository.php', + 'lib/Repository/Audit/ImportAuditRunRepository.php', + 'lib/Repository/Audit/UserLifecycleAuditRepository.php', + 'lib/Repository/Audit/SystemAuditLogRepository.php', + ]; + + foreach ($files as $file) { + $content = $this->readProjectFile($file); + $this->assertSame( + 0, + preg_match('/\[[\"\"][a-z_]+_ids[\"\"]\]\s*\?\?\s*\(\$filters\[[\"\"][a-z_]+_id[\"\"]\]/', $content), + $file + ); + } + } + + public function testDataEndpointsUseDirectoryFilterSchema(): void + { + foreach ($this->hardCutGridEndpointSchemaFiles() as $file => $expectedHelperCall) { + $content = $this->readProjectFile($file); + $this->assertStringContainsString($expectedHelperCall, $content, $file); + + $schemaFile = str_contains($expectedHelperCall, '/runs-filter-schema.php') + ? dirname($file) . '/runs-filter-schema.php' + : dirname($file) . '/filter-schema.php'; + $schemaContent = $this->readProjectFile($schemaFile); + $this->assertStringContainsString('gridFilterSchema', $schemaContent, $schemaFile); + } + } + + public function testDataEndpointsUseUnifiedGuardAbilityPatternExceptSearchResults(): void + { + foreach ($this->hardCutGridEndpointFiles() as $file) { + $content = $this->readProjectFile($file); + if ($file === 'pages/search/data().php') { + $this->assertStringContainsString('Guard::requireLogin();', $content, $file); + $this->assertStringNotContainsString('Guard::requireAbility', $content, $file); + continue; + } + + $this->assertMatchesRegularExpression( + '/Guard::requireAbility(?:OrForbidden|DecisionOrForbidden)\(/', + $content, + $file + ); + $this->assertStringNotContainsString('->authorize(', $content, $file); + } + } + + public function testDataEndpointsUseUnifiedDataAndTotalResponseHelper(): void + { + foreach ($this->hardCutGridEndpointFiles() as $file) { + $content = $this->readProjectFile($file); + $this->assertStringContainsString('gridJsonDataResult(', $content, $file); + $this->assertStringNotContainsString('Router::json($result);', $content, $file); + } + } +} diff --git a/tests/Architecture/ListFilterContractTest.php b/tests/Architecture/ListFilterContractTest.php deleted file mode 100644 index b347947..0000000 --- a/tests/Architecture/ListFilterContractTest.php +++ /dev/null @@ -1,303 +0,0 @@ -readProjectFile($templateFile); - $matched = preg_match( - "/assetVersion\\('((?:modules\\/[^\\/]+\\/)?js\\/pages\\/[^']+\\.js)'\\)/", - $template, - $captures - ); - $this->assertSame(1, $matched, $templateFile . ' must reference a page module via assetVersion().'); - - return $this->readProjectFile('web/' . ltrim($captures[1], '/')); - } - - private function usesSharedListFiltersPartial(string $content): bool - { - return str_contains($content, "require templatePath('partials/app-list-filters.phtml');"); - } - - /** - * @return list - */ - private function hardCutGridEndpointFiles(): array - { - return [ - 'modules/addressbook/pages/address-book/data().php', - 'pages/search/data().php', - 'pages/admin/users/data().php', - 'pages/admin/tenants/data().php', - 'pages/admin/departments/data().php', - 'pages/admin/roles/data().php', - 'pages/admin/permissions/data().php', - 'pages/admin/mail-log/data().php', - 'pages/admin/scheduled-jobs/data().php', - 'pages/admin/scheduled-jobs/runs-data($id).php', - 'pages/admin/api-audit/data().php', - 'pages/admin/import-audit/data().php', - 'pages/admin/user-lifecycle-audit/data().php', - 'pages/admin/system-audit/data().php', - ]; - } - - /** - * @return array - */ - private function hardCutGridEndpointSchemaFiles(): array - { - return [ - 'modules/addressbook/pages/address-book/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/search/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/users/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/tenants/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/departments/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/roles/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/permissions/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/mail-log/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/scheduled-jobs/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/scheduled-jobs/runs-data($id).php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/runs-filter-schema.php'", - 'pages/admin/api-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/import-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/user-lifecycle-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - 'pages/admin/system-audit/data().php' => "gridParseFiltersFromSchemaFile(__DIR__ . '/filter-schema.php'", - ]; - } - - /** - * @return list - */ - private function listIndexTemplates(): array - { - return [ - 'modules/addressbook/pages/address-book/index(default).phtml', - 'pages/search/index(default).phtml', - 'pages/admin/users/index(default).phtml', - 'pages/admin/tenants/index(default).phtml', - 'pages/admin/departments/index(default).phtml', - 'pages/admin/roles/index(default).phtml', - 'pages/admin/permissions/index(default).phtml', - 'pages/admin/mail-log/index(default).phtml', - 'pages/admin/scheduled-jobs/index(default).phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', - ]; - } - - /** - * @return list - */ - private function listIndexActions(): array - { - return [ - 'modules/addressbook/pages/address-book/index().php', - 'pages/search/index().php', - 'pages/admin/users/index().php', - 'pages/admin/tenants/index().php', - 'pages/admin/departments/index().php', - 'pages/admin/roles/index().php', - 'pages/admin/permissions/index().php', - 'pages/admin/mail-log/index().php', - 'pages/admin/scheduled-jobs/index().php', - 'pages/admin/api-audit/index().php', - 'pages/admin/import-audit/index().php', - 'pages/admin/user-lifecycle-audit/index().php', - 'pages/admin/system-audit/index().php', - ]; - } - - /** - * @return list - */ - private function drawerListTemplates(): array - { - return [ - 'modules/addressbook/pages/address-book/index(default).phtml', - 'pages/admin/users/index(default).phtml', - 'pages/admin/tenants/index(default).phtml', - 'pages/admin/departments/index(default).phtml', - 'pages/admin/roles/index(default).phtml', - 'pages/admin/permissions/index(default).phtml', - 'pages/admin/mail-log/index(default).phtml', - 'pages/admin/scheduled-jobs/index(default).phtml', - 'pages/admin/api-audit/index(default).phtml', - 'pages/admin/import-audit/index(default).phtml', - 'pages/admin/user-lifecycle-audit/index(default).phtml', - 'pages/admin/system-audit/index(default).phtml', - ]; - } - - /** - * @return list - */ - private function searchOnlyTemplates(): array - { - return [ - 'pages/search/index(default).phtml', - ]; - } - - public function testDataEndpointsRequireGetGuard(): void - { - $files = $this->hardCutGridEndpointFiles(); - $this->assertCount(14, $files, 'Unexpected number of hard-cut grid data endpoints.'); - $this->assertNotContains('pages/admin/search/data().php', $files); - - foreach ($files as $file) { - $content = $this->readProjectFile($file); - $this->assertStringContainsString('gridRequireGetRequest();', $content, $file); - $this->assertStringNotContainsString("strtoupper((string) (requestInput()->method())) !== 'GET'", $content, $file); - } - } - - public function testDataEndpointsDoNotInlineQueryAllIndexParsing(): void - { - foreach ($this->hardCutGridEndpointFiles() as $file) { - $content = $this->readProjectFile($file); - $this->assertStringNotContainsString("requestInput()->queryAll()['", $content, $file); - $this->assertStringContainsString('gridParseFiltersFromSchemaFile', $content, $file); - } - } - - public function testDataEndpointsDoNotUseLegacySingleIdAliases(): void - { - foreach ($this->hardCutGridEndpointFiles() as $file) { - $content = $this->readProjectFile($file); - $this->assertSame( - 0, - preg_match('/\?\?\s*\([^\n]*\[[\"\"][a-z_]+_id[\"\"]\]/', $content), - $file - ); - } - } - - public function testAuditRepositoriesDoNotUseLegacySingleIdAliasFallbacks(): void - { - $files = [ - 'lib/Repository/Audit/ApiAuditLogRepository.php', - 'lib/Repository/Audit/ImportAuditRunRepository.php', - 'lib/Repository/Audit/UserLifecycleAuditRepository.php', - 'lib/Repository/Audit/SystemAuditLogRepository.php', - ]; - - foreach ($files as $file) { - $content = $this->readProjectFile($file); - $this->assertSame( - 0, - preg_match('/\[[\"\"][a-z_]+_ids[\"\"]\]\s*\?\?\s*\(\$filters\[[\"\"][a-z_]+_id[\"\"]\]/', $content), - $file - ); - } - } - - public function testDataEndpointsUseDirectoryFilterSchema(): void - { - foreach ($this->hardCutGridEndpointSchemaFiles() as $file => $expectedHelperCall) { - $content = $this->readProjectFile($file); - $this->assertStringContainsString($expectedHelperCall, $content, $file); - - $schemaFile = str_contains($expectedHelperCall, '/runs-filter-schema.php') - ? dirname($file) . '/runs-filter-schema.php' - : dirname($file) . '/filter-schema.php'; - $schemaContent = $this->readProjectFile($schemaFile); - $this->assertStringContainsString('gridFilterSchema', $schemaContent, $schemaFile); - } - } - - public function testDataEndpointsUseUnifiedGuardAbilityPatternExceptSearchResults(): void - { - foreach ($this->hardCutGridEndpointFiles() as $file) { - $content = $this->readProjectFile($file); - if ($file === 'pages/search/data().php') { - $this->assertStringContainsString('Guard::requireLogin();', $content, $file); - $this->assertStringNotContainsString('Guard::requireAbility', $content, $file); - continue; - } - - $this->assertMatchesRegularExpression( - '/Guard::requireAbility(?:OrForbidden|DecisionOrForbidden)\(/', - $content, - $file - ); - $this->assertStringNotContainsString('->authorize(', $content, $file); - } - } - - public function testDataEndpointsUseUnifiedDataAndTotalResponseHelper(): void - { - foreach ($this->hardCutGridEndpointFiles() as $file) { - $content = $this->readProjectFile($file); - $this->assertStringContainsString('gridJsonDataResult(', $content, $file); - $this->assertStringNotContainsString('Router::json($result);', $content, $file); - } - } - - public function testListTemplatesUseCentralToolbarRendererAndStandardListWrapper(): void - { - foreach ($this->listIndexTemplates() as $file) { - $content = $this->readProjectFile($file); - $moduleContent = $this->readTemplatePageModule($file); - $usesPartial = $this->usesSharedListFiltersPartial($content); - $this->assertTrue( - $usesPartial || str_contains($content, 'renderGridFilterToolbar('), - $file . ' does not render toolbar directly and does not include shared list-filters partial.' - ); - $this->assertStringContainsString('initStandardListPage(', $moduleContent, $file); - $this->assertStringNotContainsString('gridFiltersFromSchema(', $content, $file); - $this->assertStringNotContainsString('data-toolbar-toggle', $content, $file); - $this->assertStringNotContainsString('data-filter-overflow', $content, $file); - $this->assertStringNotContainsString('data-filter-toggle', $content, $file); - $this->assertSame(0, preg_match('/assertSame(0, preg_match('/]*id=\"[^\"]*-filter\"/', $content), $file); + $this->assertSame(0, preg_match('/filters:\s*\[/', $content), $file); + } + } + + public function testDrawerListTemplatesUseDrawerAndActiveFilterChips(): void + { + foreach ($this->drawerListTemplates() as $file) { + $content = $this->readProjectFile($file); + $moduleContent = $this->readTemplatePageModule($file); + $usesPartial = $this->usesSharedListFiltersPartial($content); + $this->assertStringContainsString("mode: 'drawer'", $moduleContent, $file); + if (!$usesPartial) { + $this->assertStringContainsString('data-filter-drawer-open', $content, $file); + $this->assertStringContainsString('data-filter-drawer-apply', $content, $file); + $this->assertStringContainsString('data-active-filter-chips', $content, $file); + } + $this->assertStringNotContainsString('data-filter-drawer-reset', $content, $file); + } + } + + public function testSearchOnlyTemplatesDoNotUseDrawerControls(): void + { + foreach ($this->searchOnlyTemplates() as $file) { + $content = $this->readProjectFile($file); + $moduleContent = $this->readTemplatePageModule($file); + $this->assertStringContainsString("mode: 'search-only'", $moduleContent, $file); + $this->assertStringNotContainsString('data-filter-drawer-open', $content, $file); + $this->assertStringNotContainsString('data-filter-drawer-apply', $content, $file); + $this->assertStringNotContainsString('data-active-filter-chips', $content, $file); + } + } +} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..501cd5d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,39 @@ +# Tests + +## Ziel + +Die Tests sind nach Schutzwirkung organisiert, nicht nur nach technischer Ebene. +Wichtige Architektur- und Security-Invarianten sollen schnell gezielt laufen koennen, +ohne dass jede Aenderung immer die komplette Suite im Kopf behalten muss. + +## PHPUnit-Suiten + +- `CoreCore`: gesamte Test-Suite. +- `Architecture`: alle Architektur-Contracts unter `tests/Architecture/`. +- `ArchitectureCore`: Core-Boundaries, Module-/Repository-Contracts, Taxonomie- und Sync-Contracts. +- `ArchitectureSecurity`: CSRF, PRG, File-Storage, Crypto, Logging sowie AuthZ-nahe Contracts. +- `ArchitectureUI`: UI-Contracts fuer Listen, Detailseiten, Runtime-Komponenten und A11y. +- `ArchitectureModules`: modulbezogene Struktur- und Isolations-Contracts. +- `ArchitectureMeta`: Agent-/Skill-/Meta-Contracts fuer das Repository-Setup. + +## Regeln fuer neue Architecture-Tests + +- Nur stabile Invarianten testen, keine einmaligen Migrationsdetails. +- Guards oder Risiken explizit abdecken, zum Beispiel `GR-CORE-*`, `GR-SEC-*`, `GR-TEST-*`. +- Keine Duplikate neben bereits breiten Contracts anlegen. +- String-Assertions nur verwenden, wenn kein robusterer Contract moeglich ist. +- Monolithische Sammeltests vermeiden; lieber kleine thematische Contracts. + +## Delete vs. Refactor vs. Keep + +- `keep`: testet einen echten plattformweiten Contract oder Security-Guard. +- `refactor`: Schutz ist wichtig, aber der Test ist zu datei-, markup- oder implementation-detailnah. +- `delete`: dupliziert bestehende Schutzwirkung oder prueft nur historische Altlasten. + +## Naechste Refactor-Kandidaten + +- `tests/Architecture/ModuleStructureContractTest.php` +- `tests/Architecture/FrontendComponentRuntimeContractTest.php` +- `tests/Architecture/FilterDrawerContractTest.php` +- `tests/Architecture/PublicPageContractTest.php` +- `tests/Architecture/CodexSkillsContractTest.php`