test(services): add 88 unit tests for 7 critical service classes
RateLimiterServiceTest (17): hit/isBlocked/reset/registerFailure, fail-open, sliding window, scope normalization. PermissionServiceTest (24): userHas, getUserPermissions, cache hit/miss/refresh, clearUserCache, CRUD, system permission protection. RoleServiceTest (9): createFromAdmin, updateFromAdmin, deleteByUuid, Admin role protection, duplicate code rejection. TenantServiceTest (8): CRUD, department-dependent deletion blocking. DepartmentServiceTest (14): listPaged scope filtering, groupActiveByTenantIds, createFromAdmin, deleteByUuid, syncTenants. MailServiceTest (8): send logging, MailerException handling, template metadata. UserLifecycleServiceTest (8): advisory locking, deactivation, deletion, snapshot failure skip, privileged user exclusion, manual trigger type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
324
tests/Service/Access/PermissionServiceTest.php
Normal file
324
tests/Service/Access/PermissionServiceTest.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Repository\Access\PermissionRepositoryInterface;
|
||||
use MintyPHP\Repository\Access\RolePermissionRepositoryInterface;
|
||||
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PermissionServiceTest extends TestCase
|
||||
{
|
||||
private PermissionRepositoryInterface&MockObject $permissionRepository;
|
||||
private RolePermissionRepositoryInterface&MockObject $rolePermissionRepository;
|
||||
private UserRoleRepositoryInterface&MockObject $userRoleRepository;
|
||||
private SystemAuditService&MockObject $systemAuditService;
|
||||
private SessionStoreInterface&MockObject $sessionStore;
|
||||
private PermissionService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->permissionRepository = $this->createMock(PermissionRepositoryInterface::class);
|
||||
$this->rolePermissionRepository = $this->createMock(RolePermissionRepositoryInterface::class);
|
||||
$this->userRoleRepository = $this->createMock(UserRoleRepositoryInterface::class);
|
||||
$this->systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
$this->sessionStore = $this->createMock(SessionStoreInterface::class);
|
||||
|
||||
$this->service = new PermissionService(
|
||||
$this->permissionRepository,
|
||||
$this->rolePermissionRepository,
|
||||
$this->userRoleRepository,
|
||||
$this->systemAuditService,
|
||||
$this->sessionStore,
|
||||
);
|
||||
}
|
||||
|
||||
// ── userHas ──────────────────────────────────────────────────────
|
||||
|
||||
public function testUserHasReturnsFalseForZeroUserId(): void
|
||||
{
|
||||
$this->assertFalse($this->service->userHas(0, 'users.view'));
|
||||
}
|
||||
|
||||
public function testUserHasReturnsFalseForEmptyKey(): void
|
||||
{
|
||||
$this->assertFalse($this->service->userHas(1, ''));
|
||||
$this->assertFalse($this->service->userHas(1, ' '));
|
||||
}
|
||||
|
||||
public function testUserHasReturnsTrueWhenPermissionExists(): void
|
||||
{
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn(null);
|
||||
|
||||
$this->userRoleRepository->expects($this->any())->method('listRoleIdsByUserId')
|
||||
->with(5)
|
||||
->willReturn([1, 2]);
|
||||
|
||||
$this->rolePermissionRepository->expects($this->any())->method('listPermissionKeysByRoleIds')
|
||||
->with([1, 2])
|
||||
->willReturn(['users.view', 'users.create']);
|
||||
|
||||
$this->sessionStore->expects($this->atLeastOnce())
|
||||
->method('set')
|
||||
->with('permissions', $this->callback(fn (array $v) => $v['user_id'] === 5 && $v['keys'] === ['users.view', 'users.create']));
|
||||
|
||||
$this->assertTrue($this->service->userHas(5, 'users.view'));
|
||||
}
|
||||
|
||||
// ── getUserPermissions ───────────────────────────────────────────
|
||||
|
||||
public function testGetUserPermissionsReturnsEmptyForNonPositiveUserId(): void
|
||||
{
|
||||
$this->assertSame([], $this->service->getUserPermissions(0));
|
||||
$this->assertSame([], $this->service->getUserPermissions(-1));
|
||||
}
|
||||
|
||||
public function testGetUserPermissionsUsesSessionCache(): void
|
||||
{
|
||||
$cached = ['user_id' => 7, 'keys' => ['settings.view']];
|
||||
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn($cached);
|
||||
|
||||
// Repositories should NOT be called when cache is present.
|
||||
$this->userRoleRepository->expects($this->never())->method('listRoleIdsByUserId');
|
||||
$this->rolePermissionRepository->expects($this->never())->method('listPermissionKeysByRoleIds');
|
||||
|
||||
$result = $this->service->getUserPermissions(7);
|
||||
|
||||
$this->assertSame(['settings.view'], $result);
|
||||
}
|
||||
|
||||
public function testGetUserPermissionsRefreshBypassesCache(): void
|
||||
{
|
||||
$cached = ['user_id' => 7, 'keys' => ['old.key']];
|
||||
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn($cached);
|
||||
|
||||
$this->userRoleRepository->expects($this->any())->method('listRoleIdsByUserId')
|
||||
->with(7)
|
||||
->willReturn([3]);
|
||||
|
||||
$this->rolePermissionRepository->expects($this->any())->method('listPermissionKeysByRoleIds')
|
||||
->with([3])
|
||||
->willReturn(['new.key']);
|
||||
|
||||
$this->sessionStore->expects($this->once())
|
||||
->method('set')
|
||||
->with('permissions', $this->callback(fn (array $v) => $v['keys'] === ['new.key']));
|
||||
|
||||
$result = $this->service->getUserPermissions(7, true);
|
||||
|
||||
$this->assertSame(['new.key'], $result);
|
||||
}
|
||||
|
||||
// ── getCachedPermissions ─────────────────────────────────────────
|
||||
|
||||
public function testGetCachedPermissionsReturnsEmptyWhenNoCache(): void
|
||||
{
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn(null);
|
||||
|
||||
$this->assertSame([], $this->service->getCachedPermissions(1));
|
||||
}
|
||||
|
||||
public function testGetCachedPermissionsReturnsCachedKeysForMatchingUser(): void
|
||||
{
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn(['user_id' => 4, 'keys' => ['a', 'b']]);
|
||||
|
||||
$this->assertSame(['a', 'b'], $this->service->getCachedPermissions(4));
|
||||
}
|
||||
|
||||
// ── clearUserCache ───────────────────────────────────────────────
|
||||
|
||||
public function testClearUserCacheRemovesSessionEntry(): void
|
||||
{
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn(['user_id' => 9, 'keys' => ['x']]);
|
||||
|
||||
$this->sessionStore->expects($this->once())
|
||||
->method('remove')
|
||||
->with('permissions');
|
||||
|
||||
$this->service->clearUserCache(9);
|
||||
}
|
||||
|
||||
public function testClearUserCacheIgnoresMismatchedUserId(): void
|
||||
{
|
||||
$this->sessionStore->expects($this->any())->method('get')
|
||||
->with('permissions')
|
||||
->willReturn(['user_id' => 9, 'keys' => ['x']]);
|
||||
|
||||
$this->sessionStore->expects($this->never())->method('remove');
|
||||
|
||||
$this->service->clearUserCache(42);
|
||||
}
|
||||
|
||||
// ── Delegation methods ───────────────────────────────────────────
|
||||
|
||||
public function testListDelegatesToRepository(): void
|
||||
{
|
||||
$expected = [['id' => 1, 'key' => 'a']];
|
||||
$this->permissionRepository->expects($this->any())->method('list')->willReturn($expected);
|
||||
$this->assertSame($expected, $this->service->list());
|
||||
}
|
||||
|
||||
public function testListActiveDelegatesToRepository(): void
|
||||
{
|
||||
$expected = [['id' => 2, 'key' => 'b', 'active' => 1]];
|
||||
$this->permissionRepository->expects($this->any())->method('listActive')->willReturn($expected);
|
||||
$this->assertSame($expected, $this->service->listActive());
|
||||
}
|
||||
|
||||
public function testListPagedDelegatesToRepository(): void
|
||||
{
|
||||
$options = ['page' => 1, 'limit' => 10];
|
||||
$expected = ['rows' => [], 'total' => 0];
|
||||
$this->permissionRepository->expects($this->any())->method('listPaged')->with($options)->willReturn($expected);
|
||||
$this->assertSame($expected, $this->service->listPaged($options));
|
||||
}
|
||||
|
||||
public function testFindDelegatesToRepository(): void
|
||||
{
|
||||
$this->permissionRepository->expects($this->any())->method('find')->with(5)->willReturn(['id' => 5, 'key' => 'x']);
|
||||
$this->assertSame(['id' => 5, 'key' => 'x'], $this->service->find(5));
|
||||
}
|
||||
|
||||
public function testFindByKeyDelegatesToRepository(): void
|
||||
{
|
||||
$this->permissionRepository->expects($this->any())->method('findByKey')->with('users.view')->willReturn(['id' => 1, 'key' => 'users.view']);
|
||||
$this->assertSame(['id' => 1, 'key' => 'users.view'], $this->service->findByKey('users.view'));
|
||||
}
|
||||
|
||||
// ── createFromAdmin ──────────────────────────────────────────────
|
||||
|
||||
public function testCreateFromAdminSucceeds(): void
|
||||
{
|
||||
$input = ['key' => 'custom.perm', 'description' => 'A custom permission', 'active' => '1', 'is_system' => '0'];
|
||||
|
||||
$this->permissionRepository->expects($this->any())->method('findByKey')->with('custom.perm')->willReturn(null);
|
||||
$this->permissionRepository->expects($this->any())->method('create')->willReturn(42);
|
||||
$this->systemAuditService->expects($this->once())->method('record');
|
||||
|
||||
$result = $this->service->createFromAdmin($input);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(42, $result['id']);
|
||||
$this->assertSame('custom.perm', $result['form']['key']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsEmptyKey(): void
|
||||
{
|
||||
$result = $this->service->createFromAdmin(['key' => '', 'description' => '']);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsInvalidKeyFormat(): void
|
||||
{
|
||||
$result = $this->service->createFromAdmin(['key' => 'invalid key!', 'description' => '']);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsDuplicateKey(): void
|
||||
{
|
||||
$this->permissionRepository->expects($this->any())->method('findByKey')
|
||||
->with('users.view')
|
||||
->willReturn(['id' => 1, 'key' => 'users.view']);
|
||||
|
||||
$result = $this->service->createFromAdmin(['key' => 'users.view', 'description' => '']);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
// ── updateFromAdmin ──────────────────────────────────────────────
|
||||
|
||||
public function testUpdateFromAdminSucceeds(): void
|
||||
{
|
||||
$input = ['key' => 'custom.perm', 'description' => 'Updated', 'active' => '1', 'is_system' => '0'];
|
||||
|
||||
// findByKey returns the same id → no uniqueness conflict
|
||||
$this->permissionRepository->expects($this->any())->method('findByKey')
|
||||
->with('custom.perm')
|
||||
->willReturn(['id' => 10, 'key' => 'custom.perm']);
|
||||
|
||||
$this->permissionRepository->expects($this->any())->method('find')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'key' => 'custom.perm', 'active' => 0]);
|
||||
|
||||
$this->permissionRepository->expects($this->any())->method('update')->with(10, $this->isType('array'))->willReturn(true);
|
||||
$this->systemAuditService->expects($this->once())->method('record');
|
||||
|
||||
$result = $this->service->updateFromAdmin(10, $input);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testUpdateFromAdminRejectsDuplicateKeyFromAnotherPermission(): void
|
||||
{
|
||||
$this->permissionRepository->expects($this->any())->method('findByKey')
|
||||
->with('taken.key')
|
||||
->willReturn(['id' => 99, 'key' => 'taken.key']);
|
||||
|
||||
$result = $this->service->updateFromAdmin(10, ['key' => 'taken.key', 'description' => '']);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
// ── deleteById ───────────────────────────────────────────────────
|
||||
|
||||
public function testDeleteByIdSucceeds(): void
|
||||
{
|
||||
$permission = ['id' => 5, 'key' => 'custom.perm', 'is_system' => 0];
|
||||
|
||||
$this->permissionRepository->expects($this->any())->method('find')->with(5)->willReturn($permission);
|
||||
$this->permissionRepository->expects($this->any())->method('delete')->with(5)->willReturn(true);
|
||||
$this->systemAuditService->expects($this->once())->method('record');
|
||||
|
||||
$result = $this->service->deleteById(5);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame($permission, $result['permission']);
|
||||
}
|
||||
|
||||
public function testDeleteByIdProtectsSystemPermission(): void
|
||||
{
|
||||
$permission = ['id' => 1, 'key' => 'users.view', 'is_system' => 1];
|
||||
$this->permissionRepository->expects($this->any())->method('find')->with(1)->willReturn($permission);
|
||||
|
||||
$result = $this->service->deleteById(1);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(403, $result['status']);
|
||||
$this->assertSame('system_permission_protected', $result['error']);
|
||||
}
|
||||
|
||||
public function testDeleteByIdReturnsNotFoundForMissing(): void
|
||||
{
|
||||
$this->permissionRepository->expects($this->any())->method('find')->with(999)->willReturn(null);
|
||||
|
||||
$result = $this->service->deleteById(999);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(404, $result['status']);
|
||||
$this->assertSame('not_found', $result['error']);
|
||||
}
|
||||
}
|
||||
261
tests/Service/Access/RoleServiceTest.php
Normal file
261
tests/Service/Access/RoleServiceTest.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Access;
|
||||
|
||||
use MintyPHP\Repository\Access\RoleRepositoryInterface;
|
||||
use MintyPHP\Service\Access\RoleService;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RoleServiceTest extends TestCase
|
||||
{
|
||||
private RoleRepositoryInterface&MockObject $roleRepo;
|
||||
private DirectorySettingsGateway&MockObject $settingsGateway;
|
||||
private SystemAuditService&MockObject $auditService;
|
||||
private RoleService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->roleRepo = $this->createMock(RoleRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||
$this->auditService = $this->createMock(SystemAuditService::class);
|
||||
|
||||
$this->service = new RoleService(
|
||||
$this->roleRepo,
|
||||
$this->settingsGateway,
|
||||
$this->auditService
|
||||
);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminSucceeds(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('existsByCode')
|
||||
->with('EDITOR', 0)
|
||||
->willReturn(false);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('create')
|
||||
->willReturn(42);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('find')
|
||||
->with(42)
|
||||
->willReturn(['id' => 42, 'uuid' => 'uuid-42', 'description' => 'Editor']);
|
||||
|
||||
$this->settingsGateway
|
||||
->expects($this->never())
|
||||
->method('setDefaultRoleId');
|
||||
|
||||
$this->auditService
|
||||
->expects($this->once())
|
||||
->method('record')
|
||||
->with('admin.roles.create', 'success', $this->anything());
|
||||
|
||||
$result = $this->service->createFromAdmin([
|
||||
'description' => 'Editor',
|
||||
'code' => 'EDITOR',
|
||||
'active' => 1,
|
||||
], 5);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame('uuid-42', $result['uuid']);
|
||||
$this->assertSame(42, $result['id']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsEmptyDescription(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->never())
|
||||
->method('create');
|
||||
|
||||
$result = $this->service->createFromAdmin([
|
||||
'description' => ' ',
|
||||
'code' => '',
|
||||
'active' => 1,
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminSetsDefaultRoleWhenRequested(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->method('existsByCode')
|
||||
->willReturn(false);
|
||||
|
||||
$this->roleRepo
|
||||
->method('create')
|
||||
->willReturn(99);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->any())
|
||||
->method('find')
|
||||
->with(99)
|
||||
->willReturn(['id' => 99, 'uuid' => 'uuid-99', 'description' => 'Default']);
|
||||
|
||||
$this->settingsGateway
|
||||
->expects($this->once())
|
||||
->method('setDefaultRoleId')
|
||||
->with(99);
|
||||
|
||||
$this->auditService
|
||||
->expects($this->once())
|
||||
->method('record');
|
||||
|
||||
$result = $this->service->createFromAdmin([
|
||||
'description' => 'Default',
|
||||
'code' => '',
|
||||
'active' => 1,
|
||||
'is_default' => 1,
|
||||
], 3);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testUpdateFromAdminSucceeds(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('existsByCode')
|
||||
->with('MGR', 10)
|
||||
->willReturn(false);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('find')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'uuid' => 'uuid-10', 'active' => 1]);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('update')
|
||||
->with(10, $this->anything())
|
||||
->willReturn(true);
|
||||
|
||||
$this->auditService
|
||||
->expects($this->once())
|
||||
->method('record')
|
||||
->with('admin.roles.update', 'success', $this->anything());
|
||||
|
||||
$result = $this->service->updateFromAdmin(10, [
|
||||
'description' => 'Manager',
|
||||
'code' => 'MGR',
|
||||
'active' => 1,
|
||||
], 5);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testUpdateFromAdminRejectsDuplicateCode(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('existsByCode')
|
||||
->with('ADMIN', 10)
|
||||
->willReturn(true);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->never())
|
||||
->method('update');
|
||||
|
||||
$result = $this->service->updateFromAdmin(10, [
|
||||
'description' => 'Manager',
|
||||
'code' => 'ADMIN',
|
||||
'active' => 1,
|
||||
]);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidSucceeds(): void
|
||||
{
|
||||
$role = ['id' => 7, 'uuid' => 'uuid-7', 'description' => 'Guest'];
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('uuid-7')
|
||||
->willReturn($role);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with(7)
|
||||
->willReturn(true);
|
||||
|
||||
$this->auditService
|
||||
->expects($this->once())
|
||||
->method('record')
|
||||
->with('admin.roles.delete', 'success', $this->anything());
|
||||
|
||||
$result = $this->service->deleteByUuid('uuid-7');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame($role, $result['role']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidProtectsAdminRole(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('uuid-admin')
|
||||
->willReturn(['id' => 1, 'uuid' => 'uuid-admin', 'description' => 'Admin']);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->never())
|
||||
->method('delete');
|
||||
|
||||
$result = $this->service->deleteByUuid('uuid-admin');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(403, $result['status']);
|
||||
$this->assertSame('admin_role_protected', $result['error']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->never())
|
||||
->method('findByUuid');
|
||||
|
||||
$result = $this->service->deleteByUuid(' ');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(404, $result['status']);
|
||||
$this->assertSame('not_found', $result['error']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidReturnsDeleteFailedOnRepoFailure(): void
|
||||
{
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('uuid-8')
|
||||
->willReturn(['id' => 8, 'uuid' => 'uuid-8', 'description' => 'Viewer']);
|
||||
|
||||
$this->roleRepo
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with(8)
|
||||
->willReturn(false);
|
||||
|
||||
$this->auditService
|
||||
->expects($this->never())
|
||||
->method('record');
|
||||
|
||||
$result = $this->service->deleteByUuid('uuid-8');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(500, $result['status']);
|
||||
$this->assertSame('delete_failed', $result['error']);
|
||||
}
|
||||
}
|
||||
165
tests/Service/Mail/MailServiceTest.php
Normal file
165
tests/Service/Mail/MailServiceTest.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Mail;
|
||||
|
||||
use MintyPHP\Repository\Mail\MailLogRepositoryInterface;
|
||||
use MintyPHP\Service\Mail\MailService;
|
||||
use MintyPHP\Service\Settings\SettingsSmtpGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MailServiceTest extends TestCase
|
||||
{
|
||||
private function createSmtpGatewayMock(): SettingsSmtpGateway
|
||||
{
|
||||
$gateway = $this->createMock(SettingsSmtpGateway::class);
|
||||
$gateway->method('getSmtpHost')->willReturn('localhost');
|
||||
$gateway->method('getSmtpPort')->willReturn(587);
|
||||
$gateway->method('getSmtpUser')->willReturn('');
|
||||
$gateway->method('getSmtpPassword')->willReturn('');
|
||||
$gateway->method('getSmtpSecure')->willReturn('tls');
|
||||
$gateway->method('getSmtpFrom')->willReturn('test@example.com');
|
||||
$gateway->method('getSmtpFromName')->willReturn('Test');
|
||||
|
||||
return $gateway;
|
||||
}
|
||||
|
||||
private function newService(
|
||||
?MailLogRepositoryInterface $mailLogRepository = null,
|
||||
?SettingsSmtpGateway $settingsSmtpGateway = null,
|
||||
): MailService {
|
||||
return new MailService(
|
||||
$mailLogRepository ?? $this->createMock(MailLogRepositoryInterface::class),
|
||||
$settingsSmtpGateway ?? $this->createSmtpGatewayMock(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testSendLogsBeforeSending(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['to_email'] === 'user@example.com'
|
||||
&& $data['subject'] === 'Hello'
|
||||
&& $data['status'] === 'queued';
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Hello', '<p>Hi</p>', 'Hi');
|
||||
}
|
||||
|
||||
public function testSendHandlesMailerException(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(1);
|
||||
$mailLogRepository->expects($this->once())->method('markFailed');
|
||||
$mailLogRepository->expects($this->never())->method('markSent');
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->send('user@example.com', 'Hello', '<p>Hi</p>', 'Hi');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('send_failed', $result['error']);
|
||||
}
|
||||
|
||||
public function testSendHandlesNullLogId(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(null);
|
||||
$mailLogRepository->expects($this->never())->method('markFailed');
|
||||
$mailLogRepository->expects($this->never())->method('markSent');
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->send('user@example.com', 'Hello', '<p>Hi</p>', 'Hi');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('send_failed', $result['error']);
|
||||
}
|
||||
|
||||
public function testSendWithEmptyLogIdStillReturnsError(): void
|
||||
{
|
||||
// create() returns null (e.g. DB insert failed) — error path still returns a result.
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(null);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->send('user@example.com', 'Subject', '<p>Body</p>', 'Body');
|
||||
|
||||
$this->assertArrayHasKey('ok', $result);
|
||||
$this->assertArrayHasKey('error', $result);
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testSendTemplateCallsSend(): void
|
||||
{
|
||||
// Templates won't exist in test context, so html/text will be empty strings.
|
||||
// But the send path is still exercised, creating a log entry.
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['to_email'] === 'user@example.com'
|
||||
&& $data['template'] === 'welcome';
|
||||
}))
|
||||
->willReturn(2);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->sendTemplate('welcome', ['name' => 'Alice'], 'user@example.com', 'Welcome');
|
||||
|
||||
$this->assertArrayHasKey('ok', $result);
|
||||
}
|
||||
|
||||
public function testSendCallsMarkFailedWithErrorMessage(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(5);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('markFailed')
|
||||
->with(
|
||||
$this->identicalTo(5),
|
||||
$this->isType('string'),
|
||||
);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Test', '<p>Body</p>', 'Body');
|
||||
}
|
||||
|
||||
public function testSendPassesMetaTemplateToLogEntry(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['template'] === 'invoice';
|
||||
}))
|
||||
->willReturn(3);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Invoice', '<p>Invoice</p>', 'Invoice', [
|
||||
'template' => 'invoice',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testSendWithoutMetaTemplateLogsNullTemplate(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['template'] === null;
|
||||
}))
|
||||
->willReturn(4);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Ad hoc', '<p>Ad hoc</p>', 'Ad hoc');
|
||||
}
|
||||
}
|
||||
325
tests/Service/Org/DepartmentServiceTest.php
Normal file
325
tests/Service/Org/DepartmentServiceTest.php
Normal file
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Org;
|
||||
|
||||
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
|
||||
use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use MintyPHP\Service\Org\DepartmentService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use MintyPHP\Service\User\UserServicesFactory;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DepartmentServiceTest extends TestCase
|
||||
{
|
||||
private UserServicesFactory&MockObject $userServicesFactory;
|
||||
private DepartmentRepositoryInterface&MockObject $departmentRepository;
|
||||
private DirectorySettingsGateway&MockObject $settingsGateway;
|
||||
private TenantScopeService&MockObject $scopeGateway;
|
||||
private SystemAuditService&MockObject $systemAuditService;
|
||||
private DepartmentService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userServicesFactory = $this->createMock(UserServicesFactory::class);
|
||||
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||
$this->scopeGateway = $this->createMock(TenantScopeService::class);
|
||||
$this->systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
|
||||
$this->service = new DepartmentService(
|
||||
$this->userServicesFactory,
|
||||
$this->departmentRepository,
|
||||
$this->settingsGateway,
|
||||
$this->scopeGateway,
|
||||
$this->systemAuditService
|
||||
);
|
||||
}
|
||||
|
||||
public function testListPagedRemovesTenantFilterForGlobalAccessUser(): void
|
||||
{
|
||||
$this->scopeGateway
|
||||
->expects($this->once())
|
||||
->method('hasGlobalAccess')
|
||||
->with(5)
|
||||
->willReturn(true);
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('listPaged')
|
||||
->with($this->callback(function (array $options): bool {
|
||||
return !isset($options['tenantUserId']) && !isset($options['tenantIds']);
|
||||
}))
|
||||
->willReturn([['id' => 1]]);
|
||||
|
||||
$result = $this->service->listPaged([
|
||||
'tenantUserId' => 5,
|
||||
'tenantIds' => [1, 2],
|
||||
'page' => 1,
|
||||
]);
|
||||
|
||||
$this->assertSame([['id' => 1]], $result);
|
||||
}
|
||||
|
||||
public function testListPagedKeepsTenantFilterForNonGlobalUser(): void
|
||||
{
|
||||
$this->scopeGateway
|
||||
->expects($this->once())
|
||||
->method('hasGlobalAccess')
|
||||
->with(5)
|
||||
->willReturn(false);
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('listPaged')
|
||||
->with($this->callback(function (array $options): bool {
|
||||
return isset($options['tenantUserId']) && $options['tenantUserId'] === 5;
|
||||
}))
|
||||
->willReturn([]);
|
||||
|
||||
$this->service->listPaged(['tenantUserId' => 5, 'tenantIds' => [1]]);
|
||||
}
|
||||
|
||||
public function testGroupActiveByTenantIdsGroupsAndSorts(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('listByTenantIds')
|
||||
->with([1, 2])
|
||||
->willReturn([
|
||||
['id' => 10, 'tenant_id' => 1, 'description' => 'Zebra'],
|
||||
['id' => 11, 'tenant_id' => 1, 'description' => 'Alpha'],
|
||||
['id' => 20, 'tenant_id' => 2, 'description' => 'Beta'],
|
||||
]);
|
||||
|
||||
$result = $this->service->groupActiveByTenantIds([1, 2]);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertArrayHasKey(1, $result);
|
||||
$this->assertArrayHasKey(2, $result);
|
||||
|
||||
// Tenant 1 departments sorted by description: Alpha before Zebra
|
||||
$this->assertSame('Alpha', $result[1][0]['description']);
|
||||
$this->assertSame('Zebra', $result[1][1]['description']);
|
||||
|
||||
// Tenant 2 has one department
|
||||
$this->assertCount(1, $result[2]);
|
||||
$this->assertSame('Beta', $result[2][0]['description']);
|
||||
}
|
||||
|
||||
public function testGroupActiveByTenantIdsReturnsEmptyForInvalidIds(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->never())
|
||||
->method('listByTenantIds');
|
||||
|
||||
$result = $this->service->groupActiveByTenantIds([0, -1, -5]);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testGroupActiveByTenantIdsDeduplicatesIds(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('listByTenantIds')
|
||||
->with([1])
|
||||
->willReturn([]);
|
||||
|
||||
$this->service->groupActiveByTenantIds([1, 1, 1]);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminSucceeds(): void
|
||||
{
|
||||
$input = [
|
||||
'description' => 'Sales',
|
||||
'tenant_id' => 1,
|
||||
'code' => '',
|
||||
'cost_center' => '',
|
||||
'active' => 1,
|
||||
];
|
||||
|
||||
$this->departmentRepository
|
||||
->method('existsByCode')
|
||||
->willReturn(false);
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('create')
|
||||
->willReturn(42);
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('find')
|
||||
->with(42)
|
||||
->willReturn(['id' => 42, 'uuid' => 'dept-uuid-123', 'description' => 'Sales']);
|
||||
|
||||
$this->systemAuditService
|
||||
->expects($this->once())
|
||||
->method('record')
|
||||
->with('admin.departments.create', 'success', $this->anything());
|
||||
|
||||
$result = $this->service->createFromAdmin($input, 7);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(42, $result['id']);
|
||||
$this->assertSame('dept-uuid-123', $result['uuid']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminSetsDefaultDepartmentWhenRequested(): void
|
||||
{
|
||||
$input = [
|
||||
'description' => 'Default Dept',
|
||||
'tenant_id' => 1,
|
||||
'code' => '',
|
||||
'cost_center' => '',
|
||||
'active' => 1,
|
||||
'is_default' => true,
|
||||
];
|
||||
|
||||
$this->departmentRepository->method('existsByCode')->willReturn(false);
|
||||
$this->departmentRepository->method('create')->willReturn(10);
|
||||
$this->departmentRepository->method('find')->willReturn(['id' => 10, 'uuid' => 'u1']);
|
||||
|
||||
$this->settingsGateway
|
||||
->expects($this->once())
|
||||
->method('setDefaultDepartmentId')
|
||||
->with(10);
|
||||
|
||||
$result = $this->service->createFromAdmin($input, 1);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsEmptyDescription(): void
|
||||
{
|
||||
$input = [
|
||||
'description' => '',
|
||||
'tenant_id' => 1,
|
||||
'code' => '',
|
||||
'cost_center' => '',
|
||||
'active' => 1,
|
||||
];
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->never())
|
||||
->method('create');
|
||||
|
||||
$result = $this->service->createFromAdmin($input);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsMissingTenantId(): void
|
||||
{
|
||||
$input = [
|
||||
'description' => 'Sales',
|
||||
'tenant_id' => 0,
|
||||
'code' => '',
|
||||
'cost_center' => '',
|
||||
'active' => 1,
|
||||
];
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->never())
|
||||
->method('create');
|
||||
|
||||
$result = $this->service->createFromAdmin($input);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidSucceeds(): void
|
||||
{
|
||||
$department = ['id' => 5, 'uuid' => 'abc-123', 'description' => 'HR'];
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('abc-123')
|
||||
->willReturn($department);
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with(5)
|
||||
->willReturn(true);
|
||||
|
||||
$this->systemAuditService
|
||||
->expects($this->once())
|
||||
->method('record')
|
||||
->with('admin.departments.delete', 'success', $this->anything());
|
||||
|
||||
$result = $this->service->deleteByUuid('abc-123');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame($department, $result['department']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidReturnsNotFoundForMissing(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('nonexistent')
|
||||
->willReturn(null);
|
||||
|
||||
$this->departmentRepository
|
||||
->expects($this->never())
|
||||
->method('delete');
|
||||
|
||||
$result = $this->service->deleteByUuid('nonexistent');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(404, $result['status']);
|
||||
$this->assertSame('not_found', $result['error']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->never())
|
||||
->method('findByUuid');
|
||||
|
||||
$result = $this->service->deleteByUuid(' ');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(404, $result['status']);
|
||||
}
|
||||
|
||||
public function testSyncTenantsUsesFirstId(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->once())
|
||||
->method('setTenant')
|
||||
->with(10, 3)
|
||||
->willReturn(true);
|
||||
|
||||
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||||
$userDeptRepo->expects($this->any())->method('removeInvalidForDepartment')->with(10)->willReturn(2);
|
||||
|
||||
$this->userServicesFactory
|
||||
->expects($this->once())
|
||||
->method('createUserDepartmentRepository')
|
||||
->willReturn($userDeptRepo);
|
||||
|
||||
$result = $this->service->syncTenants(10, [3, 5, 7]);
|
||||
|
||||
$this->assertSame(2, $result);
|
||||
}
|
||||
|
||||
public function testSyncTenantsRejectsEmptyArray(): void
|
||||
{
|
||||
$this->departmentRepository
|
||||
->expects($this->never())
|
||||
->method('setTenant');
|
||||
|
||||
$result = $this->service->syncTenants(10, []);
|
||||
|
||||
$this->assertSame(-1, $result);
|
||||
}
|
||||
}
|
||||
332
tests/Service/Security/RateLimiterServiceTest.php
Normal file
332
tests/Service/Security/RateLimiterServiceTest.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Security;
|
||||
|
||||
use MintyPHP\Repository\Security\RateLimitRepositoryInterface;
|
||||
use MintyPHP\Service\Security\RateLimiterService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RateLimiterServiceTest extends TestCase
|
||||
{
|
||||
private RateLimitRepositoryInterface&MockObject $repository;
|
||||
private RateLimiterService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = $this->createMock(RateLimitRepositoryInterface::class);
|
||||
$this->service = new RateLimiterService($this->repository);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — fail-open on empty input
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitWithEmptyScopeFailsOpen(): void
|
||||
{
|
||||
$this->repository->expects($this->never())->method('findByScopeAndHash');
|
||||
|
||||
$result = $this->service->hit('', 'user@example.com', 5, 60, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
public function testHitWithEmptySubjectFailsOpen(): void
|
||||
{
|
||||
$this->repository->expects($this->never())->method('findByScopeAndHash');
|
||||
|
||||
$result = $this->service->hit('login', '', 5, 60, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — new entry creation
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitCreatesNewEntryOnFirstAttempt(): void
|
||||
{
|
||||
$this->repository->method('findByScopeAndHash')->willReturn(null);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('create')
|
||||
->with(
|
||||
'login',
|
||||
hash('sha256', 'user@example.com'),
|
||||
1,
|
||||
$this->matchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'),
|
||||
null
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->service->hit('login', 'user@example.com', 5, 60, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — increment existing entry
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitIncrementsExistingEntry(): void
|
||||
{
|
||||
$nowSql = gmdate('Y-m-d H:i:s');
|
||||
|
||||
$this->repository->method('findByScopeAndHash')->willReturn([
|
||||
'id' => 42,
|
||||
'hits' => 2,
|
||||
'window_started_at' => $nowSql,
|
||||
'blocked_until' => null,
|
||||
]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('updateStateById')
|
||||
->with(
|
||||
42,
|
||||
3, // 2 + 1
|
||||
$this->matchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'),
|
||||
null
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->service->hit('login', 'user@example.com', 5, 3600, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — blocks when over limit
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitBlocksWhenOverLimit(): void
|
||||
{
|
||||
$nowSql = gmdate('Y-m-d H:i:s');
|
||||
|
||||
$this->repository->method('findByScopeAndHash')->willReturn([
|
||||
'id' => 42,
|
||||
'hits' => 5, // at maxHits; next increment pushes to 6 > 5
|
||||
'window_started_at' => $nowSql,
|
||||
'blocked_until' => null,
|
||||
]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('updateStateById')
|
||||
->with(
|
||||
42,
|
||||
6,
|
||||
$this->anything(),
|
||||
$this->matchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/')
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->service->hit('login', 'user@example.com', 5, 3600, 300);
|
||||
|
||||
$this->assertFalse($result['allowed']);
|
||||
$this->assertGreaterThan(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — returns blocked when already blocked
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitReturnsBlockedWhenAlreadyBlocked(): void
|
||||
{
|
||||
$blockedUntil = gmdate('Y-m-d H:i:s', time() + 600);
|
||||
|
||||
$this->repository->method('findByScopeAndHash')->willReturn([
|
||||
'id' => 42,
|
||||
'hits' => 10,
|
||||
'window_started_at' => gmdate('Y-m-d H:i:s'),
|
||||
'blocked_until' => $blockedUntil,
|
||||
]);
|
||||
|
||||
// Should not update — early return when already blocked.
|
||||
$this->repository->expects($this->never())->method('updateStateById');
|
||||
|
||||
$result = $this->service->hit('login', 'user@example.com', 5, 3600, 300);
|
||||
|
||||
$this->assertFalse($result['allowed']);
|
||||
$this->assertGreaterThan(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — sliding window resets expired window
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitResetsHitsWhenWindowExpired(): void
|
||||
{
|
||||
$expiredWindowStart = gmdate('Y-m-d H:i:s', time() - 7200);
|
||||
|
||||
$this->repository->method('findByScopeAndHash')->willReturn([
|
||||
'id' => 42,
|
||||
'hits' => 4,
|
||||
'window_started_at' => $expiredWindowStart,
|
||||
'blocked_until' => null,
|
||||
]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('updateStateById')
|
||||
->with(
|
||||
42,
|
||||
1, // reset to 0 then incremented to 1
|
||||
$this->anything(),
|
||||
null
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->service->hit('login', 'user@example.com', 5, 3600, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — fail-open on storage exception
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitFailsOpenOnStorageException(): void
|
||||
{
|
||||
$this->repository->method('findByScopeAndHash')
|
||||
->willThrowException(new \RuntimeException('DB down'));
|
||||
|
||||
$result = $this->service->hit('login', 'user@example.com', 5, 60, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// hit — scope normalization
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testHitNormalizesScopeToLowercaseTrimmed(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('findByScopeAndHash')
|
||||
->with('login', $this->anything())
|
||||
->willReturn(null);
|
||||
|
||||
$this->repository->method('create')->willReturn(true);
|
||||
|
||||
$this->service->hit(' LOGIN ', 'user@example.com', 5, 60, 300);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// isBlocked — not blocked
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testIsBlockedReturnsTrueWhenBlocked(): void
|
||||
{
|
||||
$blockedUntil = gmdate('Y-m-d H:i:s', time() + 600);
|
||||
|
||||
$this->repository->method('findByScopeAndHash')->willReturn([
|
||||
'id' => 42,
|
||||
'hits' => 10,
|
||||
'window_started_at' => gmdate('Y-m-d H:i:s'),
|
||||
'blocked_until' => $blockedUntil,
|
||||
]);
|
||||
|
||||
$result = $this->service->isBlocked('login', 'user@example.com');
|
||||
|
||||
$this->assertFalse($result['allowed']);
|
||||
$this->assertGreaterThan(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
public function testIsBlockedClearsExpiredBlock(): void
|
||||
{
|
||||
$expiredBlock = gmdate('Y-m-d H:i:s', time() - 60);
|
||||
|
||||
$this->repository->method('findByScopeAndHash')->willReturn([
|
||||
'id' => 42,
|
||||
'hits' => 10,
|
||||
'window_started_at' => gmdate('Y-m-d H:i:s'),
|
||||
'blocked_until' => $expiredBlock,
|
||||
]);
|
||||
|
||||
$this->repository->expects($this->once())
|
||||
->method('updateStateById')
|
||||
->with(
|
||||
42,
|
||||
0,
|
||||
$this->matchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/'),
|
||||
null
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$result = $this->service->isBlocked('login', 'user@example.com');
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
public function testIsBlockedReturnsAllowedWhenNoRowExists(): void
|
||||
{
|
||||
$this->repository->method('findByScopeAndHash')->willReturn(null);
|
||||
|
||||
$result = $this->service->isBlocked('login', 'user@example.com');
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
public function testIsBlockedFailsOpenOnException(): void
|
||||
{
|
||||
$this->repository->method('findByScopeAndHash')
|
||||
->willThrowException(new \RuntimeException('DB down'));
|
||||
|
||||
$result = $this->service->isBlocked('login', 'user@example.com');
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// reset
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testResetDeletesEntry(): void
|
||||
{
|
||||
$this->repository->expects($this->once())
|
||||
->method('deleteByScopeAndHash')
|
||||
->with('login', hash('sha256', 'user@example.com'))
|
||||
->willReturn(true);
|
||||
|
||||
$this->service->reset('login', 'user@example.com');
|
||||
}
|
||||
|
||||
public function testResetIgnoresException(): void
|
||||
{
|
||||
$this->repository->method('deleteByScopeAndHash')
|
||||
->willThrowException(new \RuntimeException('DB down'));
|
||||
|
||||
// Should not throw — exceptions are silently ignored.
|
||||
$this->service->reset('login', 'user@example.com');
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testResetWithEmptyScopeDoesNothing(): void
|
||||
{
|
||||
$this->repository->expects($this->never())->method('deleteByScopeAndHash');
|
||||
|
||||
$this->service->reset('', 'user@example.com');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// registerFailure — delegates to apply() same as hit()
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public function testRegisterFailureBehavesLikeHit(): void
|
||||
{
|
||||
$this->repository->method('findByScopeAndHash')->willReturn(null);
|
||||
$this->repository->method('create')->willReturn(true);
|
||||
|
||||
$result = $this->service->registerFailure('login', 'user@example.com', 5, 60, 300);
|
||||
|
||||
$this->assertTrue($result['allowed']);
|
||||
$this->assertSame(0, $result['retry_after']);
|
||||
}
|
||||
}
|
||||
221
tests/Service/Tenant/TenantServiceTest.php
Normal file
221
tests/Service/Tenant/TenantServiceTest.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Tenant;
|
||||
|
||||
use MintyPHP\Repository\Org\DepartmentRepositoryInterface;
|
||||
use MintyPHP\Repository\Tenant\TenantRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\SystemAuditService;
|
||||
use MintyPHP\Service\Directory\DirectorySettingsGateway;
|
||||
use MintyPHP\Service\Tenant\TenantService;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TenantServiceTest extends TestCase
|
||||
{
|
||||
private TenantRepositoryInterface&MockObject $tenantRepository;
|
||||
private DepartmentRepositoryInterface&MockObject $departmentRepository;
|
||||
private DirectorySettingsGateway&MockObject $settingsGateway;
|
||||
private SystemAuditService&MockObject $systemAuditService;
|
||||
private TenantService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tenantRepository = $this->createMock(TenantRepositoryInterface::class);
|
||||
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||
$this->systemAuditService = $this->createMock(SystemAuditService::class);
|
||||
|
||||
$this->service = new TenantService(
|
||||
$this->tenantRepository,
|
||||
$this->departmentRepository,
|
||||
$this->settingsGateway,
|
||||
$this->systemAuditService
|
||||
);
|
||||
}
|
||||
|
||||
public function testListDelegatesToRepository(): void
|
||||
{
|
||||
$expected = [
|
||||
['id' => 1, 'description' => 'Tenant A'],
|
||||
['id' => 2, 'description' => 'Tenant B'],
|
||||
];
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('list')
|
||||
->willReturn($expected);
|
||||
|
||||
$result = $this->service->list();
|
||||
|
||||
self::assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminSucceeds(): void
|
||||
{
|
||||
$input = $this->validInput();
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('create')
|
||||
->willReturn(42);
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('find')
|
||||
->with(42)
|
||||
->willReturn(['id' => 42, 'uuid' => 'abc-uuid-123']);
|
||||
|
||||
$this->systemAuditService->expects($this->once())
|
||||
->method('record');
|
||||
|
||||
$result = $this->service->createFromAdmin($input, 5);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('abc-uuid-123', $result['uuid']);
|
||||
self::assertSame(42, $result['id']);
|
||||
self::assertSame('Test Tenant', $result['form']['description']);
|
||||
}
|
||||
|
||||
public function testCreateFromAdminRejectsEmptyDescription(): void
|
||||
{
|
||||
$input = $this->validInput();
|
||||
$input['description'] = '';
|
||||
|
||||
$this->tenantRepository->expects($this->never())
|
||||
->method('create');
|
||||
|
||||
$result = $this->service->createFromAdmin($input);
|
||||
|
||||
self::assertFalse($result['ok']);
|
||||
self::assertNotEmpty($result['errors']);
|
||||
}
|
||||
|
||||
public function testUpdateFromAdminSucceeds(): void
|
||||
{
|
||||
$input = $this->validInput();
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('find')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'uuid' => 'existing-uuid', 'status' => 'active']);
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('update')
|
||||
->with(10, $this->isType('array'))
|
||||
->willReturn(true);
|
||||
|
||||
$this->systemAuditService->expects($this->once())
|
||||
->method('record');
|
||||
|
||||
$result = $this->service->updateFromAdmin(10, $input, 5);
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('Test Tenant', $result['form']['description']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidSucceeds(): void
|
||||
{
|
||||
$tenant = ['id' => 7, 'uuid' => 'del-uuid-789'];
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('del-uuid-789')
|
||||
->willReturn($tenant);
|
||||
|
||||
$this->departmentRepository->expects($this->once())
|
||||
->method('countByTenantId')
|
||||
->with(7)
|
||||
->willReturn(0);
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('delete')
|
||||
->with(7)
|
||||
->willReturn(true);
|
||||
|
||||
$this->systemAuditService->expects($this->once())
|
||||
->method('record');
|
||||
|
||||
$result = $this->service->deleteByUuid('del-uuid-789');
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame($tenant, $result['tenant']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidBlocksWhenDepartmentsExist(): void
|
||||
{
|
||||
$tenant = ['id' => 7, 'uuid' => 'dep-uuid-789'];
|
||||
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('dep-uuid-789')
|
||||
->willReturn($tenant);
|
||||
|
||||
$this->departmentRepository->expects($this->once())
|
||||
->method('countByTenantId')
|
||||
->with(7)
|
||||
->willReturn(3);
|
||||
|
||||
$this->tenantRepository->expects($this->never())
|
||||
->method('delete');
|
||||
|
||||
$result = $this->service->deleteByUuid('dep-uuid-789');
|
||||
|
||||
self::assertFalse($result['ok']);
|
||||
self::assertSame(409, $result['status']);
|
||||
self::assertSame('tenant_has_departments', $result['error']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void
|
||||
{
|
||||
$this->tenantRepository->expects($this->never())
|
||||
->method('findByUuid');
|
||||
|
||||
$result = $this->service->deleteByUuid('');
|
||||
|
||||
self::assertFalse($result['ok']);
|
||||
self::assertSame(404, $result['status']);
|
||||
self::assertSame('not_found', $result['error']);
|
||||
}
|
||||
|
||||
public function testDeleteByUuidReturnsNotFoundForMissingTenant(): void
|
||||
{
|
||||
$this->tenantRepository->expects($this->once())
|
||||
->method('findByUuid')
|
||||
->with('nonexistent-uuid')
|
||||
->willReturn(null);
|
||||
|
||||
$result = $this->service->deleteByUuid('nonexistent-uuid');
|
||||
|
||||
self::assertFalse($result['ok']);
|
||||
self::assertSame(404, $result['status']);
|
||||
self::assertSame('not_found', $result['error']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function validInput(): array
|
||||
{
|
||||
return [
|
||||
'description' => 'Test Tenant',
|
||||
'status' => 'active',
|
||||
'address' => '',
|
||||
'postal_code' => '',
|
||||
'city' => '',
|
||||
'country' => '',
|
||||
'region' => '',
|
||||
'vat_id' => '',
|
||||
'tax_number' => '',
|
||||
'phone' => '',
|
||||
'fax' => '',
|
||||
'email' => '',
|
||||
'support_email' => '',
|
||||
'support_phone' => '',
|
||||
'billing_email' => '',
|
||||
'website' => '',
|
||||
'privacy_url' => '',
|
||||
'imprint_url' => '',
|
||||
'primary_color' => '',
|
||||
'primary_color_use_default' => true,
|
||||
'default_theme' => '',
|
||||
'allow_user_theme_mode' => '',
|
||||
];
|
||||
}
|
||||
}
|
||||
235
tests/Service/User/UserLifecycleServiceTest.php
Normal file
235
tests/Service/User/UserLifecycleServiceTest.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\User;
|
||||
|
||||
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Service\User\UserLifecycleService;
|
||||
use MintyPHP\Service\User\UserSettingsGateway;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserLifecycleServiceTest extends TestCase
|
||||
{
|
||||
private UserReadRepositoryInterface&MockObject $userReadRepository;
|
||||
private UserWriteRepositoryInterface&MockObject $userWriteRepository;
|
||||
private UserSettingsGateway&MockObject $settingsGateway;
|
||||
private UserLifecycleAuditService&MockObject $auditService;
|
||||
private DatabaseSessionRepository&MockObject $databaseSessionRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$this->userWriteRepository = $this->createMock(UserWriteRepositoryInterface::class);
|
||||
$this->settingsGateway = $this->createMock(UserSettingsGateway::class);
|
||||
$this->auditService = $this->createMock(UserLifecycleAuditService::class);
|
||||
$this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
||||
|
||||
// Default: lock succeeds, release is a no-op
|
||||
$this->databaseSessionRepository->method('acquireAdvisoryLock')->willReturn(true);
|
||||
$this->databaseSessionRepository->method('releaseAdvisoryLock');
|
||||
|
||||
// Default: no privileged users
|
||||
$this->userReadRepository->method('listPrivilegedUserIdsByPermissionKeys')->willReturn([]);
|
||||
}
|
||||
|
||||
private function createService(): UserLifecycleService
|
||||
{
|
||||
return new UserLifecycleService(
|
||||
$this->userReadRepository,
|
||||
$this->userWriteRepository,
|
||||
$this->settingsGateway,
|
||||
$this->auditService,
|
||||
$this->databaseSessionRepository
|
||||
);
|
||||
}
|
||||
|
||||
public function testRunReturnsLockedWhenLockNotAcquired(): void
|
||||
{
|
||||
$this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
||||
$this->databaseSessionRepository->method('acquireAdvisoryLock')->willReturn(false);
|
||||
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);
|
||||
|
||||
$this->userWriteRepository->expects($this->never())->method('setInactiveByIds');
|
||||
$this->userWriteRepository->expects($this->never())->method('deleteByIds');
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertTrue($result['locked']);
|
||||
$this->assertSame('lock_not_acquired', $result['error']);
|
||||
}
|
||||
|
||||
public function testRunSkipsWhenDeactivateDaysIsZero(): void
|
||||
{
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(0);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);
|
||||
|
||||
$this->userReadRepository->expects($this->never())->method('listIdsForAutoDeactivate');
|
||||
$this->userReadRepository->expects($this->never())->method('listIdsForAutoDelete');
|
||||
$this->userWriteRepository->expects($this->never())->method('setInactiveByIds');
|
||||
$this->userWriteRepository->expects($this->never())->method('deleteByIds');
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(0, $result['deactivated_count']);
|
||||
$this->assertSame(0, $result['deleted_count']);
|
||||
$this->assertSame(0, $result['policy']['deactivate_days']);
|
||||
$this->assertSame(0, $result['policy']['delete_days']);
|
||||
}
|
||||
|
||||
public function testRunDeactivatesUsers(): void
|
||||
{
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(0);
|
||||
|
||||
$this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([1, 2]);
|
||||
|
||||
$user1 = ['id' => 1, 'email' => 'a@example.com'];
|
||||
$user2 = ['id' => 2, 'email' => 'b@example.com'];
|
||||
$this->userReadRepository->method('find')->willReturnCallback(
|
||||
fn (int $id) => match ($id) {
|
||||
1 => $user1,
|
||||
2 => $user2,
|
||||
default => null,
|
||||
}
|
||||
);
|
||||
|
||||
$this->userWriteRepository->expects($this->once())
|
||||
->method('setInactiveByIds')
|
||||
->with([1, 2], null)
|
||||
->willReturn(2);
|
||||
|
||||
$this->userWriteRepository->expects($this->once())
|
||||
->method('bumpAuthzVersionByUserIds')
|
||||
->with([1, 2]);
|
||||
|
||||
$this->auditService->expects($this->exactly(2))->method('logDeactivate');
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(2, $result['deactivated_count']);
|
||||
}
|
||||
|
||||
public function testRunDeletesUsers(): void
|
||||
{
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);
|
||||
|
||||
$this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([]);
|
||||
$this->userReadRepository->method('listIdsForAutoDelete')->willReturn([3]);
|
||||
|
||||
$user3 = ['id' => 3, 'email' => 'c@example.com'];
|
||||
$this->userReadRepository->method('find')->willReturn($user3);
|
||||
|
||||
$this->auditService->expects($this->once())
|
||||
->method('logDeleteWithSnapshot')
|
||||
->willReturn(42);
|
||||
|
||||
$this->userWriteRepository->expects($this->once())
|
||||
->method('deleteByIds')
|
||||
->with([3])
|
||||
->willReturn(1);
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame(1, $result['deleted_count']);
|
||||
$this->assertSame(0, $result['skipped_count']);
|
||||
}
|
||||
|
||||
public function testRunSkipsDeleteWhenSnapshotFails(): void
|
||||
{
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);
|
||||
|
||||
$this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([]);
|
||||
$this->userReadRepository->method('listIdsForAutoDelete')->willReturn([5]);
|
||||
|
||||
$user5 = ['id' => 5, 'email' => 'e@example.com'];
|
||||
$this->userReadRepository->method('find')->willReturn($user5);
|
||||
|
||||
$this->auditService->expects($this->once())
|
||||
->method('logDeleteWithSnapshot')
|
||||
->willReturn(0);
|
||||
|
||||
$this->auditService->expects($this->once())
|
||||
->method('logDeleteFailure');
|
||||
|
||||
$this->userWriteRepository->expects($this->never())->method('deleteByIds');
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertSame(1, $result['skipped_count']);
|
||||
$this->assertSame(0, $result['deleted_count']);
|
||||
}
|
||||
|
||||
public function testRunReleasesLockEvenOnException(): void
|
||||
{
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(0);
|
||||
|
||||
$this->userReadRepository->method('listIdsForAutoDeactivate')
|
||||
->willThrowException(new \RuntimeException('DB error'));
|
||||
|
||||
$this->databaseSessionRepository = $this->createMock(DatabaseSessionRepository::class);
|
||||
$this->databaseSessionRepository->method('acquireAdvisoryLock')->willReturn(true);
|
||||
$this->databaseSessionRepository->expects($this->once())->method('releaseAdvisoryLock');
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('unexpected_error', $result['error']);
|
||||
}
|
||||
|
||||
public function testRunReportsPrivilegedUserCount(): void
|
||||
{
|
||||
$this->userReadRepository = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$this->userReadRepository->method('listPrivilegedUserIdsByPermissionKeys')
|
||||
->willReturn([10, 20, 30]);
|
||||
$this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([]);
|
||||
$this->userReadRepository->method('listIdsForAutoDelete')->willReturn([]);
|
||||
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(90);
|
||||
|
||||
$result = $this->createService()->run();
|
||||
|
||||
$this->assertSame(3, $result['skipped_privileged_count']);
|
||||
}
|
||||
|
||||
public function testRunTriggerTypeIsManualWhenActorProvided(): void
|
||||
{
|
||||
$this->settingsGateway->method('getUserInactivityDeactivateDays')->willReturn(30);
|
||||
$this->settingsGateway->method('getUserInactivityDeleteDays')->willReturn(0);
|
||||
|
||||
$this->userReadRepository->method('listIdsForAutoDeactivate')->willReturn([7]);
|
||||
|
||||
$user7 = ['id' => 7, 'email' => 'g@example.com'];
|
||||
$this->userReadRepository->method('find')->willReturn($user7);
|
||||
|
||||
$this->userWriteRepository->method('setInactiveByIds')->willReturn(1);
|
||||
|
||||
$this->auditService->expects($this->once())
|
||||
->method('logDeactivate')
|
||||
->with(
|
||||
$this->isType('string'),
|
||||
'manual',
|
||||
$this->isType('array'),
|
||||
99,
|
||||
$user7,
|
||||
'success',
|
||||
null
|
||||
);
|
||||
|
||||
$result = $this->createService()->run(99);
|
||||
|
||||
$this->assertSame(1, $result['deactivated_count']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user