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'); } expect(ProxyHost::query()->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()->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(); });