major update
This commit is contained in:
150
tests/App/Bootstrap/EnvValidatorTest.php
Normal file
150
tests/App/Bootstrap/EnvValidatorTest.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\App\Bootstrap;
|
||||
|
||||
use MintyPHP\App\Bootstrap\EnvValidator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class EnvValidatorTest extends TestCase
|
||||
{
|
||||
public function testValidateArrayAcceptsValidDevConfiguration(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'dev',
|
||||
'APP_DEBUG' => 'true',
|
||||
'APP_CRYPTO_KEY' => '',
|
||||
'APP_URL' => 'http://localhost:8080',
|
||||
]));
|
||||
|
||||
$this->assertSame([], $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayRequiresAppUrlInDevelopment(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'dev',
|
||||
'APP_URL' => '',
|
||||
]));
|
||||
|
||||
$this->assertContains('APP_URL must be set in development', $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayRequiresProductionCryptoKeyAndUrl(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'prod',
|
||||
'APP_DEBUG' => 'false',
|
||||
'APP_CRYPTO_KEY' => '',
|
||||
'APP_URL' => '',
|
||||
]));
|
||||
|
||||
$this->assertContains('APP_URL must be set in production', $errors);
|
||||
$this->assertContains('APP_CRYPTO_KEY must be set in production', $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayAcceptsMissingAppUrlInTestEnv(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'test',
|
||||
'APP_URL' => '',
|
||||
]));
|
||||
|
||||
$this->assertSame([], $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayRejectsInvalidCryptoKeyFormat(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_CRYPTO_KEY' => 'invalid-key-format',
|
||||
]));
|
||||
|
||||
$this->assertContains('APP_CRYPTO_KEY must be 64-char hex or base64 encoded 32-byte key', $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayAcceptsValidHexCryptoKeyInProduction(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'prod',
|
||||
'APP_DEBUG' => 'false',
|
||||
'APP_URL' => 'https://example.test',
|
||||
'APP_CRYPTO_KEY' => str_repeat('a', 64),
|
||||
'TENANT_SCOPE_STRICT' => 'true',
|
||||
]));
|
||||
|
||||
$this->assertSame([], $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayRequiresHttpsAppUrlInProduction(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'prod',
|
||||
'APP_DEBUG' => 'false',
|
||||
'APP_URL' => 'http://example.test',
|
||||
'APP_CRYPTO_KEY' => str_repeat('a', 64),
|
||||
'TENANT_SCOPE_STRICT' => 'true',
|
||||
]));
|
||||
|
||||
$this->assertContains('APP_URL must use https in production', $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayRejectsLocalhostAppUrlInProduction(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'prod',
|
||||
'APP_DEBUG' => 'false',
|
||||
'APP_URL' => 'https://localhost',
|
||||
'APP_CRYPTO_KEY' => str_repeat('a', 64),
|
||||
'TENANT_SCOPE_STRICT' => 'true',
|
||||
]));
|
||||
|
||||
$this->assertContains('APP_URL must not use localhost or an IP host in production', $errors);
|
||||
}
|
||||
|
||||
public function testValidateArrayRejectsIpHostAppUrlInProduction(): void
|
||||
{
|
||||
$errors = EnvValidator::validateArray($this->baseEnv([
|
||||
'APP_ENV' => 'prod',
|
||||
'APP_DEBUG' => 'false',
|
||||
'APP_URL' => 'https://127.0.0.1',
|
||||
'APP_CRYPTO_KEY' => str_repeat('a', 64),
|
||||
'TENANT_SCOPE_STRICT' => 'true',
|
||||
]));
|
||||
|
||||
$this->assertContains('APP_URL must not use localhost or an IP host in production', $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $override
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function baseEnv(array $override = []): array
|
||||
{
|
||||
$storagePath = sys_get_temp_dir();
|
||||
|
||||
return array_merge([
|
||||
'APP_NAME' => 'IMVS',
|
||||
'APP_ENV' => 'dev',
|
||||
'APP_DEBUG' => 'true',
|
||||
'APP_TIMEZONE' => 'Europe/Berlin',
|
||||
'APP_STORAGE_PATH' => $storagePath,
|
||||
'APP_LOCALE' => 'de',
|
||||
'APP_LOCALES' => 'de,en',
|
||||
'APP_I18N_DOMAIN' => 'default',
|
||||
'APP_URL' => 'http://localhost:8080',
|
||||
'APP_CRYPTO_KEY' => '',
|
||||
'SESSION_NAME' => 'IMVS',
|
||||
'TENANT_SCOPE_STRICT' => 'true',
|
||||
'DB_HOST' => 'db',
|
||||
'DB_PORT' => '3306',
|
||||
'DB_NAME' => 'imvs',
|
||||
'DB_USER' => 'imvs',
|
||||
'DB_PASS' => 'imvs',
|
||||
'CACHE_SERVERS' => 'memcached:11211',
|
||||
'FIREWALL_CONCURRENCY' => '10',
|
||||
'FIREWALL_SPINLOCK_SECONDS' => '0.15',
|
||||
'FIREWALL_INTERVAL_SECONDS' => '300',
|
||||
'FIREWALL_CACHE_PREFIX' => 'fw_concurrency_',
|
||||
'FIREWALL_REVERSE_PROXY' => 'false',
|
||||
], $override);
|
||||
}
|
||||
}
|
||||
44
tests/Architecture/AsideNavigationContractTest.php
Normal file
44
tests/Architecture/AsideNavigationContractTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AsideNavigationContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
public function testAdminAsideUsesGroupedDetailsStorageContract(): void
|
||||
{
|
||||
$content = $this->readProjectFile('templates/partials/app-main-aside.phtml');
|
||||
|
||||
$this->assertStringContainsString('data-aside-details-storage="aside-admin-sections-v1"', $content);
|
||||
$this->assertStringContainsString('data-aside-details-open-active="1"', $content);
|
||||
$this->assertStringContainsString('<details data-details-key="<?php e($groupKey); ?>"', $content);
|
||||
|
||||
$this->assertStringContainsString("'key' => 'admin-organization'", $content);
|
||||
$this->assertStringContainsString("'key' => 'admin-roles-permissions'", $content);
|
||||
$this->assertStringContainsString("'key' => 'admin-automation'", $content);
|
||||
$this->assertStringContainsString("'key' => 'admin-monitoring'", $content);
|
||||
$this->assertStringContainsString("'key' => 'admin-logs'", $content);
|
||||
$this->assertStringContainsString("'key' => 'admin-system'", $content);
|
||||
}
|
||||
|
||||
public function testAsidePanelsUseSharedDetailsOpenStateHelper(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/js/components/app-toggle-aside-panels.js');
|
||||
|
||||
$this->assertStringContainsString("import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';", $content);
|
||||
$this->assertStringContainsString('initPersistedDetailsGroup({', $content);
|
||||
$this->assertStringContainsString("panel.dataset.asideDetailsOpenActive === '1'", $content);
|
||||
}
|
||||
|
||||
public function testDetailStateUsesSharedDetailsOpenStateHelper(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/js/components/app-details-state.js');
|
||||
|
||||
$this->assertStringContainsString("import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';", $content);
|
||||
$this->assertStringContainsString('initPersistedDetailsGroup({', $content);
|
||||
$this->assertStringNotContainsString('window.localStorage.setItem(storageKey, JSON.stringify(openKeys));', $content);
|
||||
}
|
||||
}
|
||||
281
tests/Architecture/AuthzAdminContractTest.php
Normal file
281
tests/Architecture/AuthzAdminContractTest.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AuthzAndApiLoginContractTest extends TestCase
|
||||
{
|
||||
public function testUsersDataActionRequiresUsersViewPermission(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/users/data().php');
|
||||
$this->assertStringContainsString('Guard::requireLogin();', $content);
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $content);
|
||||
$this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW', $content);
|
||||
}
|
||||
|
||||
public function testUsersActivateActionRequiresUsersUpdatePermission(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/users/activate($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $content);
|
||||
$this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE', $content);
|
||||
}
|
||||
|
||||
public function testUsersDeactivateActionRequiresUsersUpdatePermission(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/users/deactivate($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $content);
|
||||
$this->assertStringContainsString('UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DEACTIVATE', $content);
|
||||
}
|
||||
|
||||
public function testUsersBulkActionPermissionMappingIsConsistent(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/users/bulk($action).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $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('createAuthorizationService()', $indexContent);
|
||||
$this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $indexContent);
|
||||
|
||||
$dataContent = $this->readProjectFile('pages/admin/tenants/data().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $dataContent);
|
||||
$this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW', $dataContent);
|
||||
|
||||
$createContent = $this->readProjectFile('pages/admin/tenants/create().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $createContent);
|
||||
$this->assertStringContainsString('TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE', $createContent);
|
||||
|
||||
$editContent = $this->readProjectFile('pages/admin/tenants/edit($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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 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 testAdminTenantDeleteUsesCentralPolicy(): void
|
||||
{
|
||||
$deleteContent = $this->readProjectFile('pages/admin/tenants/delete($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $indexContent);
|
||||
$this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $indexContent);
|
||||
|
||||
$dataContent = $this->readProjectFile('pages/admin/departments/data().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $dataContent);
|
||||
$this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_VIEW', $dataContent);
|
||||
|
||||
$createContent = $this->readProjectFile('pages/admin/departments/create().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $createContent);
|
||||
$this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_CREATE', $createContent);
|
||||
|
||||
$editContent = $this->readProjectFile('pages/admin/departments/edit($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $deleteContent);
|
||||
$this->assertStringContainsString('DepartmentAuthorizationPolicy::ABILITY_ADMIN_DEPARTMENTS_DELETE', $deleteContent);
|
||||
}
|
||||
|
||||
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 testAdminRolesActionsUseCentralPolicies(): void
|
||||
{
|
||||
$indexContent = $this->readProjectFile('pages/admin/roles/index().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $indexContent);
|
||||
$this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $indexContent);
|
||||
|
||||
$dataContent = $this->readProjectFile('pages/admin/roles/data().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $dataContent);
|
||||
$this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW', $dataContent);
|
||||
|
||||
$createContent = $this->readProjectFile('pages/admin/roles/create().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $createContent);
|
||||
$this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_CREATE', $createContent);
|
||||
|
||||
$editContent = $this->readProjectFile('pages/admin/roles/edit($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $deleteContent);
|
||||
$this->assertStringContainsString('RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_DELETE', $deleteContent);
|
||||
}
|
||||
|
||||
public function testAdminPermissionsActionsUseCentralPolicies(): void
|
||||
{
|
||||
$indexContent = $this->readProjectFile('pages/admin/permissions/index().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $indexContent);
|
||||
$this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $indexContent);
|
||||
|
||||
$dataContent = $this->readProjectFile('pages/admin/permissions/data().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $dataContent);
|
||||
$this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW', $dataContent);
|
||||
|
||||
$createContent = $this->readProjectFile('pages/admin/permissions/create().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $createContent);
|
||||
$this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_CREATE', $createContent);
|
||||
|
||||
$editContent = $this->readProjectFile('pages/admin/permissions/edit($id).php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $deleteContent);
|
||||
$this->assertStringContainsString('PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_DELETE', $deleteContent);
|
||||
}
|
||||
|
||||
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 testAdminSettingsActionsUseCentralPolicies(): void
|
||||
{
|
||||
$indexContent = $this->readProjectFile('pages/admin/settings/index().php');
|
||||
$this->assertStringContainsString('createAuthorizationService()', $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('createAuthorizationService()', $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 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 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('createAuthorizationService()', $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('createAuthorizationService()', $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::requirePermission(PermissionService::PERMISSIONS_VIEW);", $permissionContent);
|
||||
$this->assertStringContainsString("foreach ((\$result['data'] ?? []) as \$row)", $permissionContent);
|
||||
|
||||
$settingsContent = $this->readProjectFile('pages/api/v1/settings/index().php');
|
||||
$this->assertStringContainsString("ApiResponse::requirePermission(PermissionService::SETTINGS_VIEW);", $settingsContent);
|
||||
$this->assertStringContainsString("ApiResponse::requirePermission(PermissionService::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('PermissionService::USERS_SELF_UPDATE', $meContent);
|
||||
$this->assertStringContainsString("ApiResponse::validationError(['profile' => array_values(\$errors)]);", $meContent);
|
||||
|
||||
$passwordContent = $this->readProjectFile('pages/api/v1/me/password().php');
|
||||
$this->assertStringContainsString("ApiResponse::requireMethod('POST');", $passwordContent);
|
||||
$this->assertStringContainsString('PermissionService::USERS_SELF_UPDATE', $passwordContent);
|
||||
|
||||
$avatarContent = $this->readProjectFile('pages/api/v1/me/avatar().php');
|
||||
$this->assertStringContainsString("define('MINTY_ALLOW_OUTPUT', true);", $avatarContent);
|
||||
$this->assertStringContainsString('PermissionService::USERS_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('PermissionService::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('normalizeUserWriteInputToInternalIds($input)', $usersIndex);
|
||||
$this->assertStringContainsString("'tenant_ids' => 'use_tenant_uuids'", $usersIndex);
|
||||
$this->assertStringContainsString("'role_ids' => 'use_role_uuids'", $usersIndex);
|
||||
$this->assertStringContainsString("'department_ids' => 'use_department_uuids'", $usersIndex);
|
||||
|
||||
$usersShow = $this->readProjectFile('pages/api/v1/users/show($id).php');
|
||||
$this->assertStringContainsString('normalizeUserWriteInputToInternalIds($input)', $usersShow);
|
||||
$this->assertStringContainsString("'primary_tenant_id' => 'use_primary_tenant_uuid'", $usersShow);
|
||||
}
|
||||
|
||||
public function testApiDepartmentEndpointsUseTenantUuidAndNoTenantIdExposure(): void
|
||||
{
|
||||
$departmentIndex = $this->readProjectFile('pages/api/v1/departments/index().php');
|
||||
$this->assertStringContainsString("ApiResponse::validationError(['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('createRolePermissionRepository()', $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);
|
||||
}
|
||||
|
||||
private function readProjectFile(string $path): string
|
||||
{
|
||||
$root = realpath(__DIR__ . '/../..');
|
||||
$this->assertNotFalse($root, 'Project root not found.');
|
||||
$fullPath = $root . '/' . $path;
|
||||
$this->assertFileExists($fullPath, 'File not found: ' . $path);
|
||||
$content = file_get_contents($fullPath);
|
||||
$this->assertNotFalse($content, 'Could not read file: ' . $path);
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
192
tests/Architecture/AuthzApiContractTest.php
Normal file
192
tests/Architecture/AuthzApiContractTest.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
183
tests/Architecture/AuthzUiContractTest.php
Normal file
183
tests/Architecture/AuthzUiContractTest.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
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);
|
||||
|
||||
$iconBar = $this->readProjectFile('templates/partials/app-main-aside-icon-bar.phtml');
|
||||
$this->assertStringContainsString("\$viewAuth['layout']", $iconBar);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
58
tests/Architecture/CodexSkillsContractTest.php
Normal file
58
tests/Architecture/CodexSkillsContractTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CodexSkillsContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
public function testCoreGuardrailsSkillAndReferencesExist(): void
|
||||
{
|
||||
$skill = $this->readProjectFile('tools/codex-skills/core-guardrails/SKILL.md');
|
||||
$this->assertStringContainsString('name: core-guardrails', $skill);
|
||||
$this->assertStringContainsString('MUST', $skill);
|
||||
$this->assertStringContainsString('MUST NOT', $skill);
|
||||
$this->assertStringContainsString('Out of Scope', $skill);
|
||||
|
||||
$this->readProjectFile('tools/codex-skills/core-guardrails/references/boundaries-core.md');
|
||||
$this->readProjectFile('tools/codex-skills/core-guardrails/references/boundaries-ui.md');
|
||||
$this->readProjectFile('tools/codex-skills/core-guardrails/references/quality-gates.md');
|
||||
$this->readProjectFile('tools/codex-skills/core-guardrails/references/source-map.md');
|
||||
}
|
||||
|
||||
public function testStarterkitPlannerSkillContainsDecisionCompleteTemplateContract(): void
|
||||
{
|
||||
$skill = $this->readProjectFile('tools/codex-skills/starterkit-planner/SKILL.md');
|
||||
$this->assertStringContainsString('name: starterkit-planner', $skill);
|
||||
$this->assertStringContainsString('Decision-complete Implementierungsplan', $skill);
|
||||
|
||||
$this->readProjectFile('tools/codex-skills/starterkit-planner/references/plan-template.md');
|
||||
$this->readProjectFile('tools/codex-skills/starterkit-planner/references/tradeoff-matrix.md');
|
||||
}
|
||||
|
||||
public function testCodexPromptsDocumentationReferencesBothSkillsAndExtensionScope(): void
|
||||
{
|
||||
$prompts = $this->readProjectFile('docs/codex-prompts.md');
|
||||
$this->assertStringContainsString('core-guardrails', $prompts);
|
||||
$this->assertStringContainsString('starterkit-planner', $prompts);
|
||||
$this->assertStringContainsString('Out of Scope: Extensions', $prompts);
|
||||
}
|
||||
|
||||
public function testDocsIndexReferencesCodexPrompts(): void
|
||||
{
|
||||
$index = $this->readProjectFile('docs/index.md');
|
||||
$this->assertStringContainsString('/docs/codex-prompts.md', $index);
|
||||
}
|
||||
|
||||
public function testCodexSkillsSyncScriptSupportsCheckAndApply(): void
|
||||
{
|
||||
$script = $this->readProjectFile('bin/codex-skills-sync.sh');
|
||||
$this->assertStringContainsString('--check', $script);
|
||||
$this->assertStringContainsString('--apply', $script);
|
||||
$this->assertStringContainsString('mode="check"', $script);
|
||||
$this->assertStringContainsString('core-guardrails', $script);
|
||||
$this->assertStringContainsString('starterkit-planner', $script);
|
||||
}
|
||||
}
|
||||
60
tests/Architecture/ContainerFactoryContractTest.php
Normal file
60
tests/Architecture/ContainerFactoryContractTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
class ContainerFactoryContractTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return array<int, array{0: class-string}>
|
||||
*/
|
||||
public static function factoryClassProvider(): array
|
||||
{
|
||||
return [
|
||||
[\MintyPHP\Service\Access\AccessServicesFactory::class],
|
||||
[\MintyPHP\Service\AddressBook\AddressBookServicesFactory::class],
|
||||
[\MintyPHP\Service\Audit\AuditServicesFactory::class],
|
||||
[\MintyPHP\Service\Auth\AuthServicesFactory::class],
|
||||
[\MintyPHP\Service\Branding\BrandingServicesFactory::class],
|
||||
[\MintyPHP\Service\Directory\DirectoryServicesFactory::class],
|
||||
[\MintyPHP\Service\Import\ImportServicesFactory::class],
|
||||
[\MintyPHP\Service\Mail\MailServicesFactory::class],
|
||||
[\MintyPHP\Service\Scheduler\SchedulerServicesFactory::class],
|
||||
[\MintyPHP\Service\Security\SecurityServicesFactory::class],
|
||||
[\MintyPHP\Service\Settings\SettingServicesFactory::class],
|
||||
[\MintyPHP\Service\Tenant\TenantServicesFactory::class],
|
||||
[\MintyPHP\Service\User\UserServicesFactory::class],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('factoryClassProvider')]
|
||||
public function testContainerFactoriesCachePublicNoArgCreateMethods(string $factoryClass): void
|
||||
{
|
||||
$factory = app($factoryClass);
|
||||
$this->assertInstanceOf($factoryClass, $factory);
|
||||
|
||||
$reflection = new ReflectionClass($factoryClass);
|
||||
$methods = array_filter(
|
||||
$reflection->getMethods(ReflectionMethod::IS_PUBLIC),
|
||||
static fn (ReflectionMethod $method): bool =>
|
||||
!$method->isStatic()
|
||||
&& str_starts_with($method->getName(), 'create')
|
||||
&& $method->getNumberOfRequiredParameters() === 0
|
||||
);
|
||||
usort($methods, static fn (ReflectionMethod $a, ReflectionMethod $b): int => strcmp($a->getName(), $b->getName()));
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$methodName = $method->getName();
|
||||
$first = $factory->{$methodName}();
|
||||
$second = $factory->{$methodName}();
|
||||
|
||||
$this->assertIsObject($first, sprintf('%s::%s() must return an object', $factoryClass, $methodName));
|
||||
$this->assertInstanceOf($first::class, $second, sprintf('%s::%s() must be type-stable', $factoryClass, $methodName));
|
||||
$this->assertSame($first, $second, sprintf('%s::%s() must return the cached instance', $factoryClass, $methodName));
|
||||
}
|
||||
}
|
||||
}
|
||||
90
tests/Architecture/CoreStarterkitContractTest.php
Normal file
90
tests/Architecture/CoreStarterkitContractTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class CoreStarterkitContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
public function testPagesDoNotInstantiateServicesRepositoriesOrGatewaysDirectly(): void
|
||||
{
|
||||
$violations = $this->findPatternMatchesInPhpFiles('pages', '/\bnew\s+[\\\\A-Za-z0-9_]+(?:Service|Gateway|Repository)\s*\(/');
|
||||
$this->assertSame([], $violations, "Direct service/repository/gateway instantiation found in pages/:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testPagesDoNotUseDatabaseFacadeDirectly(): void
|
||||
{
|
||||
$violations = $this->findPatternMatchesInPhpFiles('pages', '/\bDB::/');
|
||||
$this->assertSame([], $violations, "Direct DB facade usage found in pages/:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testAdminPagesDoNotUseFactoryCreateChains(): void
|
||||
{
|
||||
$violations = $this->findPatternMatchesInPhpFiles('pages/admin', '/app\([^\n]*Factory::class\)->create/');
|
||||
$this->assertSame([], $violations, "Factory create-chain usage found in pages/admin/:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testAdminPagesDoNotReferenceFactoryClasses(): void
|
||||
{
|
||||
$violations = $this->findPatternMatchesInPhpFiles('pages/admin', '/Factory::class/');
|
||||
$this->assertSame([], $violations, "Factory class usage found in pages/admin/:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
public function testPagesUseRequestInputInsteadOfSuperglobals(): void
|
||||
{
|
||||
$patterns = [
|
||||
'/\$_POST/' => '$_POST',
|
||||
'/\$_GET/' => '$_GET',
|
||||
'/\$_FILES/' => '$_FILES',
|
||||
'/REQUEST_METHOD/' => 'REQUEST_METHOD',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern => $label) {
|
||||
$violations = $this->findPatternMatchesInPhpFiles('pages', $pattern);
|
||||
$this->assertSame([], $violations, "Superglobal {$label} usage found in pages/:\n" . implode("\n", $violations));
|
||||
}
|
||||
}
|
||||
|
||||
public function testApiPagesDoNotReadJsonBodyDirectly(): void
|
||||
{
|
||||
$violations = $this->findPatternMatchesInPhpFiles('pages/api/v1', '/ApiResponse::readJsonBody\s*\(/');
|
||||
$this->assertSame([], $violations, "ApiResponse::readJsonBody usage found in pages/api/v1/:\n" . implode("\n", $violations));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function findPatternMatchesInPhpFiles(string $relativeDirectory, string $pattern): array
|
||||
{
|
||||
$root = $this->projectRootPath();
|
||||
$basePath = $root . '/' . $relativeDirectory;
|
||||
$this->assertDirectoryExists($basePath, 'Directory not found: ' . $relativeDirectory);
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath));
|
||||
$violations = [];
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || $file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file->getPathname());
|
||||
$this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname());
|
||||
|
||||
if (!preg_match($pattern, $content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativePath = str_replace($root . '/', '', $file->getPathname());
|
||||
$violations[] = $relativePath;
|
||||
}
|
||||
|
||||
sort($violations);
|
||||
return $violations;
|
||||
}
|
||||
}
|
||||
68
tests/Architecture/DetailActionPolicyContractTest.php
Normal file
68
tests/Architecture/DetailActionPolicyContractTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DetailActionPolicyContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function migratedConfirmFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/users/edit(default).phtml',
|
||||
'pages/admin/users/_form.phtml',
|
||||
'pages/admin/tenants/edit(default).phtml',
|
||||
'pages/admin/tenants/_form.phtml',
|
||||
'pages/admin/departments/edit(default).phtml',
|
||||
'pages/admin/roles/edit(default).phtml',
|
||||
'pages/admin/permissions/edit(default).phtml',
|
||||
'pages/admin/settings/index(default).phtml',
|
||||
'templates/partials/app-details-titlebar.phtml',
|
||||
'templates/partials/app-details-aside-actions.phtml',
|
||||
];
|
||||
}
|
||||
|
||||
public function testDetailPageFactoryUsesActionPolicyModule(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/js/core/app-detail-page-factory.js');
|
||||
$this->assertStringContainsString("import { initDetailActionPolicy } from './app-details-action-policy.js';", $content);
|
||||
$this->assertStringContainsString('const actionPolicy = initDetailActionPolicy(', $content);
|
||||
$this->assertStringContainsString('actionPolicy.isSubmitting()', $content);
|
||||
}
|
||||
|
||||
public function testDetailsPartialsExposeActionPolicyContract(): void
|
||||
{
|
||||
$titlebar = $this->readProjectFile('templates/partials/app-details-titlebar.phtml');
|
||||
$this->assertStringContainsString('data-detail-action-policy="1"', $titlebar);
|
||||
$this->assertStringContainsString('data-detail-action-kind=', $titlebar);
|
||||
$this->assertStringContainsString('data-detail-confirm-message=', $titlebar);
|
||||
|
||||
$asideActions = $this->readProjectFile('templates/partials/app-details-aside-actions.phtml');
|
||||
$this->assertStringContainsString('data-detail-confirm-message=', $asideActions);
|
||||
|
||||
$danger = $this->readProjectFile('templates/partials/app-danger-zone-delete-field.phtml');
|
||||
$this->assertStringContainsString('data-detail-action-kind="delete"', $danger);
|
||||
}
|
||||
|
||||
public function testMigratedFilesNoLongerUseInlineConfirmHandlers(): void
|
||||
{
|
||||
foreach ($this->migratedConfirmFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringNotContainsString('onsubmit="return confirm(', $content, $file);
|
||||
$this->assertStringNotContainsString('onclick="return confirm(', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMigratedFilesExposeDataDetailConfirmContract(): void
|
||||
{
|
||||
foreach ($this->migratedConfirmFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString('data-detail-confirm-message', $content, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
tests/Architecture/DetailPageContractTest.php
Normal file
58
tests/Architecture/DetailPageContractTest.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DetailPageContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function standardDetailFormFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/users/_form.phtml',
|
||||
'pages/admin/tenants/_form.phtml',
|
||||
'pages/admin/departments/_form.phtml',
|
||||
'pages/admin/roles/_form.phtml',
|
||||
'pages/admin/permissions/_form.phtml',
|
||||
'pages/admin/scheduled-jobs/edit(default).phtml',
|
||||
'pages/admin/settings/index(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
public function testScopedDetailFormsUseStandardDetailOptInAttribute(): void
|
||||
{
|
||||
foreach ($this->standardDetailFormFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString('data-standard-detail-form="1"', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testAppInitIncludesStandardDetailPageAutoInitComponent(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/js/app-init.js');
|
||||
$this->assertStringContainsString("import './components/app-standard-detail-page.js';", $content);
|
||||
}
|
||||
|
||||
public function testDetailsTitlebarExposesUnsavedMessageAndPrimarySaveMarkers(): void
|
||||
{
|
||||
$content = $this->readProjectFile('templates/partials/app-details-titlebar.phtml');
|
||||
$this->assertStringContainsString('data-detail-unsaved-message=', $content);
|
||||
$this->assertStringContainsString('data-detail-action-policy="1"', $content);
|
||||
$this->assertStringContainsString('data-detail-action-kind=', $content);
|
||||
$this->assertStringContainsString('data-detail-save-primary="1"', $content);
|
||||
}
|
||||
|
||||
public function testImportsWizardFormsUseStandardDetailOptIn(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/imports/index(default).phtml');
|
||||
$this->assertStringContainsString('id="imports-upload-form"', $content);
|
||||
$this->assertStringContainsString('id="imports-mapping-form"', $content);
|
||||
$this->assertStringContainsString('id="imports-commit-form"', $content);
|
||||
$this->assertSame(3, substr_count($content, 'data-standard-detail-form="1"'));
|
||||
}
|
||||
}
|
||||
89
tests/Architecture/DetailValidationSummaryContractTest.php
Normal file
89
tests/Architecture/DetailValidationSummaryContractTest.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DetailValidationSummaryContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function detailTemplateFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/users/create(default).phtml',
|
||||
'pages/admin/users/edit(default).phtml',
|
||||
'pages/admin/tenants/create(default).phtml',
|
||||
'pages/admin/tenants/edit(default).phtml',
|
||||
'pages/admin/departments/create(default).phtml',
|
||||
'pages/admin/departments/edit(default).phtml',
|
||||
'pages/admin/roles/create(default).phtml',
|
||||
'pages/admin/roles/edit(default).phtml',
|
||||
'pages/admin/permissions/create(default).phtml',
|
||||
'pages/admin/permissions/edit(default).phtml',
|
||||
'pages/admin/scheduled-jobs/edit(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function detailActionFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/users/create().php',
|
||||
'pages/admin/users/edit($id).php',
|
||||
'pages/admin/tenants/create().php',
|
||||
'pages/admin/tenants/edit($id).php',
|
||||
'pages/admin/departments/create().php',
|
||||
'pages/admin/departments/edit($id).php',
|
||||
'pages/admin/roles/create().php',
|
||||
'pages/admin/roles/edit($id).php',
|
||||
'pages/admin/permissions/create().php',
|
||||
'pages/admin/permissions/edit($id).php',
|
||||
'pages/admin/scheduled-jobs/edit($id).php',
|
||||
];
|
||||
}
|
||||
|
||||
public function testScopedDetailTemplatesUseSharedValidationSummaryPartial(): void
|
||||
{
|
||||
foreach ($this->detailTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString("require templatePath('partials/app-details-validation-summary.phtml');", $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testScopedDetailTemplatesDoNotInlineLegacyErrorLoop(): void
|
||||
{
|
||||
foreach ($this->detailTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertSame(0, preg_match('/foreach\s*\(\$errors\s+as\s+\$error\)/', $content), $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSharedPartialExposesValidationSummaryContract(): void
|
||||
{
|
||||
$content = $this->readProjectFile('templates/partials/app-details-validation-summary.phtml');
|
||||
$this->assertStringContainsString('data-validation-summary', $content);
|
||||
$this->assertStringContainsString('data-validation-summary-autofocus=', $content);
|
||||
$this->assertStringContainsString('data-validation-error-link', $content);
|
||||
$this->assertStringNotContainsString('class="notice"', $content);
|
||||
}
|
||||
|
||||
public function testScopedDetailActionsProvideStructuredValidationSummaryErrors(): void
|
||||
{
|
||||
foreach ($this->detailActionFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString('$validationSummaryErrors = $errorBag->toArray();', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDefaultStyleGroupIncludesValidationSummaryComponentCss(): void
|
||||
{
|
||||
$content = $this->readProjectFile('config/assets.php');
|
||||
$this->assertStringContainsString("'css/components/app-details-validation-summary.css'", $content);
|
||||
}
|
||||
}
|
||||
113
tests/Architecture/FilterDrawerContractTest.php
Normal file
113
tests/Architecture/FilterDrawerContractTest.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class FilterDrawerContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
private function usesSharedListFiltersPartial(string $content): bool
|
||||
{
|
||||
return str_contains($content, "require templatePath('partials/app-list-filters.phtml');");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function drawerTemplateFiles(): array
|
||||
{
|
||||
return [
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
public function testDrawerTemplatesUseStandardListWrapperForFilterExperience(): void
|
||||
{
|
||||
foreach ($this->drawerTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$usesPartial = $this->usesSharedListFiltersPartial($content);
|
||||
$this->assertStringContainsString('initStandardListPage(', $content, $file);
|
||||
$this->assertStringContainsString("mode: 'drawer'", $content, $file);
|
||||
$this->assertStringNotContainsString('initListFilterExperience(', $content, $file);
|
||||
if (!$usesPartial) {
|
||||
$this->assertStringContainsString('role="dialog"', $content, $file);
|
||||
$this->assertStringContainsString('aria-modal="true"', $content, $file);
|
||||
$this->assertStringContainsString('aria-labelledby="', $content, $file);
|
||||
$this->assertStringContainsString('data-filter-live-region', $content, $file);
|
||||
}
|
||||
$this->assertStringNotContainsString('data-filter-drawer-draft-hint', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testAddressBookSaveFilterReadsAppliedGridState(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/address-book/index(default).phtml');
|
||||
|
||||
$this->assertStringContainsString('new URL(gridConfig.baseUrl()).searchParams', $content);
|
||||
$this->assertStringNotContainsString('const state = collectFilterState();', $content);
|
||||
$this->assertStringContainsString('data-filter-chips-clear-all-label', $content);
|
||||
$this->assertStringContainsString('data-filter-chips-remove-label', $content);
|
||||
$this->assertStringContainsString('data-filter-live-applied-message', $content);
|
||||
$this->assertStringContainsString('data-filter-live-removed-message', $content);
|
||||
$this->assertStringContainsString('data-filter-live-cleared-message', $content);
|
||||
}
|
||||
|
||||
public function testUsersResetKeepsTenantParam(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/users/index(default).phtml');
|
||||
|
||||
$this->assertStringContainsString("preserveFilterParams: ['tenant']", $content);
|
||||
}
|
||||
|
||||
public function testFilterDrawerAndExperienceImplementFocusTrapAndDirtyApplyUi(): void
|
||||
{
|
||||
$drawerJs = $this->readProjectFile('web/js/components/app-filter-drawer.js');
|
||||
$this->assertStringContainsString("drawer.setAttribute('role', 'dialog')", $drawerJs);
|
||||
$this->assertStringContainsString("drawer.setAttribute('aria-modal', 'true')", $drawerJs);
|
||||
$this->assertStringContainsString("if (event.key !== 'Tab') {return;}", $drawerJs);
|
||||
$this->assertStringContainsString('if (lastTrigger && typeof lastTrigger.focus === \'function\'', $drawerJs);
|
||||
|
||||
$experienceJs = $this->readProjectFile('web/js/pages/app-list-filter-experience.js');
|
||||
$this->assertStringContainsString('countDraftChanges(', $experienceJs);
|
||||
$this->assertStringContainsString('countActiveDraftFilters(', $experienceJs);
|
||||
$this->assertStringContainsString('drawerController.setApplyEnabled(changedCount > 0);', $experienceJs);
|
||||
$this->assertStringContainsString('drawerController.setApplyCount(activeCount);', $experienceJs);
|
||||
$this->assertStringContainsString('announceLive(', $experienceJs);
|
||||
|
||||
$stateJs = $this->readProjectFile('web/js/pages/app-list-filter-state.js');
|
||||
$this->assertStringContainsString('buildChipsFromMeta', $stateJs);
|
||||
$this->assertStringContainsString('removeChipFromMetaState', $stateJs);
|
||||
$this->assertStringContainsString('clearMetaState', $stateJs);
|
||||
}
|
||||
|
||||
public function testSharedListFiltersPartialKeepsDrawerAccessibilityContract(): void
|
||||
{
|
||||
$content = $this->readProjectFile('templates/partials/app-list-filters.phtml');
|
||||
$this->assertStringContainsString('renderGridFilterToolbar(', $content);
|
||||
$this->assertStringContainsString('data-filter-drawer-open', $content);
|
||||
$this->assertStringContainsString('data-filter-drawer-apply', $content);
|
||||
$this->assertStringContainsString('data-active-filter-chips', $content);
|
||||
$this->assertStringContainsString('data-filter-chips-clear-all-label', $content);
|
||||
$this->assertStringContainsString('data-filter-chips-remove-label', $content);
|
||||
$this->assertStringContainsString('role="dialog"', $content);
|
||||
$this->assertStringContainsString('aria-modal="true"', $content);
|
||||
$this->assertStringContainsString('aria-labelledby=', $content);
|
||||
$this->assertStringContainsString('data-filter-live-region', $content);
|
||||
$this->assertStringContainsString('data-filter-live-applied-message', $content);
|
||||
$this->assertStringContainsString('data-filter-live-removed-message', $content);
|
||||
$this->assertStringContainsString('data-filter-live-cleared-message', $content);
|
||||
}
|
||||
}
|
||||
287
tests/Architecture/ListFilterContractTest.php
Normal file
287
tests/Architecture/ListFilterContractTest.php
Normal file
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ListFilterContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
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 [
|
||||
'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 [
|
||||
'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 [
|
||||
'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 [
|
||||
'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 [
|
||||
'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);
|
||||
$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(', $content, $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);
|
||||
$usesPartial = $this->usesSharedListFiltersPartial($content);
|
||||
$this->assertStringContainsString("mode: 'drawer'", $content, $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);
|
||||
$this->assertStringContainsString("mode: 'search-only'", $content, $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
tests/Architecture/ListTitlebarContractTest.php
Normal file
49
tests/Architecture/ListTitlebarContractTest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ListTitlebarContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function titlebarTemplateFiles(): array
|
||||
{
|
||||
return [
|
||||
'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',
|
||||
'pages/address-book/index(default).phtml',
|
||||
'pages/search/index(default).phtml',
|
||||
'pages/help/hotkeys(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
public function testTemplatesUseSharedListTitlebarPartial(): void
|
||||
{
|
||||
foreach ($this->titlebarTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString("require templatePath('partials/app-list-titlebar.phtml');", $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTemplatesDoNotInlineLegacyListTitlebarMarkup(): void
|
||||
{
|
||||
foreach ($this->titlebarTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertSame(0, preg_match('/<div\s+class=\"app-list-titlebar\"/', $content), $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
tests/Architecture/ListUiSharedPartialsContractTest.php
Normal file
73
tests/Architecture/ListUiSharedPartialsContractTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ListUiSharedPartialsContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function listTabsTemplateFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/users/index(default).phtml',
|
||||
'pages/admin/departments/index(default).phtml',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function purgeTitlebarTemplateFiles(): array
|
||||
{
|
||||
return [
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
public function testTabsTemplatesUseSharedTabsPartial(): void
|
||||
{
|
||||
foreach ($this->listTabsTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString("require templatePath('partials/app-list-tabs.phtml');", $content, $file);
|
||||
$this->assertSame(0, preg_match('/<div\s+class=\"app-list-tabs\"/', $content), $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPurgeTitlebarTemplatesUseSharedPurgePartial(): void
|
||||
{
|
||||
foreach ($this->purgeTitlebarTemplateFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString("require templatePath('partials/app-list-purge-action.phtml');", $content, $file);
|
||||
$this->assertSame(0, preg_match('/<form\s+id=\"[^\"]*purge-form\"/', $content), $file);
|
||||
$this->assertStringNotContainsString('onclick="return confirm(\'', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTabsAndPurgePartialsExposeExpectedMarkupContract(): void
|
||||
{
|
||||
$tabsPartial = $this->readProjectFile('templates/partials/app-list-tabs.phtml');
|
||||
$this->assertStringContainsString('class="app-list-tabs"', $tabsPartial);
|
||||
$this->assertStringContainsString("class=\"<?php e(\$tabActive ? 'tab-link is-active' : 'tab-link'); ?>\"", $tabsPartial);
|
||||
|
||||
$purgePartial = $this->readProjectFile('templates/partials/app-list-purge-action.phtml');
|
||||
$this->assertStringContainsString('method="post"', $purgePartial);
|
||||
$this->assertStringContainsString('Session::getCsrfInput();', $purgePartial);
|
||||
$this->assertStringContainsString('data-confirm-message="<?php e($listPurgeConfirmMessage); ?>"', $purgePartial);
|
||||
$this->assertStringNotContainsString('onclick="return confirm(\'', $purgePartial);
|
||||
}
|
||||
|
||||
public function testAppInitIncludesGlobalConfirmActionComponent(): void
|
||||
{
|
||||
$content = $this->readProjectFile('web/js/app-init.js');
|
||||
$this->assertStringContainsString("import './components/app-confirm-actions.js';", $content);
|
||||
}
|
||||
}
|
||||
26
tests/Architecture/ProjectFileAssertionSupport.php
Normal file
26
tests/Architecture/ProjectFileAssertionSupport.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
trait ProjectFileAssertionSupport
|
||||
{
|
||||
private function projectRootPath(): string
|
||||
{
|
||||
$root = realpath(__DIR__ . '/../..');
|
||||
$this->assertNotFalse($root, 'Project root not found.');
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
private function readProjectFile(string $path): string
|
||||
{
|
||||
$root = $this->projectRootPath();
|
||||
$fullPath = $root . '/' . $path;
|
||||
$this->assertFileExists($fullPath, 'File not found: ' . $path);
|
||||
|
||||
$content = file_get_contents($fullPath);
|
||||
$this->assertNotFalse($content, 'Could not read file: ' . $path);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
174
tests/Architecture/StatusTaxonomyContractTest.php
Normal file
174
tests/Architecture/StatusTaxonomyContractTest.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Architecture;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StatusTaxonomyContractTest extends TestCase
|
||||
{
|
||||
use ProjectFileAssertionSupport;
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function taxonomyEnumFiles(): array
|
||||
{
|
||||
return [
|
||||
'lib/Domain/Taxonomy/SystemAuditOutcome.php',
|
||||
'lib/Domain/Taxonomy/SystemAuditChannel.php',
|
||||
'lib/Domain/Taxonomy/UserLifecycleAction.php',
|
||||
'lib/Domain/Taxonomy/UserLifecycleTriggerType.php',
|
||||
'lib/Domain/Taxonomy/UserLifecycleStatus.php',
|
||||
'lib/Domain/Taxonomy/ImportAuditStatus.php',
|
||||
'lib/Domain/Taxonomy/ScheduledJobStatus.php',
|
||||
'lib/Domain/Taxonomy/ScheduledJobRunStatus.php',
|
||||
'lib/Domain/Taxonomy/ScheduledJobTriggerType.php',
|
||||
'lib/Domain/Taxonomy/MailLogStatus.php',
|
||||
'lib/Domain/Taxonomy/TenantStatus.php',
|
||||
'lib/Domain/Taxonomy/SchedulerRuntimeResult.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,list<string>>
|
||||
*/
|
||||
private function taxonomySchemaExpectations(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/system-audit/filter-schema.php' => [
|
||||
'SystemAuditOutcome::values()',
|
||||
'SystemAuditChannel::values()',
|
||||
],
|
||||
'pages/admin/user-lifecycle-audit/filter-schema.php' => [
|
||||
'gridEnumSanitizer(UserLifecycleAction::class)',
|
||||
'gridEnumSanitizer(UserLifecycleStatus::class)',
|
||||
'gridEnumSanitizer(UserLifecycleTriggerType::class)',
|
||||
],
|
||||
'pages/admin/import-audit/filter-schema.php' => [
|
||||
'ImportAuditStatus::values()',
|
||||
],
|
||||
'pages/admin/scheduled-jobs/filter-schema.php' => [
|
||||
'ScheduledJobStatus::values()',
|
||||
],
|
||||
'pages/admin/scheduled-jobs/runs-filter-schema.php' => [
|
||||
'ScheduledJobRunStatus::values()',
|
||||
'ScheduledJobTriggerType::values()',
|
||||
],
|
||||
'pages/admin/mail-log/filter-schema.php' => [
|
||||
'MailLogStatus::values()',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function taxonomyDataEndpointFiles(): array
|
||||
{
|
||||
return [
|
||||
'pages/admin/system-audit/data().php',
|
||||
'pages/admin/user-lifecycle-audit/data().php',
|
||||
'pages/admin/import-audit/data().php',
|
||||
'pages/admin/scheduled-jobs/data().php',
|
||||
'pages/admin/scheduled-jobs/runs-data($id).php',
|
||||
'pages/admin/mail-log/data().php',
|
||||
'pages/admin/tenants/data().php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function taxonomyLiteralGuardFiles(): array
|
||||
{
|
||||
return [
|
||||
'lib/Service/Audit/SystemAuditService.php',
|
||||
'lib/Repository/Audit/SystemAuditLogRepository.php',
|
||||
'lib/Http/ApiSystemAuditReporter.php',
|
||||
'lib/Service/Audit/UserLifecycleAuditService.php',
|
||||
'lib/Repository/Audit/UserLifecycleAuditRepository.php',
|
||||
'lib/Service/Audit/ImportAuditService.php',
|
||||
'lib/Repository/Audit/ImportAuditRunRepository.php',
|
||||
'lib/Service/Scheduler/SchedulerRunService.php',
|
||||
'lib/Repository/Scheduler/ScheduledJobRepository.php',
|
||||
'lib/Repository/Scheduler/ScheduledJobRunRepository.php',
|
||||
'lib/Repository/Scheduler/SchedulerRuntimeRepository.php',
|
||||
'lib/Service/Mail/MailService.php',
|
||||
'lib/Repository/Mail/MailLogRepository.php',
|
||||
'lib/Service/Tenant/TenantService.php',
|
||||
'lib/Repository/Tenant/TenantRepository.php',
|
||||
'lib/Repository/Stats/AdminStatsRepository.php',
|
||||
'pages/admin/system-audit/data().php',
|
||||
'pages/admin/user-lifecycle-audit/data().php',
|
||||
'pages/admin/import-audit/data().php',
|
||||
'pages/admin/scheduled-jobs/data().php',
|
||||
'pages/admin/scheduled-jobs/runs-data($id).php',
|
||||
'pages/admin/mail-log/data().php',
|
||||
'pages/admin/tenants/data().php',
|
||||
'pages/admin/system-audit/index().php',
|
||||
'pages/admin/user-lifecycle-audit/index().php',
|
||||
'pages/admin/import-audit/index().php',
|
||||
'pages/admin/scheduled-jobs/index().php',
|
||||
'pages/admin/mail-log/index().php',
|
||||
];
|
||||
}
|
||||
|
||||
public function testTaxonomyEnumFilesUseSharedTraitAndRequiredMethods(): void
|
||||
{
|
||||
foreach ($this->taxonomyEnumFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString('enum ', $content, $file);
|
||||
$this->assertStringContainsString('use SupportsStringTaxonomy;', $content, $file);
|
||||
$this->assertStringContainsString('function labelToken()', $content, $file);
|
||||
$this->assertStringContainsString('function badgeVariant()', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFilterSchemasReadTaxonomyValuesFromEnums(): void
|
||||
{
|
||||
foreach ($this->taxonomySchemaExpectations() as $file => $snippets) {
|
||||
$content = $this->readProjectFile($file);
|
||||
foreach ($snippets as $snippet) {
|
||||
$this->assertStringContainsString($snippet, $content, $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testDataEndpointsUseEnumBasedBadgeAndLabelResolution(): void
|
||||
{
|
||||
foreach ($this->taxonomyDataEndpointFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
$this->assertStringContainsString('->badgeVariant()', $content, $file);
|
||||
$this->assertStringContainsString('->labelToken()', $content, $file);
|
||||
}
|
||||
}
|
||||
|
||||
public function testNoLegacyLiteralTaxonomyChecksRemainInMigratedFiles(): void
|
||||
{
|
||||
$forbiddenPatterns = [
|
||||
"/\\bstatus\\s*=\\s*'(running|success|partial|failed|skipped|queued|sent|active|inactive)'/",
|
||||
"/\\boutcome\\s*=\\s*'(success|failed|denied)'/",
|
||||
"/\\bchannel\\s*=\\s*'(web|api|scheduler|cli)'/",
|
||||
"/\\btrigger_type\\s*=\\s*'(scheduler|manual|cron|system)'/",
|
||||
"/\\baction\\s*=\\s*'(deactivate|delete|restore)'/",
|
||||
"/['\\\"]statuses['\\\"]\\s*=>\\s*'failed,skipped'/",
|
||||
"/['\\\"]actions['\\\"]\\s*=>\\s*'restore'/",
|
||||
"/in_array\\([^\\n]*\\[(?:[^\\]]*'(?:success|failed|denied|running|partial|skipped|queued|sent|active|inactive|scheduler|manual|web|api|cli|deactivate|delete|restore|ok|lock_not_acquired|unexpected_error)')/",
|
||||
];
|
||||
|
||||
foreach ($this->taxonomyLiteralGuardFiles() as $file) {
|
||||
$content = $this->readProjectFile($file);
|
||||
foreach ($forbiddenPatterns as $pattern) {
|
||||
$this->assertSame(0, preg_match($pattern, $content), $file . ' pattern=' . $pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testImportAuditIndexTemplateDoesNotUseLegacyStatusMapper(): void
|
||||
{
|
||||
$content = $this->readProjectFile('pages/admin/import-audit/index(default).phtml');
|
||||
$this->assertStringNotContainsString('const statusLabel = (value) =>', $content);
|
||||
$this->assertStringNotContainsString("if (key === 'running')", $content);
|
||||
$this->assertStringContainsString('cell.label || \'-\'', $content);
|
||||
}
|
||||
}
|
||||
78
tests/Domain/Taxonomy/TaxonomyEnumContractTest.php
Normal file
78
tests/Domain/Taxonomy/TaxonomyEnumContractTest.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Domain\Taxonomy;
|
||||
|
||||
use MintyPHP\Domain\Taxonomy\ImportAuditStatus;
|
||||
use MintyPHP\Domain\Taxonomy\MailLogStatus;
|
||||
use MintyPHP\Domain\Taxonomy\ScheduledJobRunStatus;
|
||||
use MintyPHP\Domain\Taxonomy\ScheduledJobStatus;
|
||||
use MintyPHP\Domain\Taxonomy\ScheduledJobTriggerType;
|
||||
use MintyPHP\Domain\Taxonomy\SchedulerRuntimeResult;
|
||||
use MintyPHP\Domain\Taxonomy\SystemAuditChannel;
|
||||
use MintyPHP\Domain\Taxonomy\SystemAuditOutcome;
|
||||
use MintyPHP\Domain\Taxonomy\TenantStatus;
|
||||
use MintyPHP\Domain\Taxonomy\UserLifecycleAction;
|
||||
use MintyPHP\Domain\Taxonomy\UserLifecycleStatus;
|
||||
use MintyPHP\Domain\Taxonomy\UserLifecycleTriggerType;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TaxonomyEnumContractTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return iterable<string, array{0: class-string<\BackedEnum>, 1: list<string>}>
|
||||
*/
|
||||
public static function enumProvider(): iterable
|
||||
{
|
||||
yield 'SystemAuditOutcome' => [SystemAuditOutcome::class, ['success', 'failed', 'denied']];
|
||||
yield 'SystemAuditChannel' => [SystemAuditChannel::class, ['web', 'api', 'scheduler', 'cli']];
|
||||
yield 'UserLifecycleAction' => [UserLifecycleAction::class, ['deactivate', 'delete', 'restore']];
|
||||
yield 'UserLifecycleTriggerType' => [UserLifecycleTriggerType::class, ['manual', 'cron', 'system']];
|
||||
yield 'UserLifecycleStatus' => [UserLifecycleStatus::class, ['success', 'skipped', 'failed']];
|
||||
yield 'ImportAuditStatus' => [ImportAuditStatus::class, ['running', 'success', 'partial', 'failed']];
|
||||
yield 'ScheduledJobStatus' => [ScheduledJobStatus::class, ['running', 'success', 'failed', 'skipped']];
|
||||
yield 'ScheduledJobRunStatus' => [ScheduledJobRunStatus::class, ['success', 'failed', 'skipped']];
|
||||
yield 'ScheduledJobTriggerType' => [ScheduledJobTriggerType::class, ['scheduler', 'manual']];
|
||||
yield 'MailLogStatus' => [MailLogStatus::class, ['queued', 'sent', 'failed']];
|
||||
yield 'TenantStatus' => [TenantStatus::class, ['active', 'inactive']];
|
||||
yield 'SchedulerRuntimeResult' => [SchedulerRuntimeResult::class, ['ok', 'lock_not_acquired', 'unexpected_error']];
|
||||
}
|
||||
|
||||
#[DataProvider('enumProvider')]
|
||||
public function testValuesReturnExpectedList(string $enumClass, array $expectedValues): void
|
||||
{
|
||||
$this->assertSame($expectedValues, $enumClass::values());
|
||||
}
|
||||
|
||||
#[DataProvider('enumProvider')]
|
||||
public function testTryNormalizeAndNormalizeOr(string $enumClass, array $expectedValues): void
|
||||
{
|
||||
$firstValue = $expectedValues[0];
|
||||
$firstCase = $enumClass::from($firstValue);
|
||||
|
||||
$this->assertSame($firstValue, $enumClass::tryNormalize($firstValue)?->value);
|
||||
$this->assertSame($firstValue, $enumClass::tryNormalize(strtoupper($firstValue))?->value);
|
||||
$this->assertSame($firstValue, $enumClass::tryNormalize(' ' . strtoupper($firstValue) . ' ')?->value);
|
||||
$this->assertNull($enumClass::tryNormalize(''));
|
||||
$this->assertNull($enumClass::tryNormalize('not-valid'));
|
||||
|
||||
$this->assertSame($firstValue, $enumClass::normalizeOr('not-valid', $firstCase)->value);
|
||||
$this->assertSame($firstValue, $enumClass::normalizeOr($firstValue, $firstCase)->value);
|
||||
}
|
||||
|
||||
#[DataProvider('enumProvider')]
|
||||
public function testBadgeVariantAndLabelTokenAreDefined(string $enumClass, array $expectedValues): void
|
||||
{
|
||||
foreach ($expectedValues as $value) {
|
||||
$case = $enumClass::from($value);
|
||||
|
||||
$labelToken = $case->labelToken();
|
||||
$badgeVariant = $case->badgeVariant();
|
||||
|
||||
$this->assertIsString($labelToken);
|
||||
$this->assertNotSame('', trim($labelToken));
|
||||
$this->assertIsString($badgeVariant);
|
||||
$this->assertNotSame('', trim($badgeVariant));
|
||||
}
|
||||
}
|
||||
}
|
||||
132
tests/Http/ApiSystemAuditReporterTest.php
Normal file
132
tests/Http/ApiSystemAuditReporterTest.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Http;
|
||||
|
||||
use MintyPHP\Http\ApiAuth;
|
||||
use MintyPHP\Http\ApiSystemAuditReporter;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ApiSystemAuditReporterTest extends TestCase
|
||||
{
|
||||
private array $serverBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->serverBackup = $_SERVER;
|
||||
$_SERVER = [];
|
||||
RequestContext::resetForTests();
|
||||
ApiAuth::authenticate();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->serverBackup;
|
||||
RequestContext::resetForTests();
|
||||
ApiAuth::authenticate();
|
||||
}
|
||||
|
||||
public function testItRecordsMutatingRequest(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/users/123';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'api.request',
|
||||
'success',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['endpoint_key'] ?? '') === '/api/v1/users/{id}'
|
||||
&& ($metadata['status_code'] ?? null) === 201
|
||||
&& ($metadata['status_class'] ?? '') === '2xx'
|
||||
&& ($metadata['auth_mode'] ?? '') === 'public'
|
||||
&& ($metadata['write_method'] ?? null) === true
|
||||
&& ($metadata['security_endpoint'] ?? null) === false;
|
||||
})
|
||||
);
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(201);
|
||||
}
|
||||
|
||||
public function testItSkipsNonSecurityGetSuccess(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/users';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->never())->method('record');
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(200);
|
||||
}
|
||||
|
||||
public function testItRecordsDeniedSecurityGetFailure(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/me/tokens';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'api.request',
|
||||
'denied',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['endpoint_key'] ?? '') === '/api/v1/me/tokens'
|
||||
&& ($metadata['status_code'] ?? null) === 401
|
||||
&& ($metadata['status_class'] ?? '') === '4xx'
|
||||
&& ($metadata['security_endpoint'] ?? null) === true;
|
||||
})
|
||||
);
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(401, 'unauthorized');
|
||||
}
|
||||
|
||||
public function testItRecordsFailedServerError(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/users';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'api.request',
|
||||
'failed',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['status_code'] ?? null) === 500
|
||||
&& ($metadata['status_class'] ?? '') === '5xx'
|
||||
&& ($metadata['write_method'] ?? null) === false;
|
||||
})
|
||||
);
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(500, 'unexpected_error');
|
||||
}
|
||||
|
||||
public function testFinishIsIdempotent(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/tenants';
|
||||
|
||||
$audit = $this->createMock(SystemAuditService::class);
|
||||
$audit->expects($this->once())->method('record');
|
||||
|
||||
$reporter = new ApiSystemAuditReporter($audit);
|
||||
$reporter->start();
|
||||
$reporter->finish(201);
|
||||
$reporter->finish(201);
|
||||
}
|
||||
}
|
||||
53
tests/Http/RequestContextTest.php
Normal file
53
tests/Http/RequestContextTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Http;
|
||||
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RequestContextTest extends TestCase
|
||||
{
|
||||
private array $serverBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->serverBackup = $_SERVER;
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->serverBackup;
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
public function testItGeneratesStableRequestIdPerRequest(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/admin/users?x=1';
|
||||
|
||||
RequestContext::start();
|
||||
$first = RequestContext::id();
|
||||
$second = RequestContext::id();
|
||||
|
||||
$this->assertSame($first, $second);
|
||||
$this->assertMatchesRegularExpression('/^[a-f0-9-]{36}$/', $first);
|
||||
}
|
||||
|
||||
public function testItAppliesOverridesForChannelAndPath(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/admin/settings';
|
||||
|
||||
RequestContext::start([
|
||||
'channel' => 'scheduler',
|
||||
'path' => '/bin/scheduler-run.php',
|
||||
]);
|
||||
|
||||
$context = RequestContext::context();
|
||||
|
||||
$this->assertSame('scheduler', $context['channel']);
|
||||
$this->assertSame('/bin/scheduler-run.php', $context['path']);
|
||||
$this->assertSame('POST', $context['method']);
|
||||
}
|
||||
}
|
||||
@@ -80,4 +80,16 @@ class RepoQueryTest extends TestCase
|
||||
$this->assertSame(['foo = ?'], $where);
|
||||
$this->assertSame(['bar'], $params);
|
||||
}
|
||||
|
||||
public function testNormalizeStringList(): void
|
||||
{
|
||||
$list = RepoQuery::normalizeStringList(' a, b, a ,, c ');
|
||||
$this->assertSame(['a', 'b', 'c'], $list);
|
||||
|
||||
$list = RepoQuery::normalizeStringList([' A ', ['B', 'a'], ''], 10, static fn (string $value): string => strtolower($value));
|
||||
$this->assertSame(['a', 'b'], $list);
|
||||
|
||||
$list = RepoQuery::normalizeStringList('1,2,3,4', 2);
|
||||
$this->assertSame(['1', '2'], $list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Repository\Access\PermissionRepository;
|
||||
use MintyPHP\Repository\Access\RolePermissionRepository;
|
||||
use MintyPHP\Repository\Access\UserRoleRepository;
|
||||
use MintyPHP\Service\Access\AccessServicesFactory;
|
||||
use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Access\DepartmentAuthorizationPolicy;
|
||||
use MintyPHP\Service\Access\PermissionAuthorizationPolicy;
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
use MintyPHP\Service\Access\RoleAuthorizationPolicy;
|
||||
use MintyPHP\Service\Access\SettingsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Access\TenantAuthorizationPolicy;
|
||||
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AccessServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new AccessServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(PermissionRepository::class, $factory->createPermissionRepository());
|
||||
$this->assertSame($factory->createPermissionRepository(), $factory->createPermissionRepository());
|
||||
|
||||
$this->assertInstanceOf(RolePermissionRepository::class, $factory->createRolePermissionRepository());
|
||||
$this->assertSame($factory->createRolePermissionRepository(), $factory->createRolePermissionRepository());
|
||||
|
||||
$this->assertInstanceOf(UserRoleRepository::class, $factory->createUserRoleRepository());
|
||||
$this->assertSame($factory->createUserRoleRepository(), $factory->createUserRoleRepository());
|
||||
|
||||
$this->assertInstanceOf('MintyPHP\\Service\\Access\\PermissionService', $factory->createPermissionService());
|
||||
$this->assertSame($factory->createPermissionService(), $factory->createPermissionService());
|
||||
|
||||
$this->assertInstanceOf(PermissionGateway::class, $factory->createPermissionGateway());
|
||||
$this->assertSame($factory->createPermissionGateway(), $factory->createPermissionGateway());
|
||||
|
||||
$this->assertInstanceOf(UserAuthorizationPolicy::class, $factory->createUserAuthorizationPolicy());
|
||||
$this->assertSame($factory->createUserAuthorizationPolicy(), $factory->createUserAuthorizationPolicy());
|
||||
|
||||
$this->assertInstanceOf(RoleAuthorizationPolicy::class, $factory->createRoleAuthorizationPolicy());
|
||||
$this->assertSame($factory->createRoleAuthorizationPolicy(), $factory->createRoleAuthorizationPolicy());
|
||||
|
||||
$this->assertInstanceOf(PermissionAuthorizationPolicy::class, $factory->createPermissionAuthorizationPolicy());
|
||||
$this->assertSame($factory->createPermissionAuthorizationPolicy(), $factory->createPermissionAuthorizationPolicy());
|
||||
|
||||
$this->assertInstanceOf(SettingsAuthorizationPolicy::class, $factory->createSettingsAuthorizationPolicy());
|
||||
$this->assertSame($factory->createSettingsAuthorizationPolicy(), $factory->createSettingsAuthorizationPolicy());
|
||||
|
||||
$this->assertInstanceOf(TenantAuthorizationPolicy::class, $factory->createTenantAuthorizationPolicy());
|
||||
$this->assertSame($factory->createTenantAuthorizationPolicy(), $factory->createTenantAuthorizationPolicy());
|
||||
|
||||
$this->assertInstanceOf(DepartmentAuthorizationPolicy::class, $factory->createDepartmentAuthorizationPolicy());
|
||||
$this->assertSame($factory->createDepartmentAuthorizationPolicy(), $factory->createDepartmentAuthorizationPolicy());
|
||||
|
||||
$this->assertInstanceOf(AuthorizationService::class, $factory->createAuthorizationService());
|
||||
$this->assertSame($factory->createAuthorizationService(), $factory->createAuthorizationService());
|
||||
}
|
||||
}
|
||||
36
tests/Service/Access/AuthorizationPolicyTestSupport.php
Normal file
36
tests/Service/Access/AuthorizationPolicyTestSupport.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
|
||||
trait AuthorizationPolicyTestSupport
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<int, string>> $permissionsByUser
|
||||
*/
|
||||
private function permissionGatewayAllowing(array $permissionsByUser): PermissionGateway
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission) use ($permissionsByUser): bool {
|
||||
return in_array($permission, $permissionsByUser[$userId] ?? [], true);
|
||||
});
|
||||
|
||||
return $permissionGateway;
|
||||
}
|
||||
|
||||
private function assertForbiddenDecision(mixed $decision, string $error = 'forbidden'): void
|
||||
{
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame($error, $decision->error());
|
||||
}
|
||||
|
||||
private function assertDeniedDecision(mixed $decision, int $status, string $error): void
|
||||
{
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame($status, $decision->status());
|
||||
$this->assertSame($error, $decision->error());
|
||||
}
|
||||
}
|
||||
@@ -52,5 +52,23 @@ class AuthorizationServiceTest extends TestCase
|
||||
$this->assertSame(500, $decision->status());
|
||||
$this->assertSame('authorization_policy_missing', $decision->error());
|
||||
}
|
||||
}
|
||||
|
||||
public function testAllowReturnsBoolDecisionShortcut(): void
|
||||
{
|
||||
$policy = new class implements AuthorizationPolicyInterface {
|
||||
public function supports(string $ability): bool
|
||||
{
|
||||
return $ability === 'users.test';
|
||||
}
|
||||
|
||||
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||
{
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
};
|
||||
|
||||
$service = new AuthorizationService([$policy]);
|
||||
$this->assertTrue($service->allow('users.test', ['actor_user_id' => 7]));
|
||||
$this->assertFalse($service->allow('users.unknown', ['actor_user_id' => 7]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
{
|
||||
use AuthorizationPolicyTestSupport;
|
||||
|
||||
public function testViewRequiresDepartmentsViewPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
@@ -27,22 +29,14 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 5,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testViewReturnsCreateCapabilityAndTenantScopeData(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::DEPARTMENTS_VIEW, PermissionService::DEPARTMENTS_CREATE], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::DEPARTMENTS_VIEW, PermissionService::DEPARTMENTS_CREATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -82,15 +76,14 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 7,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testCreateDeniedInStrictScopeWithoutTenantAssignments(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 7 && $permission === PermissionService::DEPARTMENTS_CREATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
7 => [PermissionService::DEPARTMENTS_CREATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -106,16 +99,14 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 7,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testCreateReturnsScopeCapabilitiesWhenAllowed(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 7 && $permission === PermissionService::DEPARTMENTS_CREATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
7 => [PermissionService::DEPARTMENTS_CREATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -149,16 +140,14 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'target_department_id' => 17,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testEditContextDeniesOutOfScopeDepartment(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::DEPARTMENTS_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::DEPARTMENTS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -172,21 +161,14 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'target_department_id' => 17,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testEditSubmitRequiresDepartmentsUpdatePermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $permission === PermissionService::DEPARTMENTS_VIEW;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::DEPARTMENTS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -204,15 +186,14 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'target_department_id' => 17,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testDeleteRequiresPermissionAndScope(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 9 && $permission === PermissionService::DEPARTMENTS_DELETE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::DEPARTMENTS_DELETE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -231,9 +212,9 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testDeleteDeniesOutOfScopeDepartment(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 9 && $permission === PermissionService::DEPARTMENTS_DELETE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::DEPARTMENTS_DELETE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -247,7 +228,6 @@ class DepartmentAuthorizationPolicyTest extends TestCase
|
||||
'target_department_id' => 44,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PermissionAuthorizationPolicyTest extends TestCase
|
||||
{
|
||||
use AuthorizationPolicyTestSupport;
|
||||
|
||||
public function testAdminViewRequiresPermissionsViewPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
@@ -22,22 +24,14 @@ class PermissionAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 9,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testAdminViewIncludesCreateCapability(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::PERMISSIONS_VIEW, PermissionService::PERMISSIONS_CREATE], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::PERMISSIONS_VIEW, PermissionService::PERMISSIONS_CREATE],
|
||||
]);
|
||||
|
||||
$policy = new PermissionAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_VIEW, [
|
||||
@@ -61,25 +55,18 @@ class PermissionAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 3,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testEditContextReturnsCapabilitiesAndSystemDeleteRestriction(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [
|
||||
PermissionService::PERMISSIONS_VIEW,
|
||||
PermissionService::PERMISSIONS_UPDATE,
|
||||
PermissionService::PERMISSIONS_DELETE,
|
||||
], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [
|
||||
PermissionService::PERMISSIONS_VIEW,
|
||||
PermissionService::PERMISSIONS_UPDATE,
|
||||
PermissionService::PERMISSIONS_DELETE,
|
||||
],
|
||||
]);
|
||||
|
||||
$policy = new PermissionAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_CONTEXT, [
|
||||
@@ -96,15 +83,9 @@ class PermissionAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testEditSubmitRequiresPermissionsUpdatePermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $permission === PermissionService::PERMISSIONS_VIEW;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::PERMISSIONS_VIEW],
|
||||
]);
|
||||
|
||||
$policy = new PermissionAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(PermissionAuthorizationPolicy::ABILITY_ADMIN_PERMISSIONS_EDIT_SUBMIT, [
|
||||
@@ -113,9 +94,7 @@ class PermissionAuthorizationPolicyTest extends TestCase
|
||||
'target_is_system' => false,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testDeleteRequiresPermissionsDeletePermission(): void
|
||||
@@ -131,7 +110,6 @@ class PermissionAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 12,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PermissionGatewayTest extends TestCase
|
||||
{
|
||||
public function testUserHasDelegatesToService(): void
|
||||
{
|
||||
$permissionService = $this->createMock(PermissionService::class);
|
||||
$permissionService->expects($this->once())
|
||||
->method('userHas')
|
||||
->with(7, 'users.view')
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = new PermissionGateway($permissionService);
|
||||
$this->assertTrue($gateway->userHas(7, 'users.view'));
|
||||
}
|
||||
|
||||
public function testListPagedDelegatesToService(): void
|
||||
{
|
||||
$permissionService = $this->createMock(PermissionService::class);
|
||||
$permissionService->expects($this->once())
|
||||
->method('listPaged')
|
||||
->with(['limit' => 10])
|
||||
->willReturn(['data' => [], 'total' => 0]);
|
||||
|
||||
$gateway = new PermissionGateway($permissionService);
|
||||
$this->assertSame(['data' => [], 'total' => 0], $gateway->listPaged(['limit' => 10]));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RoleAuthorizationPolicyTest extends TestCase
|
||||
{
|
||||
use AuthorizationPolicyTestSupport;
|
||||
|
||||
public function testAdminViewRequiresRolesViewPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
@@ -22,22 +24,14 @@ class RoleAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 7,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testAdminViewIncludesCreateCapability(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 7) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::ROLES_VIEW, PermissionService::ROLES_CREATE], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
7 => [PermissionService::ROLES_VIEW, PermissionService::ROLES_CREATE],
|
||||
]);
|
||||
|
||||
$policy = new RoleAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_VIEW, [
|
||||
@@ -61,8 +55,7 @@ class RoleAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 11,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testEditContextRequiresTargetRoleId(): void
|
||||
@@ -76,24 +69,14 @@ class RoleAuthorizationPolicyTest extends TestCase
|
||||
'target_role_id' => 0,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testEditContextReturnsCapabilities(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [
|
||||
PermissionService::ROLES_VIEW,
|
||||
PermissionService::ROLES_UPDATE,
|
||||
], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::ROLES_VIEW, PermissionService::ROLES_UPDATE],
|
||||
]);
|
||||
|
||||
$policy = new RoleAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_CONTEXT, [
|
||||
@@ -109,15 +92,9 @@ class RoleAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testEditSubmitRequiresRolesUpdatePermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $permission === PermissionService::ROLES_VIEW;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::ROLES_VIEW],
|
||||
]);
|
||||
|
||||
$policy = new RoleAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(RoleAuthorizationPolicy::ABILITY_ADMIN_ROLES_EDIT_SUBMIT, [
|
||||
@@ -125,9 +102,7 @@ class RoleAuthorizationPolicyTest extends TestCase
|
||||
'target_role_id' => 22,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testDeleteRequiresRolesDeletePermission(): void
|
||||
@@ -143,7 +118,6 @@ class RoleAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 6,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SettingsAuthorizationPolicyTest extends TestCase
|
||||
{
|
||||
use AuthorizationPolicyTestSupport;
|
||||
|
||||
public function testViewRequiresSettingsViewPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
@@ -22,22 +24,14 @@ class SettingsAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 4,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testViewReturnsUpdateCapability(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::SETTINGS_VIEW, PermissionService::SETTINGS_UPDATE], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
4 => [PermissionService::SETTINGS_VIEW, PermissionService::SETTINGS_UPDATE],
|
||||
]);
|
||||
|
||||
$policy = new SettingsAuthorizationPolicy($permissionGateway);
|
||||
$decision = $policy->authorize(SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_VIEW, [
|
||||
@@ -61,8 +55,7 @@ class SettingsAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 8,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testBrandingUpdateRequiresSettingsUpdatePermission(): void
|
||||
@@ -78,8 +71,7 @@ class SettingsAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 8,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testTokenRevokeRequiresSettingsUpdatePermission(): void
|
||||
@@ -95,8 +87,7 @@ class SettingsAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 8,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testUserLifecycleRunRequiresSettingsUpdatePermission(): void
|
||||
@@ -112,7 +103,6 @@ class SettingsAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 8,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TenantAuthorizationPolicyTest extends TestCase
|
||||
{
|
||||
use AuthorizationPolicyTestSupport;
|
||||
|
||||
public function testViewRequiresTenantsViewPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
@@ -28,22 +30,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 11,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testViewReturnsCapabilitiesAndScopeData(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 11) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::TENANTS_VIEW, PermissionService::TENANTS_CREATE], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::TENANTS_VIEW, PermissionService::TENANTS_CREATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -85,21 +79,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 11,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testCreateReturnsSsoAndCustomFieldCapabilities(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 11) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::TENANTS_CREATE, PermissionService::TENANTS_SSO_MANAGE], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::TENANTS_CREATE, PermissionService::TENANTS_SSO_MANAGE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway);
|
||||
@@ -115,9 +102,9 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testEditContextAllowsInScopeViewer(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 11 && $permission === PermissionService::TENANTS_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::TENANTS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -152,16 +139,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'target_tenant_id' => 22,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testEditSubmitRequiresTenantsUpdatePermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 11 && $permission === PermissionService::TENANTS_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::TENANTS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -176,18 +161,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'input' => ['description' => 'Tenant A'],
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testEditSubmitDeniesSsoFieldsWithoutSsoPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
return $userId === 11 && ($permission === PermissionService::TENANTS_VIEW || $permission === PermissionService::TENANTS_UPDATE);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::TENANTS_VIEW, PermissionService::TENANTS_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -202,16 +183,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'input' => ['microsoft_enabled' => '1'],
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testCustomFieldsManageRequiresPermissionAndScope(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 9 && $permission === PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::CUSTOM_FIELDS_MANAGE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -230,9 +209,9 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testCustomFieldsManageDeniedWhenOutOfScope(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 9 && $permission === PermissionService::CUSTOM_FIELDS_MANAGE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::CUSTOM_FIELDS_MANAGE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -246,15 +225,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'target_tenant_id' => 5,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testAvatarViewAllowsTenantViewerInScope(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 15 && $permission === PermissionService::TENANTS_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
15 => [PermissionService::TENANTS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -283,15 +261,14 @@ class TenantAuthorizationPolicyTest extends TestCase
|
||||
'target_tenant_id' => 9,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testDeleteRequiresPermissionAndScope(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::TENANTS_DELETE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::TENANTS_DELETE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
|
||||
94
tests/Service/Access/UiAccessServiceTest.php
Normal file
94
tests/Service/Access/UiAccessServiceTest.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Service\Access\AuthorizationDecision;
|
||||
use MintyPHP\Service\Access\AuthorizationService;
|
||||
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
|
||||
use MintyPHP\Service\Access\UiCapabilityMap;
|
||||
use MintyPHP\Service\Access\UiAccessService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UiAccessServiceTest extends TestCase
|
||||
{
|
||||
public function testAllowCachesDecisionPerAbilityAndContext(): void
|
||||
{
|
||||
$authorizationService = $this->createMock(AuthorizationService::class);
|
||||
$authorizationService->expects($this->once())
|
||||
->method('authorize')
|
||||
->with(OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW, [
|
||||
'actor_user_id' => 7,
|
||||
'scoped_tenant_id' => 3,
|
||||
])
|
||||
->willReturn(AuthorizationDecision::allow());
|
||||
|
||||
$service = new UiAccessService($authorizationService);
|
||||
|
||||
$this->assertTrue($service->allow(7, OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW, ['scoped_tenant_id' => 3]));
|
||||
$this->assertTrue($service->allow(7, OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW, ['scoped_tenant_id' => 3]));
|
||||
}
|
||||
|
||||
public function testResolveMapSupportsStringAndStructuredDefinition(): void
|
||||
{
|
||||
$authorizationService = $this->createMock(AuthorizationService::class);
|
||||
$authorizationService->expects($this->exactly(2))
|
||||
->method('authorize')
|
||||
->willReturnCallback(static function (string $ability, array $context): AuthorizationDecision {
|
||||
if ($ability === OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW) {
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
if ($ability === OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW
|
||||
&& (int) ($context['target_tenant_id'] ?? 0) === 99) {
|
||||
return AuthorizationDecision::allow();
|
||||
}
|
||||
|
||||
return AuthorizationDecision::deny(403, 'forbidden');
|
||||
});
|
||||
|
||||
$service = new UiAccessService($authorizationService);
|
||||
$result = $service->resolveMap(11, [
|
||||
'stats' => OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW,
|
||||
'mail_log' => [
|
||||
'ability' => OperationsAuthorizationPolicy::ABILITY_ADMIN_MAIL_LOG_VIEW,
|
||||
'context' => ['target_tenant_id' => 99],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame([
|
||||
'stats' => true,
|
||||
'mail_log' => true,
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testLayoutCapabilitiesExposeExpectedKeys(): void
|
||||
{
|
||||
$authorizationService = $this->createMock(AuthorizationService::class);
|
||||
$authorizationService->method('authorize')->willReturn(AuthorizationDecision::deny(403, 'forbidden'));
|
||||
|
||||
$service = new UiAccessService($authorizationService);
|
||||
$result = $service->layoutCapabilities(5);
|
||||
|
||||
foreach (array_keys(UiCapabilityMap::LAYOUT) as $key) {
|
||||
$this->assertArrayHasKey($key, $result);
|
||||
$this->assertFalse($result[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
public function testPageCapabilitiesUsesResolveMapContract(): void
|
||||
{
|
||||
$authorizationService = $this->createMock(AuthorizationService::class);
|
||||
$authorizationService->expects($this->once())
|
||||
->method('authorize')
|
||||
->with(OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW, [
|
||||
'actor_user_id' => 9,
|
||||
])
|
||||
->willReturn(AuthorizationDecision::allow());
|
||||
|
||||
$service = new UiAccessService($authorizationService);
|
||||
$result = $service->pageCapabilities(9, [
|
||||
'can_view_jobs' => OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW,
|
||||
]);
|
||||
|
||||
$this->assertSame(['can_view_jobs' => true], $result);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserAuthorizationPolicyTest extends TestCase
|
||||
{
|
||||
use AuthorizationPolicyTestSupport;
|
||||
|
||||
public function testAdminViewRequiresUsersViewPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
@@ -24,9 +26,7 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 13,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testAdminCreateRequiresUsersCreatePermission(): void
|
||||
@@ -43,8 +43,7 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 13,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testAdminActivateRequiresUsersUpdatePermission(): void
|
||||
@@ -64,16 +63,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 22,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testAdminActivateReturnsPermissionDeniedWhenTenantScopeFails(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 11 && $permission === PermissionService::USERS_UPDATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
11 => [PermissionService::USERS_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -87,16 +84,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 22,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testAdminDeactivateAllowsSelfWithoutScopeLookup(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 9 && $permission === PermissionService::USERS_UPDATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::USERS_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->never())->method('canAccess');
|
||||
@@ -126,15 +121,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'bulk_action' => 'delete',
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testAdminDeleteReturnsPermissionDeniedWhenTenantScopeFails(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 7 && $permission === PermissionService::USERS_DELETE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
7 => [PermissionService::USERS_DELETE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -148,9 +142,7 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 17,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testAdminAccessPdfBulkRequiresPermission(): void
|
||||
@@ -169,15 +161,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'actor_user_id' => 21,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testAdminApiTokensManageRequiresScopeForOtherUsers(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 8 && $permission === PermissionService::API_TOKENS_MANAGE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
8 => [PermissionService::API_TOKENS_MANAGE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -191,25 +182,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 18,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testAdminSendAccessAllowsSelfWithSelfUpdatePermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 30) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($permission === PermissionService::USERS_SELF_UPDATE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
30 => [PermissionService::USERS_SELF_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->never())->method('canAccess');
|
||||
@@ -225,15 +205,9 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testEditContextAllowsOwnAccountWithUsersSelfUpdate(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 30) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $permission === PermissionService::USERS_SELF_UPDATE;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
30 => [PermissionService::USERS_SELF_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->never())->method('canAccess');
|
||||
@@ -272,22 +246,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 66,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testEditContextHidesSecurityArtifactsWithoutAuditPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $permission === PermissionService::USERS_VIEW;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::USERS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -311,15 +277,9 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testEditContextShowsSecurityArtifactsWithAuditPermission(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($permission, [PermissionService::USERS_VIEW, PermissionService::USERS_VIEW_AUDIT], true);
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::USERS_VIEW, PermissionService::USERS_VIEW_AUDIT],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -343,15 +303,9 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testEditSubmitRejectsUserWithoutEditCapability(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static function (int $userId, string $permission): bool {
|
||||
if ($userId !== 9) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $permission === PermissionService::USERS_VIEW;
|
||||
});
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
9 => [PermissionService::USERS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -369,16 +323,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 55,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertForbiddenDecision($decision);
|
||||
}
|
||||
|
||||
public function testAvatarUploadAllowsOwnAccountWithSelfUpdate(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 19 && $permission === PermissionService::USERS_SELF_UPDATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
19 => [PermissionService::USERS_SELF_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->never())->method('canAccess');
|
||||
@@ -407,15 +359,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 42,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('permission_denied', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
||||
}
|
||||
|
||||
public function testAvatarViewAllowsInScopeAddressBookViewer(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::ADDRESS_BOOK_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::ADDRESS_BOOK_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -449,15 +400,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'target_user_id' => 70,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame('forbidden', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
||||
}
|
||||
|
||||
public function testApiShowReturnsNotFoundWhenTenantScopeFails(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 4 && $permission === PermissionService::USERS_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
4 => [PermissionService::USERS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -472,16 +422,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'scoped_tenant_id' => null,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(404, $decision->status());
|
||||
$this->assertSame('not_found', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 404, 'not_found');
|
||||
}
|
||||
|
||||
public function testApiShowReturnsNotFoundForScopedTokenCrossTenantResource(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 4 && $permission === PermissionService::USERS_VIEW);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
4 => [PermissionService::USERS_VIEW],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -500,16 +448,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'scoped_tenant_id' => 2,
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(404, $decision->status());
|
||||
$this->assertSame('not_found', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 404, 'not_found');
|
||||
}
|
||||
|
||||
public function testApiCreateForScopedTokenRejectsOutOfScopeTenantIds(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::USERS_CREATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::USERS_CREATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
||||
@@ -520,16 +466,14 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'input' => ['tenant_ids' => [9, 10]],
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('tenant_scoped_token_forbidden', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden');
|
||||
}
|
||||
|
||||
public function testApiCreateForScopedTokenEnforcesScopedTenantInInput(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::USERS_CREATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
5 => [PermissionService::USERS_CREATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
||||
@@ -547,9 +491,9 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
|
||||
public function testApiUpdateForScopedTokenRejectsOutOfScopePrimaryTenant(): void
|
||||
{
|
||||
$permissionGateway = $this->createMock(PermissionGateway::class);
|
||||
$permissionGateway->method('userHas')
|
||||
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 6 && $permission === PermissionService::USERS_UPDATE);
|
||||
$permissionGateway = $this->permissionGatewayAllowing([
|
||||
6 => [PermissionService::USERS_UPDATE],
|
||||
]);
|
||||
|
||||
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
||||
$scopeGateway->expects($this->once())
|
||||
@@ -569,8 +513,6 @@ class UserAuthorizationPolicyTest extends TestCase
|
||||
'input' => ['primary_tenant_id' => 7],
|
||||
]);
|
||||
|
||||
$this->assertFalse($decision->isAllowed());
|
||||
$this->assertSame(403, $decision->status());
|
||||
$this->assertSame('tenant_scoped_token_forbidden', $decision->error());
|
||||
$this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden');
|
||||
}
|
||||
}
|
||||
|
||||
86
tests/Service/AddressBook/AddressBookServiceTest.php
Normal file
86
tests/Service/AddressBook/AddressBookServiceTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\AddressBook;
|
||||
|
||||
use MintyPHP\Service\AddressBook\AddressBookAvatarGateway;
|
||||
use MintyPHP\Service\AddressBook\AddressBookCustomFieldGateway;
|
||||
use MintyPHP\Service\AddressBook\AddressBookDirectoryGateway;
|
||||
use MintyPHP\Service\AddressBook\AddressBookService;
|
||||
use MintyPHP\Service\User\UserAccountService;
|
||||
use MintyPHP\Service\User\UserAssignmentService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AddressBookServiceTest extends TestCase
|
||||
{
|
||||
public function testBuildIndexContextBuildsTenantDepartmentMapForGlobalScope(): void
|
||||
{
|
||||
$userAccountService = $this->createMock(UserAccountService::class);
|
||||
$userAssignmentService = $this->createMock(UserAssignmentService::class);
|
||||
$directoryGateway = $this->createMock(AddressBookDirectoryGateway::class);
|
||||
$customFieldGateway = $this->createMock(AddressBookCustomFieldGateway::class);
|
||||
$avatarGateway = $this->createMock(AddressBookAvatarGateway::class);
|
||||
|
||||
$directoryGateway
|
||||
->expects($this->once())
|
||||
->method('getUserTenantIds')
|
||||
->with(7)
|
||||
->willReturn([]);
|
||||
$directoryGateway
|
||||
->expects($this->once())
|
||||
->method('listTenants')
|
||||
->willReturn([
|
||||
['id' => 1, 'uuid' => 'tenant-a', 'description' => 'Tenant A'],
|
||||
['id' => 2, 'uuid' => 'tenant-b', 'description' => 'Tenant B'],
|
||||
]);
|
||||
$directoryGateway
|
||||
->expects($this->exactly(2))
|
||||
->method('isStrictScope')
|
||||
->willReturn(false);
|
||||
$directoryGateway
|
||||
->expects($this->once())
|
||||
->method('listDepartments')
|
||||
->willReturn([
|
||||
['id' => 10, 'tenant_id' => 1, 'description' => 'Sales'],
|
||||
['id' => 20, 'tenant_id' => 2, 'description' => 'Support'],
|
||||
]);
|
||||
$directoryGateway
|
||||
->expects($this->never())
|
||||
->method('listDepartmentsByTenantIds');
|
||||
$directoryGateway
|
||||
->expects($this->once())
|
||||
->method('listActiveRoles')
|
||||
->willReturn([]);
|
||||
|
||||
$customFieldGateway
|
||||
->expects($this->once())
|
||||
->method('extractFilterSpec')
|
||||
->with([], 7)
|
||||
->willReturn([
|
||||
'definitions' => [],
|
||||
'query' => [],
|
||||
]);
|
||||
$customFieldGateway
|
||||
->expects($this->once())
|
||||
->method('listOptionsByDefinitionIds')
|
||||
->with([], true)
|
||||
->willReturn([]);
|
||||
|
||||
$service = new AddressBookService(
|
||||
$userAccountService,
|
||||
$userAssignmentService,
|
||||
$directoryGateway,
|
||||
$customFieldGateway,
|
||||
$avatarGateway
|
||||
);
|
||||
|
||||
$context = $service->buildIndexContext(7, []);
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'tenant-a' => ['10'],
|
||||
'tenant-b' => ['20'],
|
||||
],
|
||||
$context['tenantDepartmentMap']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\AddressBook;
|
||||
|
||||
use MintyPHP\Service\AddressBook\AddressBookAvatarGateway;
|
||||
use MintyPHP\Service\AddressBook\AddressBookCustomFieldGateway;
|
||||
use MintyPHP\Service\AddressBook\AddressBookDirectoryGateway;
|
||||
use MintyPHP\Service\AddressBook\AddressBookService;
|
||||
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AddressBookServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new AddressBookServicesFactory();
|
||||
|
||||
$directoryGatewayA = $factory->createAddressBookDirectoryGateway();
|
||||
$directoryGatewayB = $factory->createAddressBookDirectoryGateway();
|
||||
$this->assertInstanceOf(AddressBookDirectoryGateway::class, $directoryGatewayA);
|
||||
$this->assertSame($directoryGatewayA, $directoryGatewayB);
|
||||
|
||||
$customFieldGatewayA = $factory->createAddressBookCustomFieldGateway();
|
||||
$customFieldGatewayB = $factory->createAddressBookCustomFieldGateway();
|
||||
$this->assertInstanceOf(AddressBookCustomFieldGateway::class, $customFieldGatewayA);
|
||||
$this->assertSame($customFieldGatewayA, $customFieldGatewayB);
|
||||
|
||||
$avatarGatewayA = $factory->createAddressBookAvatarGateway();
|
||||
$avatarGatewayB = $factory->createAddressBookAvatarGateway();
|
||||
$this->assertInstanceOf(AddressBookAvatarGateway::class, $avatarGatewayA);
|
||||
$this->assertSame($avatarGatewayA, $avatarGatewayB);
|
||||
|
||||
$addressBookServiceA = $factory->createAddressBookService();
|
||||
$addressBookServiceB = $factory->createAddressBookService();
|
||||
$this->assertInstanceOf(AddressBookService::class, $addressBookServiceA);
|
||||
$this->assertSame($addressBookServiceA, $addressBookServiceB);
|
||||
}
|
||||
}
|
||||
66
tests/Service/Audit/ApiAuditServiceTest.php
Normal file
66
tests/Service/Audit/ApiAuditServiceTest.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\Audit\ApiAuditLogRepository;
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ApiAuditServiceTest extends TestCase
|
||||
{
|
||||
private array $serverBackup = [];
|
||||
private array $getBackup = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->serverBackup = $_SERVER;
|
||||
$this->getBackup = $_GET;
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER = $this->serverBackup;
|
||||
$_GET = $this->getBackup;
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
public function testCurrentRequestIdIsNullBeforeStart(): void
|
||||
{
|
||||
$service = new ApiAuditService(new ApiAuditLogRepository());
|
||||
$this->assertNull($service->currentRequestId());
|
||||
}
|
||||
|
||||
public function testCurrentRequestIdIsGeneratedForRegularRequests(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/me?x=1';
|
||||
$_GET = ['x' => '1'];
|
||||
|
||||
$service = new ApiAuditService(new ApiAuditLogRepository());
|
||||
$service->startRequestContext();
|
||||
|
||||
$requestId = $service->currentRequestId();
|
||||
$this->assertNotNull($requestId);
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
|
||||
$requestId
|
||||
);
|
||||
|
||||
$service->startRequestContext();
|
||||
$this->assertSame($requestId, $service->currentRequestId());
|
||||
}
|
||||
|
||||
public function testCurrentRequestIdStaysNullForOptionsRequests(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'OPTIONS';
|
||||
$_SERVER['REQUEST_URI'] = '/api/v1/me';
|
||||
$_GET = [];
|
||||
|
||||
$service = new ApiAuditService(new ApiAuditLogRepository());
|
||||
$service->startRequestContext();
|
||||
|
||||
$this->assertNull($service->currentRequestId());
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\Audit\ApiAuditLogRepository;
|
||||
use MintyPHP\Repository\Audit\UserLifecycleAuditRepository;
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
use MintyPHP\Service\Audit\AuditServicesFactory;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AuditServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new AuditServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(ApiAuditLogRepository::class, $factory->createApiAuditLogRepository());
|
||||
$this->assertSame($factory->createApiAuditLogRepository(), $factory->createApiAuditLogRepository());
|
||||
|
||||
$this->assertInstanceOf(UserLifecycleAuditRepository::class, $factory->createUserLifecycleAuditRepository());
|
||||
$this->assertSame($factory->createUserLifecycleAuditRepository(), $factory->createUserLifecycleAuditRepository());
|
||||
|
||||
$this->assertInstanceOf(ApiAuditService::class, $factory->createApiAuditService());
|
||||
$this->assertSame($factory->createApiAuditService(), $factory->createApiAuditService());
|
||||
|
||||
$this->assertInstanceOf(UserLifecycleAuditService::class, $factory->createUserLifecycleAuditService());
|
||||
$this->assertSame($factory->createUserLifecycleAuditService(), $factory->createUserLifecycleAuditService());
|
||||
}
|
||||
}
|
||||
42
tests/Service/Audit/SystemAuditRedactionServiceTest.php
Normal file
42
tests/Service/Audit/SystemAuditRedactionServiceTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Service\Audit\SystemAuditRedactionService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SystemAuditRedactionServiceTest extends TestCase
|
||||
{
|
||||
public function testSensitiveKeysAreRedacted(): void
|
||||
{
|
||||
$service = new SystemAuditRedactionService();
|
||||
|
||||
$redacted = $service->redactMetadata([
|
||||
'email' => 'john@example.com',
|
||||
'token' => 'abc',
|
||||
'active' => 1,
|
||||
'nested' => ['password' => 'secret'],
|
||||
]);
|
||||
|
||||
$this->assertSame('[REDACTED]', $redacted['email']);
|
||||
$this->assertSame('[REDACTED]', $redacted['token']);
|
||||
$this->assertSame(1, $redacted['active']);
|
||||
$this->assertSame('[REDACTED]', $redacted['nested']['password']);
|
||||
}
|
||||
|
||||
public function testBuildChangeSetUsesAllowlistAndRedactsOtherValues(): void
|
||||
{
|
||||
$service = new SystemAuditRedactionService();
|
||||
|
||||
$changeSet = $service->buildChangeSet(
|
||||
['active' => 0, 'description' => 'old'],
|
||||
['active' => 1, 'description' => 'new']
|
||||
);
|
||||
|
||||
$this->assertSame(['active', 'description'], $changeSet['changed_fields']);
|
||||
$this->assertSame(0, $changeSet['changes']['active']['before']);
|
||||
$this->assertSame(1, $changeSet['changes']['active']['after']);
|
||||
$this->assertSame('[REDACTED]', $changeSet['changes']['description']['before']);
|
||||
$this->assertSame('[REDACTED]', $changeSet['changes']['description']['after']);
|
||||
}
|
||||
}
|
||||
82
tests/Service/Audit/SystemAuditServiceTest.php
Normal file
82
tests/Service/Audit/SystemAuditServiceTest.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Http\RequestContext;
|
||||
use MintyPHP\Repository\Audit\SystemAuditLogRepository;
|
||||
use MintyPHP\Service\Audit\SystemAuditRedactionService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Settings\SettingGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SystemAuditServiceTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
RequestContext::resetForTests();
|
||||
}
|
||||
|
||||
public function testRecordReturnsNullWhenDisabled(): void
|
||||
{
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->expects($this->never())->method('create');
|
||||
|
||||
$settings = $this->createMock(SettingGateway::class);
|
||||
$settings->expects($this->once())->method('isSystemAuditEnabled')->willReturn(false);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings);
|
||||
|
||||
$this->assertNull($service->record('admin.users.update', 'success'));
|
||||
}
|
||||
|
||||
public function testRecordWritesWhenEnabled(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
$_SERVER['REQUEST_URI'] = '/admin/users/edit/abc';
|
||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'phpunit';
|
||||
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->expects($this->once())->method('create')->willReturn(5);
|
||||
|
||||
$settings = $this->createMock(SettingGateway::class);
|
||||
$settings->method('isSystemAuditEnabled')->willReturn(true);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings);
|
||||
|
||||
$id = $service->record('admin.users.update', 'success', [
|
||||
'target_type' => 'user',
|
||||
'target_id' => 10,
|
||||
'before' => ['active' => 0],
|
||||
'after' => ['active' => 1],
|
||||
]);
|
||||
|
||||
$this->assertSame(5, $id);
|
||||
}
|
||||
|
||||
public function testRecordIsFailOpenOnRepositoryError(): void
|
||||
{
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->method('create')->willThrowException(new \RuntimeException('db down'));
|
||||
|
||||
$settings = $this->createMock(SettingGateway::class);
|
||||
$settings->method('isSystemAuditEnabled')->willReturn(true);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings);
|
||||
|
||||
$this->assertNull($service->record('admin.users.update', 'success'));
|
||||
}
|
||||
|
||||
public function testPurgeUsesConfiguredRetention(): void
|
||||
{
|
||||
$repo = $this->createMock(SystemAuditLogRepository::class);
|
||||
$repo->expects($this->once())->method('purgeOlderThanDays')->with(120)->willReturn(3);
|
||||
|
||||
$settings = $this->createMock(SettingGateway::class);
|
||||
$settings->method('getSystemAuditRetentionDays')->willReturn(120);
|
||||
|
||||
$service = new SystemAuditService($repo, new SystemAuditRedactionService(), $settings);
|
||||
|
||||
$this->assertSame(3, $service->purgeExpired());
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Auth;
|
||||
|
||||
use MintyPHP\Service\Auth\ApiTokenService;
|
||||
use MintyPHP\Service\Auth\AuthService;
|
||||
use MintyPHP\Service\Auth\AuthServicesFactory;
|
||||
use MintyPHP\Service\Auth\EmailVerificationService;
|
||||
use MintyPHP\Service\Auth\MicrosoftOidcService;
|
||||
use MintyPHP\Service\Auth\MicrosoftOidcStateStoreService;
|
||||
use MintyPHP\Service\Auth\PasswordResetService;
|
||||
use MintyPHP\Service\Auth\RememberMeService;
|
||||
use MintyPHP\Service\Auth\SsoUserLinkService;
|
||||
use MintyPHP\Service\Auth\TenantSsoService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AuthServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new AuthServicesFactory();
|
||||
|
||||
$apiTokenServiceA = $factory->createApiTokenService();
|
||||
$apiTokenServiceB = $factory->createApiTokenService();
|
||||
$this->assertInstanceOf(ApiTokenService::class, $apiTokenServiceA);
|
||||
$this->assertSame($apiTokenServiceA, $apiTokenServiceB);
|
||||
|
||||
$passwordResetServiceA = $factory->createPasswordResetService();
|
||||
$passwordResetServiceB = $factory->createPasswordResetService();
|
||||
$this->assertInstanceOf(PasswordResetService::class, $passwordResetServiceA);
|
||||
$this->assertSame($passwordResetServiceA, $passwordResetServiceB);
|
||||
|
||||
$emailVerificationServiceA = $factory->createEmailVerificationService();
|
||||
$emailVerificationServiceB = $factory->createEmailVerificationService();
|
||||
$this->assertInstanceOf(EmailVerificationService::class, $emailVerificationServiceA);
|
||||
$this->assertSame($emailVerificationServiceA, $emailVerificationServiceB);
|
||||
|
||||
$rememberMeServiceA = $factory->createRememberMeService();
|
||||
$rememberMeServiceB = $factory->createRememberMeService();
|
||||
$this->assertInstanceOf(RememberMeService::class, $rememberMeServiceA);
|
||||
$this->assertSame($rememberMeServiceA, $rememberMeServiceB);
|
||||
|
||||
$authServiceA = $factory->createAuthService();
|
||||
$authServiceB = $factory->createAuthService();
|
||||
$this->assertInstanceOf(AuthService::class, $authServiceA);
|
||||
$this->assertSame($authServiceA, $authServiceB);
|
||||
|
||||
$ssoUserLinkServiceA = $factory->createSsoUserLinkService();
|
||||
$ssoUserLinkServiceB = $factory->createSsoUserLinkService();
|
||||
$this->assertInstanceOf(SsoUserLinkService::class, $ssoUserLinkServiceA);
|
||||
$this->assertSame($ssoUserLinkServiceA, $ssoUserLinkServiceB);
|
||||
|
||||
$tenantSsoServiceA = $factory->createTenantSsoService();
|
||||
$tenantSsoServiceB = $factory->createTenantSsoService();
|
||||
$this->assertInstanceOf(TenantSsoService::class, $tenantSsoServiceA);
|
||||
$this->assertSame($tenantSsoServiceA, $tenantSsoServiceB);
|
||||
|
||||
$stateStoreA = $factory->createMicrosoftOidcStateStore();
|
||||
$stateStoreB = $factory->createMicrosoftOidcStateStore();
|
||||
$this->assertInstanceOf(MicrosoftOidcStateStoreService::class, $stateStoreA);
|
||||
$this->assertSame($stateStoreA, $stateStoreB);
|
||||
|
||||
$microsoftOidcServiceA = $factory->createMicrosoftOidcService();
|
||||
$microsoftOidcServiceB = $factory->createMicrosoftOidcService();
|
||||
$this->assertInstanceOf(MicrosoftOidcService::class, $microsoftOidcServiceA);
|
||||
$this->assertSame($microsoftOidcServiceA, $microsoftOidcServiceB);
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,12 @@ class MicrosoftOidcStateStoreServiceTest extends TestCase
|
||||
$store->store('state-a', ['tenant_id' => 5, 'nonce' => 'n1']);
|
||||
|
||||
$first = $store->consume('state-a');
|
||||
$this->assertTrue($first['ok'] ?? false);
|
||||
$this->assertTrue($first['ok']);
|
||||
$this->assertSame(5, (int) (($first['entry']['tenant_id'] ?? 0)));
|
||||
|
||||
$second = $store->consume('state-a');
|
||||
$this->assertFalse($second['ok'] ?? true);
|
||||
$this->assertSame('state_invalid', (string) ($second['error'] ?? ''));
|
||||
$this->assertFalse($second['ok']);
|
||||
$this->assertSame('state_invalid', (string) $second['error']);
|
||||
}
|
||||
|
||||
public function testConsumeReturnsExpiredForOldState(): void
|
||||
@@ -45,8 +45,8 @@ class MicrosoftOidcStateStoreServiceTest extends TestCase
|
||||
]);
|
||||
|
||||
$result = $store->consume('state-old');
|
||||
$this->assertFalse($result['ok'] ?? true);
|
||||
$this->assertSame('state_expired', (string) ($result['error'] ?? ''));
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('state_expired', (string) $result['error']);
|
||||
}
|
||||
|
||||
public function testStoreRespectsMaxEntriesAndPrunesOldStates(): void
|
||||
@@ -58,13 +58,13 @@ class MicrosoftOidcStateStoreServiceTest extends TestCase
|
||||
$store->store('state-3', ['created_at' => time() - 1]);
|
||||
|
||||
$invalid = $store->consume('state-1');
|
||||
$this->assertFalse($invalid['ok'] ?? true);
|
||||
$this->assertSame('state_invalid', (string) ($invalid['error'] ?? ''));
|
||||
$this->assertFalse($invalid['ok']);
|
||||
$this->assertSame('state_invalid', (string) $invalid['error']);
|
||||
|
||||
$valid2 = $store->consume('state-2');
|
||||
$this->assertTrue($valid2['ok'] ?? false);
|
||||
$this->assertTrue($valid2['ok']);
|
||||
|
||||
$valid3 = $store->consume('state-3');
|
||||
$this->assertTrue($valid3['ok'] ?? false);
|
||||
$this->assertTrue($valid3['ok']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Branding;
|
||||
|
||||
use MintyPHP\Service\Branding\BrandingFaviconService;
|
||||
use MintyPHP\Service\Branding\BrandingLogoService;
|
||||
use MintyPHP\Service\Branding\BrandingServicesFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BrandingServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new BrandingServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(BrandingLogoService::class, $factory->createBrandingLogoService());
|
||||
$this->assertSame($factory->createBrandingLogoService(), $factory->createBrandingLogoService());
|
||||
|
||||
$this->assertInstanceOf(BrandingFaviconService::class, $factory->createBrandingFaviconService());
|
||||
$this->assertSame($factory->createBrandingFaviconService(), $factory->createBrandingFaviconService());
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Directory;
|
||||
|
||||
use MintyPHP\Repository\Access\RoleRepository;
|
||||
use MintyPHP\Repository\Org\DepartmentRepository;
|
||||
use MintyPHP\Repository\Tenant\TenantRepository;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Directory\DirectoryScopeGateway;
|
||||
use MintyPHP\Service\Directory\DirectoryServicesFactory;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DirectoryServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new DirectoryServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(TenantRepository::class, $factory->createTenantRepository());
|
||||
$this->assertSame($factory->createTenantRepository(), $factory->createTenantRepository());
|
||||
|
||||
$this->assertInstanceOf(DepartmentRepository::class, $factory->createDepartmentRepository());
|
||||
$this->assertSame($factory->createDepartmentRepository(), $factory->createDepartmentRepository());
|
||||
|
||||
$this->assertInstanceOf(RoleRepository::class, $factory->createRoleRepository());
|
||||
$this->assertSame($factory->createRoleRepository(), $factory->createRoleRepository());
|
||||
|
||||
$this->assertInstanceOf(DirectorySettingsGateway::class, $factory->createDirectorySettingsGateway());
|
||||
$this->assertSame($factory->createDirectorySettingsGateway(), $factory->createDirectorySettingsGateway());
|
||||
|
||||
$this->assertInstanceOf(DirectoryScopeGateway::class, $factory->createDirectoryScopeGateway());
|
||||
$this->assertSame($factory->createDirectoryScopeGateway(), $factory->createDirectoryScopeGateway());
|
||||
|
||||
$this->assertInstanceOf(TenantService::class, $factory->createTenantService());
|
||||
$this->assertSame($factory->createTenantService(), $factory->createTenantService());
|
||||
|
||||
$this->assertInstanceOf(DepartmentService::class, $factory->createDepartmentService());
|
||||
$this->assertSame($factory->createDepartmentService(), $factory->createDepartmentService());
|
||||
|
||||
$this->assertInstanceOf(RoleService::class, $factory->createRoleService());
|
||||
$this->assertSame($factory->createRoleService(), $factory->createRoleService());
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Import;
|
||||
|
||||
use MintyPHP\Service\Audit\ImportAuditService;
|
||||
use MintyPHP\Service\Import\ImportService;
|
||||
use MintyPHP\Service\Import\ImportServicesFactory;
|
||||
use MintyPHP\Service\Import\ImportStateStoreService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ImportServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new ImportServicesFactory();
|
||||
|
||||
$importServiceA = $factory->createImportService();
|
||||
$importServiceB = $factory->createImportService();
|
||||
$this->assertInstanceOf(ImportService::class, $importServiceA);
|
||||
$this->assertSame($importServiceA, $importServiceB);
|
||||
|
||||
$auditServiceA = $factory->createImportAuditService();
|
||||
$auditServiceB = $factory->createImportAuditService();
|
||||
$this->assertInstanceOf(ImportAuditService::class, $auditServiceA);
|
||||
$this->assertSame($auditServiceA, $auditServiceB);
|
||||
|
||||
$stateStoreA = $factory->createImportStateStoreService();
|
||||
$stateStoreB = $factory->createImportStateStoreService();
|
||||
$this->assertInstanceOf(ImportStateStoreService::class, $stateStoreA);
|
||||
$this->assertSame($stateStoreA, $stateStoreB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Mail;
|
||||
|
||||
use MintyPHP\Repository\Mail\MailLogRepository;
|
||||
use MintyPHP\Service\Mail\MailLogService;
|
||||
use MintyPHP\Service\Mail\MailService;
|
||||
use MintyPHP\Service\Mail\MailServicesFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MailServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new MailServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(MailLogRepository::class, $factory->createMailLogRepository());
|
||||
$this->assertSame($factory->createMailLogRepository(), $factory->createMailLogRepository());
|
||||
|
||||
$this->assertInstanceOf(MailService::class, $factory->createMailService());
|
||||
$this->assertSame($factory->createMailService(), $factory->createMailService());
|
||||
|
||||
$this->assertInstanceOf(MailLogService::class, $factory->createMailLogService());
|
||||
$this->assertSame($factory->createMailLogService(), $factory->createMailLogService());
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ class ScheduleCalculatorTest extends TestCase
|
||||
$next = $calculator->calculateNextRunUtc($job, $reference);
|
||||
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $next);
|
||||
$this->assertSame('2026-02-22 12:00:00', $next?->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'));
|
||||
$this->assertSame('2026-02-22 12:00:00', $next->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testCalculateNextRunUtcDailyMovesToNextDayWhenTimeAlreadyPassed(): void
|
||||
@@ -38,7 +38,7 @@ class ScheduleCalculatorTest extends TestCase
|
||||
$next = $calculator->calculateNextRunUtc($job, $reference);
|
||||
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $next);
|
||||
$this->assertSame('2026-02-23 08:00:00', $next?->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'));
|
||||
$this->assertSame('2026-02-23 08:00:00', $next->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function testNormalizeWeekdaysCsvRemovesDuplicatesAndInvalidValues(): void
|
||||
@@ -50,4 +50,3 @@ class ScheduleCalculatorTest extends TestCase
|
||||
$this->assertSame('1,3,7', $normalized);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace MintyPHP\Tests\Service\Scheduler;
|
||||
use MintyPHP\Repository\Scheduler\ScheduledJobRepository;
|
||||
use MintyPHP\Repository\Scheduler\ScheduledJobRunRepository;
|
||||
use MintyPHP\Repository\Scheduler\SchedulerRuntimeRepository;
|
||||
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Scheduler\ScheduleCalculator;
|
||||
use MintyPHP\Service\Scheduler\ScheduledJobRegistry;
|
||||
use MintyPHP\Service\Scheduler\ScheduledJobService;
|
||||
@@ -17,6 +19,18 @@ class SchedulerRunServiceTest extends TestCase
|
||||
{
|
||||
$scheduledJobService = $this->createMock(ScheduledJobService::class);
|
||||
$scheduledJobService->expects($this->once())->method('ensureSystemJobs');
|
||||
$systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$systemAuditService->expects($this->once())
|
||||
->method('record')
|
||||
->with(
|
||||
'scheduler.run',
|
||||
'failed',
|
||||
$this->callback(static function (array $context): bool {
|
||||
$metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : [];
|
||||
return ($metadata['trigger_type'] ?? '') === 'manual'
|
||||
&& ($metadata['run_status'] ?? '') === 'failed';
|
||||
})
|
||||
);
|
||||
|
||||
$service = new SchedulerRunService(
|
||||
$scheduledJobService,
|
||||
@@ -24,7 +38,9 @@ class SchedulerRunServiceTest extends TestCase
|
||||
$this->createMock(ScheduledJobRunRepository::class),
|
||||
$this->createMock(SchedulerRuntimeRepository::class),
|
||||
$this->createMock(ScheduledJobRegistry::class),
|
||||
new ScheduleCalculator()
|
||||
new ScheduleCalculator(),
|
||||
$this->createMock(DatabaseSessionRepository::class),
|
||||
$systemAuditService
|
||||
);
|
||||
|
||||
$result = $service->runJobNow(0, 0);
|
||||
@@ -32,4 +48,95 @@ class SchedulerRunServiceTest extends TestCase
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('invalid_request', $result['error']);
|
||||
}
|
||||
|
||||
public function testRunJobNowSkippedRunIsAuditedAsSuccessOutcome(): void
|
||||
{
|
||||
$scheduledJobService = $this->createMock(ScheduledJobService::class);
|
||||
$scheduledJobService->expects($this->once())->method('ensureSystemJobs');
|
||||
|
||||
$scheduledJobRepository = $this->createMock(ScheduledJobRepository::class);
|
||||
$scheduledJobRepository->expects($this->once())
|
||||
->method('find')
|
||||
->with(7)
|
||||
->willReturn([
|
||||
'id' => 7,
|
||||
'job_key' => 'user_lifecycle_run',
|
||||
'enabled' => 1,
|
||||
'next_run_at' => gmdate('Y-m-d H:i:s'),
|
||||
'catchup_once' => 1,
|
||||
]);
|
||||
$scheduledJobRepository->expects($this->once())
|
||||
->method('markRunning')
|
||||
->with(7, $this->isType('string'))
|
||||
->willReturn(false);
|
||||
|
||||
$scheduledJobRunRepository = $this->createMock(ScheduledJobRunRepository::class);
|
||||
$scheduledJobRunRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->isType('array'))
|
||||
->willReturn(1);
|
||||
|
||||
$databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
||||
$databaseSessionRepository->expects($this->once())
|
||||
->method('acquireAdvisoryLock')
|
||||
->with('scheduled_jobs_runner', 0)
|
||||
->willReturn(true);
|
||||
$databaseSessionRepository->expects($this->once())
|
||||
->method('releaseAdvisoryLock')
|
||||
->with('scheduled_jobs_runner');
|
||||
|
||||
$records = [];
|
||||
$systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$systemAuditService->expects($this->exactly(2))
|
||||
->method('record')
|
||||
->willReturnCallback(static function (string $eventType, string $outcome, array $context) use (&$records): int {
|
||||
$records[] = [
|
||||
'event_type' => $eventType,
|
||||
'outcome' => $outcome,
|
||||
'context' => $context,
|
||||
];
|
||||
return 1;
|
||||
});
|
||||
|
||||
$service = new SchedulerRunService(
|
||||
$scheduledJobService,
|
||||
$scheduledJobRepository,
|
||||
$scheduledJobRunRepository,
|
||||
$this->createMock(SchedulerRuntimeRepository::class),
|
||||
$this->createMock(ScheduledJobRegistry::class),
|
||||
new ScheduleCalculator(),
|
||||
$databaseSessionRepository,
|
||||
$systemAuditService
|
||||
);
|
||||
|
||||
$result = $service->runJobNow(7, 42);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame('skipped', $result['status']);
|
||||
$this->assertSame('job_already_running', $result['error']);
|
||||
$this->assertCount(2, $records);
|
||||
|
||||
$jobRecord = null;
|
||||
$runRecord = null;
|
||||
foreach ($records as $record) {
|
||||
if (($record['event_type'] ?? '') === 'scheduler.job.run') {
|
||||
$jobRecord = $record;
|
||||
}
|
||||
if (($record['event_type'] ?? '') === 'scheduler.run') {
|
||||
$runRecord = $record;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertNotNull($jobRecord);
|
||||
$this->assertNotNull($runRecord);
|
||||
$this->assertSame('success', $jobRecord['outcome']);
|
||||
$this->assertSame('success', $runRecord['outcome']);
|
||||
|
||||
$jobMetadata = is_array($jobRecord['context']['metadata'] ?? null) ? $jobRecord['context']['metadata'] : [];
|
||||
$runMetadata = is_array($runRecord['context']['metadata'] ?? null) ? $runRecord['context']['metadata'] : [];
|
||||
$this->assertSame('manual', $jobMetadata['trigger_type'] ?? null);
|
||||
$this->assertSame('skipped', $jobMetadata['run_status'] ?? null);
|
||||
$this->assertSame('manual', $runMetadata['trigger_type'] ?? null);
|
||||
$this->assertSame('skipped', $runMetadata['run_status'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Scheduler;
|
||||
|
||||
use MintyPHP\Service\Scheduler\ScheduledJobService;
|
||||
use MintyPHP\Service\Scheduler\SchedulerRunService;
|
||||
use MintyPHP\Service\Scheduler\SchedulerServicesFactory;
|
||||
use MintyPHP\Service\User\UserLifecycleService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SchedulerServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new SchedulerServicesFactory();
|
||||
|
||||
$scheduledJobServiceA = $factory->createScheduledJobService();
|
||||
$scheduledJobServiceB = $factory->createScheduledJobService();
|
||||
$this->assertInstanceOf(ScheduledJobService::class, $scheduledJobServiceA);
|
||||
$this->assertSame($scheduledJobServiceA, $scheduledJobServiceB);
|
||||
|
||||
$schedulerRunServiceA = $factory->createSchedulerRunService();
|
||||
$schedulerRunServiceB = $factory->createSchedulerRunService();
|
||||
$this->assertInstanceOf(SchedulerRunService::class, $schedulerRunServiceA);
|
||||
$this->assertSame($schedulerRunServiceA, $schedulerRunServiceB);
|
||||
|
||||
$userLifecycleServiceA = $factory->createUserLifecycleService();
|
||||
$userLifecycleServiceB = $factory->createUserLifecycleService();
|
||||
$this->assertInstanceOf(UserLifecycleService::class, $userLifecycleServiceA);
|
||||
$this->assertSame($userLifecycleServiceA, $userLifecycleServiceB);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Security;
|
||||
|
||||
use MintyPHP\Repository\Security\RateLimitRepository;
|
||||
use MintyPHP\Service\Security\RateLimiterService;
|
||||
use MintyPHP\Service\Security\SecurityServicesFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SecurityServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new SecurityServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(RateLimitRepository::class, $factory->createRateLimitRepository());
|
||||
$this->assertSame($factory->createRateLimitRepository(), $factory->createRateLimitRepository());
|
||||
|
||||
$this->assertInstanceOf(RateLimiterService::class, $factory->createRateLimiterService());
|
||||
$this->assertSame($factory->createRateLimiterService(), $factory->createRateLimiterService());
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Settings;
|
||||
|
||||
use MintyPHP\Service\Settings\SettingGateway;
|
||||
use MintyPHP\Service\Settings\SettingService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SettingGatewayTest extends TestCase
|
||||
{
|
||||
public function testGetDefaultTenantIdDelegatesToService(): void
|
||||
{
|
||||
$settingService = $this->createMock(SettingService::class);
|
||||
$settingService->expects($this->once())
|
||||
->method('getDefaultTenantId')
|
||||
->willReturn(42);
|
||||
|
||||
$gateway = new SettingGateway($settingService);
|
||||
$this->assertSame(42, $gateway->getDefaultTenantId());
|
||||
}
|
||||
|
||||
public function testSetDefaultRoleIdDelegatesToService(): void
|
||||
{
|
||||
$settingService = $this->createMock(SettingService::class);
|
||||
$settingService->expects($this->once())
|
||||
->method('setDefaultRoleId')
|
||||
->with(7, null)
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = new SettingGateway($settingService);
|
||||
$this->assertTrue($gateway->setDefaultRoleId(7));
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Settings;
|
||||
|
||||
use MintyPHP\Repository\Settings\SettingRepository;
|
||||
use MintyPHP\Service\Settings\SettingCacheService;
|
||||
use MintyPHP\Service\Settings\SettingGateway;
|
||||
use MintyPHP\Service\Settings\SettingServicesFactory;
|
||||
use MintyPHP\Service\Settings\ThemeConfigService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SettingServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new SettingServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(SettingRepository::class, $factory->createSettingRepository());
|
||||
$this->assertSame($factory->createSettingRepository(), $factory->createSettingRepository());
|
||||
|
||||
$this->assertInstanceOf('MintyPHP\\Service\\Settings\\SettingService', $factory->createSettingService());
|
||||
$this->assertSame($factory->createSettingService(), $factory->createSettingService());
|
||||
|
||||
$this->assertInstanceOf(SettingCacheService::class, $factory->createSettingCacheService());
|
||||
$this->assertSame($factory->createSettingCacheService(), $factory->createSettingCacheService());
|
||||
|
||||
$this->assertInstanceOf(ThemeConfigService::class, $factory->createThemeConfigService());
|
||||
$this->assertSame($factory->createThemeConfigService(), $factory->createThemeConfigService());
|
||||
|
||||
$this->assertInstanceOf(SettingGateway::class, $factory->createSettingGateway());
|
||||
$this->assertSame($factory->createSettingGateway(), $factory->createSettingGateway());
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Tenant;
|
||||
|
||||
use MintyPHP\Repository\Org\DepartmentRepository;
|
||||
use MintyPHP\Repository\Tenant\TenantRepository;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Service\Access\PermissionGateway;
|
||||
use MintyPHP\Service\Tenant\TenantAvatarService;
|
||||
use MintyPHP\Service\Tenant\TenantFaviconService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\Tenant\TenantServicesFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TenantServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryReturnsStableInstancesPerFactoryObject(): void
|
||||
{
|
||||
$factory = new TenantServicesFactory();
|
||||
|
||||
$this->assertInstanceOf(TenantRepository::class, $factory->createTenantRepository());
|
||||
$this->assertSame($factory->createTenantRepository(), $factory->createTenantRepository());
|
||||
|
||||
$this->assertInstanceOf(DepartmentRepository::class, $factory->createDepartmentRepository());
|
||||
$this->assertSame($factory->createDepartmentRepository(), $factory->createDepartmentRepository());
|
||||
|
||||
$this->assertInstanceOf(UserTenantRepository::class, $factory->createUserTenantRepository());
|
||||
$this->assertSame($factory->createUserTenantRepository(), $factory->createUserTenantRepository());
|
||||
|
||||
$this->assertInstanceOf(PermissionGateway::class, $factory->createPermissionGateway());
|
||||
$this->assertSame($factory->createPermissionGateway(), $factory->createPermissionGateway());
|
||||
|
||||
$this->assertInstanceOf(TenantScopeService::class, $factory->createTenantScopeService());
|
||||
$this->assertSame($factory->createTenantScopeService(), $factory->createTenantScopeService());
|
||||
|
||||
$this->assertInstanceOf(TenantAvatarService::class, $factory->createTenantAvatarService());
|
||||
$this->assertSame($factory->createTenantAvatarService(), $factory->createTenantAvatarService());
|
||||
|
||||
$this->assertInstanceOf(TenantFaviconService::class, $factory->createTenantFaviconService());
|
||||
$this->assertSame($factory->createTenantFaviconService(), $factory->createTenantFaviconService());
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,36 @@ class UserPasswordPolicyServiceTest extends TestCase
|
||||
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
|
||||
public function testValidateRequiresPasswordWhenRequired(): void
|
||||
{
|
||||
$service = new UserPasswordPolicyService();
|
||||
$errors = $service->validate('', '', true, 'test@example.com');
|
||||
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
|
||||
public function testValidateRejectsMismatch(): void
|
||||
{
|
||||
$service = new UserPasswordPolicyService();
|
||||
$errors = $service->validate('Abcd1234$efg', 'Abcd1234$xxx', true, 'test@example.com');
|
||||
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
|
||||
public function testValidateAcceptsStrongPassword(): void
|
||||
{
|
||||
$service = new UserPasswordPolicyService();
|
||||
$errors = $service->validate('StrongPass1!', 'StrongPass1!', true, 'test@example.com');
|
||||
|
||||
$this->assertSame([], $errors);
|
||||
}
|
||||
|
||||
public function testValidateRejectsPasswordContainingEmail(): void
|
||||
{
|
||||
$service = new UserPasswordPolicyService();
|
||||
$errors = $service->validate('test@example.comA1!', 'test@example.comA1!', true, 'test@example.com');
|
||||
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\User;
|
||||
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserServicesFactoryTest extends TestCase
|
||||
{
|
||||
public function testFactoryCachesCoreInstances(): void
|
||||
{
|
||||
$factory = new UserServicesFactory();
|
||||
|
||||
$this->assertSame($factory->createUserAccountService(), $factory->createUserAccountService());
|
||||
$this->assertSame($factory->createUserAssignmentService(), $factory->createUserAssignmentService());
|
||||
$this->assertSame($factory->createUserTenantContextService(), $factory->createUserTenantContextService());
|
||||
$this->assertSame($factory->createUserPasswordService(), $factory->createUserPasswordService());
|
||||
$this->assertSame($factory->createUserReadRepository(), $factory->createUserReadRepository());
|
||||
$this->assertSame($factory->createUserWriteRepository(), $factory->createUserWriteRepository());
|
||||
$this->assertSame($factory->createUserListQueryRepository(), $factory->createUserListQueryRepository());
|
||||
$this->assertSame($factory->createUserSavedFilterRepository(), $factory->createUserSavedFilterRepository());
|
||||
$this->assertSame($factory->createUserTenantRepository(), $factory->createUserTenantRepository());
|
||||
$this->assertSame($factory->createUserRoleRepository(), $factory->createUserRoleRepository());
|
||||
$this->assertSame($factory->createUserDepartmentRepository(), $factory->createUserDepartmentRepository());
|
||||
$this->assertSame($factory->createUserPasswordPolicyService(), $factory->createUserPasswordPolicyService());
|
||||
$this->assertSame($factory->createUserAvatarService(), $factory->createUserAvatarService());
|
||||
$this->assertSame($factory->createUserSavedFilterService(), $factory->createUserSavedFilterService());
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service;
|
||||
|
||||
use MintyPHP\Service\User\UserPasswordPolicyService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserServicePasswordTest extends TestCase
|
||||
{
|
||||
private UserPasswordPolicyService $policy;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->policy = new UserPasswordPolicyService();
|
||||
}
|
||||
|
||||
public function testRequiresPasswordWhenRequired(): void
|
||||
{
|
||||
$errors = $this->policy->validate('', '', true, 'test@example.com');
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
|
||||
public function testRejectsMismatch(): void
|
||||
{
|
||||
$errors = $this->policy->validate('Abcd1234$efg', 'Abcd1234$xxx', true, 'test@example.com');
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
|
||||
public function testAcceptsStrongPassword(): void
|
||||
{
|
||||
$errors = $this->policy->validate('StrongPass1!', 'StrongPass1!', true, 'test@example.com');
|
||||
$this->assertSame([], $errors);
|
||||
}
|
||||
|
||||
public function testRejectsPasswordContainingEmail(): void
|
||||
{
|
||||
$errors = $this->policy->validate('test@example.comA1!', 'test@example.comA1!', true, 'test@example.com');
|
||||
$this->assertNotEmpty($errors);
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,5 @@
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
require __DIR__ . '/../lib/Support/helpers.php';
|
||||
$container = require __DIR__ . '/../lib/App/registerContainer.php';
|
||||
setAppContainer($container);
|
||||
|
||||
Reference in New Issue
Block a user