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']); } #[DataProvider('groupActiveByTenantIdsInputProvider')] public function testGroupActiveByTenantIdsNormalizesInput(array $tenantIds, array $expectedRepositoryIds): void { $this->departmentRepository ->expects($expectedRepositoryIds ? $this->once() : $this->never()) ->method('listByTenantIds') ->with($expectedRepositoryIds) ->willReturn([]); $this->assertSame([], $this->service->groupActiveByTenantIds($tenantIds)); } 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']); } #[DataProvider('invalidDepartmentInputProvider')] public function testCreateFromAdminRejectsInvalidInput(array $input): void { $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 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 ->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); } 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, ]], ]; } }