Files
coolify/tests/Feature/ServerUpdatePrivateKeyApiTest.php
Andras Bacsai 9e7cb60b0d feat: add import restore options, OAuth confirmation, and fixes
- database import: add keep_owners (restore PostgreSQL owners and
  privileges) and sqlite_database (pick the target SQLite file) to the
  API, OpenAPI spec, and import form
- oauth: require password confirmation and support team selection on
  OAuth login
- remote process: send team broadcast events to the team of the server
  that ran the process instead of the causer's user id
- servers API: handle the legacy build server setting
- compose: support parent bind paths and volume driver options
- dns: improve record hints and provider management
- notifications: fix throttling and missing backup notifications
- ui: rework the confirmation modal
- add feature and unit tests for the new behavior
2026-09-30 13:35:22 +02:00

248 lines
9.1 KiB
PHP

<?php
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\TestResponse;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('app.maintenance.driver', 'file');
config()->set('cache.default', 'array');
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
session(['currentTeam' => $this->team]);
$this->oldPrivateKey = createServerUpdatePrivateKeyApiKey($this->team, 'Old Key');
$this->newPrivateKey = createServerUpdatePrivateKeyApiKey($this->team, 'New Key');
$this->server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->oldPrivateKey->id,
]);
$token = $this->user->createToken('write-token', ['write']);
$token->accessToken->forceFill(['team_id' => $this->team->id])->save();
$this->bearerToken = $token->plainTextToken;
});
function createServerUpdatePrivateKeyApiKey(Team $team, string $name): PrivateKey
{
return PrivateKey::create([
'name' => $name,
'private_key' => generateSSHKey('ed25519')['private'],
'team_id' => $team->id,
]);
}
function patchServerUpdatePrivateKeyApi(object $test, Server $server, string $bearerToken, array $payload): TestResponse
{
return $test->withHeaders([
'Authorization' => 'Bearer '.$bearerToken,
'Content-Type' => 'application/json',
])->patchJson('/api/v1/servers/'.$server->uuid, $payload);
}
function postServerApi(object $test, string $bearerToken, array $payload): TestResponse
{
return $test->withHeaders([
'Authorization' => 'Bearer '.$bearerToken,
'Content-Type' => 'application/json',
])->postJson('/api/v1/servers', $payload);
}
it('updates the server private key from private_key_uuid', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'private_key_uuid' => $this->newPrivateKey->uuid,
])->assertCreated()
->assertJson(['uuid' => $this->server->uuid]);
expect($this->server->fresh()->private_key_id)->toBe($this->newPrivateKey->id);
});
it('returns not found for an unknown private_key_uuid and leaves the key unchanged', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'private_key_uuid' => 'unknown-private-key-uuid',
])->assertNotFound()
->assertJson(['message' => 'Private key not found.']);
expect($this->server->fresh()->private_key_id)->toBe($this->oldPrivateKey->id);
});
it('does not allow attaching a private key from another team', function () {
$otherTeam = Team::factory()->create();
$otherTeamPrivateKey = createServerUpdatePrivateKeyApiKey($otherTeam, 'Other Team Key');
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'private_key_uuid' => $otherTeamPrivateKey->uuid,
])->assertNotFound()
->assertJson(['message' => 'Private key not found.']);
expect($this->server->fresh()->private_key_id)->toBe($this->oldPrivateKey->id);
});
it('keeps the existing private key when private_key_uuid is omitted', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'name' => 'Renamed Server',
])->assertCreated()
->assertJson(['uuid' => $this->server->uuid]);
$server = $this->server->fresh();
expect($server->name)->toBe('Renamed Server')
->and($server->private_key_id)->toBe($this->oldPrivateKey->id);
});
it('can change a build-only server to the combined role via API', function () {
$this->server->settings()->update(['server_role' => 'build']);
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'both',
])->assertCreated()
->assertJson(['uuid' => $this->server->uuid]);
$settings = $this->server->settings->fresh();
expect($settings->server_role->value)->toBe('both');
});
it('updates the server role through the API', function (string $role) {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => $role,
])->assertCreated();
$settings = $this->server->settings->fresh();
expect($settings->server_role->value)->toBe($role);
})->with([
'build only' => ['build'],
'deployment and build' => ['both'],
]);
it('accepts the deprecated build server API field as a server role', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'is_build_server' => true,
])->assertCreated();
expect($this->server->settings->fresh()->server_role->value)->toBe('build');
});
it('requires a dedicated build server before selecting deployment only', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'deployment',
])->assertUnprocessable()
->assertJsonValidationErrors('server_role');
$combinedServer = Server::factory()->create(['team_id' => $this->team->id]);
$combinedServer->settings()->update([
'is_reachable' => true,
'is_usable' => true,
'server_role' => 'both',
'is_build_server' => false,
]);
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'deployment',
])->assertUnprocessable()
->assertJsonValidationErrors('server_role');
$buildServer = Server::factory()->create(['team_id' => $this->team->id]);
$buildServer->settings()->update([
'is_reachable' => true,
'is_usable' => true,
'server_role' => 'build',
'is_build_server' => true,
]);
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'deployment',
])->assertCreated();
expect($this->server->settings->fresh()->server_role->value)->toBe('deployment');
});
it('creates a server with an API server role', function () {
$response = postServerApi($this, $this->bearerToken, [
'name' => 'API Build Server',
'ip' => '192.0.2.55',
'private_key_uuid' => $this->oldPrivateKey->uuid,
'server_role' => 'build',
])->assertCreated();
$server = Server::whereUuid($response->json('uuid'))->firstOrFail();
expect($server->settings->server_role->value)->toBe('build')
->and($server->settings->is_build_server)->toBeTrue();
});
it('keeps the legacy build server flag in sync when the API changes the role', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'build',
])->assertCreated();
expect($this->server->settings->fresh()->is_build_server)->toBeTrue();
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'both',
])->assertCreated();
expect($this->server->settings->fresh()->is_build_server)->toBeFalse();
});
it('rejects an invalid API server role', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_role' => 'invalid',
])->assertUnprocessable()
->assertJsonValidationErrors('server_role');
});
it('rejects an invalid disk usage check frequency without partially updating the server', function () {
$this->server->proxy->set('type', 'TRAEFIK');
$this->server->save();
$this->server->settings()->update(['server_role' => 'both']);
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'name' => 'Renamed Server',
'server_role' => 'build',
'proxy_type' => 'none',
'server_disk_usage_check_frequency' => 'not a valid schedule',
])->assertUnprocessable()
->assertJson([
'message' => 'Validation failed.',
'errors' => [
'server_disk_usage_check_frequency' => ['Invalid Cron / Human expression for Disk Usage Check Frequency.'],
],
]);
$server = $this->server->fresh();
expect($server->name)->not->toBe('Renamed Server')
->and($server->settings->server_role->value)->toBe('both')
->and($server->proxy->get('type'))->toBe('TRAEFIK');
});
it('updates the disk usage notification interval', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_disk_usage_notification_interval_hours' => 48,
])->assertCreated();
expect($this->server->settings->fresh()->server_disk_usage_notification_interval_hours)->toBe(48);
});
it('rejects an out of range disk usage notification interval', function () {
patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [
'server_disk_usage_notification_interval_hours' => 0,
])->assertUnprocessable()
->assertJsonValidationErrors('server_disk_usage_notification_interval_hours');
expect($this->server->settings->fresh()->server_disk_usage_notification_interval_hours)->toBe(24);
});