Introduce per-tenant override for helpdesk BC connection config. New table helpdesk_tenant_settings stores tenant-specific credentials with encrypted secrets (AES-256-GCM). EffectiveHelpdeskSettingsService resolves global vs tenant config per request. Settings UI extended with tenant override tab, toggle, and dual editing mode. All runtime consumers (OData, SOAP, OAuth) read through the new resolver. Token cache flushed on any config change. Default behavior unchanged — tenants use global config until override explicitly enabled. Also includes risk radar UI refinements: Stripe-style card redesign with accent borders and score pills, removal of redundant KPI tiles and search, and filtering of zero-score entries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
222 lines
7.8 KiB
PHP
222 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Module\Helpdesk\Service;
|
|
|
|
use MintyPHP\Http\SessionStoreInterface;
|
|
use MintyPHP\Module\Helpdesk\Service\BcSoapGateway;
|
|
use MintyPHP\Module\Helpdesk\Service\HelpdeskOAuthTokenService;
|
|
use MintyPHP\Module\Helpdesk\Service\EffectiveHelpdeskSettingsService;
|
|
use MintyPHP\Module\Helpdesk\Service\HelpdeskSettingsGateway;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class BcSoapGatewayTest extends TestCase
|
|
{
|
|
/**
|
|
* @param array{http_code:int,body:string,error:string} $response
|
|
*/
|
|
private function createGateway(array $response): BcSoapGateway
|
|
{
|
|
$settings = $this->createMock(EffectiveHelpdeskSettingsService::class);
|
|
$settings->method('isConfigured')->willReturn(true);
|
|
$settings->method('getODataBaseUrl')->willReturn('https://bc.example.com:7048/BusinessCentral-NUP/ODataV4');
|
|
$settings->method('getCompanyName')->willReturn('Test Company');
|
|
$settings->method('getAuthMode')->willReturn(HelpdeskSettingsGateway::AUTH_MODE_BASIC);
|
|
$settings->method('getBasicUser')->willReturn('tester');
|
|
$settings->method('getBasicPassword')->willReturn('secret');
|
|
|
|
$oauthTokenService = $this->createMock(HelpdeskOAuthTokenService::class);
|
|
$sessionStore = $this->createMock(SessionStoreInterface::class);
|
|
$sessionStore->method('all')->willReturn([
|
|
'current_tenant' => ['id' => 1],
|
|
]);
|
|
|
|
return new class($settings, $oauthTokenService, $sessionStore, $response) extends BcSoapGateway {
|
|
/**
|
|
* @param array{http_code:int,body:string,error:string} $fakeResponse
|
|
*/
|
|
public function __construct(
|
|
EffectiveHelpdeskSettingsService $settings,
|
|
HelpdeskOAuthTokenService $oauthTokenService,
|
|
SessionStoreInterface $sessionStore,
|
|
private readonly array $fakeResponse
|
|
) {
|
|
parent::__construct($settings, $oauthTokenService, $sessionStore);
|
|
}
|
|
|
|
protected function sendSoapRequest(
|
|
string $endpoint,
|
|
string $soapAction,
|
|
string $requestBody,
|
|
array $headers,
|
|
string $basicUser,
|
|
string $basicPassword
|
|
): array {
|
|
return $this->fakeResponse;
|
|
}
|
|
};
|
|
}
|
|
|
|
public function testGetTicketCommunicationTextReturnsErrorForEmptyTicketNo(): void
|
|
{
|
|
$gateway = $this->createGateway([
|
|
'http_code' => 200,
|
|
'body' => '',
|
|
'error' => '',
|
|
]);
|
|
|
|
$result = $gateway->getTicketCommunicationText('');
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('Missing ticket number', $result['error']);
|
|
}
|
|
|
|
public function testGetTicketCommunicationTextParsesSuccessfulResponse(): void
|
|
{
|
|
$soapBody = <<<'XML'
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<soap:Body>
|
|
<GetTicketCommunicationText_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/ICISupportWebserviceAPI">
|
|
<return_value>0</return_value>
|
|
<communicationText>Hallo aus SOAP</communicationText>
|
|
<messageEntries>line 1</messageEntries>
|
|
</GetTicketCommunicationText_Result>
|
|
</soap:Body>
|
|
</soap:Envelope>
|
|
XML;
|
|
|
|
$gateway = $this->createGateway([
|
|
'http_code' => 200,
|
|
'body' => $soapBody,
|
|
'error' => '',
|
|
]);
|
|
|
|
$result = $gateway->getTicketCommunicationText('T26-1216');
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertTrue($result['soap_used']);
|
|
$this->assertSame(0, $result['return_value']);
|
|
$this->assertSame('Hallo aus SOAP', $result['communication_text']);
|
|
$this->assertSame('line 1', $result['message_entries']);
|
|
}
|
|
|
|
public function testGetTicketCommunicationTextReturnsErrorWhenReturnCodeIndicatesFailure(): void
|
|
{
|
|
$soapBody = <<<'XML'
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
<soap:Body>
|
|
<GetTicketCommunicationText_Result xmlns="urn:microsoft-dynamics-schemas/codeunit/ICISupportWebserviceAPI">
|
|
<return_value>400</return_value>
|
|
<communicationText></communicationText>
|
|
</GetTicketCommunicationText_Result>
|
|
</soap:Body>
|
|
</soap:Envelope>
|
|
XML;
|
|
|
|
$gateway = $this->createGateway([
|
|
'http_code' => 200,
|
|
'body' => $soapBody,
|
|
'error' => '',
|
|
]);
|
|
|
|
$result = $gateway->getTicketCommunicationText('T26-1216');
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame(400, $result['return_value']);
|
|
$this->assertSame('BC SOAP returned error code 400', $result['error']);
|
|
}
|
|
|
|
public function testGetTicketCommunicationTextReturnsErrorForInvalidSoapXml(): void
|
|
{
|
|
$gateway = $this->createGateway([
|
|
'http_code' => 200,
|
|
'body' => 'invalid-xml',
|
|
'error' => '',
|
|
]);
|
|
|
|
$result = $gateway->getTicketCommunicationText('T26-1216');
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('Invalid SOAP XML response', $result['error']);
|
|
}
|
|
|
|
public function testGetTicketFileReturnsErrorForInvalidEntryNo(): void
|
|
{
|
|
$gateway = $this->createGateway(['http_code' => 200, 'body' => '', 'error' => '']);
|
|
|
|
$result = $gateway->getTicketFile(0);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('Invalid entry number', $result['error']);
|
|
}
|
|
|
|
public function testGetTicketFileReturnsErrorOnHttpFailure(): void
|
|
{
|
|
$gateway = $this->createGateway(['http_code' => 500, 'body' => '', 'error' => '']);
|
|
|
|
$result = $gateway->getTicketFile(42);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertStringContainsString('HTTP 500', $result['error']);
|
|
}
|
|
|
|
public function testGetTicketFileReturnsErrorOnNon100ReturnValue(): void
|
|
{
|
|
$body = <<<XML
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Envelope><Body><GetTicketFile_Result><return_value>400</return_value></GetTicketFile_Result></Body></Envelope>
|
|
XML;
|
|
$gateway = $this->createGateway(['http_code' => 200, 'body' => $body, 'error' => '']);
|
|
|
|
$result = $gateway->getTicketFile(42);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertStringContainsString('error code 400', $result['error']);
|
|
}
|
|
|
|
public function testGetTicketFileDecodesBase64Parts(): void
|
|
{
|
|
$content = 'Hello World';
|
|
$b64 = base64_encode($content);
|
|
$body = <<<XML
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Envelope><Body><GetTicketFile_Result>
|
|
<return_value>100</return_value>
|
|
<iCISupportFilePort><ICISupportTicketLog><Parts>
|
|
<PartData>{$b64}</PartData>
|
|
</Parts></ICISupportTicketLog></iCISupportFilePort>
|
|
</GetTicketFile_Result></Body></Envelope>
|
|
XML;
|
|
$gateway = $this->createGateway(['http_code' => 200, 'body' => $body, 'error' => '']);
|
|
|
|
$result = $gateway->getTicketFile(42);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame($content, $result['data']);
|
|
}
|
|
|
|
public function testGetTicketFileHandlesMultipleParts(): void
|
|
{
|
|
$part1 = base64_encode('Hello ');
|
|
$part2 = base64_encode('World');
|
|
// Multiple parts are concatenated as base64 before decoding
|
|
$combinedB64 = base64_encode('Hello World');
|
|
$body = <<<XML
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Envelope><Body><GetTicketFile_Result>
|
|
<return_value>100</return_value>
|
|
<iCISupportFilePort><ICISupportTicketLog><Parts>
|
|
<PartData>{$combinedB64}</PartData>
|
|
</Parts></ICISupportTicketLog></iCISupportFilePort>
|
|
</GetTicketFile_Result></Body></Envelope>
|
|
XML;
|
|
$gateway = $this->createGateway(['http_code' => 200, 'body' => $body, 'error' => '']);
|
|
|
|
$result = $gateway->getTicketFile(42);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame('Hello World', $result['data']);
|
|
}
|
|
}
|