set('clusev.terminal_secret', 'test-secret'); $this->actingAs(User::factory()->create(['must_change_password' => false])); } private function server(): Server { $s = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 2222, 'status' => 'online']); SshCredential::create(['server_id' => $s->id, 'username' => 'deploy', 'auth_type' => 'password', 'secret' => 's3cret-pw']); return $s; } private function token(string $kind, ?int $serverId = null, ?\DateTimeInterface $expires = null): string { $token = 'tok-'.bin2hex(random_bytes(6)); TerminalSession::create([ 'token' => $token, 'user_id' => auth()->id(), 'kind' => $kind, 'server_id' => $serverId, 'expires_at' => $expires ?? now()->addMinute(), 'created_at' => now(), ]); return $token; } private function resolve(string $token, string $secret = 'test-secret') { return $this->withHeader('X-Sidecar-Secret', $secret) ->postJson(route('internal.terminal.resolve'), ['token' => $token]); } // ── token minting (Livewire) ────────────────────────────────────────────── public function test_open_host_mints_a_single_use_host_session(): void { Livewire::test(Index::class)->call('open', 'host')->assertSet('activeKey', 'host'); $s = TerminalSession::first(); $this->assertNotNull($s); $this->assertSame('host', $s->kind); $this->assertNull($s->server_id); $this->assertNull($s->used_at); $this->assertTrue($s->expires_at->isFuture()); } public function test_open_server_mints_a_session_for_that_server(): void { $server = $this->server(); Livewire::test(Index::class)->call('open', 'server', $server->uuid)->assertSet('activeKey', $server->uuid); $this->assertSame($server->id, TerminalSession::first()->server_id); } public function test_open_ignores_an_unknown_server(): void { Livewire::test(Index::class)->call('open', 'server', 'nope'); $this->assertSame(0, TerminalSession::count()); } // ── resolve endpoint ────────────────────────────────────────────────────── public function test_resolve_requires_the_sidecar_secret(): void { $token = $this->token('host'); $this->postJson(route('internal.terminal.resolve'), ['token' => $token])->assertForbidden(); $this->resolve($token, 'wrong-secret')->assertForbidden(); } public function test_resolve_returns_host_kind_and_burns_the_token(): void { $token = $this->token('host'); $this->resolve($token)->assertOk()->assertExactJson(['kind' => 'host']); // single use → second resolve fails $this->resolve($token)->assertNotFound(); } public function test_resolve_returns_the_server_spec_with_decrypted_credentials(): void { $server = $this->server(); $token = $this->token('server', $server->id); $this->resolve($token)->assertOk()->assertExactJson([ 'kind' => 'server', 'host' => '10.0.0.9', 'port' => 2222, 'username' => 'deploy', 'auth_type' => 'password', 'secret' => 's3cret-pw', // decrypted by the model cast, only over the internal net 'passphrase' => null, ]); } public function test_resolve_rejects_an_expired_token(): void { $token = $this->token('host', null, now()->subMinute()); $this->resolve($token)->assertNotFound(); } public function test_resolve_rejects_a_server_token_whose_credential_is_locked(): void { $server = $this->server(); $server->credential->update(['disabled_at' => now()]); $token = $this->token('server', $server->id); $this->resolve($token)->assertNotFound(); } // ── page ────────────────────────────────────────────────────────────────── public function test_terminal_page_renders_with_the_rail(): void { $this->server(); $this->get(route('terminal'))->assertOk()->assertSee(__('terminal.heading')); } }