201 lines
7.9 KiB
PHP
201 lines
7.9 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\ProxyHosts;
|
|
use App\Models\Operator;
|
|
use App\Models\ProxyHost;
|
|
use App\Services\Proxy\CertificateInspector;
|
|
use App\Support\Settings;
|
|
use Livewire\Livewire;
|
|
|
|
function proxyPage(): \Livewire\Features\SupportTesting\Testable
|
|
{
|
|
return Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')->test(ProxyHosts::class);
|
|
}
|
|
|
|
it('adds a hostname and lists it', function () {
|
|
proxyPage()
|
|
->set('hostname', 'files.clupilot.com')
|
|
->set('purpose', 'public')
|
|
->call('add')
|
|
->assertSee('files.clupilot.com');
|
|
|
|
expect(ProxyHost::query()->where('hostname', 'files.clupilot.com')->exists())->toBeTrue();
|
|
});
|
|
|
|
/**
|
|
* Der Dienstbenutzer wählt NAMEN, nicht Konfiguration. Wäre hier alles erlaubt,
|
|
* was durch die Prüfung schlüpft, könnte über den root-eigenen Helfer Caddy
|
|
* beigebracht werden, was immer der Proxy kann — und `install-agent.sh` hält
|
|
* fest, warum das die Freigabe wertlos machte.
|
|
*/
|
|
it('refuses anything that is not a hostname', function () {
|
|
foreach ([
|
|
'files.clupilot.com {\n reverse_proxy evil',
|
|
'https://files.clupilot.com',
|
|
'files.clupilot.com/pfad',
|
|
'kein punkt drin',
|
|
'-anfang.clupilot.com',
|
|
'../../etc/passwd',
|
|
] as $attempt) {
|
|
proxyPage()->set('hostname', $attempt)->call('add')->assertHasErrors('hostname');
|
|
}
|
|
|
|
// Nur die von Hand angelegten zählen: die Seite gleicht beim Öffnen die
|
|
// Namen aus der Umgebung ein, also ist die Tabelle nie leer.
|
|
expect(ProxyHost::query()->where('source', 'manual')->count())->toBe(0);
|
|
});
|
|
|
|
it('refuses the same hostname twice', function () {
|
|
ProxyHost::create(['hostname' => 'files.clupilot.com', 'purpose' => 'public']);
|
|
|
|
proxyPage()->set('hostname', 'files.clupilot.com')->call('add')->assertHasErrors('hostname');
|
|
});
|
|
|
|
/**
|
|
* Entfernen nimmt den Namen aus der Liste, NICHT aus dem laufenden Proxy. Zwei
|
|
* Entscheidungen in einem Klick, und die zweite nimmt eine Seite vom Netz.
|
|
*/
|
|
it('removes from the list without touching the running proxy', function () {
|
|
$host = ProxyHost::create(['hostname' => 'alt.clupilot.com', 'purpose' => 'public']);
|
|
|
|
proxyPage()->call('remove', $host->id)->assertSee(__('proxy.removed'));
|
|
|
|
expect(ProxyHost::query()->where('source', 'manual')->count())->toBe(0);
|
|
});
|
|
|
|
/**
|
|
* Der Zustand wird GEMESSEN. Das ist der ganze Grund für diese Seite: bei
|
|
* `files.clupilot.com` waren DNS, Umgebungsvariable und Maschine richtig, und
|
|
* es gab trotzdem kein Zertifikat — keine der drei Einstellungen hätte das
|
|
* verraten.
|
|
*/
|
|
it('records what the certificate check actually found', function () {
|
|
ProxyHost::create(['hostname' => 'gut.clupilot.com', 'purpose' => 'public']);
|
|
ProxyHost::create(['hostname' => 'kaputt.clupilot.com', 'purpose' => 'public']);
|
|
|
|
$this->swap(CertificateInspector::class, new class extends CertificateInspector
|
|
{
|
|
public function inspect(string $hostname, int $timeoutSeconds = 8): array
|
|
{
|
|
return $hostname === 'gut.clupilot.com'
|
|
? ['ok' => true, 'expires_at' => now()->addDays(75), 'issuer' => "Let's Encrypt", 'error' => null]
|
|
: ['ok' => false, 'expires_at' => null, 'issuer' => null, 'error' => 'tlsv1 alert internal error'];
|
|
}
|
|
});
|
|
|
|
proxyPage()->call('checkCertificates');
|
|
|
|
$gut = ProxyHost::query()->where('hostname', 'gut.clupilot.com')->first();
|
|
$kaputt = ProxyHost::query()->where('hostname', 'kaputt.clupilot.com')->first();
|
|
|
|
expect($gut->daysLeft())->toBeGreaterThan(70)
|
|
->and($gut->isExpiringSoon())->toBeFalse()
|
|
->and($gut->certificate_error)->toBeNull()
|
|
->and($kaputt->certificate_expires_at)->toBeNull()
|
|
->and($kaputt->certificate_error)->toContain('internal error');
|
|
});
|
|
|
|
/**
|
|
* Dreißig Tage: bei neunzig Tagen Laufzeit hätte die Erneuerung längst laufen
|
|
* müssen. Was darunter steht, ist kein knappes Zertifikat, sondern eine
|
|
* Erneuerung, die nicht stattfindet.
|
|
*/
|
|
it('calls a certificate with under thirty days left a problem', function () {
|
|
$host = new ProxyHost(['hostname' => 'x.clupilot.com']);
|
|
$host->certificate_expires_at = now()->addDays(12);
|
|
|
|
expect($host->isExpiringSoon())->toBeTrue()
|
|
->and($host->daysLeft())->toBeLessThan(14);
|
|
|
|
$host->certificate_expires_at = now()->addDays(60);
|
|
expect($host->isExpiringSoon())->toBeFalse();
|
|
});
|
|
|
|
/**
|
|
* Die Liste, die der root-eigene Helfer liest: NAMEN und Zweck, eine je Zeile.
|
|
* Keine Caddy-Blöcke.
|
|
*/
|
|
it('lists hostnames for the host helper, and nothing else', function () {
|
|
ProxyHost::create(['hostname' => 'files.clupilot.com', 'purpose' => 'public']);
|
|
ProxyHost::create(['hostname' => 'admin.clupilot.com', 'purpose' => 'console']);
|
|
|
|
$this->artisan('clupilot:proxy-hosts')
|
|
->expectsOutput('admin.clupilot.com console')
|
|
->expectsOutput('files.clupilot.com public')
|
|
->assertSuccessful();
|
|
});
|
|
|
|
it('keeps the acme contact address, and offers the owner as the fallback', function () {
|
|
$owner = Operator::factory()->role('Owner')->create(['email' => 'inhaber@clupilot.com']);
|
|
|
|
Livewire::actingAs($owner, 'operator')->test(ProxyHosts::class)
|
|
->assertSee('inhaber@clupilot.com')
|
|
->set('acmeEmail', 'ssl@clupilot.com')
|
|
->call('saveAcmeEmail');
|
|
|
|
expect(Settings::get('proxy.acme_email'))->toBe('ssl@clupilot.com');
|
|
});
|
|
|
|
it('is closed to an operator without the permission', function () {
|
|
Livewire::actingAs(Operator::factory()->create(), 'operator')
|
|
->test(ProxyHosts::class)
|
|
->assertForbidden();
|
|
});
|
|
|
|
/**
|
|
* Der Fund, der diese Seite zweimal falsch gemacht hat.
|
|
*
|
|
* Die erste Fassung war ein LEERES Register: sie zeigte nichts, während die
|
|
* Installation längst ein halbes Dutzend Namen bediente, deren Zertifikate der
|
|
* Betreiber gerade sehen wollte. Eine Übersicht, die man erst befüllen muss,
|
|
* beantwortet „was habe ich" nicht.
|
|
*/
|
|
it('shows the hostnames this installation already serves, without being told', function () {
|
|
expect(ProxyHost::query()->count())->toBe(0);
|
|
|
|
proxyPage()->assertSee('files.clupilot.test');
|
|
|
|
$entdeckt = ProxyHost::query()->where('hostname', 'files.clupilot.test')->first();
|
|
|
|
expect($entdeckt)->not->toBeNull()
|
|
->and($entdeckt->isFromConfig())->toBeTrue();
|
|
});
|
|
|
|
/**
|
|
* Ein Name aus der Umgebung käme beim nächsten Abgleich wieder. Ein Knopf, der
|
|
* nichts bewirkt, ist schlimmer als keiner — er wird einmal geglaubt.
|
|
*/
|
|
it('will not let go of a hostname that comes from the environment', function () {
|
|
proxyPage();
|
|
$ausDerKonfiguration = ProxyHost::query()->where('source', 'config')->firstOrFail();
|
|
|
|
proxyPage()->call('remove', $ausDerKonfiguration->id)
|
|
->assertSee(__('proxy.remove_from_config'));
|
|
|
|
expect(ProxyHost::query()->whereKey($ausDerKonfiguration->id)->exists())->toBeTrue();
|
|
});
|
|
|
|
/**
|
|
* Die Übersicht in Zahlen — das, was ein Betreiber wissen will, bevor er eine
|
|
* Liste liest.
|
|
*/
|
|
it('counts what is valid, what expires soon and what is broken', function () {
|
|
proxyPage();
|
|
ProxyHost::query()->delete();
|
|
|
|
ProxyHost::create(['hostname' => 'gut.clupilot.com', 'purpose' => 'public'])
|
|
->forceFill(['certificate_expires_at' => now()->addDays(70), 'certificate_checked_at' => now()])->save();
|
|
ProxyHost::create(['hostname' => 'knapp.clupilot.com', 'purpose' => 'public'])
|
|
->forceFill(['certificate_expires_at' => now()->addDays(9), 'certificate_checked_at' => now()])->save();
|
|
ProxyHost::create(['hostname' => 'kaputt.clupilot.com', 'purpose' => 'public'])
|
|
->forceFill(['certificate_checked_at' => now(), 'certificate_error' => 'tlsv1 alert internal error'])->save();
|
|
|
|
$summary = Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
|
|
->test(ProxyHosts::class)
|
|
->viewData('summary');
|
|
|
|
expect($summary['valid'])->toBe(1)
|
|
->and($summary['soon'])->toBe(1)
|
|
->and($summary['failing'])->toBe(1);
|
|
});
|