diff --git a/app/Actions/Server/ValidateServer.php b/app/Actions/Server/ValidateServer.php index 57be1a5a60..6c0b86f284 100644 --- a/app/Actions/Server/ValidateServer.php +++ b/app/Actions/Server/ValidateServer.php @@ -3,6 +3,7 @@ namespace App\Actions\Server; use App\Models\Server; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; class ValidateServer @@ -38,7 +39,13 @@ class ValidateServer 'validation_logs' => null, ]); if ($server->hetzner_server_id) { - $status = $server->refreshHetznerState(); + try { + $status = $server->refreshHetznerState(); + } catch (\Throwable $e) { + // The provider API can be down or the token revoked; the SSH checks below still decide. + Log::warning('Could not refresh the Hetzner server status before validation', ['server_id' => $server->id, 'error' => $e->getMessage()]); + $status = null; + } if (in_array($status, ['initializing', 'starting', 'stopping', 'off', 'deleting', 'rebuilding', 'migrating'], true)) { $this->error = $status === 'off' ? 'Hetzner server is off. Power it on before validating.' diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index 748c53ba2d..2da0016e00 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -8,6 +8,7 @@ use App\Events\ServerValidated; use App\Jobs\CheckAndStartSentinelJob; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\Log; use Livewire\Attributes\Locked; use Livewire\Component; @@ -117,7 +118,13 @@ class ValidateAndInstall extends Component $this->authorize('update', $this->server); Server::throttleValidation($this->server->team_id, $this->server->ip); if ($this->server->hetzner_server_id) { - $status = $this->server->refreshHetznerState(); + try { + $status = $this->server->refreshHetznerState(); + } catch (\Throwable $e) { + // The provider API can be down or the token revoked; the SSH checks below still decide. + Log::warning('Could not refresh the Hetzner server status before validation', ['server_id' => $this->server->id, 'error' => $e->getMessage()]); + $status = null; + } $this->server->refresh(); if (in_array($status, ['initializing', 'starting', 'stopping', 'off', 'deleting', 'rebuilding', 'migrating'], true)) { diff --git a/tests/Feature/HetznerServerValidationTest.php b/tests/Feature/HetznerServerValidationTest.php index e87fd380ba..0d6cd91fb4 100644 --- a/tests/Feature/HetznerServerValidationTest.php +++ b/tests/Feature/HetznerServerValidationTest.php @@ -10,6 +10,7 @@ use App\Models\Team; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Process; use Livewire\Livewire; uses(RefreshDatabase::class); @@ -84,3 +85,19 @@ it('blocks validation modal connection checks when the Hetzner server is off', f 'validation_logs' => 'Hetzner server is off. Power it on before validating.', ]); }); + +it('validates over SSH when the Hetzner API call fails', function () { + Http::fake(['https://api.hetzner.cloud/v1/servers/456' => Http::response(['error' => ['code' => 'unauthorized']], 401)]); + $this->server->update(['ip' => '203.0.113.10']); + Process::fake(); + + Livewire::test(ValidateAndInstall::class, ['server' => $this->server]) + ->call('validateConnection') + ->assertSet('error', null) + ->assertSet('uptime', true) + ->assertDispatched('validateOS'); + + Process::fake(['*' => Process::result(errorOutput: 'ssh: connect to host 203.0.113.10 port 22: Connection refused', exitCode: 255)]); + + expect(fn () => ValidateServer::run($this->server))->toThrow(Exception::class, 'Connection refused'); +});