Refactor architecture test contracts

This commit is contained in:
2026-03-19 18:25:19 +01:00
parent 4dd6d451f6
commit 1e993e470c
23 changed files with 1311 additions and 1190 deletions

View File

@@ -0,0 +1,58 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class ApiResourceContractTest extends TestCase
{
use ProjectFileAssertionSupport;
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);
}
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);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class ApiSchemaContractTest extends TestCase
{
use ProjectFileAssertionSupport;
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 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);
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthLoginFrontendCleanupContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testLoginCssDoesNotContainLegacyBackToLoginSelector(): void
{
$content = $this->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);
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthRegisterSecurityContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testRegisterPageVerifiesCsrfBeforeHandlingPostPayload(): void
{
$content = $this->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
);
}
}

View File

@@ -1,281 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzAdminContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testUsersDataActionRequiresUsersViewPermission(): void
{
$content = $this->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);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzAdminMasterDataContractTest extends TestCase
{
use ProjectFileAssertionSupport;
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);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzAdminSettingsContractTest extends TestCase
{
use ProjectFileAssertionSupport;
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);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzAdminTenantsContractTest extends TestCase
{
use ProjectFileAssertionSupport;
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);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzAdminUsersContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testUsersDataActionRequiresUsersViewPermission(): void
{
$content = $this->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);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzApiBootstrapContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testApiLoginIsBootstrappedAsPublic(): void
{
$content = $this->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);
}
}

View File

@@ -1,192 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzApiContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testApiLoginIsBootstrappedAsPublic(): void
{
$content = $this->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);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class AuthzUiActionContractTest extends TestCase
{
use AuthzUiContractSupport;
use ProjectFileAssertionSupport;
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);
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace MintyPHP\Tests\Architecture;
trait AuthzUiContractSupport
{
/**
* @return array<string, array{0: string, 1: string, 2: string|null}>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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;
}
}

View File

@@ -1,357 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class AuthzUiContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testAdminTenantTemplatesUseServerCapabilities(): void
{
$indexTemplate = $this->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<string, array{0: string, 1: string, 2: string|null}>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzUiLayoutContractTest extends TestCase
{
use ProjectFileAssertionSupport;
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 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);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AuthzUiTemplateContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testAdminTenantTemplatesUseServerCapabilities(): void
{
$indexTemplate = $this->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);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class ListActionContractTest extends TestCase
{
use ListContractFiles;
use ProjectFileAssertionSupport;
public function testListActionsExposeSplitToolbarSchemasAndChipMeta(): void
{
foreach ($this->listIndexActions() as $file) {
$content = $this->readProjectFile($file);
$this->assertStringContainsString('$searchToolbarFilterSchema', $content, $file);
$this->assertStringContainsString('$drawerToolbarFilterSchema', $content, $file);
$this->assertStringContainsString('$filterChipMeta', $content, $file);
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace MintyPHP\Tests\Architecture;
trait ListContractFiles
{
private function readTemplatePageModule(string $templateFile): string
{
$template = $this->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<string>
*/
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<string,string>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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<string>
*/
private function searchOnlyTemplates(): array
{
return [
'pages/search/index(default).phtml',
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class ListDataEndpointContractTest extends TestCase
{
use ListContractFiles;
use ProjectFileAssertionSupport;
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);
}
}
}

View File

@@ -1,303 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class ListFilterContractTest extends TestCase
{
use ProjectFileAssertionSupport;
private function readTemplatePageModule(string $templateFile): string
{
$template = $this->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<string>
*/
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<string,string>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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('/<select id=\"[^\"]*-filter\"/', $content), $file);
$this->assertSame(0, preg_match('/<input[^>]*id=\"[^\"]*-filter\"/', $content), $file);
$this->assertSame(0, preg_match('/filters:\s*\[/', $content), $file);
}
}
public function testListActionsExposeSplitToolbarSchemasAndChipMeta(): void
{
foreach ($this->listIndexActions() as $file) {
$content = $this->readProjectFile($file);
$this->assertStringContainsString('$searchToolbarFilterSchema', $content, $file);
$this->assertStringContainsString('$drawerToolbarFilterSchema', $content, $file);
$this->assertStringContainsString('$filterChipMeta', $content, $file);
}
}
public function testDrawerListTemplatesUseDrawerAndActiveFilterChips(): void
{
foreach ($this->drawerListTemplates() as $file) {
$content = $this->readProjectFile($file);
$moduleContent = $this->readTemplatePageModule($file);
$usesPartial = $this->usesSharedListFiltersPartial($content);
$this->assertStringContainsString("mode: 'drawer'", $moduleContent, $file);
if (!$usesPartial) {
$this->assertStringContainsString('data-filter-drawer-open', $content, $file);
$this->assertStringContainsString('data-filter-drawer-apply', $content, $file);
$this->assertStringContainsString('data-active-filter-chips', $content, $file);
}
$this->assertStringNotContainsString('data-filter-drawer-reset', $content, $file);
}
}
public function testSearchOnlyTemplatesDoNotUseDrawerControls(): void
{
foreach ($this->searchOnlyTemplates() as $file) {
$content = $this->readProjectFile($file);
$moduleContent = $this->readTemplatePageModule($file);
$this->assertStringContainsString("mode: 'search-only'", $moduleContent, $file);
$this->assertStringNotContainsString('data-filter-drawer-open', $content, $file);
$this->assertStringNotContainsString('data-filter-drawer-apply', $content, $file);
$this->assertStringNotContainsString('data-active-filter-chips', $content, $file);
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class ListTemplateContractTest extends TestCase
{
use ListContractFiles;
use ProjectFileAssertionSupport;
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('/<select id=\"[^\"]*-filter\"/', $content), $file);
$this->assertSame(0, preg_match('/<input[^>]*id=\"[^\"]*-filter\"/', $content), $file);
$this->assertSame(0, preg_match('/filters:\s*\[/', $content), $file);
}
}
public function testDrawerListTemplatesUseDrawerAndActiveFilterChips(): void
{
foreach ($this->drawerListTemplates() as $file) {
$content = $this->readProjectFile($file);
$moduleContent = $this->readTemplatePageModule($file);
$usesPartial = $this->usesSharedListFiltersPartial($content);
$this->assertStringContainsString("mode: 'drawer'", $moduleContent, $file);
if (!$usesPartial) {
$this->assertStringContainsString('data-filter-drawer-open', $content, $file);
$this->assertStringContainsString('data-filter-drawer-apply', $content, $file);
$this->assertStringContainsString('data-active-filter-chips', $content, $file);
}
$this->assertStringNotContainsString('data-filter-drawer-reset', $content, $file);
}
}
public function testSearchOnlyTemplatesDoNotUseDrawerControls(): void
{
foreach ($this->searchOnlyTemplates() as $file) {
$content = $this->readProjectFile($file);
$moduleContent = $this->readTemplatePageModule($file);
$this->assertStringContainsString("mode: 'search-only'", $moduleContent, $file);
$this->assertStringNotContainsString('data-filter-drawer-open', $content, $file);
$this->assertStringNotContainsString('data-filter-drawer-apply', $content, $file);
$this->assertStringNotContainsString('data-active-filter-chips', $content, $file);
}
}
}

39
tests/README.md Normal file
View File

@@ -0,0 +1,39 @@
# Tests
## Ziel
Die Tests sind nach Schutzwirkung organisiert, nicht nur nach technischer Ebene.
Wichtige Architektur- und Security-Invarianten sollen schnell gezielt laufen koennen,
ohne dass jede Aenderung immer die komplette Suite im Kopf behalten muss.
## PHPUnit-Suiten
- `CoreCore`: gesamte Test-Suite.
- `Architecture`: alle Architektur-Contracts unter `tests/Architecture/`.
- `ArchitectureCore`: Core-Boundaries, Module-/Repository-Contracts, Taxonomie- und Sync-Contracts.
- `ArchitectureSecurity`: CSRF, PRG, File-Storage, Crypto, Logging sowie AuthZ-nahe Contracts.
- `ArchitectureUI`: UI-Contracts fuer Listen, Detailseiten, Runtime-Komponenten und A11y.
- `ArchitectureModules`: modulbezogene Struktur- und Isolations-Contracts.
- `ArchitectureMeta`: Agent-/Skill-/Meta-Contracts fuer das Repository-Setup.
## Regeln fuer neue Architecture-Tests
- Nur stabile Invarianten testen, keine einmaligen Migrationsdetails.
- Guards oder Risiken explizit abdecken, zum Beispiel `GR-CORE-*`, `GR-SEC-*`, `GR-TEST-*`.
- Keine Duplikate neben bereits breiten Contracts anlegen.
- String-Assertions nur verwenden, wenn kein robusterer Contract moeglich ist.
- Monolithische Sammeltests vermeiden; lieber kleine thematische Contracts.
## Delete vs. Refactor vs. Keep
- `keep`: testet einen echten plattformweiten Contract oder Security-Guard.
- `refactor`: Schutz ist wichtig, aber der Test ist zu datei-, markup- oder implementation-detailnah.
- `delete`: dupliziert bestehende Schutzwirkung oder prueft nur historische Altlasten.
## Naechste Refactor-Kandidaten
- `tests/Architecture/ModuleStructureContractTest.php`
- `tests/Architecture/FrontendComponentRuntimeContractTest.php`
- `tests/Architecture/FilterDrawerContractTest.php`
- `tests/Architecture/PublicPageContractTest.php`
- `tests/Architecture/CodexSkillsContractTest.php`