'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' => 'Operations']]) ->assertSee('Extra SSH keys') ->assertSee('Existing keys from the Hostinger account.') ->assertSee('Search SSH keys') ->assertSee('Operations') ->assertSeeHtml('x-text="current">Select SSH keys') ->assertDontSeeHtml('Operations') ->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('Hostinger deletes a VPS only at the end of its paid period.') ->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'); });