0, 'is_api_enabled' => true, 'fqdn' => 'https://coolify-a.test']); $this->team = Team::factory()->create(); $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.88.0.10', 'name' => 'migrate-me', ]); $destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail(); $project = Project::factory()->create(['team_id' => $this->team->id]); $environment = $project->environments()->first() ?? Environment::factory()->create(['project_id' => $project->id]); Application::factory()->create([ 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'name' => 'app-on-server', 'ports_exposes' => '3000', ]); }); test('migrate exports imports via http and completes locally', function () { Http::fake([ 'http://target.test/api/v1/servers/import' => Http::response([ 'dry_run' => false, 'server_uuid' => $this->server->uuid, 'claimed' => true, 'export_id' => 'remote-export', 'created' => ['applications' => 1], 'warnings' => [], ], 201), ]); $result = app(ServerTransferMigrator::class)->migrate( server: $this->server, targetUrl: 'http://target.test', targetToken: 'target-token-xyz', writeRemote: false, ); expect($result['server_uuid'])->toBe($this->server->uuid) ->and($result['target_url'])->toBe('http://target.test') ->and($result['import']['claimed'])->toBeTrue() ->and($result['message'])->toContain('migrated'); $this->server->refresh(); expect(data_get($this->server->server_metadata, 'transfer.status'))->toBe('transferred') ->and((bool) $this->server->settings->force_disabled)->toBeTrue(); Http::assertSent(function ($request) { return $request->url() === 'http://target.test/api/v1/servers/import' && $request->hasHeader('Authorization', 'Bearer target-token-xyz') && data_get($request->data(), 'claim') === true && data_get($request->data(), 'bundle.server.uuid') === $this->server->uuid; }); }); test('migrate rewrites localhost target when running in docker style env', function () { // Simulate container: create a temp marker if missing is hard; instead assert host rewrite helper via migrate call // with Http fake matching host.docker.internal when /.dockerenv exists — skip if not in docker. if (! file_exists('/.dockerenv') && ! is_file('/run/.containerenv')) { expect(true)->toBeTrue(); return; } Http::fake([ 'http://host.docker.internal:8001/api/v1/servers/import' => Http::response([ 'dry_run' => false, 'server_uuid' => $this->server->uuid, 'claimed' => true, 'warnings' => [], ], 201), ]); app(ServerTransferMigrator::class)->migrate( $this->server, 'http://localhost:8001', 'token', ); Http::assertSent(fn ($request) => str_contains($request->url(), 'host.docker.internal:8001')); }); test('migrate fails clearly when target is unreachable', function () { Http::fake([ 'http://down.test/*' => Http::failedConnection(), ]); expect(fn () => app(ServerTransferMigrator::class)->migrate( $this->server, 'http://down.test', 'token', ))->toThrow(RuntimeException::class, 'Could not reach target'); }); test('migrate fails when target returns error', function () { Http::fake([ 'http://target.test/api/v1/servers/import' => Http::response([ 'message' => 'A server with IP/domain already exists', ], 422), ]); expect(fn () => app(ServerTransferMigrator::class)->migrate( $this->server, 'http://target.test', 'token', ))->toThrow(RuntimeException::class, 'Target import failed'); // Source must remain unmanaged-away only after successful remote import+complete. $this->server->refresh(); expect(data_get($this->server->server_metadata, 'transfer.status'))->not->toBe('transferred') ->and((bool) $this->server->settings->force_disabled)->toBeFalse(); }); test('migrate surfaces recovery guidance when complete fails after successful remote import', function () { Http::fake([ 'http://target.test/api/v1/servers/import' => Http::response([ 'dry_run' => false, 'server_uuid' => $this->server->uuid, 'claimed' => true, 'warnings' => [], ], 201), ]); $claimer = Mockery::mock(ServerTransferClaimer::class)->makePartial(); $claimer->shouldReceive('markTransferred') ->once() ->andThrow(new RuntimeException('simulated complete failure')); app()->instance(ServerTransferClaimer::class, $claimer); expect(fn () => app(ServerTransferMigrator::class)->migrate( $this->server, 'http://target.test', 'token', ))->toThrow(RuntimeException::class, 'Retry complete'); });