diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 1e77528830..4b0af48c2a 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -11,6 +11,7 @@ use App\Http\Controllers\Controller; use App\Jobs\DeleteResourceJob; use App\Jobs\ValidateAndInstallServerJob; use App\Models\Application; +use App\Models\CloudProviderToken; use App\Models\PrivateKey; use App\Models\Project; use App\Models\Server as ModelsServer; @@ -900,9 +901,24 @@ class ServersController extends Controller return response()->json(['message' => 'Local server cannot be deleted.'], 400); } - $deleteFromProvider = filter_var($request->query('delete_from_provider', false), FILTER_VALIDATE_BOOLEAN); - if ($deleteFromProvider && ! $server->hetzner_server_id && ! $server->vultr_instance_id && ! $server->digitalocean_droplet_id) { - return response()->json(['message' => 'Server is not linked to a cloud provider.'], 422); + $deleteFromProvider = filter_var($request->query('delete_from_provider', false), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if (is_null($deleteFromProvider)) { + return response()->json(['message' => 'delete_from_provider must be a boolean.'], 422); + } + if ($deleteFromProvider) { + $linkedProviders = array_keys(array_filter([ + 'hetzner' => $server->hetzner_server_id, + 'vultr' => $server->vultr_instance_id, + 'digitalocean' => $server->digitalocean_droplet_id, + ])); + if (empty($linkedProviders)) { + return response()->json(['message' => 'Server is not linked to a cloud provider.'], 422); + } + foreach ($linkedProviders as $provider) { + if (! CloudProviderToken::whereTeamId($server->team_id)->whereProvider($provider)->exists()) { + return response()->json(['message' => "No {$provider} token found for this team. Add one before deleting the server from the cloud provider."], 422); + } + } } if ($force) { diff --git a/tests/Feature/Api/ServerDeleteFromProviderApiTest.php b/tests/Feature/Api/ServerDeleteFromProviderApiTest.php index 4e83efd113..190b759488 100644 --- a/tests/Feature/Api/ServerDeleteFromProviderApiTest.php +++ b/tests/Feature/Api/ServerDeleteFromProviderApiTest.php @@ -1,6 +1,7 @@ create(['team_id' => $this->team->id, 'provider' => $provider]); $server = Server::factory()->create(['team_id' => $this->team->id, ...$attributes]); deleteServerViaApi($this->token, $server->uuid, '?delete_from_provider=true')->assertOk(); @@ -51,9 +53,25 @@ it('deletes from the linked cloud provider when delete_from_provider is true', f && $params[5] === true && $params[7] === true); })->with([ - 'hetzner' => [['hetzner_server_id' => 123]], - 'vultr' => [['vultr_instance_id' => 'vultr-instance-id']], - 'digitalocean' => [['digitalocean_droplet_id' => 456]], + 'hetzner' => ['hetzner', ['hetzner_server_id' => 123]], + 'vultr' => ['vultr', ['vultr_instance_id' => 'vultr-instance-id']], + 'digitalocean' => ['digitalocean', ['digitalocean_droplet_id' => 456]], +]); + +it('keeps the server when the team has no token for the linked cloud provider', function (string $provider, array $attributes) { + CloudProviderToken::factory()->create(['provider' => $provider]); + $server = Server::factory()->create(['team_id' => $this->team->id, ...$attributes]); + + deleteServerViaApi($this->token, $server->uuid, '?delete_from_provider=true') + ->assertStatus(422) + ->assertJson(['message' => "No {$provider} token found for this team. Add one before deleting the server from the cloud provider."]); + + expect(Server::find($server->id))->not->toBeNull(); + DeleteServer::assertNotPushed(); +})->with([ + 'hetzner' => ['hetzner', ['hetzner_server_id' => 123]], + 'vultr' => ['vultr', ['vultr_instance_id' => 'vultr-instance-id']], + 'digitalocean' => ['digitalocean', ['digitalocean_droplet_id' => 456]], ]); it('rejects delete_from_provider when the server has no cloud provider link', function () { @@ -78,3 +96,23 @@ it('does not delete a server of another team from the cloud provider', function expect(Server::find($otherServer->id))->not->toBeNull(); DeleteServer::assertNotPushed(); }); + +it('rejects an invalid delete_from_provider value without deleting the server', function (string $value) { + CloudProviderToken::factory()->create(['team_id' => $this->team->id, 'provider' => 'hetzner']); + $server = Server::factory()->create(['team_id' => $this->team->id, 'hetzner_server_id' => 123]); + + deleteServerViaApi($this->token, $server->uuid, "?delete_from_provider={$value}") + ->assertStatus(422) + ->assertJson(['message' => 'delete_from_provider must be a boolean.']); + + expect(Server::find($server->id))->not->toBeNull(); + DeleteServer::assertNotPushed(); +})->with(['ture', 'yes-please', '2']); + +it('deletes only from Coolify when delete_from_provider is false', function () { + $server = Server::factory()->create(['team_id' => $this->team->id, 'hetzner_server_id' => 123]); + + deleteServerViaApi($this->token, $server->uuid, '?delete_from_provider=false')->assertOk(); + + DeleteServer::assertPushed(fn ($action, array $params) => $params[0] === $server->id && $params[1] === false); +});