createMock(UpdateRepository::class); $bcGateway = $bcGateway ?? $this->createMock(BcODataGateway::class); return new UpdateService($repository, $bcGateway); } private function makeBcTicket(string $no = 'TK-001', string $category = 'UPDATE', string $customerNo = 'D10001', string $customerName = 'Acme Corp'): array { return [ 'No' => $no, 'Customer_No' => $customerNo, 'Cust_Name' => $customerName, 'Category_1_Code' => $category, 'Ticket_State' => 'Offen', 'Created_On' => '2026-04-01T10:00:00Z', 'Last_Activity_Date' => '2026-04-15T14:30:00Z', 'Support_User_Name' => 'Max Mustermann', ]; } // ── Assign tests ───────────────────────────────────────────────── public function testAssignUpdateHappyPath(): void { $repo = $this->createMock(UpdateRepository::class); $repo->method('findByTicketNo')->willReturn(null); $repo->expects($this->once())->method('insert')->willReturn(1); $service = $this->createService($repo); $result = $service->assignUpdate( self::TENANT_ID, 'TK-001', 'D10001', 'Acme Corp', 'UPDATE', 'DNS00001', 'example.com', '2026-02-mysyde', 'Initial update', self::USER_ID ); $this->assertTrue($result['ok']); $this->assertSame(1, $result['id']); $this->assertEmpty($result['errors']); } public function testAssignUpdateUpdatesExistingRecord(): void { $repo = $this->createMock(UpdateRepository::class); $repo->method('findByTicketNo')->willReturn([ 'id' => 5, 'ticket_no' => 'TK-001', 'status' => 'open', ]); $repo->expects($this->once())->method('updateAssignment')->willReturn(true); $repo->expects($this->never())->method('insert'); $service = $this->createService($repo); $result = $service->assignUpdate( self::TENANT_ID, 'TK-001', 'D10001', 'Acme Corp', 'UPDATE', 'DNS00002', 'new.example.com', '2026-03-update', 'Changed domain', self::USER_ID ); $this->assertTrue($result['ok']); $this->assertSame(5, $result['id']); } public function testAssignUpdateRejectsEmptyTicketNo(): void { $service = $this->createService(); $result = $service->assignUpdate( self::TENANT_ID, '', 'D10001', 'Acme Corp', 'UPDATE', 'DNS00001', 'example.com', '', '', self::USER_ID ); $this->assertFalse($result['ok']); $this->assertArrayHasKey('ticket_no', $result['errors']); } public function testAssignUpdateRejectsEmptyDebitorNo(): void { $service = $this->createService(); $result = $service->assignUpdate( self::TENANT_ID, 'TK-001', '', 'Acme Corp', 'UPDATE', 'DNS00001', 'example.com', '', '', self::USER_ID ); $this->assertFalse($result['ok']); $this->assertArrayHasKey('debitor_no', $result['errors']); } public function testAssignUpdateRejectsInvalidCategory(): void { $service = $this->createService(); $result = $service->assignUpdate( self::TENANT_ID, 'TK-001', 'D10001', 'Acme Corp', 'BUGFIX', 'DNS00001', 'example.com', '', '', self::USER_ID ); $this->assertFalse($result['ok']); $this->assertArrayHasKey('category_code', $result['errors']); } public function testAssignUpdateSetsStatusAssignedWhenDomainProvided(): void { $repo = $this->createMock(UpdateRepository::class); $repo->method('findByTicketNo')->willReturn(null); $repo->expects($this->once()) ->method('insert') ->with($this->callback(function (array $data): bool { return $data['status'] === 'assigned' && $data['domain_no'] === 'DNS00001'; })) ->willReturn(1); $service = $this->createService($repo); $service->assignUpdate( self::TENANT_ID, 'TK-001', 'D10001', 'Acme Corp', 'UPDATE', 'DNS00001', 'example.com', '', '', self::USER_ID ); } public function testAssignUpdateSetsStatusOpenWhenNoDomainOrGitea(): void { $repo = $this->createMock(UpdateRepository::class); $repo->method('findByTicketNo')->willReturn(null); $repo->expects($this->once()) ->method('insert') ->with($this->callback(function (array $data): bool { return $data['status'] === 'open' && $data['domain_no'] === '' && $data['gitea_path'] === ''; })) ->willReturn(1); $service = $this->createService($repo); $service->assignUpdate( self::TENANT_ID, 'TK-001', 'D10001', 'Acme Corp', 'UPDATE', '', '', '', '', self::USER_ID ); } public function testAssignUpdateHandlesInsertFailure(): void { $repo = $this->createMock(UpdateRepository::class); $repo->method('findByTicketNo')->willReturn(null); $repo->method('insert')->willReturn(null); $service = $this->createService($repo); $result = $service->assignUpdate( self::TENANT_ID, 'TK-001', 'D10001', 'Acme Corp', 'UPDATE', 'DNS00001', 'example.com', '', '', self::USER_ID ); $this->assertFalse($result['ok']); $this->assertArrayHasKey('general', $result['errors']); } // ── Merge list tests ───────────────────────────────────────────── public function testGetUpdatesListMergesBcWithLocalData(): void { $bcGateway = $this->createMock(BcODataGateway::class); $bcGateway->method('fetchUpdateTickets')->willReturn([ 'tickets' => [ $this->makeBcTicket('TK-001', 'UPDATE'), $this->makeBcTicket('TK-002', 'UPDATE-HF'), $this->makeBcTicket('TK-003', 'UPDATE'), ], 'truncated' => false, ]); $repo = $this->createMock(UpdateRepository::class); $repo->method('findAllByTenant')->willReturn([ ['ticket_no' => 'TK-001', 'domain_no' => 'DNS00001', 'domain_url' => 'example.com', 'gitea_path' => '2026-02-mysyde', 'notes' => '', 'status' => 'assigned', 'id' => 1], ]); $service = $this->createService($repo, $bcGateway); $result = $service->getUpdatesList(self::TENANT_ID); $this->assertCount(3, $result['rows']); // TK-001 should be enriched with local data $tk001 = $result['rows'][0]; $this->assertSame('TK-001', $tk001['ticket_no']); $this->assertSame('assigned', $tk001['status']); $this->assertSame('DNS00001', $tk001['domain_no']); $this->assertSame('2026-02-mysyde', $tk001['gitea_path']); // TK-002 should be open (no local record) $tk002 = $result['rows'][1]; $this->assertSame('TK-002', $tk002['ticket_no']); $this->assertSame('open', $tk002['status']); $this->assertSame('', $tk002['domain_no']); } public function testGetUpdatesListFiltersByStatus(): void { $bcGateway = $this->createMock(BcODataGateway::class); $bcGateway->method('fetchUpdateTickets')->willReturn([ 'tickets' => [ $this->makeBcTicket('TK-001'), $this->makeBcTicket('TK-002'), ], 'truncated' => false, ]); $repo = $this->createMock(UpdateRepository::class); $repo->method('findAllByTenant')->willReturn([ ['ticket_no' => 'TK-001', 'domain_no' => 'DNS00001', 'domain_url' => '', 'gitea_path' => '', 'notes' => '', 'status' => 'assigned', 'id' => 1], ]); $service = $this->createService($repo, $bcGateway); $result = $service->getUpdatesList(self::TENANT_ID, ['status' => 'open']); $this->assertCount(1, $result['rows']); $this->assertSame('TK-002', $result['rows'][0]['ticket_no']); } public function testGetUpdatesListFiltersByCategory(): void { $bcGateway = $this->createMock(BcODataGateway::class); $bcGateway->method('fetchUpdateTickets')->willReturn([ 'tickets' => [ $this->makeBcTicket('TK-001', 'UPDATE'), $this->makeBcTicket('TK-002', 'UPDATE-HF'), ], 'truncated' => false, ]); $repo = $this->createMock(UpdateRepository::class); $repo->method('findAllByTenant')->willReturn([]); $service = $this->createService($repo, $bcGateway); $result = $service->getUpdatesList(self::TENANT_ID, ['category' => 'UPDATE-HF']); $this->assertCount(1, $result['rows']); $this->assertSame('TK-002', $result['rows'][0]['ticket_no']); } // ── Static helper tests ────────────────────────────────────────── public function testStatusLabelsReturnExpectedValues(): void { $this->assertNotEmpty(UpdateService::statusLabel('open')); $this->assertNotEmpty(UpdateService::statusLabel('assigned')); } public function testCategoryLabelsReturnExpectedValues(): void { $this->assertNotEmpty(UpdateService::categoryLabel('UPDATE')); $this->assertNotEmpty(UpdateService::categoryLabel('UPDATE-HF')); } public function testStatusVariantsReturnValidValues(): void { $validVariants = ['neutral', 'info', 'warning', 'success', 'error']; $this->assertContains(UpdateService::statusVariant('open'), $validVariants); $this->assertContains(UpdateService::statusVariant('assigned'), $validVariants); } public function testTicketStateLabelsReturnExpectedValues(): void { $this->assertNotEmpty(UpdateService::ticketStateLabel('Offen')); $this->assertNotEmpty(UpdateService::ticketStateLabel('In Bearbeitung')); $this->assertNotEmpty(UpdateService::ticketStateLabel('Erledigt')); $this->assertNotEmpty(UpdateService::ticketStateLabel('Geschlossen')); } public function testTicketStateVariantsReturnValidValues(): void { $validVariants = ['neutral', 'info', 'warning', 'success', 'error']; $this->assertContains(UpdateService::ticketStateVariant('Offen'), $validVariants); $this->assertContains(UpdateService::ticketStateVariant('Erledigt'), $validVariants); $this->assertContains(UpdateService::ticketStateVariant('Geschlossen'), $validVariants); } }