put('ssh.private_key', "-----BEGIN OPENSSH PRIVATE KEY-----\nTEST\n-----END OPENSSH PRIVATE KEY-----", Operator::factory()->create()); }); it('trägt alles, was die Brücke braucht — und nichts davon im Klartext an den Browser', function () { $host = Host::factory()->active()->create(['ssh_host_key' => 'SHA256:abc']); $operator = Operator::factory()->role('Owner')->create(); $ticket = TerminalTicket::issue($host, $operator); // Undurchsichtig: keine Adresse, kein Name, nichts Ratbares. expect($ticket)->toMatch('/^[a-f0-9]{64}$/') ->and($ticket)->not->toContain($host->wg_ip) ->and($ticket)->not->toContain($host->name); $payload = TerminalTicket::redeem($ticket); expect($payload['host_uuid'])->toBe($host->uuid) ->and($payload['operator_id'])->toBe($operator->id) // Die TUNNELADRESSE, nie die öffentliche: der Container steht im // Tunnel, und die öffentliche IP wäre der Weg, den SecureHostFirewall // ausdrücklich zumacht. ->and($payload['ip'])->toBe($host->wg_ip) ->and($payload['user'])->toBe('root') ->and($payload['fingerprint'])->toBe('SHA256:abc') ->and($payload['private_key'])->toContain('BEGIN OPENSSH PRIVATE KEY'); }); it('trägt genau eine Sitzung', function () { $ticket = TerminalTicket::issue( Host::factory()->active()->create(), Operator::factory()->role('Owner')->create(), ); expect(TerminalTicket::redeem($ticket))->not->toBeNull() // Die zweite Einlösung läuft ins Leere. Das Löschen beim Lesen IST die // Regel — ein Ticket, das zweimal trägt, ist ein Nachschlüssel. ->and(TerminalTicket::redeem($ticket))->toBeNull(); }); it('trägt nach dreißig Sekunden nicht mehr', function () { $ticket = TerminalTicket::issue( Host::factory()->active()->create(), Operator::factory()->role('Owner')->create(), ); $this->travel(TerminalTicket::TTL_SECONDS + 1)->seconds(); expect(TerminalTicket::redeem($ticket))->toBeNull(); }); it('öffnet mit dem Ticket für einen Host keine Sitzung auf einem anderen', function () { $a = Host::factory()->active()->create(); $b = Host::factory()->active()->create(); $operator = Operator::factory()->role('Owner')->create(); $payload = TerminalTicket::redeem(TerminalTicket::issue($a, $operator)); expect($payload['ip'])->toBe($a->wg_ip) ->and($payload['ip'])->not->toBe($b->wg_ip); }); it('gibt kein Ticket ohne hinterlegten Schlüssel aus', function () { // Ein Ticket ohne Schlüssel führt zu einem Fenster, das aufgeht und nie // verbindet — der Fehler gehört hierher, nicht in den Container. app(SecretVault::class)->forget('ssh.private_key'); // Diese Installation hat in .env tatsächlich einen SSH-Schlüssel // konfiguriert (CLUPILOT_SSH_PRIVATE_KEY_PATH), auf den SecretVault::get() // zurückfällt, solange kein Tresor-Eintrag existiert (siehe // OnboardingChecksTest, derselbe Rückfall) — direkt zurückgesetzt, damit // der Test nicht vom Entwicklerrechner abhängt. config()->set('provisioning.ssh.private_key', ''); expect(fn () => TerminalTicket::issue( Host::factory()->active()->create(), Operator::factory()->role('Owner')->create(), ))->toThrow(RuntimeException::class); });