feat(helpdesk): align module with core list/drawer standards
- add helpdesk module pages, services, settings and tests - standardize debtor list on drawer/grid contracts and robust filter drawer behavior - add helpdesk aside panel navigation and settings visibility provider - switch primary list slug to helpdesk/debitor and remove helpdesk/search compatibility - include required core contract updates for list contracts and detail/drawer integration
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BcODataGatewayTest extends TestCase
|
||||
{
|
||||
private function createGatewayWithSettings(bool $isConfigured = true): BcODataGateway
|
||||
{
|
||||
$settings = $this->createMock(HelpdeskSettingsGateway::class);
|
||||
$settings->method('isConfigured')->willReturn($isConfigured);
|
||||
$settings->method('getODataBaseUrl')->willReturn('https://bc.example.com/ODataV4');
|
||||
$settings->method('getCompanyName')->willReturn('Test Company');
|
||||
$settings->method('buildEntityUrl')->willReturnCallback(
|
||||
static fn (string $entity): string => "https://bc.example.com/ODataV4/Company('Test%20Company')/" . $entity
|
||||
);
|
||||
$settings->method('getAuthMode')->willReturn('basic');
|
||||
$settings->method('getBasicUser')->willReturn('testuser');
|
||||
$settings->method('getBasicPassword')->willReturn('testpass');
|
||||
|
||||
return new BcODataGateway($settings);
|
||||
}
|
||||
|
||||
public function testSearchCustomersReturnsEmptyForEmptyQuery(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$results = $gateway->searchCustomers('');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testSearchCustomersReturnsEmptyForWhitespaceQuery(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$results = $gateway->searchCustomers(' ');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testGetCustomerReturnsNullForEmptyNo(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$result = $gateway->getCustomer('');
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testGetContactsForCustomerReturnsEmptyForEmptyName(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$results = $gateway->getContactsForCustomer('10254', '');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testGetTicketsForCustomerReturnsEmptyForEmptyName(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$results = $gateway->getTicketsForCustomer('10254', '');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testGetTicketReturnsNullForEmptyNo(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$result = $gateway->getTicket('');
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testSearchCustomersThrowsWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$gateway->searchCustomers('test');
|
||||
}
|
||||
|
||||
public function testGetCustomerReturnsNullWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$result = $gateway->getCustomer('10001');
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testGetTicketReturnsNullWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$result = $gateway->getTicket('T0001');
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testTestConnectionReturnsNotOkWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$result = $gateway->testConnection();
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('Configuration incomplete', $result['error']);
|
||||
}
|
||||
|
||||
public function testSearchCustomersRejectsODataInjectionWithParentheses(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$gateway->searchCustomers("') or true or contains(Name,'");
|
||||
}
|
||||
|
||||
public function testSearchCustomersRejectsODataInjectionWithSingleQuotes(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$gateway->searchCustomers("test' eq 'hack");
|
||||
}
|
||||
|
||||
public function testGetCustomerRejectsODataInjectionCharacters(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$gateway->getCustomer("10254' or '1'='1");
|
||||
}
|
||||
|
||||
public function testSearchCustomersAllowsGermanUmlauts(): void
|
||||
{
|
||||
// Should not throw InvalidArgumentException — umlauts are valid search characters
|
||||
// Will throw RuntimeException because cURL can't reach fake URL, which is expected
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
try {
|
||||
$gateway->searchCustomers('Müller Straße');
|
||||
} catch (\InvalidArgumentException) {
|
||||
$this->fail('Umlauts should be allowed in search queries');
|
||||
} catch (\RuntimeException) {
|
||||
// Expected: cURL fails against fake URL
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSearchCustomersAllowsNumbersAndDashes(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
try {
|
||||
$gateway->searchCustomers('10254-A');
|
||||
} catch (\InvalidArgumentException) {
|
||||
$this->fail('Numbers and dashes should be allowed in search queries');
|
||||
} catch (\RuntimeException) {
|
||||
// Expected: cURL fails against fake URL
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetTicketLogReturnsEmptyForEmptyNo(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$results = $gateway->getTicketLog('');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testGetTicketLogReturnsEmptyWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$results = $gateway->getTicketLog('T26-1214');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testGetContactsForCustomerReturnsEmptyWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$results = $gateway->getContactsForCustomer('10254', 'Test GmbH');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testGetTicketsForCustomerReturnsEmptyWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$results = $gateway->getTicketsForCustomer('10254', 'Test GmbH');
|
||||
$this->assertSame([], $results);
|
||||
}
|
||||
|
||||
public function testListCustomersThrowsWhenNotConfigured(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings(false);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$gateway->listCustomers('', '', 10, 0, 'Name', 'asc');
|
||||
}
|
||||
|
||||
public function testListCustomersRejectsInvalidCityFilterCharacters(): void
|
||||
{
|
||||
$gateway = $this->createGatewayWithSettings();
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$gateway->listCustomers('', 'Berlin)', 10, 0, 'Name', 'asc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorDetailService;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DebitorDetailServiceTest extends TestCase
|
||||
{
|
||||
private function createService(?BcODataGateway $gateway = null, bool $isConfigured = true): DebitorDetailService
|
||||
{
|
||||
$gateway = $gateway ?? $this->createMock(BcODataGateway::class);
|
||||
$settings = $this->createMock(HelpdeskSettingsGateway::class);
|
||||
$settings->method('isConfigured')->willReturn($isConfigured);
|
||||
|
||||
return new DebitorDetailService($gateway, $settings);
|
||||
}
|
||||
|
||||
// --- loadDetail() (legacy) ---
|
||||
|
||||
public function testLoadDetailReturnsNotFoundForEmptyCustomerNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadDetail('');
|
||||
$this->assertSame('not_found', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDetailReturnsNotConfiguredWhenBcNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->loadDetail('10254');
|
||||
$this->assertSame('not_configured', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDetailReturnsNotFoundWhenCustomerMissing(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())->method('getCustomer')->with('99999')->willReturn(null);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDetail('99999');
|
||||
$this->assertSame('not_found', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDetailReturnsSuccessWithAllData(): void
|
||||
{
|
||||
$customer = ['No' => '10254', 'Name' => 'Musterfirma GmbH'];
|
||||
$contacts = [['No' => 'KT0001', 'Name' => 'Max Mustermann']];
|
||||
$tickets = [['No' => 'T0001', 'Description' => 'Problem X']];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())->method('getCustomer')->with('10254')->willReturn($customer);
|
||||
$gateway->expects($this->once())->method('getContactsForCustomer')->with('10254', 'Musterfirma GmbH')->willReturn($contacts);
|
||||
$gateway->expects($this->once())->method('getTicketsForCustomer')->with('10254', 'Musterfirma GmbH')->willReturn($tickets);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDetail('10254');
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertSame($customer, $result['customer']);
|
||||
$this->assertSame($contacts, $result['contacts']);
|
||||
$this->assertSame($tickets, $result['tickets']);
|
||||
}
|
||||
|
||||
public function testLoadDetailReturnsErrorOnBcConnectionFailure(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willThrowException(new \RuntimeException('timeout'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDetail('10254');
|
||||
$this->assertSame('error', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadDetailStillSucceedsWhenContactsFail(): void
|
||||
{
|
||||
$customer = ['No' => '10254', 'Name' => 'Musterfirma GmbH'];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())->method('getCustomer')->with('10254')->willReturn($customer);
|
||||
$gateway->method('getContactsForCustomer')->willThrowException(new \RuntimeException('contacts fail'));
|
||||
$gateway->method('getTicketsForCustomer')->willReturn([]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDetail('10254');
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertSame([], $result['contacts']);
|
||||
$this->assertSame('contacts fail', $result['contactsError']);
|
||||
}
|
||||
|
||||
public function testLoadDetailStillSucceedsWhenTicketsFail(): void
|
||||
{
|
||||
$customer = ['No' => '10254', 'Name' => 'Musterfirma GmbH'];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())->method('getCustomer')->with('10254')->willReturn($customer);
|
||||
$gateway->method('getContactsForCustomer')->willReturn([]);
|
||||
$gateway->method('getTicketsForCustomer')->willThrowException(new \RuntimeException('tickets fail'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadDetail('10254');
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertSame([], $result['tickets']);
|
||||
$this->assertSame('tickets fail', $result['ticketsError']);
|
||||
}
|
||||
|
||||
// --- loadCustomer() ---
|
||||
|
||||
public function testLoadCustomerReturnsNotFoundForEmptyNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadCustomer('');
|
||||
$this->assertSame('not_found', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadCustomerReturnsNotConfiguredWhenNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->loadCustomer('10001');
|
||||
$this->assertSame('not_configured', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadCustomerReturnsNotFoundWhenCustomerNull(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willReturn(null);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadCustomer('99999');
|
||||
$this->assertSame('not_found', $result['status']);
|
||||
}
|
||||
|
||||
public function testLoadCustomerReturnsSuccessWithCustomerData(): void
|
||||
{
|
||||
$customer = ['No' => '10001', 'Name' => 'Test GmbH', 'Support_Type' => 'Premium'];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willReturn($customer);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadCustomer('10001');
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertSame($customer, $result['customer']);
|
||||
$this->assertArrayNotHasKey('contacts', $result);
|
||||
$this->assertArrayNotHasKey('tickets', $result);
|
||||
}
|
||||
|
||||
public function testLoadCustomerReturnsErrorOnException(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getCustomer')->willThrowException(new \RuntimeException('Connection failed'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadCustomer('10001');
|
||||
|
||||
$this->assertSame('error', $result['status']);
|
||||
$this->assertSame('BC connection failed', $result['error']);
|
||||
}
|
||||
|
||||
// --- loadContacts() ---
|
||||
|
||||
public function testLoadContactsReturnsFalseForEmptyCustomerNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadContacts('', 'Test GmbH');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadContactsReturnsFalseForEmptyCustomerName(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadContacts('10001', '');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadContactsReturnsFalseWhenNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->loadContacts('10001', 'Test GmbH');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadContactsReturnsOkWithContacts(): void
|
||||
{
|
||||
$contacts = [['No' => 'KT001', 'Name' => 'Max Mustermann']];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getContactsForCustomer')->willReturn($contacts);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadContacts('10001', 'Test GmbH');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame($contacts, $result['contacts']);
|
||||
}
|
||||
|
||||
public function testLoadContactsReturnsErrorOnException(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getContactsForCustomer')->willThrowException(new \RuntimeException('API error'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadContacts('10001', 'Test GmbH');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('API error', $result['error']);
|
||||
}
|
||||
|
||||
// --- loadTickets() ---
|
||||
|
||||
public function testLoadTicketsReturnsFalseForEmptyCustomerNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadTickets('', 'Test GmbH');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadTicketsReturnsFalseForEmptyCustomerName(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadTickets('10001', '');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadTicketsReturnsFalseWhenNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->loadTickets('10001', 'Test GmbH');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadTicketsReturnsOkWithTickets(): void
|
||||
{
|
||||
$tickets = [['No' => 'T001', 'Description' => 'Test ticket', 'Ticket_State' => 'Open']];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getTicketsForCustomer')->willReturn($tickets);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadTickets('10001', 'Test GmbH');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame($tickets, $result['tickets']);
|
||||
}
|
||||
|
||||
public function testLoadTicketsReturnsErrorOnException(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getTicketsForCustomer')->willThrowException(new \RuntimeException('Timeout'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadTickets('10001', 'Test GmbH');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('Timeout', $result['error']);
|
||||
}
|
||||
|
||||
// --- loadTicketLog() ---
|
||||
|
||||
public function testLoadTicketLogReturnsFalseForEmptyTicketNo(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->loadTicketLog('');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadTicketLogReturnsFalseWhenNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->loadTicketLog('T001');
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testLoadTicketLogReturnsOkWithLog(): void
|
||||
{
|
||||
$log = [
|
||||
['Entry_No' => 1, 'Type' => 'Nachricht', 'Created_By' => 'NKS', 'Support_Ticket_No' => 'T001'],
|
||||
['Entry_No' => 2, 'Type' => 'Status', 'Created_By' => 'NKS', 'Support_Ticket_No' => 'T001'],
|
||||
];
|
||||
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getTicketLog')->willReturn($log);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadTicketLog('T001');
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
$this->assertSame($log, $result['log']);
|
||||
}
|
||||
|
||||
public function testLoadTicketLogReturnsErrorOnException(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->method('getTicketLog')->willThrowException(new \RuntimeException('API error'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->loadTicketLog('T001');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('API error', $result['error']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\BcODataGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\DebitorSearchService;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DebitorSearchServiceTest extends TestCase
|
||||
{
|
||||
private function createService(?BcODataGateway $gateway = null, bool $isConfigured = true): DebitorSearchService
|
||||
{
|
||||
$gateway = $gateway ?? $this->createMock(BcODataGateway::class);
|
||||
$settings = $this->createMock(HelpdeskSettingsGateway::class);
|
||||
$settings->method('isConfigured')->willReturn($isConfigured);
|
||||
|
||||
return new DebitorSearchService($gateway, $settings);
|
||||
}
|
||||
|
||||
public function testSearchReturnsEmptyStatusForEmptyQuery(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->search('');
|
||||
$this->assertSame('empty', $result['status']);
|
||||
$this->assertSame([], $result['results']);
|
||||
}
|
||||
|
||||
public function testSearchReturnsTooShortForSingleCharacter(): void
|
||||
{
|
||||
$service = $this->createService();
|
||||
$result = $service->search('A');
|
||||
$this->assertSame('too_short', $result['status']);
|
||||
}
|
||||
|
||||
public function testSearchReturnsNotConfiguredWhenBcNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->search('Test Company');
|
||||
$this->assertSame('not_configured', $result['status']);
|
||||
$this->assertNotEmpty($result['error']);
|
||||
}
|
||||
|
||||
public function testSearchReturnsSuccessWithResults(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())
|
||||
->method('searchCustomers')
|
||||
->with('Musterfirma')
|
||||
->willReturn([
|
||||
['No' => '10254', 'Name' => 'Musterfirma GmbH', 'City' => 'Berlin'],
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->search('Musterfirma');
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertCount(1, $result['results']);
|
||||
$this->assertSame('10254', $result['results'][0]['No']);
|
||||
}
|
||||
|
||||
public function testSearchReturnsNoResultsWhenEmpty(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())
|
||||
->method('searchCustomers')
|
||||
->willReturn([]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->search('xyz-nonexistent');
|
||||
$this->assertSame('no_results', $result['status']);
|
||||
$this->assertSame([], $result['results']);
|
||||
}
|
||||
|
||||
public function testSearchReturnsErrorOnBcConnectionFailure(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())
|
||||
->method('searchCustomers')
|
||||
->willThrowException(new \RuntimeException('Connection refused'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->search('Musterfirma');
|
||||
$this->assertSame('error', $result['status']);
|
||||
$this->assertNotEmpty($result['error']);
|
||||
}
|
||||
|
||||
public function testListForGridReturnsNotConfiguredWhenBcNotConfigured(): void
|
||||
{
|
||||
$service = $this->createService(null, false);
|
||||
$result = $service->listForGrid([]);
|
||||
|
||||
$this->assertSame('not_configured', $result['status']);
|
||||
$this->assertSame([], $result['rows']);
|
||||
$this->assertSame(0, $result['total']);
|
||||
}
|
||||
|
||||
public function testListForGridReturnsSuccessWithRowsAndTotal(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())
|
||||
->method('listCustomers')
|
||||
->with('Muster', 'Berlin', 10, 0, 'Name', 'asc')
|
||||
->willReturn([
|
||||
'rows' => [
|
||||
['No' => '10000', 'Name' => 'Muster GmbH', 'City' => 'Berlin'],
|
||||
],
|
||||
'total' => 42,
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->listForGrid([
|
||||
'search' => 'Muster',
|
||||
'city' => 'Berlin',
|
||||
'limit' => 10,
|
||||
'offset' => 0,
|
||||
'order' => 'Name',
|
||||
'dir' => 'asc',
|
||||
]);
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
$this->assertCount(1, $result['rows']);
|
||||
$this->assertSame(42, $result['total']);
|
||||
}
|
||||
|
||||
public function testListForGridReturnsErrorWhenGatewayFails(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())
|
||||
->method('listCustomers')
|
||||
->willThrowException(new \RuntimeException('Connection refused'));
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->listForGrid(['search' => 'Muster']);
|
||||
|
||||
$this->assertSame('error', $result['status']);
|
||||
$this->assertSame([], $result['rows']);
|
||||
$this->assertSame(0, $result['total']);
|
||||
}
|
||||
|
||||
public function testListForGridNormalizesLimitAndOffsetBounds(): void
|
||||
{
|
||||
$gateway = $this->createMock(BcODataGateway::class);
|
||||
$gateway->expects($this->once())
|
||||
->method('listCustomers')
|
||||
->with('', '', 100, 0, 'Name', 'asc')
|
||||
->willReturn([
|
||||
'rows' => [],
|
||||
'total' => 0,
|
||||
]);
|
||||
|
||||
$service = $this->createService($gateway);
|
||||
$result = $service->listForGrid([
|
||||
'limit' => 999,
|
||||
'offset' => -5,
|
||||
'order' => 'Name',
|
||||
'dir' => 'asc',
|
||||
]);
|
||||
|
||||
$this->assertSame('success', $result['status']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskOAuthTokenService;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskTokenRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HelpdeskOAuthTokenServiceTest extends TestCase
|
||||
{
|
||||
private function createService(?string $authMode = null, ?string $cachedToken = null): HelpdeskOAuthTokenService
|
||||
{
|
||||
$settings = $this->createMock(HelpdeskSettingsGateway::class);
|
||||
$settings->method('getAuthMode')->willReturn($authMode ?? HelpdeskSettingsGateway::AUTH_MODE_BASIC);
|
||||
|
||||
$tokenRepo = $this->createMock(HelpdeskTokenRepository::class);
|
||||
$tokenRepo->method('findValidToken')->willReturn($cachedToken);
|
||||
|
||||
return new HelpdeskOAuthTokenService($settings, $tokenRepo);
|
||||
}
|
||||
|
||||
public function testReturnsNullWhenAuthModeIsBasic(): void
|
||||
{
|
||||
$service = $this->createService(HelpdeskSettingsGateway::AUTH_MODE_BASIC);
|
||||
$this->assertNull($service->getAccessToken(1));
|
||||
}
|
||||
|
||||
public function testReturnsCachedTokenWhenAvailable(): void
|
||||
{
|
||||
$service = $this->createService(HelpdeskSettingsGateway::AUTH_MODE_OAUTH2, 'cached-bearer-token');
|
||||
$this->assertSame('cached-bearer-token', $service->getAccessToken(1));
|
||||
}
|
||||
|
||||
public function testReturnsNullWhenNoCachedTokenExists(): void
|
||||
{
|
||||
$service = $this->createService(HelpdeskSettingsGateway::AUTH_MODE_OAUTH2, null);
|
||||
$this->assertNull($service->getAccessToken(1));
|
||||
}
|
||||
|
||||
public function testReturnsNullForBasicAuthRegardlessOfTenantId(): void
|
||||
{
|
||||
$service = $this->createService(HelpdeskSettingsGateway::AUTH_MODE_BASIC);
|
||||
$this->assertNull($service->getAccessToken(99));
|
||||
}
|
||||
|
||||
public function testDoesNotQueryCacheWhenAuthModeIsBasic(): void
|
||||
{
|
||||
$settings = $this->createMock(HelpdeskSettingsGateway::class);
|
||||
$settings->method('getAuthMode')->willReturn(HelpdeskSettingsGateway::AUTH_MODE_BASIC);
|
||||
|
||||
$tokenRepo = $this->createMock(HelpdeskTokenRepository::class);
|
||||
$tokenRepo->expects($this->never())->method('findValidToken');
|
||||
|
||||
$service = new HelpdeskOAuthTokenService($settings, $tokenRepo);
|
||||
$service->getAccessToken(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
||||
use MintyPHP\Repository\Settings\SettingRepositoryInterface;
|
||||
use MintyPHP\Service\Settings\SettingsCryptoGatewayInterface;
|
||||
use MintyPHP\Service\Settings\SettingsMetadataGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HelpdeskSettingsGatewayTest extends TestCase
|
||||
{
|
||||
private function createGateway(?SettingRepositoryInterface $settings = null, ?SettingsCryptoGatewayInterface $crypto = null): HelpdeskSettingsGateway
|
||||
{
|
||||
$settings = $settings ?? $this->createMock(SettingRepositoryInterface::class);
|
||||
$crypto = $crypto ?? $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
|
||||
return new HelpdeskSettingsGateway(new SettingsMetadataGateway($settings), $crypto);
|
||||
}
|
||||
|
||||
public function testGetAuthModeDefaultsToBasic(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn(null);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertSame('basic', $gateway->getAuthMode());
|
||||
}
|
||||
|
||||
public function testGetAuthModeReturnsOAuth2WhenSet(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn('oauth2');
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertSame('oauth2', $gateway->getAuthMode());
|
||||
}
|
||||
|
||||
public function testSetAuthModeRejectsInvalidMode(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertFalse($gateway->setAuthMode('invalid'));
|
||||
}
|
||||
|
||||
public function testSetODataBaseUrlRejectsHttp(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertFalse($gateway->setODataBaseUrl('http://insecure.example.com'));
|
||||
}
|
||||
|
||||
public function testSetODataBaseUrlAcceptsHttps(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->once())
|
||||
->method('set')
|
||||
->with(HelpdeskSettingsGateway::KEY_ODATA_BASE_URL, 'https://bc.example.com/OData', 'helpdesk.bc_odata_base_url')
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertTrue($gateway->setODataBaseUrl('https://bc.example.com/OData'));
|
||||
}
|
||||
|
||||
public function testGetBasicPasswordReturnsNullWhenDecryptFails(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn('encrypted-value');
|
||||
|
||||
$crypto = $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
$crypto->method('decryptString')->willThrowException(new \RuntimeException('bad key'));
|
||||
|
||||
$gateway = $this->createGateway($settings, $crypto);
|
||||
$this->assertNull($gateway->getBasicPassword());
|
||||
}
|
||||
|
||||
public function testSetBasicPasswordEncryptsAndPersists(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->once())
|
||||
->method('set')
|
||||
->with(HelpdeskSettingsGateway::KEY_BASIC_PASSWORD_ENC, 'enc-pw', 'helpdesk.bc_basic_password_enc')
|
||||
->willReturn(true);
|
||||
|
||||
$crypto = $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
$crypto->expects($this->once())
|
||||
->method('encryptString')
|
||||
->with('my-password')
|
||||
->willReturn('enc-pw');
|
||||
|
||||
$gateway = $this->createGateway($settings, $crypto);
|
||||
$this->assertTrue($gateway->setBasicPassword('my-password'));
|
||||
}
|
||||
|
||||
public function testSetBasicPasswordClearsWhenEmpty(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->once())
|
||||
->method('set')
|
||||
->with(HelpdeskSettingsGateway::KEY_BASIC_PASSWORD_ENC, null, 'helpdesk.bc_basic_password_enc')
|
||||
->willReturn(true);
|
||||
|
||||
$crypto = $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
$crypto->expects($this->never())->method('encryptString');
|
||||
|
||||
$gateway = $this->createGateway($settings, $crypto);
|
||||
$this->assertTrue($gateway->setBasicPassword(''));
|
||||
}
|
||||
|
||||
public function testSetBasicPasswordReturnsFalseOnEncryptionFailure(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
|
||||
$crypto = $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
$crypto->method('encryptString')->willThrowException(new \RuntimeException('encryption failed'));
|
||||
|
||||
$gateway = $this->createGateway($settings, $crypto);
|
||||
$this->assertFalse($gateway->setBasicPassword('my-password'));
|
||||
}
|
||||
|
||||
public function testBuildEntityUrlConstructsCorrectUrl(): void
|
||||
{
|
||||
$callMap = [
|
||||
[HelpdeskSettingsGateway::KEY_ODATA_BASE_URL, 'https://bc.example.com/ODataV4'],
|
||||
[HelpdeskSettingsGateway::KEY_COMPANY_NAME, 'Test Company'],
|
||||
];
|
||||
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnCallback(static function (string $key) use ($callMap): ?string {
|
||||
foreach ($callMap as [$k, $v]) {
|
||||
if ($k === $key) {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$url = $gateway->buildEntityUrl('Integration_Customer_Card');
|
||||
$this->assertSame("https://bc.example.com/ODataV4/Company('Test%20Company')/Integration_Customer_Card", $url);
|
||||
}
|
||||
|
||||
public function testValidateConfigurationReportsMissingBasicCredentials(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn(null);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$missing = $gateway->validateConfiguration();
|
||||
$this->assertNotEmpty($missing);
|
||||
$this->assertContains('BC Basic Auth User', $missing);
|
||||
$this->assertContains('BC Basic Auth Password', $missing);
|
||||
}
|
||||
|
||||
public function testIsConfiguredReturnsTrueWhenBasicCredentialsPresent(): void
|
||||
{
|
||||
$callMap = [
|
||||
[HelpdeskSettingsGateway::KEY_AUTH_MODE, 'basic'],
|
||||
[HelpdeskSettingsGateway::KEY_BASIC_USER, 'testuser'],
|
||||
[HelpdeskSettingsGateway::KEY_BASIC_PASSWORD_ENC, 'encrypted'],
|
||||
];
|
||||
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturnCallback(static function (string $key) use ($callMap): ?string {
|
||||
foreach ($callMap as [$k, $v]) {
|
||||
if ($k === $key) {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
$crypto = $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
$crypto->method('decryptString')->willReturn('decrypted-password');
|
||||
|
||||
$gateway = $this->createGateway($settings, $crypto);
|
||||
$this->assertTrue($gateway->isConfigured());
|
||||
}
|
||||
|
||||
public function testSetOAuthTokenEndpointRejectsHttp(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->never())->method('set');
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertFalse($gateway->setOAuthTokenEndpoint('http://login.microsoftonline.com/token'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
||||
|
||||
use MintyPHP\Module\Helpdesk\Service\HelpdeskTokenRepository;
|
||||
use MintyPHP\Service\Settings\SettingsCryptoGatewayInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for HelpdeskTokenRepository edge-case validation paths.
|
||||
*
|
||||
* Note: DB-dependent paths (findValidToken with actual DB, storeToken INSERT)
|
||||
* require integration tests against a real database. These unit tests cover
|
||||
* the input validation and error handling that runs before DB access.
|
||||
*/
|
||||
class HelpdeskTokenRepositoryTest extends TestCase
|
||||
{
|
||||
private function createRepository(?SettingsCryptoGatewayInterface $crypto = null): HelpdeskTokenRepository
|
||||
{
|
||||
$crypto = $crypto ?? $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
|
||||
return new HelpdeskTokenRepository($crypto);
|
||||
}
|
||||
|
||||
public function testFindValidTokenReturnsNullForZeroTenantId(): void
|
||||
{
|
||||
$repo = $this->createRepository();
|
||||
$this->assertNull($repo->findValidToken(0));
|
||||
}
|
||||
|
||||
public function testFindValidTokenReturnsNullForNegativeTenantId(): void
|
||||
{
|
||||
$repo = $this->createRepository();
|
||||
$this->assertNull($repo->findValidToken(-1));
|
||||
}
|
||||
|
||||
public function testStoreTokenReturnsFalseForZeroTenantId(): void
|
||||
{
|
||||
$repo = $this->createRepository();
|
||||
$this->assertFalse($repo->storeToken(0, 'token', new \DateTimeImmutable('+1 hour')));
|
||||
}
|
||||
|
||||
public function testStoreTokenReturnsFalseForNegativeTenantId(): void
|
||||
{
|
||||
$repo = $this->createRepository();
|
||||
$this->assertFalse($repo->storeToken(-5, 'token', new \DateTimeImmutable('+1 hour')));
|
||||
}
|
||||
|
||||
public function testStoreTokenReturnsFalseForEmptyAccessToken(): void
|
||||
{
|
||||
$repo = $this->createRepository();
|
||||
$this->assertFalse($repo->storeToken(1, '', new \DateTimeImmutable('+1 hour')));
|
||||
}
|
||||
|
||||
public function testStoreTokenReturnsFalseWhenEncryptionFails(): void
|
||||
{
|
||||
$crypto = $this->createMock(SettingsCryptoGatewayInterface::class);
|
||||
$crypto->method('encryptString')->willThrowException(new \RuntimeException('encryption failed'));
|
||||
|
||||
$repo = $this->createRepository($crypto);
|
||||
$this->assertFalse($repo->storeToken(1, 'valid-token', new \DateTimeImmutable('+1 hour')));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user