diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 2c2195ea3..e43026a72 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -705,17 +705,17 @@ class ServersController extends Controller $validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) { return str($proxyType->value)->lower(); }); - if ($validProxyTypes->contains(str($request->proxy_type)->lower())) { - $server->changeProxy($request->proxy_type, async: true); - } else { + if (! $validProxyTypes->contains(str($request->proxy_type)->lower())) { return response()->json(['message' => 'Invalid proxy type.'], 422); } } - $server->update($request->only(['name', 'description', 'ip', 'port', 'user'])); - if ($request->is_build_server) { - $server->settings()->update([ - 'is_build_server' => $request->is_build_server, - ]); + $updateFields = $request->only(['name', 'description', 'ip', 'port', 'user']); + if ($request->filled('private_key_uuid')) { + $privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first(); + if (! $privateKey) { + return response()->json(['message' => 'Private key not found.'], 404); + } + $updateFields['private_key_id'] = $privateKey->id; } if ($request->has('server_disk_usage_check_frequency') && ! validate_cron_expression($request->server_disk_usage_check_frequency)) { @@ -725,11 +725,22 @@ class ServersController extends Controller ], 422); } + $server->update($updateFields); + if ($request->has('is_build_server')) { + $server->settings()->update([ + 'is_build_server' => $request->boolean('is_build_server'), + ]); + } + $advancedSettings = $request->only(['concurrent_builds', 'dynamic_timeout', 'deployment_queue_limit', 'server_disk_usage_notification_threshold', 'server_disk_usage_check_frequency', 'connection_timeout']); if (! empty($advancedSettings)) { $server->settings()->update(array_filter($advancedSettings, fn ($value) => ! is_null($value))); } + if ($request->proxy_type) { + $server->changeProxy($request->proxy_type, async: true); + } + if ($request->instant_validate) { ValidateServer::dispatch($server); } diff --git a/tests/Feature/ServerUpdatePrivateKeyApiTest.php b/tests/Feature/ServerUpdatePrivateKeyApiTest.php new file mode 100644 index 000000000..8bc5f1d0e --- /dev/null +++ b/tests/Feature/ServerUpdatePrivateKeyApiTest.php @@ -0,0 +1,130 @@ +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); +} + +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 disable build server mode via API', function () { + $this->server->settings()->update(['is_build_server' => true]); + + patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [ + 'is_build_server' => false, + ])->assertCreated() + ->assertJson(['uuid' => $this->server->uuid]); + + expect($this->server->settings->fresh()->is_build_server)->toBeFalse(); +}); + +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(['is_build_server' => false]); + + patchServerUpdatePrivateKeyApi($this, $this->server, $this->bearerToken, [ + 'name' => 'Renamed Server', + 'is_build_server' => true, + '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->is_build_server)->toBeFalse() + ->and($server->proxy->get('type'))->toBe('TRAEFIK'); +});