mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
340 lines
13 KiB
PHP
340 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Exceptions\RateLimitException;
|
|
use App\Services\HostingerService;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Tests\TestCase;
|
|
|
|
uses(TestCase::class);
|
|
|
|
beforeEach(function () {
|
|
Http::preventStrayRequests();
|
|
});
|
|
|
|
it('fetches Hostinger provisioning options', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/data-centers' => Http::response([
|
|
['id' => 19, 'name' => 'nl-ams', 'city' => 'Amsterdam', 'location' => 'nl'],
|
|
]),
|
|
'https://developers.hostinger.com/api/vps/v1/templates' => Http::response([
|
|
['id' => 1130, 'name' => 'Ubuntu 24.04 LTS'],
|
|
]),
|
|
'https://developers.hostinger.com/api/billing/v1/catalog?category=VPS' => Http::response([
|
|
[
|
|
'id' => 'hostingercom-vps-kvm2',
|
|
'name' => 'KVM 2',
|
|
'category' => 'VPS',
|
|
'prices' => [
|
|
[
|
|
'id' => 'hostingercom-vps-kvm2-usd-1m',
|
|
'currency' => 'USD',
|
|
'price' => 1799,
|
|
'first_period_price' => 899,
|
|
'period' => 1,
|
|
'period_unit' => 'month',
|
|
],
|
|
],
|
|
],
|
|
]),
|
|
]);
|
|
|
|
$service = new HostingerService('test-token');
|
|
|
|
expect($service->getDataCenters()[0]['city'])->toBe('Amsterdam')
|
|
->and($service->getTemplates()[0]['name'])->toBe('Ubuntu 24.04 LTS')
|
|
->and($service->getCatalogItems()[0]['prices'][0]['price'])->toBe(1799);
|
|
});
|
|
|
|
it('returns only KVM plans from the Hostinger VPS catalog', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/billing/v1/catalog?category=VPS' => Http::response([
|
|
['id' => 'hostingercom-vps-kvm1', 'name' => 'KVM 1', 'prices' => []],
|
|
['id' => 'hostingercom-vps-kvmminecraftalex', 'name' => 'Game Panel 1', 'prices' => []],
|
|
['id' => 'hostingercom-vps-kvm8', 'name' => 'KVM 8', 'prices' => []],
|
|
['id' => 'hostingercom-vps-kvmminecraftwolf', 'name' => 'Game Panel 8', 'prices' => []],
|
|
['name' => 'Missing id', 'prices' => []],
|
|
]),
|
|
]);
|
|
|
|
expect(collect((new HostingerService('test-token'))->getCatalogItems())->pluck('id')->all())
|
|
->toBe(['hostingercom-vps-kvm1', 'hostingercom-vps-kvm8']);
|
|
});
|
|
|
|
it('fetches and attaches Hostinger account SSH keys', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/public-keys' => Http::response([
|
|
'data' => [['id' => 42, 'name' => 'Operations', 'key' => 'ssh-ed25519 AAAA']],
|
|
]),
|
|
'https://developers.hostinger.com/api/vps/v1/public-keys/attach/17923' => Http::response([
|
|
'id' => 456,
|
|
'state' => 'running',
|
|
]),
|
|
]);
|
|
|
|
$service = new HostingerService('test-token');
|
|
|
|
expect($service->getPublicKeys()[0]['id'])->toBe(42)
|
|
->and($service->attachPublicKeys(17923, ['42'])['id'])->toBe(456);
|
|
|
|
Http::assertSent(fn ($request) => $request->url() === 'https://developers.hostinger.com/api/vps/v1/public-keys/attach/17923'
|
|
&& $request['ids'] === [42]);
|
|
});
|
|
|
|
it('fetches Hostinger post-install scripts', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/post-install-scripts' => Http::response([
|
|
'data' => [['id' => 73, 'name' => 'Bootstrap Coolify']],
|
|
]),
|
|
]);
|
|
|
|
expect((new HostingerService('test-token'))->getPostInstallScripts()[0]['id'])->toBe(73);
|
|
});
|
|
|
|
it('purchases a Hostinger virtual machine with its setup options', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
|
|
'order' => ['id' => 2957086, 'status' => 'completed'],
|
|
'virtual_machine' => [
|
|
'id' => 17923,
|
|
'hostname' => 'coolify-test.example.com',
|
|
'state' => 'creating',
|
|
'ipv4' => [['address' => '203.0.113.10']],
|
|
],
|
|
]),
|
|
]);
|
|
|
|
$virtualMachine = (new HostingerService('test-token'))->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm2-usd-1m',
|
|
'setup' => [
|
|
'data_center_id' => 19,
|
|
'template_id' => 1130,
|
|
'hostname' => 'coolify-test.example.com',
|
|
'enable_backups' => true,
|
|
'public_key' => [
|
|
'name' => 'Coolify',
|
|
'key' => 'ssh-ed25519 AAAA test@example.com',
|
|
],
|
|
],
|
|
]);
|
|
|
|
expect($virtualMachine['id'])->toBe(17923);
|
|
|
|
Http::assertSent(fn ($request) => $request->method() === 'POST'
|
|
&& $request->url() === 'https://developers.hostinger.com/api/vps/v1/virtual-machines'
|
|
&& $request->hasHeader('Authorization', 'Bearer test-token')
|
|
&& $request['item_id'] === 'hostingercom-vps-kvm2-usd-1m'
|
|
&& $request['setup']['data_center_id'] === 19
|
|
&& $request['setup']['template_id'] === 1130
|
|
&& $request['setup']['enable_backups'] === true
|
|
&& $request['setup']['public_key']['key'] === 'ssh-ed25519 AAAA test@example.com');
|
|
});
|
|
|
|
it('only sends a fully qualified hostname to Hostinger', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
|
|
'virtual_machine' => ['id' => 17923, 'state' => 'creating'],
|
|
]),
|
|
]);
|
|
|
|
$service = new HostingerService('test-token');
|
|
$service->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm1-usd-1m',
|
|
'setup' => ['data_center_id' => 19, 'template_id' => 1130, 'hostname' => 'coolify-server'],
|
|
]);
|
|
$service->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm1-usd-1m',
|
|
'setup' => ['data_center_id' => 19, 'template_id' => 1130, 'hostname' => 'coolify.example.com'],
|
|
]);
|
|
|
|
$sentHostnames = Http::recorded()->filter(fn ($record) => $record[0]->method() === 'POST')->values()->map(fn ($record) => $record[0]['setup']['hostname'] ?? null)->all();
|
|
|
|
expect($sentHostnames)->toBe([null, 'coolify.example.com']);
|
|
});
|
|
|
|
it('does not retry a failed Hostinger purchase', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => fn ($request) => $request->method() === 'GET'
|
|
? Http::response([])
|
|
: Http::response(['message' => 'Server error'], 500),
|
|
]);
|
|
|
|
expect(fn () => (new HostingerService('test-token'))->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm2-usd-1m',
|
|
'setup' => ['data_center_id' => 19, 'template_id' => 1130],
|
|
]))->toThrow(Exception::class, 'Hostinger API error: Server error');
|
|
|
|
Http::assertSentCount(3);
|
|
expect(Http::recorded()->filter(fn ($record) => $record[0]->method() === 'POST'))->toHaveCount(1);
|
|
});
|
|
|
|
it('sets up a charged Hostinger VPS when the purchase returns an error', function () {
|
|
$listRequests = 0;
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines/2008976/setup' => Http::response([
|
|
'id' => 2008976,
|
|
'state' => 'creating',
|
|
'ipv4' => [['address' => '72.61.180.163']],
|
|
]),
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => function ($request) use (&$listRequests) {
|
|
if ($request->method() === 'POST') {
|
|
return Http::response(['message' => '[VPS:2004] Wrong hostname FQDN format'], 422);
|
|
}
|
|
|
|
return Http::response($listRequests++ === 0
|
|
? [['id' => 100, 'state' => 'running']]
|
|
: [['id' => 100, 'state' => 'running'], ['id' => 2008976, 'state' => 'initial']]);
|
|
},
|
|
]);
|
|
|
|
$virtualMachine = (new HostingerService('test-token'))->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm1-usd-1m',
|
|
'setup' => ['data_center_id' => 19, 'template_id' => 1077, 'enable_backups' => false],
|
|
]);
|
|
|
|
expect($virtualMachine['id'])->toBe(2008976)
|
|
->and($virtualMachine['state'])->toBe('creating');
|
|
|
|
Http::assertSent(fn ($request) => $request->url() === 'https://developers.hostinger.com/api/vps/v1/virtual-machines/2008976/setup'
|
|
&& $request['data_center_id'] === 19
|
|
&& $request['template_id'] === 1077
|
|
&& $request['enable_backups'] === false);
|
|
});
|
|
|
|
it('returns a charged Hostinger VPS that still needs setup when the setup retry fails', function () {
|
|
$listRequests = 0;
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines/2008976/setup' => Http::response(['message' => 'Setup failed'], 422),
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => function ($request) use (&$listRequests) {
|
|
if ($request->method() === 'POST') {
|
|
return Http::response(['message' => 'Setup failed'], 422);
|
|
}
|
|
|
|
return Http::response($listRequests++ === 0 ? [] : [['id' => 2008976, 'state' => 'initial']]);
|
|
},
|
|
]);
|
|
|
|
$virtualMachine = (new HostingerService('test-token'))->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm1-usd-1m',
|
|
'setup' => ['data_center_id' => 19, 'template_id' => 1077],
|
|
]);
|
|
|
|
expect($virtualMachine)->toBe(['id' => 2008976, 'state' => 'initial']);
|
|
});
|
|
|
|
it('reports a Hostinger purchase whose payment is still processing', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
|
|
'id' => 2957086,
|
|
'subscription_id' => 'Azz353Uhl1xC54pR0',
|
|
'status' => 'payment_initiated',
|
|
'message' => 'Payment is being processed.',
|
|
], 202),
|
|
]);
|
|
Log::shouldReceive('warning')->once()->with('Hostinger VPS order did not return a virtual machine', [
|
|
'id' => 2957086,
|
|
'subscription_id' => 'Azz353Uhl1xC54pR0',
|
|
'status' => 'payment_initiated',
|
|
'message' => 'Payment is being processed.',
|
|
]);
|
|
|
|
expect(fn () => (new HostingerService('test-token'))->purchaseVirtualMachine([
|
|
'item_id' => 'hostingercom-vps-kvm2-usd-1m',
|
|
'setup' => ['data_center_id' => 19, 'template_id' => 1130],
|
|
]))->toThrow(Exception::class, 'Hostinger order 2957086 is waiting for payment. Finish the VPS setup in hPanel.');
|
|
});
|
|
|
|
it('extracts a Hostinger public IPv4 address before falling back to IPv6', function () {
|
|
$virtualMachine = [
|
|
'ipv4' => [['address' => '203.0.113.10']],
|
|
'ipv6' => [['address' => '2001:db8::10']],
|
|
];
|
|
|
|
$service = new HostingerService('test-token');
|
|
|
|
expect($service->getPublicIpAddress($virtualMachine))->toBe('203.0.113.10')
|
|
->and($service->getPublicIpAddress(['ipv4' => [], 'ipv6' => $virtualMachine['ipv6']]))->toBe('2001:db8::10');
|
|
});
|
|
|
|
it('waits for Hostinger to assign a public IP', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923' => Http::response([
|
|
'id' => 17923,
|
|
'state' => 'running',
|
|
'ipv4' => [['address' => '203.0.113.10']],
|
|
]),
|
|
]);
|
|
|
|
$service = new HostingerService('test-token');
|
|
$virtualMachine = $service->waitForPublicIp([
|
|
'id' => 17923,
|
|
'state' => 'creating',
|
|
'ipv4' => [],
|
|
], sleepMilliseconds: 0);
|
|
|
|
expect($service->getPublicIpAddress($virtualMachine))->toBe('203.0.113.10');
|
|
});
|
|
|
|
it('stops waiting for a Hostinger public IP after 10 attempts by default', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923' => Http::response([
|
|
'id' => 17923,
|
|
'state' => 'initial',
|
|
'ipv4' => [],
|
|
]),
|
|
]);
|
|
|
|
$service = new HostingerService('test-token');
|
|
$virtualMachine = $service->waitForPublicIp([
|
|
'id' => 17923,
|
|
'state' => 'initial',
|
|
'ipv4' => [],
|
|
], sleepMilliseconds: 0);
|
|
|
|
expect($service->getPublicIpAddress($virtualMachine))->toBeNull();
|
|
Http::assertSentCount(10);
|
|
});
|
|
|
|
it('finds a Hostinger virtual machine by public IP', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
|
|
[
|
|
'id' => 17923,
|
|
'ipv4' => [['address' => '203.0.113.10']],
|
|
'ipv6' => [['address' => '2001:db8::10']],
|
|
],
|
|
]),
|
|
]);
|
|
|
|
$service = new HostingerService('test-token');
|
|
|
|
expect($service->findVirtualMachineByIp('203.0.113.10')['id'])->toBe(17923)
|
|
->and($service->findVirtualMachineByIp('2001:db8::10')['id'])->toBe(17923)
|
|
->and($service->findVirtualMachineByIp('198.51.100.1'))->toBeNull();
|
|
});
|
|
|
|
it('starts a Hostinger virtual machine', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923/start' => Http::response([
|
|
'id' => 456,
|
|
'name' => 'start',
|
|
'state' => 'running',
|
|
]),
|
|
]);
|
|
|
|
$action = (new HostingerService('test-token'))->startVirtualMachine(17923);
|
|
|
|
expect($action['state'])->toBe('running');
|
|
});
|
|
|
|
it('raises the shared rate limit exception for Hostinger throttling', function () {
|
|
Http::fake([
|
|
'https://developers.hostinger.com/api/vps/v1/data-centers' => Http::response([
|
|
'message' => 'Too many requests.',
|
|
], 429, ['Retry-After' => '30']),
|
|
]);
|
|
|
|
expect(fn () => (new HostingerService('test-token'))->getDataCenters())
|
|
->toThrow(RateLimitException::class, 'Rate limit exceeded. Please try again later.');
|
|
});
|