Refactor architecture test contracts
This commit is contained in:
58
tests/Architecture/ApiResourceContractTest.php
Normal file
58
tests/Architecture/ApiResourceContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
66
tests/Architecture/ApiSchemaContractTest.php
Normal file
66
tests/Architecture/ApiSchemaContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
82
tests/Architecture/AuthzAdminMasterDataContractTest.php
Normal file
82
tests/Architecture/AuthzAdminMasterDataContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
50
tests/Architecture/AuthzAdminSettingsContractTest.php
Normal file
50
tests/Architecture/AuthzAdminSettingsContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
64
tests/Architecture/AuthzAdminTenantsContractTest.php
Normal file
64
tests/Architecture/AuthzAdminTenantsContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
98
tests/Architecture/AuthzAdminUsersContractTest.php
Normal file
98
tests/Architecture/AuthzAdminUsersContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
76
tests/Architecture/AuthzApiBootstrapContractTest.php
Normal file
76
tests/Architecture/AuthzApiBootstrapContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
97
tests/Architecture/AuthzUiActionContractTest.php
Normal file
97
tests/Architecture/AuthzUiActionContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
tests/Architecture/AuthzUiContractSupport.php
Normal file
124
tests/Architecture/AuthzUiContractSupport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
49
tests/Architecture/AuthzUiLayoutContractTest.php
Normal file
49
tests/Architecture/AuthzUiLayoutContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
102
tests/Architecture/AuthzUiTemplateContractTest.php
Normal file
102
tests/Architecture/AuthzUiTemplateContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
21
tests/Architecture/ListActionContractTest.php
Normal file
21
tests/Architecture/ListActionContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
160
tests/Architecture/ListContractFiles.php
Normal file
160
tests/Architecture/ListContractFiles.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
106
tests/Architecture/ListDataEndpointContractTest.php
Normal file
106
tests/Architecture/ListDataEndpointContractTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
tests/Architecture/ListTemplateContractTest.php
Normal file
60
tests/Architecture/ListTemplateContractTest.php
Normal 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
39
tests/README.md
Normal 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`
|
||||
Reference in New Issue
Block a user