From 2c253039031499deca29487636e2fb55a1dd8247 Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 14 Jun 2026 10:24:42 +0200 Subject: [PATCH] feat(ssh): add FleetService::testConnection credential probe Connect + trivial exec against a server's stored credential; never throws, returns {ok,error}. Used by the create-server guard to verify the SSH login before persisting. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Services/FleetService.php | 23 +++++++++++++++++++ tests/Feature/FleetTestConnectionTest.php | 27 +++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/Feature/FleetTestConnectionTest.php diff --git a/app/Services/FleetService.php b/app/Services/FleetService.php index 517a462..7a48792 100644 --- a/app/Services/FleetService.php +++ b/app/Services/FleetService.php @@ -118,6 +118,29 @@ class FleetService return array_map(fn (array $l): string => implode("\n", $l), $parts); } + /** + * Probe the server's stored credential: connect + a trivial exec. NEVER throws — + * returns the failure reason instead, for the create-server guard to surface. + * Uses a short timeout so an unreachable host fails fast. + * + * @return array{ok: bool, error: ?string} + */ + public function testConnection(Server $server): array + { + try { + $ssh = (new SshClient($this->vault, timeout: 10))->connect($server); + try { + [, $code] = $ssh->run('true'); + } finally { + $ssh->disconnect(); + } + + return ['ok' => $code === 0, 'error' => null]; + } catch (Throwable $e) { + return ['ok' => false, 'error' => $e->getMessage()]; + } + } + // ── Metrics (light — used by the poller and dashboard fallback) ────── /** @return array{cpu:int,mem:int,disk:int,load:float} */ diff --git a/tests/Feature/FleetTestConnectionTest.php b/tests/Feature/FleetTestConnectionTest.php new file mode 100644 index 0000000..eec06a1 --- /dev/null +++ b/tests/Feature/FleetTestConnectionTest.php @@ -0,0 +1,27 @@ + 'x', 'ip' => '127.0.0.1', 'ssh_port' => 1, 'status' => 'pending']); + $server->credential()->create([ + 'username' => 'root', 'auth_type' => 'password', 'secret' => 'nope', + ]); + $server->load('credential'); + + $res = app(FleetService::class)->testConnection($server); + + $this->assertFalse($res['ok']); + $this->assertNotNull($res['error']); + } +}