Files
FOSSBilling/src/modules/Servicehosting/tests/Unit/ServiceTest.php
T
Adam Daley adcd85f52b Improve overdue order suspension handling (#4072)
* Improve account suspension workflow

* Address suspension workflow review feedback

* Refine update patcher tests
2026-07-28 19:19:44 +01:00

1362 lines
46 KiB
PHP

<?php
/**
* Copyright 2022-2026 FOSSBilling
* SPDX-License-Identifier: Apache-2.0.
*
* @copyright FOSSBilling (https://www.fossbilling.org)
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
*/
declare(strict_types=1);
use Box\Mod\Order\Service as OrderService;
use Box\Mod\Servicehosting\Entity\ServiceHosting;
use Box\Mod\Servicehosting\Entity\ServiceHostingHp;
use Box\Mod\Servicehosting\Entity\ServiceHostingServer;
use Box\Mod\Servicehosting\Repository\ServiceHostingHpRepository;
use Box\Mod\Servicehosting\Repository\ServiceHostingRepository;
use Box\Mod\Servicehosting\Repository\ServiceHostingServerRepository;
use Box\Mod\Servicehosting\Service;
use Doctrine\ORM\EntityManagerInterface;
use function Tests\Helpers\container;
use function Tests\Helpers\moduleService;
afterEach(function (): void {
Mockery::close();
});
test('batch enriches hosting accounts with orders and clients', function (): void {
$service = new Service();
$account = [
'id' => 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();
});