clusev/tests/Feature/TerminalTest.php

139 lines
4.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\Terminal\Index;
use App\Models\Server;
use App\Models\SshCredential;
use App\Models\TerminalSession;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class TerminalTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
config()->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'));
}
}