Files
FOSSBilling/tests/Unit/Server/Manager/DirectadminTest.php
T
Adam Daley c3fbd5f835 Fix DirectAdmin account updates silently ignoring custom package values
Hand-extracted from b364588f3 (#4166), same rationale as the Plesk fix
in 3848e6258 -- this piece is unrelated to everything else that huge
commit bundled together and stands on its own.

Two bugs in Server_Manager_Directadmin, both against the same
Server_Package::__call() silent-failure trap (undefined method calls
error_log and return '' rather than fail loudly):

- modifyAccount() built its own separate $fields array instead of
  reusing getCustomPackageFields() (the array createAccount() already
  builds correctly). It called getHasAnonymousFtp(), getHasCatchAll(),
  getHasCgi(), getHasCron(), getHasPhp(), getHasSpamFilter(),
  getHasShell(), getHasSsl(), getMaxEmailForwarders(),
  getMaxEmailLists(), and getMaxEmailAutoresponders() -- none of which
  exist on Server_Package. Every DirectAdmin account update through
  FOSSBilling silently reset aftp/catchall/cgi/cron/php/spam/ssh/ssl to
  OFF and email quotas to 0/empty, regardless of the hosting plan's
  actual configuration.
- getCustomPackageFields() itself (used by createAccount(), and now
  reused by modifyAccount()) had already been fixed for most fields
  but still called getMaxEmailForwarders()/getMaxEmailLists()/
  getMaxEmailAutoresponders() for nemailf/nemailml/nemailr, hitting
  the same silent-empty trap for those three on account creation too.

modifyAccount() now calls getCustomPackageFields($package) and layers
its own dnscontrol/sysinfo/action/ns1/ns2/user fields on top, instead
of maintaining a second, drifted-out-of-sync copy. nemailf/nemailml/
nemailr now read getCustomValue() like every other field in that
method. Introduces getCustomPackageFlag(), matching the equivalent
helper in the Plesk fix: treats a missing custom value as $default
(so already-covered flags like dnscontrol/sysinfo can default to 'ON'
without every other flag having to), and runs a present value through
FOSSBilling\Tools::normalizeBoolean() so a stored 'false'/'0' string
isn't truthy under a plain PHP ?: check.

createDirectadminManager() (an HTTP-client-injecting test double) and
the two tests exercising modifyAccount() are ported from the same
upstream commit; that helper's own origin (adcd85f52, "Improve overdue
order suspension handling") was declined earlier in this branch's
triage as feature-shaped, so it's recreated here rather than pulled in
via that commit.

composer test: 2471 passed. composer phpstan: no errors.
2026-08-16 20:05:57 +01:00

136 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\HttpClientInterface;
function invokeDirectadminParseResponse(Server_Manager_Directadmin $manager, string $data): array
{
$reflection = new ReflectionClass($manager);
$method = $reflection->getMethod('parseResponse');
return $method->invokeArgs($manager, [$data]);
}
function createDirectadminManager(HttpClientInterface $httpClient): Server_Manager_Directadmin
{
return new class(['host' => 'directadmin.example.com', 'username' => 'admin', 'password' => 'secret'], $httpClient) extends Server_Manager_Directadmin {
public function __construct(array $options, private readonly HttpClientInterface $httpClient)
{
parent::__construct($options);
}
public function getHttpClient(): HttpClientInterface
{
return $this->httpClient;
}
};
}
beforeEach(function (): void {
$this->manager = new Server_Manager_Directadmin([
'host' => 'directadmin.example.com',
'username' => 'admin',
'password' => 'secret',
]);
});
test('parseResponse decodes the fully-terminated apostrophe entity without a trailing semicolon', function (): void {
$result = invokeDirectadminParseResponse($this->manager, 'name=O&#39;Brien');
expect($result['name'])->toBe("O'Brien");
});
test('parseResponse decodes the legacy unterminated apostrophe entity', function (): void {
$result = invokeDirectadminParseResponse($this->manager, 'name=O&#39Brien');
expect($result['name'])->toBe("O'Brien");
});
test('modifyAccount sends custom package values to DirectAdmin', function (): void {
$requests = [];
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) use (&$requests): MockResponse {
$requests[] = ['method' => $method, 'url' => $url, 'options' => $options];
return new MockResponse('');
});
$manager = createDirectadminManager($httpClient);
$package = (new Server_Package())
->setBandwidth('1024')
->setQuota('2048')
->setMaxDomains('3')
->setMaxSubdomains('4')
->setMaxParkedDomains('5')
->setMaxFtp('6')
->setMaxSql('7')
->setMaxPop('8')
->setCustomValues([
'aftp' => '1',
'catchall' => 'false',
'cgi' => 'yes',
'cron' => 'true',
'nemailf' => '5',
'nemailml' => 'unlimited',
'nemailr' => '7',
'php' => 'on',
'spam' => 'false',
'ssh' => '1',
'ssl' => 'yes',
]);
$account = (new Server_Account())
->setUsername('example')
->setNs1('ns1.example.com')
->setNs2('ns2.example.com')
->setPackage($package);
expect($manager->modifyAccount($account))->toBeTrue()
->and($requests)->toHaveCount(1);
parse_str((string) parse_url($requests[0]['url'], PHP_URL_QUERY), $fields);
expect($fields)->toMatchArray([
'action' => 'customize',
'aftp' => 'ON',
'catchall' => 'OFF',
'cgi' => 'ON',
'cron' => 'ON',
'dnscontrol' => 'ON',
'nemailf' => '5',
'nemailml' => 'unlimited',
'nemailr' => '7',
'php' => 'ON',
'spam' => 'OFF',
'ssh' => 'ON',
'ssl' => 'ON',
'sysinfo' => 'ON',
'unemailml' => 'ON',
'user' => 'example',
]);
});
test('modifyAccount honors explicit false DNS and system information permissions', function (): void {
$requests = [];
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) use (&$requests): MockResponse {
$requests[] = ['method' => $method, 'url' => $url, 'options' => $options];
return new MockResponse('');
});
$manager = createDirectadminManager($httpClient);
$package = (new Server_Package())->setCustomValues([
'dnscontrol' => 'false',
'sysinfo' => '0',
]);
$account = (new Server_Account())
->setUsername('example')
->setPackage($package);
expect($manager->modifyAccount($account))->toBeTrue();
parse_str((string) parse_url($requests[0]['url'], PHP_URL_QUERY), $fields);
expect($fields['dnscontrol'])->toBe('OFF')
->and($fields['sysinfo'])->toBe('OFF');
});