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:
2026-04-02 17:48:27 +02:00
parent 5d07236758
commit a0d7670dd7
55 changed files with 5977 additions and 11 deletions

View File

@@ -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');
}
}

View File

@@ -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']);
}
}

View File

@@ -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']);
}
}

View File

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

View File

@@ -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'));
}
}

View File

@@ -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')));
}
}