CluPilotCloud/tests/Feature/Host/HostEnrolmentTest.php

120 lines
4.1 KiB
PHP

<?php
use App\Models\Host;
use App\Services\Wireguard\WireguardHub;
use App\Support\HostEnrolment;
use Illuminate\Support\Facades\DB;
beforeEach(function () {
fakeServices();
});
/**
* Der Code ist ein Ausweis für den Rückweg, kein Schlüssel zu einer Auskunft:
* er öffnet nichts nach außen (§5 der Spec), sondern verhindert nur, dass ein
* Gerät im Tunnel für einen fremden Host spricht.
*/
it('hands out the code exactly once and stores only its hash', function () {
$host = Host::factory()->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 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);
});