mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-21 08:25:45 +00:00
fix(vultr): validate public network and token scope
Require IPv6 when public IPv4 is disabled, constrain cloud token lookup by team and provider during deletion, and encode Vultr instance IDs in API paths.
This commit is contained in:
@@ -35,7 +35,7 @@ class DeleteServer
|
||||
);
|
||||
}
|
||||
|
||||
ray($server ? 'Deleting server from Coolify' : 'Server already deleted from Coolify, skipping Coolify deletion');
|
||||
logger()->debug($server ? 'Deleting server from Coolify' : 'Server already deleted from Coolify, skipping Coolify deletion');
|
||||
|
||||
// If server is already deleted from Coolify, skip this part
|
||||
if (! $server) {
|
||||
@@ -59,7 +59,10 @@ class DeleteServer
|
||||
$token = null;
|
||||
|
||||
if ($cloudProviderTokenId) {
|
||||
$token = CloudProviderToken::find($cloudProviderTokenId);
|
||||
$token = CloudProviderToken::where('id', $cloudProviderTokenId)
|
||||
->where('team_id', $teamId)
|
||||
->where('provider', 'hetzner')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $token) {
|
||||
@@ -97,7 +100,10 @@ class DeleteServer
|
||||
$token = null;
|
||||
|
||||
if ($cloudProviderTokenId) {
|
||||
$token = CloudProviderToken::find($cloudProviderTokenId);
|
||||
$token = CloudProviderToken::where('id', $cloudProviderTokenId)
|
||||
->where('team_id', $teamId)
|
||||
->where('provider', 'vultr')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $token) {
|
||||
@@ -107,7 +113,7 @@ class DeleteServer
|
||||
}
|
||||
|
||||
if (! $token) {
|
||||
ray('No Vultr token found for team, skipping Vultr deletion', [
|
||||
logger()->debug('No Vultr token found for team, skipping Vultr deletion', [
|
||||
'team_id' => $teamId,
|
||||
'vultr_instance_id' => $vultrInstanceId,
|
||||
]);
|
||||
@@ -118,12 +124,12 @@ class DeleteServer
|
||||
$vultrService = new VultrService($token->token);
|
||||
$vultrService->deleteInstance($vultrInstanceId);
|
||||
|
||||
ray('Deleted server from Vultr', [
|
||||
logger()->debug('Deleted server from Vultr', [
|
||||
'vultr_instance_id' => $vultrInstanceId,
|
||||
'team_id' => $teamId,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
ray('Failed to delete server from Vultr', [
|
||||
logger()->error('Failed to delete server from Vultr', [
|
||||
'error' => $e->getMessage(),
|
||||
'vultr_instance_id' => $vultrInstanceId,
|
||||
'team_id' => $teamId,
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Rules\ValidHostname;
|
||||
use App\Services\VultrService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class VultrController extends Controller
|
||||
{
|
||||
@@ -54,7 +55,22 @@ class VultrController extends Controller
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function regions(Request $request)
|
||||
#[OA\Get(
|
||||
summary: 'Get Vultr Regions',
|
||||
description: 'Get all available Vultr regions.',
|
||||
path: '/vultr/regions',
|
||||
operationId: 'get-vultr-regions',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Vultr'],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'List of Vultr regions.'),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
]
|
||||
)]
|
||||
public function regions(Request $request): JsonResponse
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
@@ -68,7 +84,22 @@ class VultrController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function plans(Request $request)
|
||||
#[OA\Get(
|
||||
summary: 'Get Vultr Plans',
|
||||
description: 'Get all available Vultr plans.',
|
||||
path: '/vultr/plans',
|
||||
operationId: 'get-vultr-plans',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Vultr'],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'List of Vultr plans.'),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
]
|
||||
)]
|
||||
public function plans(Request $request): JsonResponse
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
@@ -82,7 +113,22 @@ class VultrController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function operatingSystems(Request $request)
|
||||
#[OA\Get(
|
||||
summary: 'Get Vultr Operating Systems',
|
||||
description: 'Get all available Vultr operating systems.',
|
||||
path: '/vultr/os',
|
||||
operationId: 'get-vultr-operating-systems',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Vultr'],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'List of Vultr operating systems.'),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
]
|
||||
)]
|
||||
public function operatingSystems(Request $request): JsonResponse
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
@@ -96,7 +142,22 @@ class VultrController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function sshKeys(Request $request)
|
||||
#[OA\Get(
|
||||
summary: 'Get Vultr SSH Keys',
|
||||
description: 'Get all Vultr SSH keys available to the selected token.',
|
||||
path: '/vultr/ssh-keys',
|
||||
operationId: 'get-vultr-ssh-keys',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Vultr'],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'List of Vultr SSH keys.'),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
]
|
||||
)]
|
||||
public function sshKeys(Request $request): JsonResponse
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
@@ -110,7 +171,23 @@ class VultrController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function createServer(Request $request)
|
||||
#[OA\Post(
|
||||
summary: 'Create Vultr Server',
|
||||
description: 'Create a Vultr instance and link it as a Coolify server.',
|
||||
path: '/servers/vultr',
|
||||
operationId: 'create-vultr-server',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Vultr'],
|
||||
responses: [
|
||||
new OA\Response(response: 201, description: 'Vultr server created.'),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 422, description: 'Validation failed.'),
|
||||
new OA\Response(response: 429, description: 'Vultr API rate limit exceeded.'),
|
||||
]
|
||||
)]
|
||||
public function createServer(Request $request): JsonResponse
|
||||
{
|
||||
$allowedFields = [
|
||||
'cloud_provider_token_uuid',
|
||||
@@ -187,6 +264,10 @@ class VultrController extends Controller
|
||||
$request->offsetSet('instant_validate', false);
|
||||
}
|
||||
|
||||
if ($request->disable_public_ipv4 && ! $request->enable_ipv6) {
|
||||
return $this->networkConfigurationErrorResponse();
|
||||
}
|
||||
|
||||
$token = CloudProviderToken::whereTeamId($teamId)
|
||||
->whereUuid($this->getCloudProviderTokenUuid($request))
|
||||
->where('provider', 'vultr')
|
||||
@@ -308,4 +389,14 @@ class VultrController extends Controller
|
||||
|
||||
return implode(' ', array_slice($parts ?: [], 0, 2));
|
||||
}
|
||||
|
||||
private function networkConfigurationErrorResponse(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'enable_ipv6' => ['Enable IPv6 when disabling public IPv4.'],
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +330,9 @@ class ByVultr extends Component
|
||||
public function submit(): mixed
|
||||
{
|
||||
$this->validate();
|
||||
if (! $this->hasValidPublicNetworkConfiguration()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('create', Server::class);
|
||||
@@ -414,4 +417,15 @@ class ByVultr extends Component
|
||||
|
||||
return implode(' ', array_slice($parts ?: [], 0, 2));
|
||||
}
|
||||
|
||||
private function hasValidPublicNetworkConfiguration(): bool
|
||||
{
|
||||
if (! $this->disable_public_ipv4 || $this->enable_ipv6) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->addError('enable_ipv6', 'Enable IPv6 when disabling public IPv4.');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,19 +153,19 @@ class VultrService
|
||||
|
||||
public function getInstance(string $instanceId): array
|
||||
{
|
||||
$response = $this->request('get', "/instances/{$instanceId}");
|
||||
$response = $this->request('get', $this->instanceEndpoint($instanceId));
|
||||
|
||||
return $response['instance'] ?? [];
|
||||
}
|
||||
|
||||
public function startInstance(string $instanceId): array
|
||||
{
|
||||
return $this->request('post', "/instances/{$instanceId}/start");
|
||||
return $this->request('post', $this->instanceEndpoint($instanceId).'/start');
|
||||
}
|
||||
|
||||
public function deleteInstance(string $instanceId): void
|
||||
{
|
||||
$this->request('delete', "/instances/{$instanceId}");
|
||||
$this->request('delete', $this->instanceEndpoint($instanceId));
|
||||
}
|
||||
|
||||
public function getInstances(): array
|
||||
@@ -183,4 +183,9 @@ class VultrService
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function instanceEndpoint(string $instanceId): string
|
||||
{
|
||||
return '/instances/'.rawurlencode($instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
+141
-2
@@ -4040,7 +4040,8 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hetzner",
|
||||
"digitalocean"
|
||||
"digitalocean",
|
||||
"vultr"
|
||||
]
|
||||
},
|
||||
"team_id": {
|
||||
@@ -4098,7 +4099,8 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"hetzner",
|
||||
"digitalocean"
|
||||
"digitalocean",
|
||||
"vultr"
|
||||
],
|
||||
"example": "hetzner",
|
||||
"description": "The cloud provider."
|
||||
@@ -14156,6 +14158,139 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/vultr\/regions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Vultr"
|
||||
],
|
||||
"summary": "Get Vultr Regions",
|
||||
"description": "Get all available Vultr regions.",
|
||||
"operationId": "get-vultr-regions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of Vultr regions."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/vultr\/plans": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Vultr"
|
||||
],
|
||||
"summary": "Get Vultr Plans",
|
||||
"description": "Get all available Vultr plans.",
|
||||
"operationId": "get-vultr-plans",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of Vultr plans."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/vultr\/os": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Vultr"
|
||||
],
|
||||
"summary": "Get Vultr Operating Systems",
|
||||
"description": "Get all available Vultr operating systems.",
|
||||
"operationId": "get-vultr-operating-systems",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of Vultr operating systems."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/vultr\/ssh-keys": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Vultr"
|
||||
],
|
||||
"summary": "Get Vultr SSH Keys",
|
||||
"description": "Get all Vultr SSH keys available to the selected token.",
|
||||
"operationId": "get-vultr-ssh-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of Vultr SSH keys."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/servers\/vultr": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Vultr"
|
||||
],
|
||||
"summary": "Create Vultr Server",
|
||||
"description": "Create a Vultr instance and link it as a Coolify server.",
|
||||
"operationId": "create-vultr-server",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Vultr server created."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation failed."
|
||||
},
|
||||
"429": {
|
||||
"description": "Vultr API rate limit exceeded."
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -15458,6 +15593,10 @@
|
||||
{
|
||||
"name": "Teams",
|
||||
"description": "Teams"
|
||||
},
|
||||
{
|
||||
"name": "Vultr",
|
||||
"description": "Vultr"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+92
-2
@@ -2577,7 +2577,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
properties: { uuid: { type: string }, name: { type: string }, provider: { type: string, enum: [hetzner, digitalocean] }, team_id: { type: integer }, servers_count: { type: integer }, created_at: { type: string }, updated_at: { type: string } }
|
||||
properties: { uuid: { type: string }, name: { type: string }, provider: { type: string, enum: [hetzner, digitalocean, vultr] }, team_id: { type: integer }, servers_count: { type: integer }, created_at: { type: string }, updated_at: { type: string } }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
@@ -2605,7 +2605,7 @@ paths:
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
enum: [hetzner, digitalocean]
|
||||
enum: [hetzner, digitalocean, vultr]
|
||||
example: hetzner
|
||||
description: 'The cloud provider.'
|
||||
token:
|
||||
@@ -8984,6 +8984,93 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/vultr/regions:
|
||||
get:
|
||||
tags:
|
||||
- Vultr
|
||||
summary: 'Get Vultr Regions'
|
||||
description: 'Get all available Vultr regions.'
|
||||
operationId: get-vultr-regions
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of Vultr regions.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/vultr/plans:
|
||||
get:
|
||||
tags:
|
||||
- Vultr
|
||||
summary: 'Get Vultr Plans'
|
||||
description: 'Get all available Vultr plans.'
|
||||
operationId: get-vultr-plans
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of Vultr plans.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/vultr/os:
|
||||
get:
|
||||
tags:
|
||||
- Vultr
|
||||
summary: 'Get Vultr Operating Systems'
|
||||
description: 'Get all available Vultr operating systems.'
|
||||
operationId: get-vultr-operating-systems
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of Vultr operating systems.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/vultr/ssh-keys:
|
||||
get:
|
||||
tags:
|
||||
- Vultr
|
||||
summary: 'Get Vultr SSH Keys'
|
||||
description: 'Get all Vultr SSH keys available to the selected token.'
|
||||
operationId: get-vultr-ssh-keys
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of Vultr SSH keys.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/servers/vultr:
|
||||
post:
|
||||
tags:
|
||||
- Vultr
|
||||
summary: 'Create Vultr Server'
|
||||
description: 'Create a Vultr instance and link it as a Coolify server.'
|
||||
operationId: create-vultr-server
|
||||
responses:
|
||||
'201':
|
||||
description: 'Vultr server created.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'422':
|
||||
description: 'Validation failed.'
|
||||
'429':
|
||||
description: 'Vultr API rate limit exceeded.'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
components:
|
||||
schemas:
|
||||
Application:
|
||||
@@ -9925,3 +10012,6 @@ tags:
|
||||
-
|
||||
name: Teams
|
||||
description: Teams
|
||||
-
|
||||
name: Vultr
|
||||
description: Vultr
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
@if ($provider === 'vultr')
|
||||
Open Account → API Access.
|
||||
<br><br>
|
||||
Don't have a Vultr account? <a href='https://www.vultr.com/register-steps/' target='_blank'
|
||||
Don't have a Vultr account? <a href='https://www.vultr.com/?ref=9911335' target='_blank'
|
||||
class='underline dark:text-white'>Sign up here</a>
|
||||
<br>
|
||||
<span class="text-xs">Coolify's affiliate link, only new accounts - supports us</span>
|
||||
@@ -74,7 +74,7 @@
|
||||
@if ($provider === 'vultr')
|
||||
Open Account → API Access.
|
||||
<br><br>
|
||||
Don't have a Vultr account? <a href='https://www.vultr.com/register-steps/' target='_blank'
|
||||
Don't have a Vultr account? <a href='https://www.vultr.com/?ref=9911335' target='_blank'
|
||||
class='underline dark:text-white'>Sign up here</a>
|
||||
<br>
|
||||
<span class="text-xs">Coolify's affiliate link, only new accounts - supports us</span>
|
||||
|
||||
@@ -371,4 +371,23 @@ describe('POST /api/v1/servers/vultr', function () {
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonFragment(['ip' => '2001:db8::1']);
|
||||
});
|
||||
|
||||
test('requires IPv6 when public IPv4 is disabled', function () {
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/servers/vultr', [
|
||||
'cloud_provider_token_id' => $this->vultrToken->uuid,
|
||||
'region' => 'ewr',
|
||||
'plan' => 'vc2-1c-1gb',
|
||||
'os_id' => 2284,
|
||||
'name' => 'test-server',
|
||||
'private_key_uuid' => $this->privateKey->uuid,
|
||||
'enable_ipv6' => false,
|
||||
'disable_public_ipv4' => true,
|
||||
]);
|
||||
|
||||
$response->assertStatus(422);
|
||||
$response->assertJsonValidationErrors(['enable_ipv6']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,3 +133,18 @@ it('creates a Vultr server through the Livewire flow', function () {
|
||||
&& $request['user_data'] === base64_encode("#cloud-config\npackages:\n - curl");
|
||||
});
|
||||
});
|
||||
|
||||
it('requires IPv6 when public IPv4 is disabled', function () {
|
||||
Livewire::test(ByVultr::class)
|
||||
->set('selected_token_id', $this->vultrToken->id)
|
||||
->set('current_step', 2)
|
||||
->set('server_name', 'test-vultr-server')
|
||||
->set('selected_region', 'ewr')
|
||||
->set('selected_plan', 'vc2-1c-1gb')
|
||||
->set('selected_os_id', 2284)
|
||||
->set('private_key_id', $this->privateKey->id)
|
||||
->set('enable_ipv6', false)
|
||||
->set('disable_public_ipv4', true)
|
||||
->call('submit')
|
||||
->assertHasErrors(['enable_ipv6']);
|
||||
});
|
||||
|
||||
@@ -81,3 +81,38 @@ it('deletes a Vultr instance when requested', function () {
|
||||
|
||||
expect(Server::withTrashed()->find($server->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('does not use another team Vultr token when deleting an instance', function () {
|
||||
Http::fake([
|
||||
'https://api.vultr.com/v2/instances/instance-1' => Http::response([], 204),
|
||||
]);
|
||||
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherToken = CloudProviderToken::create([
|
||||
'team_id' => $otherTeam->id,
|
||||
'provider' => 'vultr',
|
||||
'token' => 'other-team-vultr-token',
|
||||
'name' => 'Other Team Vultr Token',
|
||||
]);
|
||||
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
'cloud_provider_token_id' => $this->vultrToken->id,
|
||||
'vultr_instance_id' => 'instance-1',
|
||||
]);
|
||||
|
||||
$server->delete();
|
||||
|
||||
DeleteServer::run(
|
||||
serverId: $server->id,
|
||||
cloudProviderTokenId: $otherToken->id,
|
||||
teamId: $this->team->id,
|
||||
deleteFromVultr: true,
|
||||
vultrInstanceId: 'instance-1'
|
||||
);
|
||||
|
||||
$request = Http::recorded()->first()[0];
|
||||
|
||||
expect($request->header('Authorization'))->toBe(['Bearer test-vultr-token']);
|
||||
});
|
||||
|
||||
@@ -150,3 +150,17 @@ it('handles empty successful responses from Vultr API', function () {
|
||||
|
||||
Http::assertSentCount(2);
|
||||
});
|
||||
|
||||
it('encodes instance IDs in request paths', function () {
|
||||
Http::fake([
|
||||
'https://api.vultr.com/v2/instances/instance%2F1' => Http::response([
|
||||
'instance' => ['id' => 'instance/1'],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$service = new VultrService('fake-token');
|
||||
|
||||
expect($service->getInstance('instance/1')['id'])->toBe('instance/1');
|
||||
|
||||
Http::assertSent(fn ($request) => $request->url() === 'https://api.vultr.com/v2/instances/instance%2F1');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user