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'Brien'); expect($result['name'])->toBe("O'Brien"); }); test('parseResponse decodes the legacy unterminated apostrophe entity', function (): void { $result = invokeDirectadminParseResponse($this->manager, 'name=O'Brien'); 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'); }); test('suspendAccount sends the suspension reason 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(str_contains($url, 'CMD_API_SHOW_USER_CONFIG') ? 'suspended=no' : ''); }); $manager = createDirectadminManager($httpClient); $account = (new Server_Account()) ->setUsername('example') ->setNote('Non-payment'); expect($manager->suspendAccount($account))->toBeTrue() ->and($requests)->toHaveCount(2); parse_str((string) parse_url($requests[1]['url'], PHP_URL_QUERY), $fields); expect($requests[1]['method'])->toBe('POST') ->and($requests[1]['url'])->toContain('CMD_API_SELECT_USERS') ->and($fields['reason'])->toBe('billing') ->and($fields['details'])->toBe('Non-payment'); }); test('suspendAccount maps a custom suspension note to other', 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(str_contains($url, 'CMD_API_SHOW_USER_CONFIG') ? 'suspended=no' : ''); }); $manager = createDirectadminManager($httpClient); $account = (new Server_Account()) ->setUsername('example') ->setNote('Terms of service violation'); expect($manager->suspendAccount($account))->toBeTrue(); parse_str((string) parse_url($requests[1]['url'], PHP_URL_QUERY), $fields); expect($fields['reason'])->toBe('other') ->and($fields['details'])->toBe('Terms of service violation'); }); test('suspendAccount omits an empty suspension reason and details', function (?string $reason): 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(str_contains($url, 'CMD_API_SHOW_USER_CONFIG') ? 'suspended=no' : ''); }); $manager = createDirectadminManager($httpClient); $account = (new Server_Account()) ->setUsername('example') ->setNote($reason); expect($manager->suspendAccount($account))->toBeTrue() ->and($requests)->toHaveCount(2); parse_str((string) parse_url($requests[1]['url'], PHP_URL_QUERY), $fields); expect($fields)->not->toHaveKey('reason') ->not->toHaveKey('details'); })->with([null, '']);