1
0
Files
breadcrumb-the-shire/tests/Service/Org/DepartmentServiceTest.php
fs d4978d1504 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>
2026-03-13 13:57:54 +01:00

326 lines
10 KiB
PHP

<?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);
}
}