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'); $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') ->assertDispatched('openModal', component: 'modals.host-shell'); $this->assertSame(0, TerminalSession::count()); } public function test_non_admin_is_refused_the_host_terminal_route(): void { $this->actingAs(User::factory()->operator()->create(['must_change_password' => false])); $this->get(route('terminal'))->assertForbidden(); } // ── 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, 'host_key' => null, // the host has no fleet TOFU pin ]); // 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, 'host_key' => null, // no TOFU pin stored for this test server ]); } public function test_resolve_includes_the_pinned_host_key_when_the_server_has_one(): void { $server = $this->server(); $server->update(['ssh_host_key' => 'ssh-ed25519 AAAAC3NzaPINNED']); $token = $this->token('server', $server->id); $this->resolve($token)->assertOk()->assertJsonPath('host_key', 'ssh-ed25519 AAAAC3NzaPINNED'); } 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_host_terminal_page_renders(): void { $this->get(route('terminal'))->assertOk()->assertSee(__('terminal.host_heading')); } // ── host-login modal + server search ─────────────────────────────────────── public function test_host_shell_modal_saves_the_singleton(): void { Livewire::test(HostShell::class) ->set('port', 22) ->set('username', 'root') ->set('authType', 'password') ->set('secret', 'hunter2pw') ->call('save') ->assertDispatched('hostShellSaved'); $hc = HostCredential::current(); $this->assertNotNull($hc); $this->assertSame('host.docker.internal', $hc->host); // fixed to the local machine, not operator-set $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(); // username 'root', port 2200, secret 'r00t-pw' // Re-save the SAME target (username/port/auth unchanged) with a blank secret → the stored // secret is kept (e.g. the operator just re-confirmed the form). Livewire::test(HostShell::class) ->assertSet('configured', true) ->set('secret', '') ->call('save') ->assertHasNoErrors(); $hc = HostCredential::current(); $this->assertSame('root', $hc->username); $this->assertSame('r00t-pw', $hc->secret); // unchanged } public function test_host_shell_requires_a_new_secret_when_the_username_changes(): void { $this->hostCredential(); // username 'root', secret 'r00t-pw' // Retargeting to a different account with a blank secret is refused — the old account's // secret must not be silently reused for a new username. Livewire::test(HostShell::class) ->set('username', 'admin') ->set('secret', '') ->call('save') ->assertHasErrors('secret'); $hc = HostCredential::current(); $this->assertSame('root', $hc->username); // untouched — save was rejected $this->assertSame('r00t-pw', $hc->secret); } 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); } }