CluPilotCloud/tests/Feature/Admin/HostNamingTest.php

202 lines
7.7 KiB
PHP

<?php
use App\Actions\StartHostOnboarding;
use App\Livewire\Admin\ConfirmDeleteDatacenter;
use App\Livewire\Admin\HostCreate;
use App\Livewire\Admin\HostDetail;
use App\Models\Datacenter;
use App\Models\Host;
use App\Support\HostName;
use App\Support\HostTakeoverCommand;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
beforeEach(function () {
Queue::fake();
fakeServices();
// Die Zone wird hier festgenagelt statt aus der .env dieser Maschine
// gelesen: `provisioning.dns.platform_zone` leitet sich sonst aus APP_URL
// ab, und dieselbe Prüfung fiele auf einer richtig konfigurierten
// Installation um. Geprüft wird die FORM des Namens, nicht die Domain
// dieser einen Kiste.
config()->set('provisioning.dns.platform_zone', 'clupilot.com');
// Die Migration von `create_datacenters_table` legt fsn und hel schon an;
// firstOrCreate steht hier, damit der Test nicht daran hängt.
Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']);
});
/** Ein Host, der so angelegt wird, wie die Konsole es tut. */
function onboard(string $dc = 'fsn'): Host
{
static $n = 0;
$n++;
return app(StartHostOnboarding::class)->run([
'datacenter' => $dc,
'public_ip' => '203.0.113.'.$n,
'root_password' => 'supersecret',
]);
}
// --- Abnahme 1 + 2 ---
it('vergibt den Namen selbst, fortlaufend je Rechenzentrum', function () {
expect(onboard()->name)->toBe('fsn-01')
->and(onboard()->name)->toBe('fsn-02');
});
it('führt je Rechenzentrum einen eigenen Zähler', function () {
Datacenter::query()->firstOrCreate(['code' => 'hel'], ['name' => 'Helsinki']);
expect(onboard('fsn')->name)->toBe('fsn-01')
->and(onboard('hel')->name)->toBe('hel-01');
});
// --- Abnahme 3: der Punkt, der die Sorgfalt trägt ---
it('gibt die Nummer eines entfernten Hosts nie wieder aus', function () {
onboard(); // fsn-01
$zweiter = onboard(); // fsn-02
onboard(); // fsn-03
// Die HÖCHSTE zu entfernen ist der gefährliche Fall: MAX(nummer) + 1
// reichte fsn-03 sofort wieder heraus, und ein zwischengespeicherter Name
// zeigte danach auf eine andere Maschine.
$zweiter->delete();
expect(onboard()->name)->toBe('fsn-04');
});
it('überlebt auch das Entfernen des höchsten Hosts', function () {
onboard(); // fsn-01
$hoechster = onboard(); // fsn-02
$hoechster->delete();
expect(onboard()->name)->toBe('fsn-03');
});
// --- Abnahme 5 ---
it('zeigt den Namen vorher an, ohne ihn zu verbrauchen', function () {
onboard(); // fsn-01 ist weg
$seite = Livewire::actingAs(admin(), 'operator')->test(HostCreate::class)
->set('datacenter', 'fsn');
$seite->assertSee('fsn-02')->assertSee('fsn-02.node.clupilot.com');
// Zweimal ansehen verbraucht nichts.
Livewire::actingAs(admin(), 'operator')->test(HostCreate::class)
->set('datacenter', 'fsn')
->assertSee('fsn-02');
expect(HostName::preview('fsn'))->toBe('fsn-02');
});
it('gibt zwei gleichzeitig geöffneten Formularen zwei verschiedene Namen', function () {
// Beide sehen fsn-01, gespeichert wird fsn-01 und fsn-02.
expect(HostName::preview('fsn'))->toBe('fsn-01')
->and(HostName::preview('fsn'))->toBe('fsn-01');
expect(onboard()->name)->toBe('fsn-01')
->and(onboard()->name)->toBe('fsn-02');
});
// --- Der Riegel ---
it('lässt zwei Hosts nicht denselben Namen tragen', function () {
onboard();
expect(fn () => Host::factory()->create(['name' => 'fsn-01']))
->toThrow(QueryException::class);
});
it('gibt zwei ähnlich geschriebenen Rechenzentrums-Codes nicht denselben Namen', function () {
// Codes von vor der Verschärfung der Prüfregel: eu_west und eu-west werden
// zur selben DNS-Bezeichnung, führen aber getrennte Zähler. Ohne das
// Weiterzählen bekäme der zweite eu-west-01 und liefe in den Index.
Datacenter::factory()->create(['code' => 'eu_west', 'name' => 'EU West (alt)']);
Datacenter::factory()->create(['code' => 'eu-west', 'name' => 'EU West']);
expect(onboard('eu_west')->name)->toBe('eu-west-01')
->and(onboard('eu-west')->name)->toBe('eu-west-02');
});
it('fällt auf eine gültige Bezeichnung zurück, wenn der Code keine hergibt', function () {
expect(HostName::label('---'))->toBe('node');
});
it('zählt ab hundert ohne Sonderfall weiter', function () {
// Der Zähler liegt seit dem Umzug in einer eigenen Tabelle (siehe
// clupilot_der_hostnamen_zaehler_ueberlebt_das_loeschen) — nicht mehr
// auf der Rechenzentrums-Zeile.
DB::table('host_name_sequences')->updateOrInsert(['code' => 'fsn'], ['next_host_number' => 100]);
expect(onboard()->name)->toBe('fsn-100');
});
// --- Der Zähler übersteht das Löschen des Rechenzentrums ---
it('gibt eine Nummer nicht wieder her, nur weil das Rechenzentrum zwischendurch gelöscht wurde', function () {
// Der Bruch aus dem Register: der Zähler lag auf der
// Rechenzentrums-ZEILE. Ein leeres Rechenzentrum lässt sich löschen — das
// bleibt bewusst so —, aber wer denselben Code neu anlegte, bekam eine
// frische Zeile mit dem Schema-Default 1 und der nächste Host hieß wieder
// fsn-01, obwohl dieser Name schon in alten Protokollen, Sicherungen und
// DNS-Zwischenspeichern auf eine ANDERE Maschine zeigt.
$dc = Datacenter::factory()->create(['code' => 'nbg', 'name' => 'Nürnberg']);
$erster = onboard('nbg');
expect($erster->name)->toBe('nbg-01');
// Rechenzentrum wird leer — das ist die Voraussetzung, unter der sich
// eine Rechenzentrums-Zeile heute löschen lässt.
$erster->delete();
// Löschen über den echten Weg (den Livewire-Bestätigungsdialog), nicht
// über $dc->delete(): genau diese Komponente stand im Registereintrag.
Livewire::actingAs(admin(), 'operator')
->test(ConfirmDeleteDatacenter::class, ['uuid' => $dc->uuid])
->call('delete');
expect(Datacenter::query()->where('code', 'nbg')->exists())->toBeFalse();
// Derselbe Code, eine neue Zeile — mit eigener uuid, eigener id.
Datacenter::factory()->create(['code' => 'nbg', 'name' => 'Nürnberg']);
// Die Zusage: eine ausgegebene Nummer wird nie wiederverwendet. Beide
// Zusicherungen tragen den Fix — ohne ihn liefe preview() wieder auf
// next_host_number=1 der frischen Zeile zurück und beide fielen rot.
expect(HostName::preview('nbg'))->toBe('nbg-02');
$zweiter = onboard('nbg');
expect($zweiter->name)->toBe('nbg-02')
->and($zweiter->name)->not->toBe($erster->name);
});
// --- Ein Name, überall derselbe ---
it('nennt den Host im DNS so, wie die Konsole ihn nennt', function () {
$host = onboard();
expect(HostName::fqdn($host->name))->toBe('fsn-01.node.clupilot.com')
->and(HostTakeoverCommand::fqdnFor($host))
->toBe(HostName::fqdn($host->name));
});
it('zeigt auf der Detailseite die IP und die Domain', function () {
$host = onboard();
$host->update(['status' => 'active', 'wg_ip' => '10.66.0.100', 'public_ip' => '203.0.113.200']);
Livewire::actingAs(admin(), 'operator')
->test(HostDetail::class, ['host' => $host])
->assertSee('203.0.113.200')
->assertSee('10.66.0.100')
// Der Name allein hilft nicht, wenn niemand die Adresse dazu kennt.
// 8006 ist die Proxmox-Oberfläche; SecureHostFirewall lässt sie nur aus
// dem Tunnel zu, und der hostsdir-Eintrag löst nur dort auf.
->assertSee('fsn-01.node.clupilot.com')
->assertSee('https://fsn-01.node.clupilot.com:8006', false);
});