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('/