coolify/tests/Unit/DigitalOceanServerStateTest.php
Andras Bacsai e01b8a057e fix(servers): retain cloud instances awaiting IPs
Persist DigitalOcean, Hetzner, and Vultr servers before public IP
assignment, then backfill placeholder addresses from provider state.

Treat partial Sentinel snapshots as non-authoritative and document the
destinations API with OpenAPI schemas.
2026-07-11 21:35:10 +02:00

101 lines
3.3 KiB
PHP

<?php
use App\Models\CloudProviderToken;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
function createDigitalOceanServerForStateTest(string $status = 'active', array $attributes = []): Server
{
$team = Team::create([
'name' => 'Test Team',
'personal_team' => false,
]);
$token = CloudProviderToken::create([
'team_id' => $team->id,
'provider' => 'digitalocean',
'token' => 'test-digitalocean-token',
'name' => 'DigitalOcean',
]);
$privateKey = PrivateKey::factory()->create(['team_id' => $team->id]);
return Server::factory()->create(array_merge([
'team_id' => $team->id,
'private_key_id' => $privateKey->id,
'cloud_provider_token_id' => $token->id,
'digitalocean_droplet_id' => 987,
'digitalocean_droplet_status' => $status,
], $attributes));
}
it('marks a DigitalOcean droplet as deleted when the provider returns 404', function () {
$server = createDigitalOceanServerForStateTest();
Http::fake([
'https://api.digitalocean.com/v2/droplets/987' => Http::response(['message' => 'not found'], 404),
]);
expect($server->refreshDigitalOceanState())->toBe('deleted');
expect($server->fresh()->digitalocean_droplet_status)->toBe('deleted');
});
it('backfills a placeholder IP from the DigitalOcean droplet state', function () {
$server = createDigitalOceanServerForStateTest('new', ['ip' => Server::PLACEHOLDER_IP]);
Http::fake([
'https://api.digitalocean.com/v2/droplets/987' => Http::response([
'droplet' => [
'id' => 987,
'status' => 'active',
'networks' => [
'v4' => [
['type' => 'public', 'ip_address' => '203.0.113.10'],
],
],
],
], 200),
]);
expect($server->refreshDigitalOceanState())->toBe('active');
expect($server->fresh()->ip)->toBe('203.0.113.10');
});
it('does not overwrite a real IP from the DigitalOcean droplet state', function () {
$server = createDigitalOceanServerForStateTest('active', ['ip' => '198.51.100.20']);
Http::fake([
'https://api.digitalocean.com/v2/droplets/987' => Http::response([
'droplet' => [
'id' => 987,
'status' => 'active',
'networks' => [
'v4' => [
['type' => 'public', 'ip_address' => '203.0.113.10'],
],
],
],
], 200),
]);
$server->refreshDigitalOceanState();
expect($server->fresh()->ip)->toBe('198.51.100.20');
});
it('does not mark a DigitalOcean droplet as deleted on transient provider errors', function () {
$server = createDigitalOceanServerForStateTest();
Http::fake([
'https://api.digitalocean.com/v2/droplets/987' => Http::response(['message' => 'server error'], 500),
]);
expect(fn () => $server->refreshDigitalOceanState())
->toThrow(Exception::class);
expect($server->fresh()->digitalocean_droplet_status)->toBe('active');
});