set('provisioning.wireguard.hub_public_key', 'HUBKEYHUBKEYHUBKEYHUBKEYHUBKEYHUBKEYHUBKEY0='); config()->set('provisioning.wireguard.endpoint', 'hub.clupilot.cloud:51820'); config()->set('provisioning.wireguard.hub_address', '10.66.0.1'); config()->set('provisioning.wireguard.subnet', '10.66.0.0/24'); config()->set('admin_access.app_host', 'clupilot.cloud'); // firstOrCreate: die Migrationen legen Rechenzentren schon an, und ein // zweites `fsn` verletzt den eindeutigen Index. Datacenter::query()->firstOrCreate( ['code' => 'fsn'], ['name' => 'Falkenstein', 'location' => 'DE', 'active' => true], ); }); function createHostAs(): \Livewire\Features\SupportTesting\Testable { return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator') ->test(HostCreate::class) ->set('name', 'pve-fsn-2') ->set('datacenter', 'fsn') ->set('public_ip', '198.51.45.9') ->set('root_password', 'ein-langes-kennwort') ->call('save'); } /** * Die eine Frage, die diese Seite beantworten muss: was gebe ich auf dem Server * ein? Steht die Zeile nicht da, ist die ganze Übernahme nicht benutzbar. */ it('shows the command exactly once, right after creating the host', function () { $page = createHostAs(); $page->assertSee('curl') ->assertSee('clupilot-bootstrap.sh') ->assertSee('--code'); // Ein frisch geladenes Bauteil — also jemand, der die Seite neu lädt — hat // nichts mehr. Der Code steht nur als Hash in der Datenbank. Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator') ->test(HostCreate::class) ->assertDontSee('curl'); }); /** * Die Zeile muss ALLES tragen, was das Skript vor dem Tunnel braucht (Spec §5). * Fehlt ein Wert, läuft sie an und endet in einem Tunnel ohne Handshake — und * das merkt man erst auf einer Maschine, die schon bestellt ist. */ it('carries every value the script needs before the tunnel', function () { $command = createHostAs()->get('command'); foreach (['--code', '--wg-private', '--wg-ip', '--hub-pubkey', '--hub-endpoint', '--api'] as $argument) { expect($command)->toContain($argument); } expect($command) ->toContain('hub.clupilot.cloud:51820') ->toContain('--api http://10.66.0.1') ->toContain('/opt/clupilot'); }); /** * Über den ÖFFENTLICHEN Hostnamen. Die Konsole läuft unter admin.…, aber die * Zeile wird auf einer Maschine ausgeführt, die den Adminbereich nicht erreichen * darf — sie ist genau dafür abgeriegelt. Ein Link auf den Konsolen-Hostnamen * liefe in eine 404 auf einem Server, an den man dann nur noch über die * Anbieterkonsole kommt. */ it('fetches the installer over the public hostname', function () { expect(createHostAs()->get('command')) ->toContain('https://clupilot.cloud/bootstrap.tar.gz') ->not->toContain('admin.'); }); /** * Der Schlüssel ist base64 und enthält +, / und =. Ohne Anführungszeichen * zerlegt die Shell die Zeile an einer Stelle, die niemand sieht. */ it('quotes the keys so a shell cannot split them', function () { $command = createHostAs()->get('command'); expect($command)->toMatch("/--wg-private '[^']+'/") ->and($command)->toMatch("/--hub-pubkey '[^']+'/"); }); /** * Der Code aus der Zeile muss der sein, mit dem der Host sich meldet. Sonst ist * die Anleitung richtig und der Ablauf trotzdem kaputt. */ it('shows a code that actually resolves to the new host', function () { $command = createHostAs()->get('command'); preg_match('/--code ([A-Za-z0-9]+)/', $command, $matches); expect(HostEnrolment::claim($matches[1])?->name)->toBe('pve-fsn-2'); }); /** * Ein leerer Hub-Schlüssel ergibt eine Zeile, die läuft und nie handshaked. * Das gehört gesagt, BEVOR jemand einen Server bestellt. */ it('warns before creating a host when the tunnel settings are missing', function () { config()->set('provisioning.wireguard.hub_public_key', ''); config()->set('provisioning.wireguard.endpoint', ''); Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator') ->test(HostCreate::class) ->assertSee('CLUPILOT_WG_HUB_PUBKEY') ->assertSee('CLUPILOT_WG_ENDPOINT'); }); /** * Die drei Schritte sind die Anleitung. Fehlt einer, fehlt genau der, den * jemand nicht von selbst weiß — meistens das Rettungssystem. */ it('spells out the three steps, rescue system first', function () { createHostAs() ->assertSee(__('hosts.takeover.step1_title')) ->assertSee(__('hosts.takeover.step2_title')) ->assertSee(__('hosts.takeover.step3_title')); }); it('refuses to create a host without the permission', function () { Livewire::actingAs(Operator::factory()->create(), 'operator') ->test(HostCreate::class) ->set('name', 'pve-fsn-3') ->set('datacenter', 'fsn') ->set('public_ip', '198.51.45.10') ->set('root_password', 'ein-langes-kennwort') ->call('save') ->assertForbidden(); expect(Host::query()->where('name', 'pve-fsn-3')->exists())->toBeFalse(); });