instances added god may help

This commit is contained in:
2026-02-23 12:58:19 +01:00
parent 25370a1a55
commit 99db252f60
290 changed files with 9858 additions and 4914 deletions

View File

@@ -0,0 +1,33 @@
<?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\PermissionGateway;
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());
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace MintyPHP\Tests\Service\Access;
use MintyPHP\Service\Access\PermissionGateway;
use PHPUnit\Framework\TestCase;
class PermissionGatewayTest extends TestCase
{
public function testUserHasDelegatesToService(): void
{
$permissionService = $this->createMock('MintyPHP\\Service\\Access\\PermissionService');
$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('MintyPHP\\Service\\Access\\PermissionService');
$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]));
}
}

View File

@@ -0,0 +1,38 @@
<?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);
}
}

View File

@@ -0,0 +1,30 @@
<?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());
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace MintyPHP\Tests\Service\Audit;
use MintyPHP\Repository\Audit\ImportAuditRunRepository;
use MintyPHP\Service\Audit\ImportAuditService;
use PHPUnit\Framework\TestCase;
class ImportAuditServiceTest extends TestCase
{
public function testStartRunNormalizesFilenameAndMappedTargets(): void
{
$captured = null;
$repository = $this->createMock(ImportAuditRunRepository::class);
$repository->expects($this->once())
->method('createRunning')
->with($this->callback(function (array $payload) use (&$captured): bool {
$captured = $payload;
return true;
}))
->willReturn(77);
$service = new ImportAuditService($repository);
$runId = $service->startRun('users', ['email', 'first_name', 'email'], '/tmp/../users.csv', 12, 5);
$this->assertSame(77, $runId);
$this->assertSame('users.csv', (string) ($captured['source_filename'] ?? ''));
$this->assertSame('email,first_name', (string) ($captured['mapped_targets_csv'] ?? ''));
$this->assertSame(12, (int) ($captured['user_id'] ?? 0));
$this->assertSame(5, (int) ($captured['current_tenant_id'] ?? 0));
}
public function testFinishRunDerivesPartialStatusAndAggregatesErrors(): void
{
$captured = null;
$repository = $this->createMock(ImportAuditRunRepository::class);
$repository->expects($this->once())
->method('finishById')
->with(
88,
$this->callback(function (array $payload) use (&$captured): bool {
$captured = $payload;
return true;
})
)
->willReturn(true);
$service = new ImportAuditService($repository);
$service->finishRun(88, [
'ok' => true,
'processed' => 3,
'created' => 1,
'skipped' => 1,
'failed' => 1,
'errors' => [
['code' => 'email_exists'],
['code' => 'email_exists'],
['code' => 'invalid_email'],
],
]);
$this->assertSame('partial', (string) ($captured['status'] ?? ''));
$this->assertSame(3, (int) ($captured['rows_total'] ?? 0));
$this->assertSame(1, (int) ($captured['created_count'] ?? 0));
$this->assertSame(1, (int) ($captured['skipped_count'] ?? 0));
$this->assertSame(1, (int) ($captured['failed_count'] ?? 0));
$errorJson = (string) ($captured['error_codes_json'] ?? '');
$this->assertStringContainsString('email_exists', $errorJson);
$this->assertStringContainsString('invalid_email', $errorJson);
}
}

View File

@@ -0,0 +1,68 @@
<?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);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace MintyPHP\Tests\Service\Auth;
use MintyPHP\Service\Auth\MicrosoftOidcStateStoreService;
use PHPUnit\Framework\TestCase;
class MicrosoftOidcStateStoreServiceTest extends TestCase
{
private array $previousSession = [];
protected function setUp(): void
{
parent::setUp();
$this->previousSession = $_SESSION ?? [];
$_SESSION = [];
}
protected function tearDown(): void
{
$_SESSION = $this->previousSession;
parent::tearDown();
}
public function testStoreAndConsumeIsSingleUse(): void
{
$store = new MicrosoftOidcStateStoreService(600, 50);
$store->store('state-a', ['tenant_id' => 5, 'nonce' => 'n1']);
$first = $store->consume('state-a');
$this->assertTrue($first['ok'] ?? false);
$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'] ?? ''));
}
public function testConsumeReturnsExpiredForOldState(): void
{
$store = new MicrosoftOidcStateStoreService(1, 50);
$store->store('state-old', [
'tenant_id' => 9,
'created_at' => time() - 120,
]);
$result = $store->consume('state-old');
$this->assertFalse($result['ok'] ?? true);
$this->assertSame('state_expired', (string) ($result['error'] ?? ''));
}
public function testStoreRespectsMaxEntriesAndPrunesOldStates(): void
{
$store = new MicrosoftOidcStateStoreService(600, 2);
$store->store('state-1', ['created_at' => time() - 3]);
$store->store('state-2', ['created_at' => time() - 2]);
$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'] ?? ''));
$valid2 = $store->consume('state-2');
$this->assertTrue($valid2['ok'] ?? false);
$valid3 = $store->consume('state-3');
$this->assertTrue($valid3['ok'] ?? false);
}
}

View File

@@ -0,0 +1,22 @@
<?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());
}
}

View File

@@ -0,0 +1,46 @@
<?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());
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace MintyPHP\Tests\Service\Import;
use MintyPHP\Service\Import\CsvReaderService;
use PHPUnit\Framework\TestCase;
class CsvReaderServiceTest extends TestCase
{
private array $tmpFiles = [];
protected function tearDown(): void
{
foreach ($this->tmpFiles as $file) {
if (is_file($file)) {
@unlink($file);
}
}
}
public function testAnalyzeDetectsSemicolonAndBom(): void
{
$file = $this->newTmpFile("\xEF\xBB\xBFfirst_name;email\nMax;max@example.com\n");
$service = new CsvReaderService();
$result = $service->analyze($file, 20000);
$this->assertTrue($result['ok']);
$this->assertSame(';', $result['delimiter']);
$this->assertSame(['first_name', 'email'], $result['headers']);
$this->assertSame(1, $result['rows_total']);
}
public function testAnalyzeReturnsInvalidHeaderForDuplicateColumns(): void
{
$file = $this->newTmpFile("email,email\nfoo@example.com,bar@example.com\n");
$service = new CsvReaderService();
$result = $service->analyze($file, 20000);
$this->assertFalse($result['ok']);
$this->assertSame('invalid_csv_header', $result['code']);
}
public function testAnalyzeReturnsEmptyCsvWhenNoDataRows(): void
{
$file = $this->newTmpFile("email\n");
$service = new CsvReaderService();
$result = $service->analyze($file, 20000);
$this->assertFalse($result['ok']);
$this->assertSame('empty_csv', $result['code']);
}
private function newTmpFile(string $content): string
{
$file = tempnam(sys_get_temp_dir(), 'im_csv_');
$this->assertNotFalse($file);
file_put_contents($file, $content);
$this->tmpFiles[] = $file;
return $file;
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace MintyPHP\Tests\Service\Import;
use MintyPHP\Service\Audit\ImportAuditService;
use MintyPHP\Service\Access\PermissionGateway;
use MintyPHP\Service\Import\CsvReaderService;
use MintyPHP\Service\Import\ImportService;
use MintyPHP\Service\Import\ImportStateStoreService;
use MintyPHP\Service\Import\ImportTempFileService;
use MintyPHP\Service\Import\Profile\ImportProfileInterface;
use MintyPHP\Service\Settings\SettingGateway;
use MintyPHP\Service\User\UserScopeGateway;
use PHPUnit\Framework\TestCase;
class ImportServiceTest extends TestCase
{
public function testAnalyzeUploadBuildsTokenStateAndAutoMapping(): void
{
$profile = new ImportServiceTestProfile('users', false);
$csvReader = $this->createMock(CsvReaderService::class);
$csvReader->expects($this->once())
->method('analyze')
->willReturn([
'ok' => true,
'delimiter' => ';',
'headers' => ['E-Mail'],
'rows_total' => 2,
]);
$tempFileService = $this->createMock(ImportTempFileService::class);
$tempFileService->expects($this->once())
->method('storeUploadedFile')
->willReturn(['ok' => true, 'path' => '/tmp/import.csv']);
$stateStore = $this->createMock(ImportStateStoreService::class);
$stateStore->expects($this->once())
->method('setState')
->with(
$this->isString(),
$this->callback(function (array $state): bool {
return (string) ($state['path'] ?? '') === '/tmp/import.csv'
&& (string) ($state['type'] ?? '') === 'users'
&& is_int($state['created_at'] ?? null);
})
);
$service = $this->newImportService($profile, $csvReader, $tempFileService, $stateStore);
$result = $service->analyzeUpload([
'tmp_name' => '/tmp/php-upload',
'name' => 'users.csv',
'size' => 10,
'error' => UPLOAD_ERR_OK,
], 'users', 7);
$this->assertTrue($result['ok']);
$this->assertSame(';', $result['delimiter']);
$this->assertSame(['E-Mail' => 'email'], $result['auto_mapping']);
}
public function testPreviewFailsWhenRequiredMappingIsMissing(): void
{
$profile = new ImportServiceTestProfile('users', false);
$stateStore = $this->createMock(ImportStateStoreService::class);
$stateStore->method('getState')->willReturn([
'token' => 'tok',
'type' => 'users',
'headers' => ['email'],
'path' => '/tmp/import.csv',
'delimiter' => ',',
'user_id' => 5,
'created_at' => time(),
]);
$service = $this->newImportService($profile, null, null, $stateStore);
$result = $service->preview('tok', ['mapping' => []], 5);
$this->assertFalse($result['ok']);
$this->assertSame('mapping_required_missing', $result['code']);
}
public function testPreviewFailsWhenAssignmentPermissionIsMissing(): void
{
$profile = new ImportServiceTestProfile('users', true);
$stateStore = $this->createMock(ImportStateStoreService::class);
$stateStore->method('getState')->willReturn([
'token' => 'tok',
'type' => 'users',
'headers' => ['email', 'tenant'],
'path' => '/tmp/import.csv',
'delimiter' => ',',
'user_id' => 0,
'created_at' => time(),
]);
$service = $this->newImportService($profile, null, null, $stateStore);
$result = $service->preview('tok', ['mapping' => ['email' => 'email', 'tenant' => 'tenant']], 0);
$this->assertFalse($result['ok']);
$this->assertSame('assignment_permission_required', $result['code']);
}
public function testMessageForCodeUsesKnownMappingAndFallback(): void
{
$service = $this->newImportService(new ImportServiceTestProfile('users', false));
$this->assertSame('Invalid file type', $service->messageForCode('invalid_file_type'));
$this->assertSame('Unexpected error during import', $service->messageForCode('something_unknown'));
}
private function newImportService(
ImportProfileInterface $profile,
?CsvReaderService $csvReader = null,
?ImportTempFileService $tempFileService = null,
?ImportStateStoreService $stateStore = null
): ImportService {
$csvReader = $csvReader ?? $this->createMock(CsvReaderService::class);
$tempFileService = $tempFileService ?? $this->createMock(ImportTempFileService::class);
$auditService = $this->createMock(ImportAuditService::class);
$stateStore = $stateStore ?? $this->createMock(ImportStateStoreService::class);
$permissionGateway = $this->createMock(PermissionGateway::class);
$permissionGateway->method('userHas')->willReturn(false);
$settingService = $this->createMock('MintyPHP\\Service\\Settings\\SettingService');
$settingGateway = new SettingGateway($settingService);
$userScopeGateway = $this->createMock(UserScopeGateway::class);
$userScopeGateway->method('hasGlobalAccess')->willReturn(false);
$userScopeGateway->method('getUserTenantIds')->willReturn([]);
return new ImportService(
$csvReader,
$tempFileService,
$auditService,
$stateStore,
[$profile->key() => $profile],
$permissionGateway,
$settingGateway,
$userScopeGateway
);
}
}
class ImportServiceTestProfile implements ImportProfileInterface
{
public function __construct(private readonly string $key, private readonly bool $requiresAssignmentPermission)
{
}
public function key(): string
{
return $this->key;
}
public function label(): string
{
return 'Test';
}
public function allowedTargets(): array
{
return [
'email' => 'Email',
'tenant' => 'Tenant',
];
}
public function requiredTargets(): array
{
return ['email'];
}
public function autoMapAliases(): array
{
return [
'email' => ['e-mail', 'email'],
'tenant' => ['tenant'],
];
}
public function supportsAssignments(): bool
{
return true;
}
public function requiresAssignmentPermission(): bool
{
return $this->requiresAssignmentPermission;
}
public function validateMappedRow(array $mapped, array $context): array
{
return ['ok' => true, 'normalized' => $mapped, 'errors' => []];
}
public function dryRunRow(array $normalized, array $context): array
{
return ['status' => 'would_create'];
}
public function commitRow(array $normalized, array $context): array
{
return ['status' => 'created'];
}
public function inFileDuplicateKey(array $normalized): ?string
{
return null;
}
public function rowIdentifier(array $normalized): string
{
return (string) ($normalized['email'] ?? '');
}
}

View File

@@ -0,0 +1,33 @@
<?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);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace MintyPHP\Tests\Service\Import;
use MintyPHP\Service\Import\ImportStateStoreService;
use MintyPHP\Service\Import\ImportTempFileService;
use PHPUnit\Framework\TestCase;
class ImportStateStoreServiceTest extends TestCase
{
protected function setUp(): void
{
$_SESSION = [];
}
public function testSetGetAndClearState(): void
{
$tempFileService = $this->createMock(ImportTempFileService::class);
$stateStore = new ImportStateStoreService($tempFileService);
$stateStore->setState('token_a', ['token' => 'token_a', 'created_at' => time(), 'path' => '/tmp/a.csv']);
$loaded = $stateStore->getState('token_a');
$this->assertIsArray($loaded);
$this->assertSame('token_a', $loaded['token']);
$stateStore->clearState('token_a');
$this->assertNull($stateStore->getState('token_a'));
}
public function testExpiredStateGetsDeletedAndReturnsNull(): void
{
$tempFileService = $this->createMock(ImportTempFileService::class);
$tempFileService->expects($this->once())
->method('delete')
->with('/tmp/expired.csv');
$stateStore = new ImportStateStoreService($tempFileService);
$stateStore->setState('token_b', [
'token' => 'token_b',
'created_at' => time() - 7201,
'path' => '/tmp/expired.csv',
]);
$this->assertNull($stateStore->getState('token_b'));
}
}

View File

@@ -0,0 +1,26 @@
<?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());
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace MintyPHP\Tests\Service\Scheduler;
use MintyPHP\Service\Scheduler\ScheduleCalculator;
use PHPUnit\Framework\TestCase;
class ScheduleCalculatorTest extends TestCase
{
public function testCalculateNextRunUtcDailySameDayWhenTimeIsInFuture(): void
{
$calculator = new ScheduleCalculator();
$job = [
'schedule_type' => 'daily',
'schedule_interval' => 1,
'timezone' => 'UTC',
'schedule_time' => '12:00',
];
$reference = new \DateTimeImmutable('2026-02-22 10:30:00', new \DateTimeZone('UTC'));
$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'));
}
public function testCalculateNextRunUtcDailyMovesToNextDayWhenTimeAlreadyPassed(): void
{
$calculator = new ScheduleCalculator();
$job = [
'schedule_type' => 'daily',
'schedule_interval' => 1,
'timezone' => 'UTC',
'schedule_time' => '08:00',
];
$reference = new \DateTimeImmutable('2026-02-22 10:30:00', new \DateTimeZone('UTC'));
$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'));
}
public function testNormalizeWeekdaysCsvRemovesDuplicatesAndInvalidValues(): void
{
$calculator = new ScheduleCalculator();
$normalized = $calculator->normalizeWeekdaysCsv(['7', '3', '3', '9', '0', '1']);
$this->assertSame('1,3,7', $normalized);
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace MintyPHP\Tests\Service\Scheduler;
use MintyPHP\Repository\Scheduler\ScheduledJobRepository;
use MintyPHP\Repository\Scheduler\ScheduledJobRunRepository;
use MintyPHP\Service\Scheduler\ScheduleCalculator;
use MintyPHP\Service\Scheduler\ScheduledJobRegistry;
use MintyPHP\Service\Scheduler\ScheduledJobService;
use PHPUnit\Framework\TestCase;
class ScheduledJobServiceTest extends TestCase
{
public function testUpdateFromAdminReturnsValidationErrorForWeeklyWithoutWeekdays(): void
{
$jobRepository = $this->createMock(ScheduledJobRepository::class);
$runRepository = $this->createMock(ScheduledJobRunRepository::class);
$registry = $this->createMock(ScheduledJobRegistry::class);
$calculator = new ScheduleCalculator();
$registry->expects($this->once())->method('definitions')->willReturn([]);
$jobRepository->expects($this->once())->method('find')->with(10)->willReturn([
'id' => 10,
'job_key' => 'user_lifecycle_run',
'label' => 'User lifecycle run',
'description' => '',
'enabled' => 1,
'timezone' => 'UTC',
'schedule_type' => 'daily',
'schedule_interval' => 1,
'schedule_time' => '02:15',
'schedule_weekdays_csv' => null,
'catchup_once' => 1,
]);
$registry->expects($this->once())->method('get')->with('user_lifecycle_run')->willReturn([
'label' => 'User lifecycle run',
'description' => '',
'allowed_schedule_types' => ['hourly', 'daily', 'weekly'],
]);
$jobRepository->expects($this->never())->method('updateJobMeta');
$service = new ScheduledJobService($jobRepository, $runRepository, $registry, $calculator);
$result = $service->updateFromAdmin(10, [
'enabled' => '1',
'timezone' => 'UTC',
'schedule_type' => 'weekly',
'schedule_interval' => '1',
'schedule_time' => '03:00',
]);
$this->assertFalse($result['ok']);
$this->assertNotEmpty($result['errors']);
}
public function testUpdateFromAdminPersistsNormalizedValuesAndReturnsFreshJob(): void
{
$jobRepository = $this->createMock(ScheduledJobRepository::class);
$runRepository = $this->createMock(ScheduledJobRunRepository::class);
$registry = $this->createMock(ScheduledJobRegistry::class);
$calculator = new ScheduleCalculator();
$registry->expects($this->exactly(2))->method('definitions')->willReturn([]);
$jobRepository->expects($this->exactly(2))->method('find')->with(11)->willReturnOnConsecutiveCalls(
[
'id' => 11,
'job_key' => 'user_lifecycle_run',
'label' => 'User lifecycle run',
'description' => 'Runs lifecycle',
'enabled' => 1,
'timezone' => 'UTC',
'schedule_type' => 'daily',
'schedule_interval' => 1,
'schedule_time' => '02:15',
'schedule_weekdays_csv' => null,
'catchup_once' => 1,
],
[
'id' => 11,
'job_key' => 'user_lifecycle_run',
'label' => 'User lifecycle run',
'description' => 'Runs lifecycle',
'enabled' => 0,
'timezone' => 'UTC',
'schedule_type' => 'daily',
'schedule_interval' => 1,
'schedule_time' => '04:00',
'schedule_weekdays_csv' => null,
'catchup_once' => 0,
'next_run_at' => null,
]
);
$registry->expects($this->once())->method('get')->with('user_lifecycle_run')->willReturn([
'label' => 'User lifecycle run',
'description' => 'Runs lifecycle',
'allowed_schedule_types' => ['hourly', 'daily', 'weekly'],
]);
$capturedUpdate = null;
$jobRepository->expects($this->once())
->method('updateJobMeta')
->with(11, $this->anything())
->willReturnCallback(function (int $id, array $data) use (&$capturedUpdate): bool {
$capturedUpdate = $data;
return true;
});
$service = new ScheduledJobService($jobRepository, $runRepository, $registry, $calculator);
$result = $service->updateFromAdmin(11, [
'timezone' => 'UTC',
'schedule_type' => 'daily',
'schedule_interval' => '1',
'schedule_time' => '04:00',
]);
$this->assertTrue($result['ok']);
$this->assertSame(11, (int) (($result['job'] ?? [])['id'] ?? 0));
$this->assertSame(0, (int) (($result['job'] ?? [])['enabled'] ?? 1));
$this->assertIsArray($capturedUpdate);
$this->assertSame(0, (int) ($capturedUpdate['enabled'] ?? -1));
$this->assertSame('UTC', (string) ($capturedUpdate['timezone'] ?? ''));
$this->assertSame('daily', (string) ($capturedUpdate['schedule_type'] ?? ''));
$this->assertSame(1, (int) ($capturedUpdate['schedule_interval'] ?? -1));
$this->assertSame('04:00', (string) ($capturedUpdate['schedule_time'] ?? ''));
$this->assertSame(0, (int) ($capturedUpdate['catchup_once'] ?? -1));
$this->assertNull($capturedUpdate['next_run_at'] ?? null);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace MintyPHP\Tests\Service\Scheduler;
use MintyPHP\Repository\Scheduler\ScheduledJobRepository;
use MintyPHP\Repository\Scheduler\ScheduledJobRunRepository;
use MintyPHP\Repository\Scheduler\SchedulerRuntimeRepository;
use MintyPHP\Service\Scheduler\ScheduleCalculator;
use MintyPHP\Service\Scheduler\ScheduledJobRegistry;
use MintyPHP\Service\Scheduler\ScheduledJobService;
use MintyPHP\Service\Scheduler\SchedulerRunService;
use PHPUnit\Framework\TestCase;
class SchedulerRunServiceTest extends TestCase
{
public function testRunJobNowRejectsInvalidRequest(): void
{
$scheduledJobService = $this->createMock(ScheduledJobService::class);
$scheduledJobService->expects($this->once())->method('ensureSystemJobs');
$service = new SchedulerRunService(
$scheduledJobService,
$this->createMock(ScheduledJobRepository::class),
$this->createMock(ScheduledJobRunRepository::class),
$this->createMock(SchedulerRuntimeRepository::class),
$this->createMock(ScheduledJobRegistry::class),
new ScheduleCalculator()
);
$result = $service->runJobNow(0, 0);
$this->assertFalse($result['ok']);
$this->assertSame('invalid_request', $result['error']);
}
}

View File

@@ -0,0 +1,33 @@
<?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);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace MintyPHP\Tests\Service\Scheduler;
use MintyPHP\Service\Scheduler\Handler\UserLifecycleJobHandler;
use MintyPHP\Service\User\UserLifecycleService;
use PHPUnit\Framework\TestCase;
class UserLifecycleJobHandlerTest extends TestCase
{
public function testExecuteReturnsSuccessWithMappedResult(): void
{
$lifecycleService = $this->createMock(UserLifecycleService::class);
$lifecycleService->expects($this->once())
->method('run')
->with(42)
->willReturn([
'ok' => true,
'run_uuid' => 'run-1',
'deactivated_count' => 3,
'deleted_count' => 2,
'skipped_count' => 1,
'duration_ms' => 99,
]);
$handler = new UserLifecycleJobHandler($lifecycleService);
$result = $handler->execute(42);
$this->assertSame('success', $result['status']);
$this->assertNull($result['error_code']);
$this->assertSame('run-1', $result['result']['run_uuid']);
$this->assertSame(3, $result['result']['deactivated_count']);
$this->assertSame(2, $result['result']['deleted_count']);
$this->assertSame(1, $result['result']['skipped_count']);
$this->assertSame(99, $result['result']['duration_ms']);
}
public function testExecuteReturnsSkippedWhenLifecycleLockIsNotAcquired(): void
{
$lifecycleService = $this->createMock(UserLifecycleService::class);
$lifecycleService->expects($this->once())
->method('run')
->with(null)
->willReturn([
'ok' => false,
'error' => 'lock_not_acquired',
]);
$handler = new UserLifecycleJobHandler($lifecycleService);
$result = $handler->execute(null);
$this->assertSame('skipped', $result['status']);
$this->assertSame('lock_not_acquired', $result['error_code']);
}
}

View File

@@ -0,0 +1,22 @@
<?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());
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace MintyPHP\Tests\Service\Settings;
use MintyPHP\Service\Settings\SettingCacheService;
use PHPUnit\Framework\TestCase;
class SettingCacheServiceTest extends TestCase
{
public function testGetReturnsNullForMissingKey(): void
{
$file = $this->tempFilePath();
file_put_contents($file, "<?php\nreturn ['app_title' => 'Demo'];\n");
$service = new SettingCacheService($file);
$this->assertNull($service->get('missing'));
}
public function testUpdateWritesAndNormalizesValues(): void
{
$file = $this->tempFilePath();
file_put_contents($file, "<?php\nreturn ['app_title' => 'Old'];\n");
$service = new SettingCacheService($file);
$this->assertTrue($service->update([
'app_title' => 'New',
'app_locale' => null,
]));
$this->assertSame('New', $service->get('app_title'));
$this->assertNull($service->get('app_locale'));
}
private function tempFilePath(): string
{
$dir = sys_get_temp_dir() . '/mintyphp_settings_cache_test_' . bin2hex(random_bytes(6));
mkdir($dir, 0775, true);
$file = $dir . '/settings.php';
$this->addToAssertionCount(1);
$this->assertDirectoryExists($dir);
return $file;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace MintyPHP\Tests\Service\Settings;
use MintyPHP\Service\Settings\SettingGateway;
use PHPUnit\Framework\TestCase;
class SettingGatewayTest extends TestCase
{
public function testGetDefaultTenantIdDelegatesToService(): void
{
$settingService = $this->createMock('MintyPHP\\Service\\Settings\\SettingService');
$settingService->expects($this->once())
->method('getDefaultTenantId')
->willReturn(42);
$gateway = new SettingGateway($settingService);
$this->assertSame(42, $gateway->getDefaultTenantId());
}
public function testSetDefaultRoleIdDelegatesToService(): void
{
$settingService = $this->createMock('MintyPHP\\Service\\Settings\\SettingService');
$settingService->expects($this->once())
->method('setDefaultRoleId')
->with(7, null)
->willReturn(true);
$gateway = new SettingGateway($settingService);
$this->assertTrue($gateway->setDefaultRoleId(7));
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace MintyPHP\Tests\Service\Settings;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Repository\Org\DepartmentRepository;
use MintyPHP\Repository\Settings\SettingRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Settings\ThemeConfigService;
use PHPUnit\Framework\TestCase;
class SettingServiceTest extends TestCase
{
public function testSetDefaultTenantIdRejectsUnknownTenant(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->expects($this->never())->method('set');
$tenantRepository = $this->createMock(TenantRepository::class);
$tenantRepository->expects($this->once())->method('find')->with(99)->willReturn(null);
$service = $this->newService($settingRepository, $tenantRepository);
$this->assertFalse($service->setDefaultTenantId(99));
}
public function testSetApiTokenDefaultTtlDaysRejectsAboveCurrentMax(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->method('getValue')->willReturnCallback(
static function (string $key): ?string {
if ($key === SettingService::API_TOKEN_MAX_TTL_DAYS_KEY) {
return '30';
}
return null;
}
);
$settingRepository->expects($this->never())->method('set');
$service = $this->newService($settingRepository);
$this->assertFalse($service->setApiTokenDefaultTtlDays(31));
}
public function testSetApiTokenMaxTtlDaysUpdatesDefaultWhenNeeded(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->method('getValue')->willReturnCallback(
static function (string $key): ?string {
if ($key === SettingService::API_TOKEN_DEFAULT_TTL_DAYS_KEY) {
return '365';
}
return null;
}
);
$calls = [];
$settingRepository->expects($this->exactly(2))
->method('set')
->willReturnCallback(
static function (string $key, ?string $value, ?string $description) use (&$calls): bool {
$calls[] = [$key, $value, $description];
return true;
}
);
$service = $this->newService($settingRepository);
$this->assertTrue($service->setApiTokenMaxTtlDays(120));
$this->assertSame(
[
[SettingService::API_TOKEN_DEFAULT_TTL_DAYS_KEY, '120', 'setting.api_token_default_ttl_days'],
[SettingService::API_TOKEN_MAX_TTL_DAYS_KEY, '120', 'setting.api_token_max_ttl_days'],
],
$calls
);
}
public function testSetUserInactivityDeleteDaysRequiresDeactivatePolicy(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->method('getValue')->willReturnCallback(
static function (string $key): ?string {
if ($key === SettingService::USER_INACTIVITY_DEACTIVATE_DAYS_KEY) {
return '0';
}
return null;
}
);
$settingRepository->expects($this->never())->method('set');
$service = $this->newService($settingRepository);
$this->assertFalse($service->setUserInactivityDeleteDays(30));
}
public function testSetApiCorsAllowedOriginsRejectsInvalidOrigins(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->expects($this->never())->method('set');
$service = $this->newService($settingRepository);
$this->assertFalse($service->setApiCorsAllowedOrigins("https://ok.example\njavascript:alert(1)"));
}
public function testSetMicrosoftAuthorityRequiresHttps(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->expects($this->never())->method('set');
$service = $this->newService($settingRepository);
$this->assertFalse($service->setMicrosoftAuthority('http://login.microsoftonline.com/common/v2.0'));
}
public function testSetAppPrimaryColorNormalizesHexWithoutHash(): void
{
$settingRepository = $this->createMock(SettingRepository::class);
$settingRepository->expects($this->once())
->method('set')
->with(SettingService::APP_PRIMARY_COLOR_KEY, '#abc', 'setting.app_primary_color')
->willReturn(true);
$service = $this->newService($settingRepository);
$this->assertTrue($service->setAppPrimaryColor('abc'));
}
private function newService(
SettingRepository $settingRepository,
?TenantRepository $tenantRepository = null,
?RoleRepository $roleRepository = null,
?DepartmentRepository $departmentRepository = null
): SettingService {
$tenantRepository = $tenantRepository ?? $this->createMock(TenantRepository::class);
$roleRepository = $roleRepository ?? $this->createMock(RoleRepository::class);
$departmentRepository = $departmentRepository ?? $this->createMock(DepartmentRepository::class);
$themeConfigService = $this->createMock(ThemeConfigService::class);
$themeConfigService->method('all')->willReturn(['light' => 'Light', 'dark' => 'Dark']);
return new SettingService(
$settingRepository,
$tenantRepository,
$roleRepository,
$departmentRepository,
$themeConfigService
);
}
}

View File

@@ -0,0 +1,33 @@
<?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());
}
}

View File

@@ -0,0 +1,42 @@
<?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());
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace MintyPHP\Tests\Service\User;
use MintyPHP\Service\User\UserPasswordPolicyService;
use PHPUnit\Framework\TestCase;
class UserPasswordPolicyServiceTest extends TestCase
{
public function testMinLengthMatchesHints(): void
{
$service = new UserPasswordPolicyService();
$this->assertSame(12, $service->minLength());
$hints = $service->hints();
$this->assertNotEmpty($hints);
$this->assertSame('min', (string) ($hints[0]['rule'] ?? ''));
}
public function testValidateRejectsWeakPassword(): void
{
$service = new UserPasswordPolicyService();
$errors = $service->validate('abc', 'abc', true, 'foo@example.com');
$this->assertNotEmpty($errors);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace MintyPHP\Tests\Service\User;
use MintyPHP\Repository\User\UserWriteRepository;
use MintyPHP\Service\User\UserPasswordPolicyService;
use MintyPHP\Service\User\UserPasswordService;
use PHPUnit\Framework\TestCase;
class UserPasswordServiceTest extends TestCase
{
public function testResetPasswordRejectsWeakPassword(): void
{
$writeRepository = $this->createMock(UserWriteRepository::class);
$writeRepository->expects($this->never())->method('setPassword');
$service = new UserPasswordService(new UserPasswordPolicyService(), $writeRepository);
$result = $service->resetPassword(10, 'abc', 'abc');
$this->assertFalse($result['ok']);
$this->assertNotEmpty($result['errors']);
}
public function testResetPasswordUpdatesRepositoryOnValidInput(): void
{
$writeRepository = $this->createMock(UserWriteRepository::class);
$writeRepository->expects($this->once())
->method('setPassword')
->with(11, 'StrongPass1!')
->willReturn(true);
$service = new UserPasswordService(new UserPasswordPolicyService(), $writeRepository);
$result = $service->resetPassword(11, 'StrongPass1!', 'StrongPass1!');
$this->assertTrue($result['ok']);
}
}

View File

@@ -0,0 +1,29 @@
<?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());
}
}

View File

@@ -2,32 +2,39 @@
namespace MintyPHP\Tests\Service;
use MintyPHP\Service\User\UserService;
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 = UserService::validatePassword('', '', true, 'test@example.com');
$errors = $this->policy->validate('', '', true, 'test@example.com');
$this->assertNotEmpty($errors);
}
public function testRejectsMismatch(): void
{
$errors = UserService::validatePassword('Abcd1234$efg', 'Abcd1234$xxx', true, 'test@example.com');
$errors = $this->policy->validate('Abcd1234$efg', 'Abcd1234$xxx', true, 'test@example.com');
$this->assertNotEmpty($errors);
}
public function testAcceptsStrongPassword(): void
{
$errors = UserService::validatePassword('StrongPass1!', 'StrongPass1!', true, 'test@example.com');
$errors = $this->policy->validate('StrongPass1!', 'StrongPass1!', true, 'test@example.com');
$this->assertSame([], $errors);
}
public function testRejectsPasswordContainingEmail(): void
{
$errors = UserService::validatePassword('test@example.comA1!', 'test@example.comA1!', true, 'test@example.com');
$errors = $this->policy->validate('test@example.comA1!', 'test@example.comA1!', true, 'test@example.com');
$this->assertNotEmpty($errors);
}
}