10, 'sld' => 'example', 'tld' => '.com', 'client_id' => 7, 'service_hosting_server_id' => 2, 'service_hosting_hp_id' => 3, 'reseller' => 0, 'ip' => '192.0.2.10', 'username' => 'example', 'created_at' => '2026-07-19 10:00:00', 'updated_at' => '2026-07-19 10:01:00', ]; $dbMock = Mockery::mock('\Box_Database'); $dbMock->shouldReceive('getAll') ->once() ->with(Mockery::pattern('/FROM client_order/'), ['hosting', 10]) ->andReturn([['id' => 50, 'service_id' => 10]]); $dbMock->shouldNotReceive('findOne'); $dbMock->shouldNotReceive('dispense'); $orderService = Mockery::mock(OrderService::class); $orderService->shouldReceive('getBatchForApi') ->once() ->with([50], Mockery::type(Model_Admin::class)) ->andReturn([[ 'id' => 50, 'service_id' => 10, 'title' => 'Example hosting', 'client' => ['id' => 7, 'email' => 'client@example.com'], ]]); $di = container(); $di['db'] = $dbMock; $di['mod_service'] = $di->protect(moduleService(['order' => $orderService])); $service->setDi($di); $admin = new Model_Admin(); $admin->loadBean(new Tests\Helpers\DummyBean()); $result = $service->getAccountsBatchForApi([$account], $admin); expect($result[0]) ->toMatchArray([ 'id' => 10, 'server_id' => 2, 'plan_id' => 3, 'client' => ['id' => 7, 'email' => 'client@example.com'], 'order' => ['id' => 50, 'service_id' => 10, 'title' => 'Example hosting'], ]) ->and($result[0]['order'])->not->toHaveKey('client'); }); test('batch returns hosting accounts without orders', function (): void { $service = new Service(); $dbMock = Mockery::mock('\Box_Database'); $dbMock->shouldReceive('getAll')->once()->andReturn([]); $di = container(); $di['db'] = $dbMock; $service->setDi($di); $result = $service->getAccountsBatchForApi([[ 'id' => 10, 'sld' => 'example', 'tld' => '.com', 'client_id' => 7, 'service_hosting_server_id' => 2, 'service_hosting_hp_id' => 3, 'reseller' => 0, ]]); expect($result[0]['order'])->toBeNull() ->and($result[0])->not->toHaveKey('client'); }); test('validate order data', function (string $field, string $exceptionMessage, int $excCode): void { $service = new Service(); $data = [ 'server_id' => 1, 'hosting_plan_id' => 2, 'sld' => 'great', 'tld' => 'com', ]; unset($data[$field]); try { $service->validateOrderData($data); expect(true)->toBeFalse('Expected FOSSBilling\Exception was not thrown.'); } catch (FOSSBilling\Exception $e) { expect($e->getMessage())->toBe($exceptionMessage); expect($e->getCode())->toBe($excCode); } })->with([ ['server_id', 'Hosting product is not configured completely. Configure server for hosting product.', 701], ['hosting_plan_id', 'Hosting product is not configured completely. Configure hosting plan for hosting product.', 702], ['sld', 'Domain name is invalid.', 703], ['tld', 'Domain extension is invalid.', 704], ]); test('action create', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->client_id = 5; $confArr = [ 'server_id' => 1, 'hosting_plan_id' => 2, 'sld' => 'great', 'tld' => 'com', ]; $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getConfig')->atLeast()->once()->andReturn($confArr); $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId($confArr['server_id']); $hostingServerModel->setIp('1.1.1.1'); $hostingPlansModel = new ServiceHostingHp(); $hostingPlansModel->setId($confArr['hosting_plan_id']); $serverRepo = Mockery::mock(ServiceHostingServerRepository::class); $serverRepo->shouldReceive('find')->atLeast()->once()->andReturn($hostingServerModel); $serverRepo->shouldIgnoreMissing(); $hpRepo = Mockery::mock(ServiceHostingHpRepository::class); $hpRepo->shouldReceive('find')->atLeast()->once()->andReturn($hostingPlansModel); $hpRepo->shouldIgnoreMissing(); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('getRepository')->with(ServiceHostingServer::class)->andReturn($serverRepo); $emMock->shouldReceive('getRepository')->with(ServiceHostingHp::class)->andReturn($hpRepo); $emMock->shouldReceive('persist')->atLeast()->once(); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $service->setDi($di); $result = $service->action_create($orderModel); expect($result)->toBeInstanceOf(ServiceHosting::class); expect($result->getServiceHostingServerId())->toBe($confArr['server_id']); expect($result->getServiceHostingHpId())->toBe($confArr['hosting_plan_id']); expect($result->getSld())->toBe($confArr['sld']); expect($result->getTld())->toBe($confArr['tld']); }); test('action renew', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $hostingServiceModel = new ServiceHosting(); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturn($hostingServiceModel); $di = container(); $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $service->setDi($di); $result = $service->action_renew($orderModel); expect($result)->toBeTrue(); }); test('action renew order without active service', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->id = 1; $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturnNull(); $di = container(); $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $service->setDi($di); expect(fn (): bool => $service->action_renew($orderModel)) ->toThrow(FOSSBilling\Exception::class, sprintf('Order %d has no active service', $orderModel->id)); }); test('action suspend', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturn($model); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('suspendAccount')->once()->with(Mockery::on( fn (Server_Account $account): bool => $account->getNote() === 'Non-payment' )); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $serviceMock->setDi($di); $result = $serviceMock->action_suspend($orderModel, 'Non-payment'); expect($result)->toBeTrue(); }); test('action suspend order without active service', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->id = 1; $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturnNull(); $di = container(); $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $service->setDi($di); expect(fn (): bool => $service->action_suspend($orderModel)) ->toThrow(FOSSBilling\Exception::class, sprintf('Order %d has no active service', $orderModel->id)); }); test('action unsuspend', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturn($model); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('unsuspendAccount')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $serviceMock->setDi($di); $result = $serviceMock->action_unsuspend($orderModel); expect($result)->toBeTrue(); }); test('action unsuspend order without active service', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->id = 1; $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturnNull(); $di = container(); $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $service->setDi($di); expect(fn (): bool => $service->action_unsuspend($orderModel)) ->toThrow(FOSSBilling\Exception::class, sprintf('Order %d has no active service', $orderModel->id)); }); test('action cancel', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturn($model); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('cancelAccount')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $serviceMock->setDi($di); $result = $serviceMock->action_cancel($orderModel); expect($result)->toBeTrue(); }); test('action cancel order without active service', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->id = 1; $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturnNull(); $di = container(); $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $service->setDi($di); expect(fn (): bool => $service->action_cancel($orderModel)) ->toThrow(FOSSBilling\Exception::class, sprintf('Order %d has no active service', $orderModel->id)); }); test('action delete', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->status = 'active'; $model = new ServiceHosting(); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getOrderService')->atLeast()->once()->andReturn($model); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('remove')->atLeast()->once(); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serviceMock->shouldReceive('action_cancel')->atLeast()->once(); $serviceMock->setDi($di); $serviceMock->action_delete($orderModel); }); test('change account plan', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->status = Model_ClientOrder::STATUS_ACTIVE; $model = new ServiceHosting(); $modelHp = new ServiceHostingHp(); $modelHp->setId(2); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('changeAccountPackage')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $serviceMock->shouldReceive('getServerPackage')->atLeast()->once()->andReturn(new Server_Package()); $serviceMock->setDi($di); $result = $serviceMock->changeAccountPlan($orderModel, $model, $modelHp); expect($result)->toBeTrue(); }); test('change account username', function (): void { $service = new Service(); $data = [ 'username' => 'u123456', ]; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->status = Model_ClientOrder::STATUS_ACTIVE; $model = new ServiceHosting(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('changeAccountUsername')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $serviceMock->setDi($di); $result = $serviceMock->changeAccountUsername($orderModel, $model, $data); expect($result)->toBeTrue(); }); test('change account username missing username', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); $data = []; expect(fn (): bool => $service->changeAccountUsername($orderModel, $model, $data)) ->toThrow(FOSSBilling\Exception::class, 'Account username is missing or is invalid'); }); test('change account ip', function (): void { $service = new Service(); $data = [ 'ip' => '1.1.1.1', ]; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->status = Model_ClientOrder::STATUS_ACTIVE; $model = new ServiceHosting(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('changeAccountIp')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $serviceMock->setDi($di); $result = $serviceMock->changeAccountIp($orderModel, $model, $data); expect($result)->toBeTrue(); }); test('change account ip missing ip', function (): void { $service = new Service(); $data = []; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); expect(fn (): bool => $service->changeAccountIp($orderModel, $model, $data)) ->toThrow(FOSSBilling\Exception::class, 'Account IP address is missing or is invalid'); }); test('change account domain', function (): void { $service = new Service(); $data = [ 'tld' => 'com', 'sld' => 'testingSld', ]; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->status = Model_ClientOrder::STATUS_ACTIVE; $model = new ServiceHosting(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('changeAccountDomain')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $serviceMock->setDi($di); $result = $serviceMock->changeAccountDomain($orderModel, $model, $data); expect($result)->toBeTrue(); }); test('change account domain missing params', function (): void { $service = new Service(); $data = []; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); expect(fn (): bool => $service->changeAccountDomain($orderModel, $model, $data)) ->toThrow(FOSSBilling\Exception::class, 'Domain SLD or TLD is missing'); }); test('change account password', function (): void { $service = new Service(); $data = [ 'password' => 'topsecret', 'password_confirm' => 'topsecret', ]; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $orderModel->status = Model_ClientOrder::STATUS_ACTIVE; $model = new ServiceHosting(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('changeAccountPassword')->atLeast()->once(); $AMresultArray = [$serverManagerMock, new Server_Account()]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $serviceMock->setDi($di); $result = $serviceMock->changeAccountPassword($orderModel, $model, $data); expect($result)->toBeTrue(); }); test('change account password missing params', function (): void { $service = new Service(); $data = []; $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); expect(fn (): bool => $service->changeAccountPassword($orderModel, $model, $data)) ->toThrow(FOSSBilling\Exception::class, 'Account password is missing or is invalid'); }); test('sync', function (): void { $service = new Service(); $orderModel = new Model_ClientOrder(); $orderModel->loadBean(new Tests\Helpers\DummyBean()); $model = new ServiceHosting(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $accountObj = new Server_Account(); $accountObj->setUsername('testUser1'); $accountObj->setIp('1.1.1.1'); $accountObj2 = new Server_Account(); $accountObj2->setUsername('testUser2'); $accountObj2->setIp('2.2.2.2'); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('synchronizeAccount')->atLeast()->once()->andReturn($accountObj2); $AMresultArray = [$serverManagerMock, $accountObj]; $serviceMock->shouldReceive('_getAM')->atLeast()->once()->andReturn($AMresultArray); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $serviceMock->setDi($di); $result = $serviceMock->sync($orderModel, $model); expect($result)->toBeTrue(); }); test('to api array', function (): void { $service = new Service(); $model = new ServiceHosting(); $model->setServiceHostingServerId(1); $model->setServiceHostingHpId(2); $hostingServer = new ServiceHostingServer(); $hostingServer->setId(1); $hostingServer->setManager('Custom'); $hostingHp = new ServiceHostingHp(); $hostingHp->setId(2); $serverRepo = Mockery::mock(ServiceHostingServerRepository::class); $serverRepo->shouldReceive('find')->atLeast()->once()->andReturn($hostingServer); $serverRepo->shouldIgnoreMissing(); $hpRepo = Mockery::mock(ServiceHostingHpRepository::class); $hpRepo->shouldReceive('find')->atLeast()->once()->andReturn($hostingHp); $hpRepo->shouldIgnoreMissing(); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('getRepository')->with(ServiceHostingServer::class)->andReturn($serverRepo); $emMock->shouldReceive('getRepository')->with(ServiceHostingHp::class)->andReturn($hpRepo); $emMock->shouldIgnoreMissing(); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getServiceOrder')->atLeast()->once(); $serverManagerCustomMock = Mockery::mock('\Server_Manager_Custom')->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $di['server_manager'] = $di->protect(fn ($manager, $config) => $serverManagerCustomMock); $service->setDi($di); $result = $service->toApiArray($model, false, new Model_Admin()); expect($result)->toBeArray(); }); test('update', function (): void { $service = new Service(); $data = [ 'username' => 'testUser', 'ip' => '1.1.1.1', ]; $model = new ServiceHosting(); $model->setId(1); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $service->setDi($di); $result = $service->update($model, $data); expect($result)->toBeTrue(); }); test('get server managers', function (): void { $service = new Service(); $result = $service->getServerManagers(); expect($result)->toBeArray(); }); test('get server manager config', function (): void { $service = new Service(); $manager = 'Custom'; $expected = [ 'label' => 'Custom Server Manager', ]; $result = $service->getServerManagerConfig($manager); expect($result)->toBeArray(); expect($result)->toEqual($expected); }); test('get server pairs', function (): void { $service = new Service(); $expected = [ '1' => 'name', '2' => 'ding', ]; $queryResult = [ [ 'id' => 1, 'name' => 'name', ], [ 'id' => 2, 'name' => 'ding', ], ]; $dbMock = Mockery::mock('\Box_Database'); $dbMock->shouldReceive('getAll')->atLeast()->once()->andReturn($queryResult); $di = container(); $di['db'] = $dbMock; $service->setDi($di); $result = $service->getServerPairs(); expect($result)->toBeArray(); expect($result)->toEqual($expected); }); test('get server search query', function (): void { $service = new Service(); $result = $service->getServersSearchQuery([]); expect($result[0])->toBeString(); expect($result[1])->toBeArray(); expect($result[1])->toEqual([]); }); test('create server', function (): void { $service = new Service(); $newId = 1; $persistedServer = null; $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('persist')->atLeast()->once()->andReturnUsing(function (ServiceHostingServer $server) use ($newId, &$persistedServer): void { $server->setId($newId); $persistedServer = $server; }); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $service->setDi($di); $name = 'newSuperFastServer'; $ip = '1.1.1.1'; $manager = 'Custom'; $data = [ 'active' => 1, 'secure' => 0, ]; $result = $service->createServer($name, $ip, $manager, $data); expect($result)->toBeInt(); expect($result)->toBe($newId); expect($persistedServer)->toBeInstanceOf(ServiceHostingServer::class); expect($persistedServer->isActive())->toBeTrue(); expect($persistedServer->isSecure())->toBeFalse(); expect($persistedServer->getMaxAccounts())->toBeNull(); }); test('delete server', function (): void { $service = new Service(); $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId(1); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('remove')->atLeast()->once(); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $service->setDi($di); $result = $service->deleteServer($hostingServerModel); expect($result)->toBeTrue(); }); test('update server', function (): void { $service = new Service(); $data = [ 'name' => 'newName', 'ip' => '1.1.1.1', 'hostname' => 'unknownStar', 'active' => 1, 'max_accounts' => '12', 'status_url' => 'na', 'ns1' => 'ns1.testserver.eu', 'ns2' => 'ns2.testserver.eu', 'ns3' => 'ns3.testserver.eu', 'ns4' => 'ns4.testserver.eu', 'manager' => 'Custom', 'username' => 'testingJohn', 'password' => 'hardToGuess', 'accesshash' => 'secret', 'secure' => 0, ]; $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId(1); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $di['loggedin_admin'] = (object) ['id' => 7]; $service->setDi($di); $result = $service->updateServer($hostingServerModel, $data); expect($result)->toBeTrue(); expect($hostingServerModel->isActive())->toBeTrue(); expect($hostingServerModel->isSecure())->toBeFalse(); expect($hostingServerModel->getMaxAccounts())->toBe(12); }); test('get server manager', function (): void { $service = new Service(); $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setManager('Custom'); $serverManagerCustom = Mockery::mock('\Server_Manager_Custom')->shouldIgnoreMissing(); $di = container(); $di['server_manager'] = $di->protect(fn ($manager, $config) => $serverManagerCustom); $service->setDi($di); $result = $service->getServerManager($hostingServerModel); expect($result)->toBeInstanceOf('\Server_Manager_Custom'); }); test('get server manager manager not defined', function (): void { $service = new Service(); $hostingServerModel = new ServiceHostingServer(); expect(fn () => $service->getServerManager($hostingServerModel)) ->toThrow(FOSSBilling\Exception::class); }); test('get server manager server manager invalid', function (): void { $service = new Service(); $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setManager('Custom'); $di = container(); $di['server_manager'] = $di->protect(fn ($manager, $config): null => null); $service->setDi($di); expect(fn () => $service->getServerManager($hostingServerModel)) ->toThrow(FOSSBilling\Exception::class, 'Server manager Custom is invalid.'); }); test('test connection', function (): void { $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('testConnection')->atLeast()->once()->andReturn(true); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serviceMock->shouldReceive('getServerManager')->atLeast()->once()->andReturn($serverManagerMock); $hostingServerModel = new ServiceHostingServer(); $result = $serviceMock->testConnection($hostingServerModel); expect($result)->toBeBool(); expect($result)->toBeTrue(); }); test('get hp pairs', function (): void { $service = new Service(); $expected = [ '1' => 'free', '2' => 'paid', ]; $queryResult = [ [ 'id' => 1, 'name' => 'free', ], [ 'id' => 2, 'name' => 'paid', ], ]; $dbMock = Mockery::mock('\Box_Database'); $dbMock->shouldReceive('getAll')->atLeast()->once()->andReturn($queryResult); $di = container(); $di['db'] = $dbMock; $service->setDi($di); $result = $service->getHpPairs(); expect($result)->toBeArray(); expect($result)->toEqual($expected); }); test('get hp search query', function (): void { $service = new Service(); $result = $service->getHpSearchQuery([]); expect($result[0])->toBeString(); expect($result[1])->toBeArray(); expect($result[1])->toEqual([]); }); test('delete hp', function (): void { $service = new Service(); $model = new ServiceHostingHp(); $model->setId(1); $repo = Mockery::mock(ServiceHostingRepository::class); $repo->shouldReceive('findOneBy')->atLeast()->once()->andReturn(null); $repo->shouldIgnoreMissing(); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('getRepository')->with(ServiceHosting::class)->andReturn($repo); $emMock->shouldReceive('remove')->atLeast()->once(); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $service->setDi($di); $result = $service->deleteHp($model); expect($result)->toBeTrue(); }); test('to hosting hp api array', function (): void { $service = new Service(); $model = new ServiceHostingHp(); $result = $service->toHostingHpApiArray($model); expect($result)->toBeArray(); expect($model->getConfig())->toBeNull(); }); test('update hp', function (): void { $service = new Service(); $data = [ 'name' => 'firstPlan', 'bandwidth' => '100000', 'quota' => '1000', 'max_addon' => '0', 'max_ft' => '1', 'max_sql' => '2', 'max_pop' => '1', 'max_sub' => '2', 'max_park' => '1', ]; $model = new ServiceHostingHp(); $model->setId(1); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $service->setDi($di); $result = $service->updateHp($model, $data); expect($result)->toBeTrue(); }); test('create hp', function (array $data, string $expectedBandwidth, string $expectedMaxFtp): void { $service = new Service(); $newId = 1; $persistedHp = null; $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('persist')->atLeast()->once()->andReturnUsing(function (ServiceHostingHp $hp) use ($newId, &$persistedHp): void { $hp->setId($newId); $persistedHp = $hp; }); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $service->setDi($di); $result = $service->createHp('Free Plan', $data); expect($result)->toBeInt(); expect($result)->toBe($newId); expect($persistedHp)->toBeInstanceOf(ServiceHostingHp::class); expect($persistedHp->getBandwidth())->toBe($expectedBandwidth); expect($persistedHp->getMaxFtp())->toBe($expectedMaxFtp); })->with([ 'provided optional limits' => [ [ 'bandwidth' => '2048', 'quota' => '4096', 'max_addon' => '2', 'max_park' => '2', 'max_sub' => '2', 'max_pop' => '2', 'max_sql' => '2', 'max_ftp' => '2', ], '2048', '2', ], 'omitted optional limits' => [ [], '1048576', '1', ], ]); test('get server package', function (): void { $service = new Service(); $model = new ServiceHostingHp(); $model->setConfig('{}'); $di = container(); $service->setDi($di); $result = $service->getServerPackage($model); expect($result)->toBeInstanceOf('\Server_Package'); }); test('get server manager with log', function (): void { $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setManager('Custom'); $clientOrderModel = new Model_ClientOrder(); $clientOrderModel->loadBean(new Tests\Helpers\DummyBean()); $serverManagerMock = Mockery::mock('\Server_Manager_Custom')->shouldIgnoreMissing(); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serviceMock->shouldReceive('getServerManager')->atLeast()->once()->andReturn($serverManagerMock); $orderServiceMock = Mockery::mock(OrderService::class); $orderServiceMock->shouldReceive('getLogger')->atLeast()->once()->andReturn(new Box_Log()); $di = container(); $di['mod_service'] = $di->protect(fn (): Mockery\MockInterface => $orderServiceMock); $serviceMock->setDi($di); $result = $serviceMock->getServerManagerWithLog($hostingServerModel, $clientOrderModel); expect($result)->toBeInstanceOf('\Server_Manager_Custom'); }); test('get manager urls', function (): void { $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setManager('Custom'); $serverManagerMock = Mockery::mock('\Server_Manager_Custom'); $serverManagerMock->shouldReceive('getLoginUrl')->atLeast()->once()->andReturn('/login'); $serverManagerMock->shouldReceive('getResellerLoginUrl')->atLeast()->once()->andReturn('/admin/login'); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serviceMock->shouldReceive('getServerManager')->atLeast()->once()->andReturn($serverManagerMock); $result = $serviceMock->getManagerUrls($hostingServerModel); expect($result)->toBeArray(); expect($result[0])->toBeString(); expect($result[1])->toBeString(); }); test('get manager urls exception', function (): void { $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setManager('Custom'); $serviceMock = Mockery::mock(Service::class)->makePartial(); $serviceMock->shouldReceive('getServerManager')->atLeast()->once()->andThrow(new Exception('Controlled unit test exception')); $result = $serviceMock->getManagerUrls($hostingServerModel); expect($result)->toBeArray(); expect($result[0])->toBeFalse(); expect($result[1])->toBeFalse(); }); test('get free tlds free tlds are not set', function (): void { $service = new Service(); $di = container(); $tldModel = new Box\Mod\Servicedomain\Entity\Tld(); $tldModel->setActive(true); $tldModel->setAllowRegister(true); $tldRepo = Mockery::mock(Box\Mod\Servicedomain\Repository\TldRepository::class); $tldRepo->shouldReceive('findBy') ->atLeast()->once() ->with(['active' => true, 'allowRegister' => true], ['id' => 'ASC']) ->andReturn([$tldModel]); $tldRepo->shouldIgnoreMissing(); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('getRepository') ->atLeast()->once() ->with(Box\Mod\Servicedomain\Entity\Tld::class) ->andReturn($tldRepo); $emMock->shouldIgnoreMissing(); $serviceDomainServiceMock = Mockery::mock(Box\Mod\Servicedomain\Service::class); $serviceDomainServiceMock->shouldReceive('tldToApiArray')->atLeast()->once()->andReturn(['tld' => '.com']); $di['em'] = $emMock; $di['mod_service'] = $di->protect(function (string $name) use ($serviceDomainServiceMock) { if (strtolower($name) === 'servicedomain') { return $serviceDomainServiceMock; } return Mockery::mock()->shouldIgnoreMissing(); }); $service->setDi($di); $product = new Box\Mod\Product\Entity\Product(); $result = $service->getFreeTlds($product); expect($result)->toBeArray(); }); test('get free tlds', function (): void { $service = new Service(); $config = [ 'free_tlds' => ['.com'], ]; $di = container(); $service->setDi($di); $product = new Box\Mod\Product\Entity\Product(); $product->setConfig(json_encode($config)); $result = $service->getFreeTlds($product); expect($result)->toBeArray(); expect($result)->not->toBeEmpty(); }); test('get server manager secret fields', function (string $manager, array $expected): void { $service = new Service(); $di = container(); $service->setDi($di); $result = $service->getServerManagerSecretFields($manager); sort($expected); $sorted = $result; sort($sorted); expect($sorted)->toBe($expected); })->with([ 'WHM' => ['Whm', ['username', 'accesshash', 'password']], 'Hestia' => ['Hestia', ['username', 'accesshash', 'password']], 'CWP' => ['CWP', ['accesshash', 'password']], 'DirectAdmin' => ['Directadmin', ['username', 'password', 'accesshash']], 'Plesk' => ['Plesk', ['username', 'password', 'accesshash']], 'unknown manager' => ['DoesNotExist', ['password', 'accesshash']], ]); test('to hosting server api array masks secrets for an admin', function (): void { $service = new Service(); $identity = new Model_Admin(); $identity->loadBean(new Tests\Helpers\DummyBean()); $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId(1); $hostingServerModel->setName('Test'); $hostingServerModel->setHostname('host.example.com'); $hostingServerModel->setIp('127.0.0.1'); $hostingServerModel->setManager('Whm'); $hostingServerModel->setUsername('real-admin'); $hostingServerModel->setAccesshash('super-secret-hash'); $di = container(); $service->setDi($di); $result = $service->toHostingServerApiArray($hostingServerModel, true, $identity); expect($result['username'])->toBeNull(); expect($result['accesshash'])->toBeNull(); expect($result['password'])->toBeNull(); expect($result['username_set'])->toBeTrue(); expect($result['accesshash_set'])->toBeTrue(); expect($result['password_set'])->toBeFalse(); expect($result['secret_fields'])->toContain('username'); expect($result['secret_fields'])->toContain('accesshash'); }); test('to hosting server api array does not leak secrets to non-admin callers', function (): void { $service = new Service(); $identity = new Model_Client(); $identity->loadBean(new Tests\Helpers\DummyBean()); $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId(1); $hostingServerModel->setName('Test'); $hostingServerModel->setIp('127.0.0.1'); $hostingServerModel->setManager('Whm'); $hostingServerModel->setAccesshash('super-secret-hash'); $di = container(); $service->setDi($di); $result = $service->toHostingServerApiArray($hostingServerModel, true, $identity); expect($result)->not->toHaveKey('username'); expect($result)->not->toHaveKey('password'); expect($result)->not->toHaveKey('accesshash'); expect($result)->not->toHaveKey('secret_fields'); }); test('updateServer keeps the existing secret when the incoming value is blank', function (): void { $service = new Service(); $data = [ 'name' => 'Test', 'ip' => '127.0.0.1', 'manager' => 'Whm', 'username' => '', 'accesshash' => Service::CREDENTIAL_KEEP_SENTINEL, 'password' => ' ', ]; $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId(1); $hostingServerModel->setName('Test'); $hostingServerModel->setIp('127.0.0.1'); $hostingServerModel->setManager('Whm'); $hostingServerModel->setUsername('real-admin'); $hostingServerModel->setAccesshash('super-secret-hash'); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $di['loggedin_admin'] = (object) ['id' => 7]; $service->setDi($di); $result = $service->updateServer($hostingServerModel, $data); expect($result)->toBeTrue(); expect($hostingServerModel->getUsername())->toBe('real-admin'); expect($hostingServerModel->getAccesshash())->toBe('super-secret-hash'); expect($hostingServerModel->getPassword())->toBeNull(); }); test('updateServer replaces the stored secret when a new value is submitted', function (): void { $service = new Service(); $data = [ 'name' => 'Test', 'ip' => '127.0.0.1', 'manager' => 'Whm', 'username' => 'new-admin', 'accesshash' => 'new-hash', ]; $hostingServerModel = new ServiceHostingServer(); $hostingServerModel->setId(1); $hostingServerModel->setName('Test'); $hostingServerModel->setIp('127.0.0.1'); $hostingServerModel->setManager('Whm'); $hostingServerModel->setUsername('real-admin'); $hostingServerModel->setAccesshash('old-hash'); $emMock = Mockery::mock(EntityManagerInterface::class); $emMock->shouldReceive('flush')->atLeast()->once(); $emMock->shouldIgnoreMissing(); $di = container(); $di['em'] = $emMock; $di['logger'] = new Box_Log(); $di['loggedin_admin'] = (object) ['id' => 7]; $service->setDi($di); $result = $service->updateServer($hostingServerModel, $data); expect($result)->toBeTrue(); expect($hostingServerModel->getUsername())->toBe('new-admin'); expect($hostingServerModel->getAccesshash())->toBe('new-hash'); }); test('_performOnService rejects expired active order', function (): void { $service = new Service(); $reflection = new ReflectionMethod($service, '_performOnService'); $expiredOrder = new Model_ClientOrder(); $expiredOrder->loadBean(new Tests\Helpers\DummyBean()); $expiredOrder->status = Model_ClientOrder::STATUS_ACTIVE; $expiredOrder->expires_at = date('Y-m-d H:i:s', time() - 3600); expect($reflection->invoke($service, $expiredOrder))->toBeFalse(); }); test('_performOnService rejects suspended order', function (): void { $service = new Service(); $reflection = new ReflectionMethod($service, '_performOnService'); $suspendedOrder = new Model_ClientOrder(); $suspendedOrder->loadBean(new Tests\Helpers\DummyBean()); $suspendedOrder->status = Model_ClientOrder::STATUS_SUSPENDED; expect($reflection->invoke($service, $suspendedOrder))->toBeFalse(); }); test('_performOnService accepts active order with future expires_at', function (): void { $service = new Service(); $reflection = new ReflectionMethod($service, '_performOnService'); $activeOrder = new Model_ClientOrder(); $activeOrder->loadBean(new Tests\Helpers\DummyBean()); $activeOrder->status = Model_ClientOrder::STATUS_ACTIVE; $activeOrder->expires_at = date('Y-m-d H:i:s', time() + 86400); expect($reflection->invoke($service, $activeOrder))->toBeTrue(); }); test('_performOnService accepts one-time active order with null expires_at', function (): void { $service = new Service(); $reflection = new ReflectionMethod($service, '_performOnService'); $oneTimeOrder = new Model_ClientOrder(); $oneTimeOrder->loadBean(new Tests\Helpers\DummyBean()); $oneTimeOrder->status = Model_ClientOrder::STATUS_ACTIVE; $oneTimeOrder->expires_at = null; expect($reflection->invoke($service, $oneTimeOrder))->toBeTrue(); });