mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
318 lines
14 KiB
PHP
318 lines
14 KiB
PHP
<?php
|
|
|
|
use App\Actions\Server\DeleteServer;
|
|
use App\Jobs\DeleteResourceJob;
|
|
use App\Jobs\MigrateServerToInstanceJob;
|
|
use App\Jobs\ReleaseManagedDnsRecordsJob;
|
|
use App\Jobs\ValidateAndInstallServerJob;
|
|
use App\Livewire\Server\Delete;
|
|
use App\Livewire\Server\Transfer;
|
|
use App\Models\Application;
|
|
use App\Models\DnsProviderZone;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\IntegrationToken;
|
|
use App\Models\ManagedDnsRecord;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use App\Services\ServerTransfer\ServerTransferClaimer;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Process;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
// A claim writes the owner marker on the host over SSH.
|
|
Process::fake();
|
|
Storage::fake('ssh-keys');
|
|
// An import validates the claimed server over SSH; the sync test queue would run it.
|
|
Queue::fake([ValidateAndInstallServerJob::class]);
|
|
config(['app.env' => 'production']);
|
|
$this->withoutVite();
|
|
|
|
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true, 'fqdn' => 'https://coolify-a.test']);
|
|
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
// The default password of UserFactory.
|
|
$this->userPassword = 'password'; // ggignore
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
|
$this->server = Server::factory()->create([
|
|
'team_id' => $this->team->id,
|
|
'private_key_id' => $this->privateKey->id,
|
|
'ip' => '10.77.0.10',
|
|
'name' => 'ui-transfer-server',
|
|
]);
|
|
});
|
|
|
|
test('server transfer page is available in production', function () {
|
|
$this->get('/server/'.$this->server->uuid.'/transfer')->assertOk();
|
|
});
|
|
|
|
test('server transfer link is shown to admins in production', function () {
|
|
$this->get('/server/'.$this->server->uuid)
|
|
->assertOk()
|
|
->assertSee('/server/'.$this->server->uuid.'/transfer', escape: false);
|
|
});
|
|
|
|
test('taking back a transferred server needs a typed confirmation', function () {
|
|
app(ServerTransferClaimer::class)->markTransferred($this->server, targetInstanceUrl: 'https://target.example.com');
|
|
|
|
$this->get('/server/'.$this->server->uuid)
|
|
->assertOk()
|
|
->assertSee('Take back a transferred server?')
|
|
->assertSee('Changes made on https://target.example.com after', escape: false);
|
|
});
|
|
|
|
test('team members cannot open the transfer page', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertRedirect(route('server.index'));
|
|
|
|
$this->get('/server/'.$this->server->uuid)
|
|
->assertOk()
|
|
->assertDontSee('/server/'.$this->server->uuid.'/transfer', escape: false);
|
|
});
|
|
|
|
test('a member who owns another team cannot use that role to transfer the server', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$otherTeam = Team::factory()->create();
|
|
$otherTeam->members()->attach($member->id, ['role' => 'owner']);
|
|
$this->actingAs($member);
|
|
|
|
session(['currentTeam' => $this->team]);
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertRedirect(route('server.index'));
|
|
|
|
session(['currentTeam' => $otherTeam]);
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertStatus(404);
|
|
});
|
|
|
|
test('transfer page renders for owned server', function () {
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertOk()
|
|
->assertSee('Transfer server')
|
|
->assertSee('Target instance URL')
|
|
->assertSee('Target API token')
|
|
->assertDontSee('Manual transfer')
|
|
->assertDontSee('Passphrase');
|
|
});
|
|
|
|
test('transfer page badge shows the management state of the server', function (string $state, string $label, array $hidden) {
|
|
$claimer = app(ServerTransferClaimer::class);
|
|
match ($state) {
|
|
'managed' => null,
|
|
'transferred' => $claimer->markTransferred($this->server, targetInstanceUrl: 'https://coolify-b.test'),
|
|
};
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->fresh()->uuid])
|
|
->assertSee($label)
|
|
->assertDontSee($hidden);
|
|
})->with([
|
|
'managed' => ['managed', 'Managed here', ['Transferred away']],
|
|
'transferred' => ['transferred', 'Transferred away', ['Managed here']],
|
|
]);
|
|
|
|
test('transfer to another instance runs in a queued job', function () {
|
|
Queue::fake();
|
|
Http::fake(['*/api/v1/servers/import/check*' => Http::response(['message' => 'ok', 'checks' => []])]);
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->set('targetUrl', 'https://8.8.8.8')
|
|
->set('targetToken', 'target-token')
|
|
->call('migrateServer')
|
|
->assertHasNoErrors()
|
|
->assertSet('targetToken', '')
|
|
->assertSee('Transfer in progress');
|
|
|
|
Queue::assertPushed(MigrateServerToInstanceJob::class);
|
|
expect(data_get($this->server->fresh()->server_metadata, 'migration.status'))->toBe('running');
|
|
});
|
|
|
|
test('transfer page shows the error of a failed transfer', function () {
|
|
$this->server->forceFill(['server_metadata' => ['migration' => [
|
|
'status' => 'failed',
|
|
'target_url' => 'https://8.8.8.8',
|
|
'error' => 'Target import failed (HTTP 401): Unauthenticated.',
|
|
]]])->save();
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertSee('Transfer failed')
|
|
->assertSee('Target import failed (HTTP 401): Unauthenticated.');
|
|
});
|
|
|
|
test('a transferred server cannot delete its cloud provider machine', function () {
|
|
Queue::fake();
|
|
$this->server->forceFill(['hetzner_server_id' => 4242])->save();
|
|
app(ServerTransferClaimer::class)->markTransferred($this->server, targetInstanceUrl: 'https://coolify-b.test');
|
|
|
|
$component = Livewire::test(Delete::class, ['server_uuid' => $this->server->uuid]);
|
|
expect(collect($component->viewData('checkboxes'))->pluck('id'))->not->toContain('delete_from_hetzner');
|
|
|
|
$component->call('delete', $this->userPassword, ['delete_from_hetzner']);
|
|
|
|
DeleteServer::assertPushed(fn ($action, array $parameters) => $parameters[1] === false && $parameters[2] === 4242);
|
|
});
|
|
|
|
test('a transferred server does not show Hostinger VPS deletion guidance', function () {
|
|
$this->server->forceFill(['hostinger_virtual_machine_id' => 4242])->save();
|
|
app(ServerTransferClaimer::class)->markTransferred($this->server, targetInstanceUrl: 'https://coolify-b.test');
|
|
|
|
Livewire::test(Delete::class, ['server_uuid' => $this->server->uuid])
|
|
->assertDontSee('Hostinger deletes')
|
|
->assertDontSee('hpanel.hostinger.com');
|
|
});
|
|
|
|
test('a managed server can still delete its cloud provider machine', function () {
|
|
$this->server->forceFill(['hetzner_server_id' => 4242])->save();
|
|
|
|
$component = Livewire::test(Delete::class, ['server_uuid' => $this->server->uuid]);
|
|
|
|
expect(collect($component->viewData('checkboxes'))->pluck('id'))->toContain('delete_from_hetzner');
|
|
});
|
|
|
|
test('deleting a transferred server deletes its resources from this instance only', function () {
|
|
Queue::fake();
|
|
$destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
|
|
$application = Application::factory()->create([
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
app(ServerTransferClaimer::class)->markTransferred($this->server, targetInstanceUrl: 'https://coolify-b.test');
|
|
|
|
$component = Livewire::test(Delete::class, ['server_uuid' => $this->server->uuid]);
|
|
expect(collect($component->viewData('checkboxes'))->pluck('id'))->not->toContain('force_delete_resources');
|
|
|
|
$component->call('delete', $this->userPassword);
|
|
|
|
Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job) => $job->resource->is($application) && $job->deleteFromCoolifyOnly);
|
|
expect(Server::find($this->server->id))->toBeNull();
|
|
});
|
|
|
|
test('deleting a transferred server forgets the DNS records of its resources without releasing them at the provider', function () {
|
|
Queue::fake();
|
|
$destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
|
|
$application = Application::factory()->create(['destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass()]);
|
|
// An archive that still tracks a record, for example from a transfer before records were forgotten at claim time.
|
|
$this->server->forceFill(['server_metadata' => ['transfer' => ['status' => 'transferred']]])->save();
|
|
$token = IntegrationToken::factory()->for($this->team)->create(['provider' => 'cloudflare']);
|
|
$zone = DnsProviderZone::factory()->create(['integration_token_id' => $token->id]);
|
|
$record = ManagedDnsRecord::factory()->owned()->create(['dns_provider_zone_id' => $zone->id]);
|
|
$record->addReference($application);
|
|
|
|
Livewire::test(Delete::class, ['server_uuid' => $this->server->uuid])->call('delete', $this->userPassword);
|
|
(new DeleteResourceJob($application->fresh(), deleteFromCoolifyOnly: true))->handle();
|
|
|
|
Queue::assertNotPushed(ReleaseManagedDnsRecordsJob::class);
|
|
expect(ManagedDnsRecord::find($record->id))->toBeNull()
|
|
->and(Application::withTrashed()->find($application->id))->toBeNull();
|
|
});
|
|
|
|
test('deleting an application from Coolify only does not touch the server', function () {
|
|
$destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
|
|
$application = Application::factory()->create([
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
Process::fake();
|
|
|
|
(new DeleteResourceJob($application, deleteFromCoolifyOnly: true))->handle();
|
|
|
|
Process::assertNothingRan();
|
|
expect(Application::withTrashed()->find($application->id))->toBeNull();
|
|
});
|
|
|
|
test('transfer with a token that the target does not accept shows an error and does not start', function () {
|
|
Queue::fake();
|
|
Http::fake(['*/api/v1/servers/import/check*' => Http::response(['message' => 'Unauthenticated.'], 401)]);
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->set('targetUrl', 'https://8.8.8.8')
|
|
->set('targetToken', 'wrong-token')
|
|
->call('migrateServer')
|
|
->assertDispatched('error')
|
|
->assertDontSee('Transfer in progress')
|
|
->assertSee('A check failed. The transfer did not start.')
|
|
->assertSee('OK: this instance can connect to the server over SSH.')
|
|
->assertSee('FAILED: the target instance accepts the API token.')
|
|
->assertSee('See the transfer log below for details.')
|
|
->assertDontSee('checks failed:');
|
|
|
|
Queue::assertNotPushed(MigrateServerToInstanceJob::class);
|
|
});
|
|
|
|
test('transfer page links to the server on the target and lists the steps after a finished transfer', function () {
|
|
$this->server->forceFill(['server_metadata' => ['migration' => [
|
|
'status' => 'succeeded',
|
|
'target_url' => 'https://8.8.8.8',
|
|
'checklist' => ['Set the webhook URL of GitHub App <b>deploy-bot</b> to https://8.8.8.8/webhooks/source/github/events.'],
|
|
'target_server_url' => 'https://8.8.8.8/server/abc',
|
|
]]])->save();
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertSee('Transfer finished')
|
|
->assertSee('Finish the transfer on the target')
|
|
->assertSee('href="https://8.8.8.8/server/abc"', escape: false)
|
|
->assertSee('Set the webhook URL of GitHub App <b>deploy-bot</b> to https://8.8.8.8/webhooks/source/github/events.')
|
|
->assertDontSee('<b>deploy-bot</b>', escape: false);
|
|
});
|
|
|
|
test('transfer page hides the post-transfer checklist', function (string $status, array $checklist) {
|
|
$this->server->forceFill(['server_metadata' => ['migration' => [
|
|
'status' => $status,
|
|
'target_url' => 'https://8.8.8.8',
|
|
'error' => 'Target import failed (HTTP 401): Unauthenticated.',
|
|
'checklist' => $checklist,
|
|
]]])->save();
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertDontSee('Finish the transfer on the target')
|
|
->assertDontSee('Set the webhook URL');
|
|
})->with([
|
|
'finished transfer without steps' => ['succeeded', []],
|
|
'failed transfer' => ['failed', ['Set the webhook URL of GitHub App deploy-bot.']],
|
|
]);
|
|
|
|
test('the transfer page of an archive offers a transfer back instead of a transfer', function () {
|
|
app(ServerTransferClaimer::class)->markTransferred($this->server, targetInstanceUrl: 'https://8.8.8.8');
|
|
Http::fake(['https://8.8.8.8/api/v1/servers/*/migrate' => Http::response(['migration' => ['status' => 'running']], 202)]);
|
|
|
|
Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid])
|
|
->assertSet('managerUrl', 'https://8.8.8.8')
|
|
->assertDontSee('Transfer to another instance')
|
|
->set('managerToken', 'token-of-b')
|
|
->call('transferBack')
|
|
->assertDispatched('success')
|
|
->assertSee('Transfer back started');
|
|
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
test('team members cannot start a transfer back', function () {
|
|
app(ServerTransferClaimer::class)->markTransferred($this->server, targetInstanceUrl: 'https://8.8.8.8');
|
|
Http::fake();
|
|
$component = Livewire::test(Transfer::class, ['server_uuid' => $this->server->uuid]);
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
|
|
$component->set('managerToken', 'token-of-b')->call('transferBack');
|
|
|
|
Http::assertNothingSent();
|
|
});
|