script('dpkg -l proxmox-ve', CommandResult::success('ii proxmox-ve 8.2')); $shell->connectWithPassword('1.2.3.4', 'root', 'pw'); expect($shell->run('dpkg -l proxmox-ve')->stdout)->toContain('proxmox-ve') ->and($shell->run('whoami')->ok())->toBeTrue() // default success ->and($shell->ran('dpkg -l proxmox-ve'))->toBeTrue(); $shell->putFile('/etc/wireguard/wg0.conf', '[Interface]'); expect($shell->files()['/etc/wireguard/wg0.conf'])->toBe('[Interface]') ->and($shell->connectionsWith('password'))->toHaveCount(1); }); it('removes a peer via the RemoveWireguardPeer job', function () { $hub = new FakeWireguardHub; $hub->addPeer('PK', '10.66.0.9'); (new RemoveWireguardPeer('PK'))->handle($hub); expect($hub->peerIps())->toBe([]); }); it('provides a deterministic VM lifecycle (FakeProxmoxClient)', function () { $pve = new FakeProxmoxClient; $a = $pve->nextVmid(); $b = $pve->nextVmid(); expect($b)->toBe($a + 1); $upid = $pve->cloneVm('pve', 9000, $a, 'nc-test'); expect($upid)->toContain((string) $a) ->and($pve->clonedVmids)->toContain($a) ->and($pve->taskStatus('pve', $upid))->toBe(['status' => 'stopped', 'exitstatus' => 'OK']) ->and($pve->vmStatus('pve', $a)['status'])->toBe('stopped'); $pve->startVm('pve', $a); expect($pve->vmStatus('pve', $a)['status'])->toBe('running') ->and($pve->guestAgentPing('pve', $a))->toBeTrue(); $pve->guestScript('occ status', 0, 'installed: true'); expect($pve->guestExec('pve', $a, 'occ status')['out-data'])->toContain('installed') ->and($pve->guestExec('pve', $a, 'whoami')['exitcode'])->toBe(0) ->and($pve->guestRan('occ status'))->toBeTrue(); }); it('can force a running task and a failed exit status (FakeProxmoxClient)', function () { $pve = new FakeProxmoxClient; $pve->forceTaskStatus = 'running'; expect($pve->taskStatus('pve', 'UPID')['status'])->toBe('running'); $pve->forceTaskStatus = null; $pve->taskExitStatus = 'clone failed'; expect($pve->taskStatus('pve', 'UPID')['exitstatus'])->toBe('clone failed'); }); it('allocates ips and tracks peers (FakeWireguardHub)', function () { $hub = new FakeWireguardHub; $a = $hub->allocateIp(); $b = $hub->allocateIp(); expect($a)->not->toBe($b); $hub->addPeer('PUBKEY', '10.66.0.9'); expect($hub->peerIps())->toBe(['PUBKEY' => '10.66.0.9']); $hub->removePeer('PUBKEY'); expect($hub->peerIps())->toBe([]); }); it('allocates addresses within a /24 subnet, skipping hub and used (LocalWireguardHub)', function () { config()->set('provisioning.wireguard.subnet', '10.66.0.0/24'); config()->set('provisioning.wireguard.hub_ip', '10.66.0.1'); Host::factory()->create(['wg_ip' => '10.66.0.2']); // .0 network, .1 hub, .2 used → first free is .3 expect((new LocalWireguardHub)->allocateIp())->toBe('10.66.0.3'); }); it('respects a non-/24 subnet (LocalWireguardHub)', function () { config()->set('provisioning.wireguard.subnet', '10.66.5.128/25'); config()->set('provisioning.wireguard.hub_ip', '10.66.5.129'); // network .128, hub .129 → first free is .130 expect((new LocalWireguardHub)->allocateIp())->toBe('10.66.5.130'); }); it('reports node capacity for a host (FakeProxmoxClient)', function () { $host = Host::factory()->create(['wg_ip' => '10.66.0.5']); $client = (new FakeProxmoxClient)->forHost($host); expect($client->listNodes())->not->toBeEmpty() ->and($client->nodeStatus('pve')['memory']['total'])->toBeGreaterThan(0) ->and($client->nodeStorage('pve')[0]['total'])->toBeGreaterThan(0); }); // Der Hetzner-DNS-Client steht in HetznerCloudDnsTest — seit dem Umzug auf die // Cloud-API (RRSets statt einzelner Records) gehört dazu mehr als zwei Fälle. // Die alte Seitenlauf-Falle, die hier stand, kann es nicht mehr geben: es gibt // keinen Lookup mehr, den sie überspringen könnte. Warum das so ist, steht im // Kopfkommentar von HttpHetznerDnsClient. it('writes and removes a real hostsdir entry on disk (FileHostDnsDirectory)', function () { $dir = sys_get_temp_dir().'/clupilot-dns-hosts-test-'.uniqid(); config()->set('provisioning.dns.hosts_dir', $dir); $writer = new FileHostDnsDirectory; // The directory itself is created on first write — dnsmasq --hostsdir // only needs it to exist, not to be pre-provisioned by anything else. $writer->write('fsn-01', 'fsn-01.node.clupilot.com', '10.66.0.11'); $path = $dir.'/fsn-01.hosts'; expect(is_file($path))->toBeTrue() ->and(trim(file_get_contents($path)))->toBe('10.66.0.11 fsn-01.node.clupilot.com'); // Overwrite is the update path — RegisterHostDns calls write() again on // retry with the same name, and that must replace, not append. $writer->write('fsn-01', 'fsn-01.node.clupilot.com', '10.66.0.99'); expect(trim(file_get_contents($path)))->toBe('10.66.0.99 fsn-01.node.clupilot.com'); $writer->remove('fsn-01'); expect(is_file($path))->toBeFalse(); File::deleteDirectory($dir); }); it('treats removing a name that was never written as done (FileHostDnsDirectory)', function () { $dir = sys_get_temp_dir().'/clupilot-dns-hosts-test-'.uniqid(); config()->set('provisioning.dns.hosts_dir', $dir); // Mirrors PurgeHost's guard (filled($host->dns_name)) not always holding // — a host that never got a name still has to purge cleanly. (new FileHostDnsDirectory)->remove('never-written'); })->throwsNoExceptions(); it('starts people at the bottom of the subnet and machines at the host offset', function () { // Ein Bereich, keine zweite Grenze: `from` verschiebt nur den Anfang. Liefe // er als Obergrenze, stünde die Vergabe irgendwann still, obwohl das // Subnetz noch Platz hat. expect((new LocalWireguardHub)->allocateIp())->toBe('10.66.0.2') ->and((new LocalWireguardHub)->allocateIp(100))->toBe('10.66.0.100'); }); it('falls back into the lower range when the host offset does not fit the subnet', function () { // Ein /26 hat 64 Adressen; der Versatz von 100 liegt ausserhalb. Eine // Vergabe, die dann "Subnetz erschöpft" meldet, während unten alles frei // ist, wäre die schlechtere Antwort — der Versatz ist eine Bevorzugung, // keine Bedingung. config()->set('provisioning.wireguard.subnet', '10.66.7.0/26'); config()->set('provisioning.wireguard.hub_ip', '10.66.7.1'); expect((new LocalWireguardHub)->allocateIp(100))->toBe('10.66.7.2'); });