create(); $code = HostEnrolment::issue($host); expect($code)->toHaveLength(32); $stored = DB::table('hosts')->where('id', $host->id)->value('enrolment_code_hash'); expect($stored)->not->toBe($code) ->and($stored)->not->toContain($code); }); it('resolves a valid code to its host and consumes it', function () { $host = Host::factory()->create(); $code = HostEnrolment::issue($host); expect(HostEnrolment::claim($code)?->id)->toBe($host->id); // Verbraucht. Ein zweiter Lauf braucht einen neuen Code aus der Konsole — // eine halb installierte Maschine wird neu aufgesetzt, nicht nachgebessert. expect(HostEnrolment::claim($code))->toBeNull(); }); /** * Der Fortschritt kommt VOR der Registrierung und darf den Code deshalb nicht * verbrauchen — sonst könnte der Host sich nach seiner ersten Meldung nie mehr * registrieren. */ it('resolves a code without consuming it, for the progress reports', function () { $host = Host::factory()->create(); $code = HostEnrolment::issue($host); expect(HostEnrolment::resolve($code)?->id)->toBe($host->id) ->and(HostEnrolment::resolve($code)?->id)->toBe($host->id); // Und danach ist er immer noch einlösbar. expect(HostEnrolment::claim($code)?->id)->toBe($host->id); }); it('refuses a code past its expiry', function () { $host = Host::factory()->create(); $code = HostEnrolment::issue($host); $this->travel(25)->hours(); expect(HostEnrolment::claim($code))->toBeNull(); }); it('refuses a code that was never issued', function () { expect(HostEnrolment::claim(str_repeat('a', 32)))->toBeNull(); }); /** * Der Hub muss den Peer kennen, BEVOR der Host im Tunnel ist — sonst käme er * nie hinein und müsste seinen Schlüssel über einen öffentlichen Weg melden. * Genau das vermeidet dieser Entwurf. */ it('allocates a tunnel address and admits the peer up front', function () { $host = Host::factory()->create(['wg_ip' => null, 'wg_pubkey' => null]); HostEnrolment::issue($host); $host->refresh(); expect($host->wg_ip)->not->toBeNull() ->and($host->wg_pubkey)->not->toBeNull(); // `peers()` ist nach dem öffentlichen Schlüssel indiziert, die Werte sind // Momentaufnahmen — geprüft wird deshalb gegen die Schlüssel. expect(array_keys(app(WireguardHub::class)->peers()))->toContain($host->wg_pubkey); }); /** * Der Riegel gegen den Rückbau. * * `wg set` gelingt nur im Provisioning-Container — dem einzigen mit NET_ADMIN, * `/dev/net/tun` und `wg0`. Diese Methode lief aber im Web-Request, also im * `app`-Container: „Unable to modify interface: Operation not permitted", und * das Anlegen eines Hosts endete für den Betreiber in einem 500 — ausgerechnet * auf der Seite, die ihm den Einmal-Code zeigen sollte. * * Der Test davor blieb dabei grün, weil die Suite den Hub gegen * FakeWireguardHub tauscht. Dieser hier prüft deshalb den WEG, nicht das * Ergebnis: der Peer gehört auf die Queue, und in der Anfrage bleibt der Hub * unberührt. */ it('registers the tunnel peer on the provisioning queue, not in the request', function () { Queue::fake(); $host = Host::factory()->create(['wg_ip' => null, 'wg_pubkey' => null]); HostEnrolment::issue($host); Queue::assertPushedOn('provisioning', ApplyHostVpnPeer::class, fn (ApplyHostVpnPeer $job) => $job->hostId === $host->id); expect(app(WireguardHub::class)->peers())->toBeEmpty(); }); it('carries the replaced key along so the old peer does not linger', function () { // Ein zweiter Übernahmeversuch tauscht den Schlüssel. Stünde der alte // nicht im Auftrag, läse der ihn aus der Zeile — wo längst der neue steht — // und der abgelöste Peer bliebe für immer auf dem Hub. $host = Host::factory()->create(['wg_ip' => null, 'wg_pubkey' => null]); HostEnrolment::issue($host); $erster = $host->refresh()->wg_pubkey; HostEnrolment::issue($host); $zweiter = $host->refresh()->wg_pubkey; expect($zweiter)->not->toBe($erster); $keys = array_keys(app(WireguardHub::class)->peers()); expect($keys)->toContain($zweiter) ->and($keys)->not->toContain($erster); }); /** * Der private Schlüssel steht in der kopierten Befehlszeile und sonst nirgends. * Ihn zu speichern hieße, ein Geheimnis aufzubewahren, das nach Task 9 des * Skripts ohnehin wertlos ist — und bis dahin eine Stelle mehr, an der es liegt. */ it('returns the private key with the code and never stores it', function () { $host = Host::factory()->create(); $enrolment = HostEnrolment::issueWithKeys($host); expect($enrolment['private_key'])->not->toBeEmpty(); $row = (array) DB::table('hosts')->where('id', $host->id)->first(); foreach ($row as $value) { expect((string) $value)->not->toContain($enrolment['private_key']); } }); /** * Ein zweiter Code macht den ersten wertlos. Sonst hätte eine Maschine, die im * Rettungssystem hängengeblieben ist, weiter einen gültigen Ausweis für einen * Host, den der Betreiber gerade neu übernimmt. */ it('invalidates the previous code when a new one is issued', function () { $host = Host::factory()->create(); $first = HostEnrolment::issue($host); $second = HostEnrolment::issue($host); expect(HostEnrolment::claim($first))->toBeNull() ->and(HostEnrolment::claim($second)?->id)->toBe($host->id); });