Files
coolify/tests/Feature/HostingerServerCreationTest.php
T

500 lines
22 KiB
PHP

<?php
use App\Actions\Server\DeleteServer;
use App\Jobs\ValidateNewCloudServerJob;
use App\Livewire\Server\Create;
use App\Livewire\Server\CreatePage;
use App\Livewire\Server\Delete;
use App\Livewire\Server\New\ByHostinger;
use App\Livewire\Server\Show;
use App\Livewire\Server\ValidateAndInstall;
use App\Models\CloudProviderToken;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Lorisleiva\Actions\Decorators\JobDecorator;
uses(RefreshDatabase::class);
beforeEach(function () {
Bus::fake([ValidateNewCloudServerJob::class]);
Http::preventStrayRequests();
config([
'cache.default' => 'array',
'session.driver' => 'array',
]);
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create([
'id' => 0,
]));
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
$this->token = CloudProviderToken::factory()->create([
'team_id' => $this->team->id,
'provider' => 'hostinger',
'token' => 'hostinger-token',
'name' => 'Production Hostinger',
]);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
});
it('offers Hostinger as a server provider', function () {
Livewire::test(Create::class)
->assertSee('Hostinger')
->assertSee(route('server.create.type', ['type' => 'hostinger']), false);
Livewire::test(CreatePage::class, ['type' => 'hostinger'])
->assertSet('title', 'Hostinger')
->assertSet('tokenProvider', 'hostinger');
});
it('uses the current cloud provider UI and Hostinger affiliate link', function () {
Livewire::test(ByHostinger::class)
->assertSee('Hostinger account')
->assertSee('https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io', false)
->assertSee("through Coolify's affiliate link.", false);
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->set('loading_data', false)
->assertSee('Hostinger server')
->assertSee('Advanced options')
->assertSee('Buy and create');
});
it('leaves paid Hostinger backups off by default', function () {
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->assertSet('enable_backups', false)
->set('loading_data', false)
->assertSee('Enable weekly Hostinger backups (extra cost)')
->assertSee('Hostinger charges extra for backups. Check the price in hPanel before you enable them.');
});
it('selects Hostinger account SSH keys with the same dropdown as Hetzner', function () {
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->set('loading_data', false)
->set('hostinger_public_keys', [['id' => 42, 'name' => '<b>Operations</b>']])
->assertSee('Extra SSH keys')
->assertSee('Existing keys from the Hostinger account.')
->assertSee('Search SSH keys')
->assertSee('Operations')
->assertSeeHtml('x-text="current">Select SSH keys</span>')
->assertDontSeeHtml('<b>Operations</b>')
->assertDontSeeHtml('id="hostinger-public-key-42-');
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->set('loading_data', false)
->set('hostinger_public_keys', [])
->assertSee('No account keys found');
});
it('purchases a Hostinger VPS and creates the linked Coolify server', 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*' => 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',
],
],
],
]),
'https://developers.hostinger.com/api/vps/v1/public-keys' => Http::response(['data' => []]),
'https://developers.hostinger.com/api/vps/v1/post-install-scripts' => Http::response(['data' => []]),
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
'order' => ['id' => 2957086, 'status' => 'completed'],
'virtual_machine' => [
'id' => 17923,
'hostname' => 'coolify-hostinger.example.com',
'state' => 'creating',
'ipv4' => [['address' => '203.0.113.10']],
],
]),
]);
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->set('server_name', 'coolify-hostinger.example.com')
->set('selected_data_center_id', 19)
->set('selected_template_id', 1130)
->set('selected_price_id', 'hostingercom-vps-kvm2-usd-1m')
->set('private_key_id', $this->privateKey->id)
->set('enable_backups', true)
->call('submit')
->assertHasNoErrors();
$server = Server::query()->where('hostinger_virtual_machine_id', 17923)->firstOrFail();
expect($server->name)->toBe('coolify-hostinger.example.com')
->and($server->ip)->toBe('203.0.113.10')
->and($server->hostinger_virtual_machine_status)->toBe('creating')
->and($server->cloud_provider_token_id)->toBe($this->token->id);
Http::assertSent(fn ($request) => $request->method() === 'POST'
&& $request->url() === 'https://developers.hostinger.com/api/vps/v1/virtual-machines'
&& $request['item_id'] === 'hostingercom-vps-kvm2-usd-1m'
&& $request['setup']['hostname'] === 'coolify-hostinger.example.com'
&& $request['setup']['public_key']['key'] === $this->privateKey->getPublicKey());
});
it('keeps a purchased Hostinger VPS linked while its public IP is pending', function () {
Http::fake([
'https://developers.hostinger.com/api/vps/v1/data-centers' => Http::response([['id' => 19, 'name' => 'nl-ams', 'city' => null]]),
'https://developers.hostinger.com/api/vps/v1/templates' => Http::response([['id' => 1130, 'name' => 'Ubuntu']]),
'https://developers.hostinger.com/api/billing/v1/catalog*' => Http::response([
['id' => 'hostingercom-vps-kvm2', 'name' => 'KVM 2', 'prices' => [['id' => 'kvm2-monthly']]],
]),
'https://developers.hostinger.com/api/vps/v1/public-keys' => Http::response(['data' => []]),
'https://developers.hostinger.com/api/vps/v1/post-install-scripts' => Http::response(['data' => []]),
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
'virtual_machine' => ['id' => 17923, 'state' => 'creating', 'ipv4' => []],
]),
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923' => Http::response([
'message' => 'Still provisioning',
], 503),
]);
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->set('server_name', 'pending-hostinger.example.com')
->set('selected_data_center_id', 19)
->set('selected_template_id', 1130)
->set('selected_price_id', 'kvm2-monthly')
->set('private_key_id', $this->privateKey->id)
->call('submit')
->assertHasNoErrors();
expect(Server::query()->where('hostinger_virtual_machine_id', 17923)->firstOrFail()->ip)
->toBe(Server::PLACEHOLDER_IP);
});
it('loads the Hostinger form when optional provider lists are not authorized', function () {
Http::fake([
'https://developers.hostinger.com/api/vps/v1/data-centers' => Http::response([['id' => 19, 'name' => 'nl-ams', 'city' => 'Amsterdam']]),
'https://developers.hostinger.com/api/vps/v1/templates' => Http::response([['id' => 1130, 'name' => 'Ubuntu']]),
'https://developers.hostinger.com/api/billing/v1/catalog*' => Http::response([
['id' => 'hostingercom-vps-kvm2', 'name' => 'KVM 2', 'prices' => [['id' => 'kvm2-monthly']]],
]),
'https://developers.hostinger.com/api/vps/v1/public-keys' => Http::response(['message' => '[VPS:2000] Unauthorized'], 403),
'https://developers.hostinger.com/api/vps/v1/post-install-scripts' => Http::response(['message' => '[VPS:2000] Unauthorized'], 403),
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
'virtual_machine' => ['id' => 17923, 'state' => 'creating', 'ipv4' => [['address' => '203.0.113.10']]],
]),
]);
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->assertSet('provider_data_error', null)
->assertSet('hostinger_public_keys', [])
->assertSet('post_install_scripts', [])
->assertSee('Buy and create')
->set('server_name', 'optional-lists.example.com')
->set('selected_data_center_id', 19)
->set('selected_template_id', 1130)
->set('selected_price_id', 'kvm2-monthly')
->set('private_key_id', $this->privateKey->id)
->call('submit')
->assertHasNoErrors();
expect(Server::query()->where('hostinger_virtual_machine_id', 17923)->exists())->toBeTrue();
});
it('revalidates the selected Hostinger price before making a purchase', function () {
Http::fake([
'https://developers.hostinger.com/api/vps/v1/data-centers' => Http::response([
['id' => 19, 'name' => 'nl-ams', 'city' => 'Amsterdam'],
]),
'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*' => Http::response([
[
'id' => 'hostingercom-vps-kvm2',
'name' => 'KVM 2',
'prices' => [['id' => 'hostingercom-vps-kvm2-usd-1m']],
],
]),
'https://developers.hostinger.com/api/vps/v1/public-keys' => Http::response(['data' => []]),
'https://developers.hostinger.com/api/vps/v1/post-install-scripts' => Http::response(['data' => []]),
]);
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->set('server_name', 'coolify-hostinger.example.com')
->set('selected_data_center_id', 19)
->set('selected_template_id', 1130)
->set('selected_price_id', 'tampered-price-id')
->set('private_key_id', $this->privateKey->id)
->call('submit')
->assertHasErrors('selected_price_id');
Http::assertNotSent(fn ($request) => $request->method() === 'POST');
});
function fakeHostingerProvisioningOptions(array $overrides = []): void
{
$price = fn (string $plan, int $period, string $unit, string $suffix) => [
'id' => "hostingercom-vps-{$plan}-usd-{$suffix}",
'currency' => 'USD',
'price' => 1000,
'first_period_price' => 500,
'period' => $period,
'period_unit' => $unit,
];
$prices = fn (string $plan) => [
$price($plan, 2, 'year', '2y'),
$price($plan, 1, 'month', '1m'),
$price($plan, 1, 'year', '1y'),
];
Http::fake(array_merge([
'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' => 1121, 'name' => 'AlmaLinux 9 with CyberPanel'],
['id' => 1130, 'name' => 'Ubuntu 24.04 LTS'],
['id' => 1077, 'name' => 'Debian 13'],
['id' => 1150, 'name' => 'Ubuntu 24.04 with Coolify'],
]),
'https://developers.hostinger.com/api/billing/v1/catalog*' => Http::response([
['id' => 'hostingercom-vps-kvm8', 'name' => 'KVM 8', 'metadata' => ['cpus' => 8], 'prices' => $prices('kvm8')],
['id' => 'hostingercom-vps-kvmminecraftalex', 'name' => 'Game Panel 1', 'metadata' => ['cpus' => 1], 'prices' => $prices('kvmminecraftalex')],
['id' => 'hostingercom-vps-kvm1', 'name' => 'KVM 1', 'metadata' => ['cpus' => 1], 'prices' => $prices('kvm1')],
['id' => 'hostingercom-vps-kvm2', 'name' => 'KVM 2', 'metadata' => ['cpus' => 2], 'prices' => $prices('kvm2')],
]),
'https://developers.hostinger.com/api/vps/v1/public-keys' => Http::response(['data' => []]),
'https://developers.hostinger.com/api/vps/v1/post-install-scripts' => Http::response(['data' => []]),
], $overrides));
}
it('lists only Hostinger KVM plans sorted by size and billing period', function () {
fakeHostingerProvisioningOptions();
$component = Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData');
expect(collect($component->instance()->priceOptions)->pluck('id')->all())->toBe([
'hostingercom-vps-kvm1-usd-1m',
'hostingercom-vps-kvm1-usd-1y',
'hostingercom-vps-kvm1-usd-2y',
'hostingercom-vps-kvm2-usd-1m',
'hostingercom-vps-kvm2-usd-1y',
'hostingercom-vps-kvm2-usd-2y',
'hostingercom-vps-kvm8-usd-1m',
'hostingercom-vps-kvm8-usd-1y',
'hostingercom-vps-kvm8-usd-2y',
]);
$component->assertDontSee('Game Panel');
});
it('splits Hostinger templates into plain operating systems and operating systems with applications', function () {
fakeHostingerProvisioningOptions();
$component = Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->assertSet('selected_os_template_id', 1130)
->assertSet('selected_app_template_id', null)
->assertSet('selected_template_id', 1130)
->assertSee('Operating system with application')
->assertSee('can conflict with the Coolify proxy on ports 80 and 443', false);
expect(collect($component->instance()->osTemplates)->pluck('name')->all())
->toBe(['Debian 13', 'Ubuntu 24.04 LTS'])
->and(collect($component->instance()->appTemplates)->pluck('name')->all())
->toBe(['AlmaLinux 9 with CyberPanel', 'Ubuntu 24.04 with Coolify']);
});
it('keeps only one Hostinger operating system selection', function () {
fakeHostingerProvisioningOptions();
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->set('selected_app_template_id', 1150)
->assertSet('selected_os_template_id', null)
->assertSet('selected_template_id', 1150)
->set('selected_os_template_id', 1077)
->assertSet('selected_app_template_id', null)
->assertSet('selected_template_id', 1077);
});
it('purchases a Hostinger VPS with an operating system that includes an application', function () {
fakeHostingerProvisioningOptions([
'https://developers.hostinger.com/api/vps/v1/virtual-machines' => Http::response([
'virtual_machine' => ['id' => 17923, 'state' => 'creating', 'ipv4' => [['address' => '203.0.113.10']]],
]),
]);
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->set('server_name', 'app-template.example.com')
->set('selected_data_center_id', 19)
->set('selected_price_id', 'hostingercom-vps-kvm2-usd-1m')
->set('selected_app_template_id', 1150)
->set('private_key_id', $this->privateKey->id)
->call('submit')
->assertHasNoErrors();
Http::assertSent(fn ($request) => $request->method() === 'POST'
&& $request->url() === 'https://developers.hostinger.com/api/vps/v1/virtual-machines'
&& $request['setup']['template_id'] === 1150);
});
it('rejects a Game Panel Hostinger plan before making a purchase', function () {
fakeHostingerProvisioningOptions();
Livewire::test(ByHostinger::class, ['selectedTokenUuid' => $this->token->uuid])
->call('loadHostingerData')
->set('server_name', 'game-panel.example.com')
->set('selected_data_center_id', 19)
->set('selected_price_id', 'hostingercom-vps-kvmminecraftalex-usd-1m')
->set('private_key_id', $this->privateKey->id)
->call('submit')
->assertHasErrors('selected_price_id');
Http::assertNotSent(fn ($request) => $request->method() === 'POST');
});
it('starts a stopped Hostinger VPS from the server page', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
'cloud_provider_token_id' => $this->token->id,
'hostinger_virtual_machine_id' => 17923,
'hostinger_virtual_machine_status' => 'stopped',
]);
Http::fake([
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923/start' => Http::response([
'id' => 456,
'state' => 'starting',
]),
]);
Livewire::test(Show::class, ['server_uuid' => $server->uuid])
->call('startHostingerVirtualMachine')
->assertSet('hostingerVirtualMachineStatus', 'starting')
->assertDispatched('success');
expect($server->fresh()->hostinger_virtual_machine_status)->toBe('starting');
});
it('refreshes the Hostinger status from the server details action before the server is validated', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
'cloud_provider_token_id' => $this->token->id,
'hostinger_virtual_machine_id' => 17923,
'hostinger_virtual_machine_status' => 'running',
]);
Http::fake([
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923' => Http::response([
'id' => 17923,
'state' => 'stopped',
'ipv4' => [['address' => $server->ip]],
]),
]);
Livewire::test(Show::class, ['server_uuid' => $server->uuid])
->assertSee('Hostinger · Running')
->call('refreshServerMetadata')
->assertSet('hostingerVirtualMachineStatus', 'stopped')
->assertDispatched('info', 'Provider status refreshed. Validate the server connection to fetch server details.')
->assertSee('Hostinger · Stopped');
});
it('does not validate a stopped Hostinger VPS over SSH', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
'cloud_provider_token_id' => $this->token->id,
'hostinger_virtual_machine_id' => 17923,
'hostinger_virtual_machine_status' => 'stopped',
]);
Http::fake([
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923' => Http::response([
'id' => 17923,
'state' => 'stopped',
'ipv4' => [['address' => $server->ip]],
]),
]);
Livewire::test(ValidateAndInstall::class, ['server' => $server])
->call('validateConnection')
->assertSet('error', 'Hostinger VPS is stopped. Power it on before validating.');
});
it('warns that deleting from Coolify does not cancel the Hostinger VPS', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
'cloud_provider_token_id' => $this->token->id,
'hostinger_virtual_machine_id' => 17923,
]);
Livewire::test(Delete::class, ['server_uuid' => $server->uuid])
->assertSee('Hostinger deletes the VPS only at the end of its paid period, and only if you select this in the confirmation dialog.')
->assertSee('https://hpanel.hostinger.com/', false)
->assertSeeHtml('<strong>Hostinger deletes a VPS only at the end of its paid period.</strong>')
->assertSee('The VPS on Hostinger will NOT be deleted, and Hostinger will keep billing for it.');
});
it('passes the Hostinger deletion choice to the server deletion', function () {
Queue::fake();
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
'cloud_provider_token_id' => $this->token->id,
'hostinger_virtual_machine_id' => 17923,
]);
Livewire::test(Delete::class, ['server_uuid' => $server->uuid])
->assertSee('Also delete VPS from Hostinger (at the end of the paid period)')
->call('delete', 'password', ['delete_from_hostinger']);
Queue::assertPushed(JobDecorator::class, fn (JobDecorator $job) => $job->getAction() instanceof DeleteServer
&& $job->getParameters()[9] === true
&& $job->getParameters()[10] === 17923);
});
it('keeps the default deletion warning for servers without a Hostinger VPS', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
]);
Livewire::test(Delete::class, ['server_uuid' => $server->uuid])
->assertSee('This operation is permanent and cannot be undone.')
->assertDontSee('Hostinger deletes a VPS');
});