From 56c3e30f43ff6899af1fd5bf8aa5ed59e67b8f6f Mon Sep 17 00:00:00 2001 From: fs Date: Thu, 19 Mar 2026 20:28:04 +0100 Subject: [PATCH] Strengthen service workflow tests --- tests/Service/Access/RoleServiceTest.php | 65 +++++++++++ tests/Service/Org/DepartmentServiceTest.php | 105 ++++++++++-------- tests/Service/User/UserAccountServiceTest.php | 101 ++++++++++++++++- 3 files changed, 224 insertions(+), 47 deletions(-) diff --git a/tests/Service/Access/RoleServiceTest.php b/tests/Service/Access/RoleServiceTest.php index ccd29b5..a59a6ad 100644 --- a/tests/Service/Access/RoleServiceTest.php +++ b/tests/Service/Access/RoleServiceTest.php @@ -84,6 +84,37 @@ class RoleServiceTest extends TestCase $this->assertNotEmpty($result['errors']); } + public function testCreateFromAdminReturnsErrorWhenRepositoryCreateFails(): void + { + $this->roleRepo + ->expects($this->once()) + ->method('existsByCode') + ->with('EDITOR', 0) + ->willReturn(false); + + $this->roleRepo + ->expects($this->once()) + ->method('create') + ->willReturn(false); + + $this->roleRepo + ->expects($this->never()) + ->method('find'); + + $this->auditService + ->expects($this->never()) + ->method('record'); + + $result = $this->service->createFromAdmin([ + 'description' => 'Editor', + 'code' => 'EDITOR', + 'active' => 1, + ], 5); + + $this->assertFalse($result['ok']); + $this->assertNotEmpty($result['errors']); + } + public function testCreateFromAdminSetsDefaultRoleWhenRequested(): void { $this->roleRepo @@ -175,6 +206,40 @@ class RoleServiceTest extends TestCase $this->assertNotEmpty($result['errors']); } + public function testUpdateFromAdminReturnsErrorWhenRepositoryUpdateFails(): 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(false); + + $this->auditService + ->expects($this->never()) + ->method('record'); + + $result = $this->service->updateFromAdmin(10, [ + 'description' => 'Manager', + 'code' => 'MGR', + 'active' => 1, + ], 5); + + $this->assertFalse($result['ok']); + $this->assertNotEmpty($result['errors']); + } + public function testDeleteByUuidSucceeds(): void { $role = ['id' => 7, 'uuid' => 'uuid-7', 'description' => 'Guest']; diff --git a/tests/Service/Org/DepartmentServiceTest.php b/tests/Service/Org/DepartmentServiceTest.php index 4579049..f3ad759 100644 --- a/tests/Service/Org/DepartmentServiceTest.php +++ b/tests/Service/Org/DepartmentServiceTest.php @@ -9,6 +9,7 @@ use MintyPHP\Service\Directory\DirectorySettingsGateway; use MintyPHP\Service\Org\DepartmentService; use MintyPHP\Service\Tenant\TenantScopeService; use MintyPHP\Service\User\UserServicesFactory; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -109,26 +110,16 @@ class DepartmentServiceTest extends TestCase $this->assertSame('Beta', $result[2][0]['description']); } - public function testGroupActiveByTenantIdsReturnsEmptyForInvalidIds(): void + #[DataProvider('groupActiveByTenantIdsInputProvider')] + public function testGroupActiveByTenantIdsNormalizesInput(array $tenantIds, array $expectedRepositoryIds): 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()) + ->expects($expectedRepositoryIds ? $this->once() : $this->never()) ->method('listByTenantIds') - ->with([1]) + ->with($expectedRepositoryIds) ->willReturn([]); - $this->service->groupActiveByTenantIds([1, 1, 1]); + $this->assertSame([], $this->service->groupActiveByTenantIds($tenantIds)); } public function testCreateFromAdminSucceeds(): void @@ -193,36 +184,9 @@ class DepartmentServiceTest extends TestCase $this->assertTrue($result['ok']); } - public function testCreateFromAdminRejectsEmptyDescription(): void + #[DataProvider('invalidDepartmentInputProvider')] + public function testCreateFromAdminRejectsInvalidInput(array $input): 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'); @@ -291,6 +255,31 @@ class DepartmentServiceTest extends TestCase $this->assertSame(404, $result['status']); } + public function testDeleteByUuidReturnsDeleteFailedOnRepositoryFailure(): void + { + $this->departmentRepository + ->expects($this->once()) + ->method('findByUuid') + ->with('abc-999') + ->willReturn(['id' => 9, 'uuid' => 'abc-999', 'description' => 'Ops']); + + $this->departmentRepository + ->expects($this->once()) + ->method('delete') + ->with(9) + ->willReturn(false); + + $this->systemAuditService + ->expects($this->never()) + ->method('record'); + + $result = $this->service->deleteByUuid('abc-999'); + + $this->assertFalse($result['ok']); + $this->assertSame(500, $result['status']); + $this->assertSame('delete_failed', $result['error']); + } + public function testSyncTenantsUsesFirstId(): void { $this->departmentRepository @@ -322,4 +311,32 @@ class DepartmentServiceTest extends TestCase $this->assertSame(-1, $result); } + + public static function groupActiveByTenantIdsInputProvider(): array + { + return [ + 'invalid ids are ignored completely' => [[0, -1, -5], []], + 'duplicate ids are reduced before querying' => [[1, 1, 1], [1]], + ]; + } + + public static function invalidDepartmentInputProvider(): array + { + return [ + 'missing description' => [[ + 'description' => '', + 'tenant_id' => 1, + 'code' => '', + 'cost_center' => '', + 'active' => 1, + ]], + 'missing tenant id' => [[ + 'description' => 'Sales', + 'tenant_id' => 0, + 'code' => '', + 'cost_center' => '', + 'active' => 1, + ]], + ]; + } } diff --git a/tests/Service/User/UserAccountServiceTest.php b/tests/Service/User/UserAccountServiceTest.php index a21230b..3bd973a 100644 --- a/tests/Service/User/UserAccountServiceTest.php +++ b/tests/Service/User/UserAccountServiceTest.php @@ -13,6 +13,7 @@ use MintyPHP\Service\User\UserAssignmentService; use MintyPHP\Service\User\UserDirectoryGateway; use MintyPHP\Service\User\UserPasswordService; use MintyPHP\Service\User\UserSettingsGateway; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class UserAccountServiceTest extends TestCase @@ -40,11 +41,22 @@ class UserAccountServiceTest extends TestCase $this->assertSame(0, (int) ($result['total'] ?? -1)); } - public function testDeleteByUuidReturnsNotFoundForEmptyUuid(): void + #[DataProvider('missingDeleteByUuidInputProvider')] + public function testDeleteByUuidReturnsNotFoundForMissingInput(string $uuid, ?array $resolvedUser): void { - $service = $this->newService(); + $readRepository = $this->createMock(UserReadRepositoryInterface::class); + if ($uuid !== '') { + $readRepository->expects($this->once()) + ->method('findByUuid') + ->with($uuid) + ->willReturn($resolvedUser); + } else { + $readRepository->expects($this->never())->method('findByUuid'); + } - $result = $service->deleteByUuid(''); + $service = $this->newService($readRepository); + + $result = $service->deleteByUuid($uuid); $this->assertFalse($result['ok']); $this->assertSame(404, (int) ($result['status'] ?? 0)); @@ -104,6 +116,31 @@ class UserAccountServiceTest extends TestCase $this->assertSame(11, (int) (($result['user'] ?? [])['id'] ?? 0)); } + public function testDeleteByUuidReturnsDeleteFailedWhenRepositoryDeleteFails(): void + { + $readRepository = $this->createMock(UserReadRepositoryInterface::class); + $readRepository->expects($this->once()) + ->method('findByUuid') + ->with('user-uuid') + ->willReturn(['id' => 11, 'uuid' => 'user-uuid']); + + $writeRepository = $this->createMock(UserWriteRepositoryInterface::class); + $writeRepository->expects($this->once()) + ->method('delete') + ->with(11) + ->willReturn(false); + + $auditService = $this->createMock(SystemAuditService::class); + $auditService->expects($this->never())->method('record'); + + $service = $this->newService($readRepository, $writeRepository, null, null, null, null, null, null, $auditService); + $result = $service->deleteByUuid('user-uuid', 44); + + $this->assertFalse($result['ok']); + $this->assertSame(500, (int) ($result['status'] ?? 0)); + $this->assertSame('delete_failed', (string) ($result['error'] ?? '')); + } + public function testDeleteByUuidsReturnsPermissionDeniedWhenNothingIsInScope(): void { $readRepository = $this->createMock(UserReadRepositoryInterface::class); @@ -160,6 +197,50 @@ class UserAccountServiceTest extends TestCase $this->assertSame('self_deactivate', (string) ($result['error'] ?? '')); } + public function testSetActiveByUuidReturnsNotFoundForMissingUser(): void + { + $readRepository = $this->createMock(UserReadRepositoryInterface::class); + $readRepository->expects($this->once()) + ->method('findByUuid') + ->with('u-missing') + ->willReturn(null); + + $writeRepository = $this->createMock(UserWriteRepositoryInterface::class); + $writeRepository->expects($this->never())->method('setActive'); + + $service = $this->newService($readRepository, $writeRepository); + $result = $service->setActiveByUuid('u-missing', true, 42); + + $this->assertFalse($result['ok']); + $this->assertSame(404, (int) ($result['status'] ?? 0)); + $this->assertSame('not_found', (string) ($result['error'] ?? '')); + } + + public function testSetActiveByUuidReturnsUpdateFailedWhenRepositoryRejects(): void + { + $readRepository = $this->createMock(UserReadRepositoryInterface::class); + $readRepository->method('findByUuid')->willReturn(['id' => 33, 'uuid' => 'u-33', 'active' => 0]); + + $writeRepository = $this->createMock(UserWriteRepositoryInterface::class); + $writeRepository->expects($this->once()) + ->method('setActive') + ->with(33, true, 90) + ->willReturn(false); + + $assignmentService = $this->createMock(UserAssignmentService::class); + $assignmentService->expects($this->never())->method('bumpAuthzVersion'); + + $auditService = $this->createMock(SystemAuditService::class); + $auditService->expects($this->never())->method('record'); + + $service = $this->newService($readRepository, $writeRepository, null, $assignmentService, null, null, null, null, $auditService); + $result = $service->setActiveByUuid('u-33', true, 90); + + $this->assertFalse($result['ok']); + $this->assertSame(500, (int) ($result['status'] ?? 0)); + $this->assertSame('update_failed', (string) ($result['error'] ?? '')); + } + public function testSetActiveByUuidUpdatesAssignmentsAndAudit(): void { $readRepository = $this->createMock(UserReadRepositoryInterface::class); @@ -238,6 +319,8 @@ class UserAccountServiceTest extends TestCase $settingsGateway->method('getDefaultTenantId')->willReturn(3); $settingsGateway->method('getDefaultRoleId')->willReturn(4); $settingsGateway->method('getDefaultDepartmentId')->willReturn(5); + $settingsGateway->method('normalizeTheme') + ->willReturnCallback(static fn (?string $theme): string => $theme ?? 'light'); $writeRepository = $this->createMock(UserWriteRepositoryInterface::class); $writeRepository->expects($this->once()) @@ -294,6 +377,8 @@ class UserAccountServiceTest extends TestCase $settingsGateway->method('getDefaultRoleId')->willReturn(null); $settingsGateway->method('getDefaultDepartmentId')->willReturn(null); $settingsGateway->method('getAppTheme')->willReturn('light'); + $settingsGateway->method('normalizeTheme') + ->willReturnCallback(static fn (?string $theme): string => $theme ?? 'light'); $writeRepository = $this->createMock(UserWriteRepositoryInterface::class); $writeRepository->expects($this->once())->method('create')->willReturn(88); @@ -358,6 +443,8 @@ class UserAccountServiceTest extends TestCase $settingsGateway->method('getDefaultTenantId')->willReturn(3); $settingsGateway->method('getDefaultRoleId')->willReturn(4); $settingsGateway->method('getDefaultDepartmentId')->willReturn(5); + $settingsGateway->method('normalizeTheme') + ->willReturnCallback(static fn (?string $theme): string => $theme ?? 'light'); $writeRepository = $this->createMock(UserWriteRepositoryInterface::class); $writeRepository->expects($this->once())->method('create')->willReturn(77); @@ -602,4 +689,12 @@ class UserAccountServiceTest extends TestCase $databaseSessionRepository ); } + + public static function missingDeleteByUuidInputProvider(): array + { + return [ + 'empty uuid' => ['', null], + 'missing user' => ['missing-uuid', null], + ]; + } }