'local']); $this->seed([UserSeeder::class, TeamSeeder::class, PrivateKeySeeder::class]); }); it('registers the interactive qemu command', function () { expect(Artisan::all()) ->toHaveKey('dev:qemu') ->toHaveKey('dev:qemu:seed') ->and(Artisan::all()['dev:qemu'])->toBeInstanceOf(ManageDevelopmentQemuVmCommand::class) ->and(Artisan::all()['dev:qemu']->getDefinition()->getArgument('profiles')->isArray())->toBeTrue() ->and(Artisan::all()['dev:qemu:seed'])->toBeInstanceOf(SeedDevelopmentQemuServerCommand::class); }); it('prevents qemu commands from running outside development', function () { config(['app.env' => 'production']); Process::fake(); expect(Artisan::call('dev:qemu', ['profiles' => ['ubuntu-root']]))->toBe(Command::FAILURE) ->and(Artisan::call('dev:qemu:seed', ['profile' => 'ubuntu-root']))->toBe(Command::FAILURE); Process::assertNothingRan(); }); it('provides root and non-root profiles for every supported distribution', function () { $profiles = collect(config('development-qemu.profiles')); expect($profiles->keys()->all())->toBe([ 'ubuntu-root', 'ubuntu-non-root', 'debian-root', 'debian-non-root', 'centos-root', 'centos-non-root', 'alpine-root', 'alpine-non-root', ])->and($profiles->pluck('ip')->unique()->count())->toBe(8) ->and($profiles->pluck('mac')->unique()->count())->toBe(8) ->and($profiles->filter(fn (array $profile) => $profile['user'] === 'root')->count())->toBe(4) ->and($profiles->filter(fn (array $profile) => $profile['user'] !== 'root')->count())->toBe(4); }); it('stores vm disks in a libvirt-accessible directory', function () { expect(config('development-qemu.storage_path'))->toStartWith('/var/lib/libvirt/images/'); }); it('automatically configures the qemu host', function () { Process::fake(function ($process) { if (str_contains($process->command, 'command -v')) { return Process::result(); } if (str_contains($process->command, 'net-info')) { return Process::result(exitCode: 1); } if (str_contains($process->command, 'network inspect')) { return Process::result(output: "172.18.0.0/16\n"); } if (str_contains($process->command, 'iptables -C')) { return Process::result(exitCode: 1); } return Process::result(); }); ConfigureDevelopmentQemuHost::run(); Process::assertRan(fn ($process) => str_contains($process->command, 'systemctl enable --now libvirtd')); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh net-define')); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh net-start')); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh net-autostart')); Process::assertRan(fn ($process) => str_contains($process->command, 'sysctl -w net.ipv4.ip_forward=1')); Process::assertRan(fn ($process) => str_contains($process->command, 'iptables -I LIBVIRT_FWI')); }); it('does not restart an active libvirt network', function () { Process::fake(function ($process) { if (str_contains($process->command, 'net-info')) { return Process::result(output: "Name: default\nActive: yes\n"); } if (str_contains($process->command, 'network inspect')) { return Process::result(output: "172.18.0.0/16\n"); } return Process::result(); }); ConfigureDevelopmentQemuHost::run(); Process::assertNotRan(fn ($process) => str_contains($process->command, 'virsh net-start')); Process::assertRan(fn ($process) => str_contains($process->command, 'iptables -D LIBVIRT_FWI')); Process::assertRan(fn ($process) => str_contains($process->command, 'iptables -I LIBVIRT_FWI 1')); }); it('seeds one predefined root qemu server', function () { $server = SeedDevelopmentQemuServer::run('ubuntu-root'); expect($server->uuid)->toBe('development-qemu-ubuntu-root') ->and($server->ip)->toBe('192.168.122.10') ->and($server->user)->toBe('root') ->and($server->team_id)->toBe(0) ->and(Server::query()->where('uuid', 'like', 'development-qemu-%')->count())->toBe(1); }); it('replaces the seeded qemu server with the selected non-root equivalent', function () { SeedDevelopmentQemuServer::run('ubuntu-root'); $server = SeedDevelopmentQemuServer::run('ubuntu-non-root'); expect($server->ip)->toBe('192.168.122.11') ->and($server->user)->toBe('coolify') ->and(Server::query()->where('uuid', 'like', 'development-qemu-%')->count())->toBe(1); }); it('deletes managed vm data and freshly creates only the selected vm', function () { $storagePath = sys_get_temp_dir().'/coolify-qemu-reset-test-'.uniqid(); config(['development-qemu.storage_path' => $storagePath]); File::ensureDirectoryExists($storagePath); File::put("{$storagePath}/coolify-dev-ubuntu-root.qcow2", 'old data'); File::put("{$storagePath}/coolify-dev-ubuntu-non-root.qcow2", 'old data'); Process::fake([ '* net-dumpxml *' => Process::result(output: ''), '* network inspect *' => Process::result(output: "172.18.0.0/16\n"), '* iptables -C *' => Process::result(exitCode: 1), '* dominfo *' => Process::result(output: 'exists'), '*' => Process::result(), ]); StartDevelopmentQemuVm::run('ubuntu-non-root'); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh destroy') && str_contains($process->command, 'coolify-dev-ubuntu-root')); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh destroy') && str_contains($process->command, 'coolify-dev-ubuntu-non-root')); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh undefine') && str_contains($process->command, 'coolify-dev-ubuntu-root')); Process::assertRan(fn ($process) => str_contains($process->command, 'virsh undefine') && str_contains($process->command, 'coolify-dev-ubuntu-non-root')); Process::assertNotRan(fn ($process) => str_contains($process->command, 'virsh start')); Process::assertRan(fn ($process) => str_contains($process->command, 'virt-install') && str_contains($process->command, 'coolify-dev-ubuntu-non-root')); Process::assertRan(fn ($process) => str_contains($process->command, 'net-update') && str_contains($process->command, 'ip-dhcp-host') && str_contains($process->command, '192.168.122.11')); Process::assertRan(fn ($process) => str_contains($process->command, 'iptables -D LIBVIRT_FWI')); Process::assertRan(fn ($process) => str_contains($process->command, 'docker exec') && str_contains($process->command, 'coolify')); expect(File::exists("{$storagePath}/coolify-dev-ubuntu-root.qcow2"))->toBeFalse() ->and(File::exists("{$storagePath}/coolify-dev-ubuntu-non-root.qcow2"))->toBeFalse(); }); it('rejects qemu vm management outside development', function () { config(['app.env' => 'production']); expect(fn () => StartDevelopmentQemuVm::run('ubuntu-root')) ->toThrow(RuntimeException::class, 'development environments'); }); it('can create a vm without a host database connection', function () { config([ 'development-qemu.storage_path' => sys_get_temp_dir().'/coolify-qemu-test-'.uniqid(), ]); DB::enableQueryLog(); Process::fake(function ($process) { if (str_contains($process->command, 'net-dumpxml')) { return Process::result(output: ''); } if (str_contains($process->command, 'network inspect')) { return Process::result(output: "172.18.0.0/16\n"); } if (str_contains($process->command, 'virsh dominfo')) { return Process::result(exitCode: 1); } if (str_contains($process->command, 'iptables -C')) { return Process::result(exitCode: 1); } return Process::result(); }); StartDevelopmentQemuVm::run('ubuntu-root'); expect(DB::getQueryLog())->toBeEmpty(); Process::assertRan(fn ($process) => str_contains($process->command, 'virt-install')); Process::assertRan(fn ($process) => str_contains($process->command, 'iptables -I LIBVIRT_FWI')); }); it('seeds through the coolify container when the host database is unavailable', function () { config(['development-qemu.storage_path' => sys_get_temp_dir().'/coolify-qemu-fallback-test-'.uniqid()]); SeedDevelopmentQemuServer::mock() ->shouldReceive('handle') ->once() ->andThrow(new QueryException('pgsql', 'select 1', [], new Exception('unavailable'))); Process::fake([ '* net-dumpxml *' => Process::result(output: ''), '* network inspect *' => Process::result(output: "172.18.0.0/16\n"), '* dominfo *' => Process::result(output: 'exists'), '*' => Process::result(), ]); ManageDevelopmentQemuVm::run('ubuntu-root'); Process::assertRan(fn ($process) => str_contains($process->command, 'docker exec coolify php artisan dev:qemu:seed') && str_contains($process->command, 'ubuntu-root')); }); it('starts and seeds root and non-root profiles together', function () { config(['development-qemu.storage_path' => sys_get_temp_dir().'/coolify-qemu-multi-test-'.uniqid()]); Process::fake([ '* net-dumpxml *' => Process::result(output: ''), '* network inspect *' => Process::result(output: "172.18.0.0/16\n"), '*' => Process::result(), ]); ManageDevelopmentQemuVm::run(['ubuntu-root', 'ubuntu-non-root']); expect(Server::query()->whereIn('uuid', [ 'development-qemu-ubuntu-root', 'development-qemu-ubuntu-non-root', ])->count())->toBe(2); Process::assertRan(fn ($process) => str_contains($process->command, 'virt-install') && str_contains($process->command, 'coolify-dev-ubuntu-root')); Process::assertRan(fn ($process) => str_contains($process->command, 'virt-install') && str_contains($process->command, 'coolify-dev-ubuntu-non-root')); });