mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
420 lines
16 KiB
PHP
420 lines
16 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Server\New\ByHetzner;
|
|
use App\Models\CloudProviderToken;
|
|
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\Http;
|
|
use Livewire\Livewire;
|
|
|
|
// Note: Full Livewire integration tests require database setup
|
|
// These tests verify the SSH key merging logic and public_net configuration
|
|
|
|
it('validates public_net configuration with IPv4 and IPv6 enabled', function () {
|
|
$enableIpv4 = true;
|
|
$enableIpv6 = true;
|
|
|
|
$publicNet = [
|
|
'enable_ipv4' => $enableIpv4,
|
|
'enable_ipv6' => $enableIpv6,
|
|
];
|
|
|
|
expect($publicNet)->toBe([
|
|
'enable_ipv4' => true,
|
|
'enable_ipv6' => true,
|
|
]);
|
|
});
|
|
|
|
it('validates public_net configuration with IPv4 only', function () {
|
|
$enableIpv4 = true;
|
|
$enableIpv6 = false;
|
|
|
|
$publicNet = [
|
|
'enable_ipv4' => $enableIpv4,
|
|
'enable_ipv6' => $enableIpv6,
|
|
];
|
|
|
|
expect($publicNet)->toBe([
|
|
'enable_ipv4' => true,
|
|
'enable_ipv6' => false,
|
|
]);
|
|
});
|
|
|
|
it('validates public_net configuration with IPv6 only', function () {
|
|
$enableIpv4 = false;
|
|
$enableIpv6 = true;
|
|
|
|
$publicNet = [
|
|
'enable_ipv4' => $enableIpv4,
|
|
'enable_ipv6' => $enableIpv6,
|
|
];
|
|
|
|
expect($publicNet)->toBe([
|
|
'enable_ipv4' => false,
|
|
'enable_ipv6' => true,
|
|
]);
|
|
});
|
|
|
|
it('validates IP address selection prefers IPv4 when both are enabled', function () {
|
|
$enableIpv4 = true;
|
|
$enableIpv6 = true;
|
|
|
|
$hetznerServer = [
|
|
'public_net' => [
|
|
'ipv4' => ['ip' => '1.2.3.4'],
|
|
'ipv6' => ['ip' => '2001:db8::1'],
|
|
],
|
|
];
|
|
|
|
$ipAddress = null;
|
|
if ($enableIpv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) {
|
|
$ipAddress = $hetznerServer['public_net']['ipv4']['ip'];
|
|
} elseif ($enableIpv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) {
|
|
$ipAddress = $hetznerServer['public_net']['ipv6']['ip'];
|
|
}
|
|
|
|
expect($ipAddress)->toBe('1.2.3.4');
|
|
});
|
|
|
|
it('validates IP address selection uses IPv6 when only IPv6 is enabled', function () {
|
|
$enableIpv4 = false;
|
|
$enableIpv6 = true;
|
|
|
|
$hetznerServer = [
|
|
'public_net' => [
|
|
'ipv4' => ['ip' => '1.2.3.4'],
|
|
'ipv6' => ['ip' => '2001:db8::1'],
|
|
],
|
|
];
|
|
|
|
$ipAddress = null;
|
|
if ($enableIpv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) {
|
|
$ipAddress = $hetznerServer['public_net']['ipv4']['ip'];
|
|
} elseif ($enableIpv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) {
|
|
$ipAddress = $hetznerServer['public_net']['ipv6']['ip'];
|
|
}
|
|
|
|
expect($ipAddress)->toBe('2001:db8::1');
|
|
});
|
|
|
|
it('validates SSH key array merging logic with Coolify key', function () {
|
|
$coolifyKeyId = 123;
|
|
$selectedHetznerKeys = [];
|
|
|
|
$sshKeys = array_merge(
|
|
[$coolifyKeyId],
|
|
$selectedHetznerKeys
|
|
);
|
|
$sshKeys = array_unique($sshKeys);
|
|
$sshKeys = array_values($sshKeys);
|
|
|
|
expect($sshKeys)->toBe([123])
|
|
->and(count($sshKeys))->toBe(1);
|
|
});
|
|
|
|
it('validates SSH key array merging with additional Hetzner keys', function () {
|
|
$coolifyKeyId = 123;
|
|
$selectedHetznerKeys = [456, 789];
|
|
|
|
$sshKeys = array_merge(
|
|
[$coolifyKeyId],
|
|
$selectedHetznerKeys
|
|
);
|
|
$sshKeys = array_unique($sshKeys);
|
|
$sshKeys = array_values($sshKeys);
|
|
|
|
expect($sshKeys)->toBe([123, 456, 789])
|
|
->and(count($sshKeys))->toBe(3);
|
|
});
|
|
|
|
it('validates deduplication when Coolify key is also in selected keys', function () {
|
|
$coolifyKeyId = 123;
|
|
$selectedHetznerKeys = [123, 456, 789];
|
|
|
|
$sshKeys = array_merge(
|
|
[$coolifyKeyId],
|
|
$selectedHetznerKeys
|
|
);
|
|
$sshKeys = array_unique($sshKeys);
|
|
$sshKeys = array_values($sshKeys);
|
|
|
|
expect($sshKeys)->toBe([123, 456, 789])
|
|
->and(count($sshKeys))->toBe(3);
|
|
});
|
|
|
|
describe('Boarding Flow Integration', function () {
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
// Create a team with owner that has boarding enabled
|
|
$this->team = Team::factory()->create([
|
|
'show_boarding' => true,
|
|
]);
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
|
|
// Set current team and act as user
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->team]);
|
|
});
|
|
|
|
test('completes boarding when server is created from onboarding', function () {
|
|
// Verify boarding is initially enabled
|
|
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
|
|
|
// Mount the component with from_onboarding flag
|
|
$component = Livewire::test(ByHetzner::class)
|
|
->set('from_onboarding', true);
|
|
|
|
// Verify the from_onboarding property is set
|
|
expect($component->get('from_onboarding'))->toBeTrue();
|
|
|
|
// After successful server creation in the actual component,
|
|
// the boarding should be marked as complete
|
|
// Note: We can't fully test the createServer method without mocking Hetzner API
|
|
// but we can verify the boarding completion logic is in place
|
|
});
|
|
|
|
test('boarding flag remains unchanged when not from onboarding', function () {
|
|
// Verify boarding is initially enabled
|
|
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
|
|
|
// Mount the component without from_onboarding flag (default false)
|
|
Livewire::test(ByHetzner::class)
|
|
->set('from_onboarding', false);
|
|
|
|
// Boarding should still be enabled since it wasn't created from onboarding
|
|
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
|
});
|
|
|
|
test('renders advanced Hetzner options in their own settings section', function () {
|
|
Livewire::test(ByHetzner::class)
|
|
->set('current_step', 2)
|
|
->assertSee('Advanced options')
|
|
->assertSee('Provider SSH keys, networking, backups, and cloud-init.');
|
|
});
|
|
|
|
test('renders advanced Hetzner option controls in the advanced section', function () {
|
|
Livewire::test(ByHetzner::class)
|
|
->set('current_step', 2)
|
|
->assertSee('Extra SSH keys')
|
|
->assertSee('Firewalls')
|
|
->assertSee('Private networks')
|
|
->assertSee('Enable Hetzner backups')
|
|
->assertSee('Add cloud-init script')
|
|
->assertSee('Adds 20% to the provider server price.');
|
|
});
|
|
|
|
test('lists account SSH keys and firewalls as listbox options and shows the selection', function () {
|
|
Livewire::test(ByHetzner::class)
|
|
->set('current_step', 2)
|
|
->set('hetznerSshKeys', [['id' => 11, 'name' => 'deploy-key']])
|
|
->set('hetznerFirewalls', [['id' => 38, 'name' => 'web-firewall'], ['id' => 39, 'name' => 'db-firewall']])
|
|
->set('selectedHetznerFirewallIds', [38])
|
|
->assertSee('deploy-key')
|
|
->assertSee('db-firewall')
|
|
->assertSeeHtml('x-text="current">web-firewall</span>')
|
|
->assertSeeHtml('x-text="current">Select SSH keys</span>');
|
|
});
|
|
|
|
test('shows the cloud init script name only when saving the script', function () {
|
|
Livewire::test(ByHetzner::class)
|
|
->set('current_step', 2)
|
|
->set('show_cloud_init_script', true)
|
|
->assertSee('Cloud-init script')
|
|
->assertSee('Save this script for later')
|
|
->assertDontSee('Saved script name')
|
|
->set('save_cloud_init_script', true)
|
|
->assertSee('Saved script name');
|
|
});
|
|
});
|
|
|
|
describe('Hetzner data loading', function () {
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$this->hetznerToken = CloudProviderToken::factory()->create([
|
|
'team_id' => $this->team->id,
|
|
'provider' => 'hetzner',
|
|
'token' => 'test-hetzner-api-token',
|
|
]);
|
|
|
|
$this->privateKey = PrivateKey::factory()->create([
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
});
|
|
|
|
test('loads firewalls and networks for the selected Hetzner token', function () {
|
|
Http::fake([
|
|
'https://api.hetzner.cloud/v1/locations*' => Http::response([
|
|
'locations' => [
|
|
['id' => 1, 'name' => 'nbg1', 'city' => 'Nuremberg', 'country' => 'DE', 'network_zone' => 'eu-central'],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
|
'server_types' => [
|
|
['id' => 1, 'name' => 'cx11', 'description' => 'CX11', 'cores' => 1, 'memory' => 2.0, 'disk' => 20, 'locations' => [['name' => 'nbg1']], 'architecture' => 'x86'],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/images*' => Http::response([
|
|
'images' => [
|
|
['id' => 15512617, 'name' => 'ubuntu-24.04', 'type' => 'system', 'deprecated' => false, 'architecture' => 'x86'],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/ssh_keys*' => Http::response([
|
|
'ssh_keys' => [],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/firewalls*' => Http::response([
|
|
'firewalls' => [
|
|
['id' => 38, 'name' => 'web-firewall', 'rules' => []],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/networks*' => Http::response([
|
|
'networks' => [
|
|
[
|
|
'id' => 456,
|
|
'name' => 'private-eu',
|
|
'ip_range' => '10.0.0.0/16',
|
|
'subnets' => [
|
|
['type' => 'cloud', 'network_zone' => 'eu-central'],
|
|
],
|
|
],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
]);
|
|
|
|
$component = Livewire::test(ByHetzner::class, ['selectedTokenUuid' => $this->hetznerToken->uuid])
|
|
->assertSet('current_step', 2)
|
|
->call('loadHetznerData');
|
|
|
|
expect($component->get('hetznerFirewalls'))->toHaveCount(1)
|
|
->and($component->get('hetznerNetworks'))->toHaveCount(1);
|
|
});
|
|
|
|
test('deletes the Hetzner server when saving it in Coolify fails', function () {
|
|
Http::fake([
|
|
'https://api.hetzner.cloud/v1/ssh_keys' => Http::response(['ssh_key' => ['id' => 123]], 201),
|
|
'https://api.hetzner.cloud/v1/ssh_keys*' => Http::response([
|
|
'ssh_keys' => [],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/servers' => Http::response([
|
|
'server' => ['id' => 456, 'public_net' => ['ipv4' => ['ip' => '203.0.113.10']]],
|
|
], 201),
|
|
'https://api.hetzner.cloud/v1/servers/456' => Http::response([], 200),
|
|
]);
|
|
|
|
$eventDispatcher = Server::getEventDispatcher();
|
|
Server::setEventDispatcher(clone $eventDispatcher);
|
|
Server::created(function (): void {
|
|
throw new RuntimeException('local persistence failed');
|
|
});
|
|
|
|
try {
|
|
Livewire::test(ByHetzner::class)
|
|
->set('current_step', 2)
|
|
->set('selected_token_id', $this->hetznerToken->id)
|
|
->set('server_name', 'test-server')
|
|
->set('selected_location', 'nbg1')
|
|
->set('selected_server_type', 'cx11')
|
|
->set('selected_image', 15512617)
|
|
->set('private_key_id', $this->privateKey->id)
|
|
->call('submit');
|
|
} finally {
|
|
Server::setEventDispatcher($eventDispatcher);
|
|
}
|
|
|
|
$this->assertDatabaseMissing('servers', ['hetzner_server_id' => 456]);
|
|
Http::assertSent(fn ($request) => $request->method() === 'DELETE'
|
|
&& $request->url() === 'https://api.hetzner.cloud/v1/servers/456');
|
|
});
|
|
|
|
test('rejects submitting without a public IP protocol before calling Hetzner', function () {
|
|
Http::fake();
|
|
|
|
Livewire::test(ByHetzner::class)
|
|
->set('current_step', 2)
|
|
->set('selected_token_id', $this->hetznerToken->id)
|
|
->set('server_name', 'test-server')
|
|
->set('selected_location', 'nbg1')
|
|
->set('selected_server_type', 'cx11')
|
|
->set('selected_image', 15512617)
|
|
->set('private_key_id', $this->privateKey->id)
|
|
->set('enable_ipv4', false)
|
|
->set('enable_ipv6', false)
|
|
->call('submit')
|
|
->assertHasErrors(['enable_ipv4', 'enable_ipv6']);
|
|
|
|
Http::assertNothingSent();
|
|
});
|
|
test('lists server types that are out of stock in the selected location as out of stock', function () {
|
|
Http::fake([
|
|
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
|
'server_types' => [
|
|
['id' => 1, 'name' => 'cx22', 'locations' => [
|
|
['id' => 1, 'name' => 'nbg1', 'available' => false, 'deprecation' => null],
|
|
['id' => 2, 'name' => 'fsn1', 'available' => true, 'deprecation' => null],
|
|
]],
|
|
['id' => 2, 'name' => 'cx32', 'locations' => [
|
|
['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null],
|
|
]],
|
|
['id' => 3, 'name' => 'cx11', 'locations' => [
|
|
['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => ['announced' => '2025-09-24T00:00:00+00:00', 'unavailable_after' => '2025-12-31T00:00:00+00:00']],
|
|
]],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/*' => Http::response(['locations' => [], 'images' => [], 'ssh_keys' => [], 'firewalls' => [], 'networks' => [], 'meta' => ['pagination' => ['next_page' => null]]], 200),
|
|
]);
|
|
|
|
$component = Livewire::test(ByHetzner::class, ['selectedTokenUuid' => $this->hetznerToken->uuid])
|
|
->call('loadHetznerData')
|
|
->set('selected_location', 'nbg1');
|
|
|
|
expect(collect($component->instance()->availableServerTypes)->mapWithKeys(fn ($type) => [$type['name'] => $type['out_of_stock']])->all())
|
|
->toBe(['cx32' => false, 'cx22' => true]);
|
|
});
|
|
|
|
test('does not create a server with a server type that is out of stock in the selected location', function () {
|
|
Http::fake([
|
|
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
|
'server_types' => [
|
|
['id' => 1, 'name' => 'cx22', 'locations' => [
|
|
['id' => 1, 'name' => 'nbg1', 'available' => false, 'deprecation' => null],
|
|
]],
|
|
],
|
|
'meta' => ['pagination' => ['next_page' => null]],
|
|
], 200),
|
|
'https://api.hetzner.cloud/v1/*' => Http::response(['locations' => [], 'images' => [], 'ssh_keys' => [], 'firewalls' => [], 'networks' => [], 'meta' => ['pagination' => ['next_page' => null]]], 200),
|
|
]);
|
|
|
|
Livewire::test(ByHetzner::class, ['selectedTokenUuid' => $this->hetznerToken->uuid])
|
|
->call('loadHetznerData')
|
|
->set('server_name', 'test-server')
|
|
->set('selected_location', 'nbg1')
|
|
->set('selected_server_type', 'cx22')
|
|
->set('selected_image', 15512617)
|
|
->set('private_key_id', $this->privateKey->id)
|
|
->call('submit')
|
|
->assertHasErrors(['selected_server_type']);
|
|
|
|
Http::assertNotSent(fn ($request) => $request->method() === 'POST');
|
|
});
|
|
});
|