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 hostCredential(): HostCredential { return HostCredential::create([ 'host' => 'host.docker.internal', 'port' => 2200, 'username' => 'root', 'auth_type' => 'password', 'secret' => 'r00t-pw', ]); } 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_when_configured(): void { $this->hostCredential(); 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_host_without_a_login_opens_the_setup_modal_and_mints_nothing(): void { Livewire::test(Index::class) ->call('open', 'host') ->assertDispatched('openModal', component: 'modals.host-shell'); $this->assertSame(0, TerminalSession::count()); } 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_the_host_ssh_spec_and_burns_the_token(): void { $this->hostCredential(); $token = $this->token('host'); $this->resolve($token)->assertOk()->assertExactJson([ 'kind' => 'server', // the sidecar SSHes into the host exactly like a fleet server 'host' => 'host.docker.internal', 'port' => 2200, 'username' => 'root', 'auth_type' => 'password', 'secret' => 'r00t-pw', // decrypted by the model cast, only over the internal net 'passphrase' => null, ]); // single use → second resolve fails $this->resolve($token)->assertNotFound(); } public function test_resolve_host_404s_when_no_host_login_is_configured(): void { $token = $this->token('host'); $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')); } // ── host-login modal + server search ─────────────────────────────────────── public function test_host_shell_modal_saves_the_singleton(): void { Livewire::test(HostShell::class) ->set('host', '10.0.0.1') ->set('port', 22) ->set('username', 'root') ->set('authType', 'password') ->set('secret', 'hunter2pw') ->call('save') ->assertDispatched('hostShellSaved'); $hc = HostCredential::current(); $this->assertNotNull($hc); $this->assertSame('10.0.0.1', $hc->host); $this->assertSame('root', $hc->username); $this->assertSame('hunter2pw', $hc->secret); // round-trips through the encrypted cast } public function test_host_shell_edit_keeps_the_stored_secret_when_left_blank(): void { $this->hostCredential(); // secret 'r00t-pw' Livewire::test(HostShell::class) ->assertSet('configured', true) ->set('username', 'admin') // change something, leave secret blank ->call('save'); $hc = HostCredential::current(); $this->assertSame('admin', $hc->username); $this->assertSame('r00t-pw', $hc->secret); // unchanged } public function test_host_shell_requires_a_new_secret_when_switching_auth_method(): void { $this->hostCredential(); // auth_type 'password', secret 'r00t-pw' Livewire::test(HostShell::class) ->set('authType', 'key') // switch method but leave the secret blank ->set('secret', '') ->call('save') ->assertHasErrors('secret'); // The stored login is untouched — no password silently reused as a key. $hc = HostCredential::current(); $this->assertSame('password', $hc->auth_type); $this->assertSame('r00t-pw', $hc->secret); } public function test_search_filters_the_server_rail(): void { Server::create(['name' => 'alpha-web', 'ip' => '10.0.0.1', 'status' => 'online']); Server::create(['name' => 'beta-db', 'ip' => '10.0.0.2', 'status' => 'online']); Livewire::test(Index::class) ->set('search', 'alpha') ->assertSee('alpha-web') ->assertDontSee('beta-db'); } public function test_search_box_appears_only_past_the_threshold(): void { $this->get(route('terminal'))->assertDontSee(__('terminal.search_placeholder')); foreach (range(1, 6) as $i) { Server::create(['name' => 'srv-'.$i, 'ip' => '10.0.0.'.$i, 'status' => 'online']); } $this->get(route('terminal'))->assertSee(__('terminal.search_placeholder')); } }