Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
peaklabs-dev committed 2026-11-02 01:01:41 +01:00
commit 6c17c29925
20 files changed
+201 -111

No files matched your search

+1 -1
View File
@@ -71,7 +71,6 @@ Thank you so much!
### Huge Sponsors
* [Context.dev](https://www.context.dev/) - Web scraping API for AI agents
* [Ginernet](https://ginernet.com/) - Hosting powerful servers in Spain
* [SerpAPI](https://serpapi.com) - Google Search API — Scrape Google and other search engines from our fast, easy, and complete API.
* [MVPS](https://www.mvps.net) - Cheap VPS servers at the highest possible quality
@@ -164,6 +163,7 @@ Thank you so much!
<a href="https://interviewpal.com"><img width="60px" alt="InterviewPal" src="https://cdn.coollabs.io/sponsors/interviewpal.svg"/></a>
<a href="https://decidable.no?utm_source=coolify.io"><img width="60px" alt="Decidable" src="https://github.com/Decidable-AS.png"/></a>
<a href="https://hosthavoc.com"><img width="60px" alt="Host Havoc" src="https://cdn.coollabs.io/sponsors/hosthavoc.png"/></a>
<a href="https://martiinii.dev"><img width="60px" alt="martiinii" src="https://github.com/martiinii.png"/></a>
...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio)
@@ -231,7 +231,7 @@ class ServerDockerRegistriesController extends Controller
#[OA\Post(
summary: 'Check a registry login on a server',
description: 'Check that the saved login of the server for a registry still works. Runs docker login with the saved credentials on the server.',
description: 'Check that the saved login of the server for a registry still works. Runs docker login with the saved credentials on the server. Needs a token with the write and deploy permissions.',
path: '/servers/{uuid}/registries/{registry}/check',
operationId: 'check-server-registry-login',
security: [['bearerAuth' => []]],
-25
View File
@@ -55,11 +55,6 @@ class GithubRunners extends Component
public bool $allowPullRequests = false;
/** Null until checked over SSH. */
public ?bool $isSysboxInstalled = null;
protected $listeners = ['sysboxInstalled' => 'checkSysbox'];
public function mount(string $server_uuid)
{
try {
@@ -178,7 +173,6 @@ class GithubRunners extends Component
$githubApp = $this->selectedApp;
$this->authorize('update', $githubApp);
if ($this->dockerMode === GithubRunnerDockerMode::Sysbox->value && ! InstallSysbox::isInstalled($this->server)) {
$this->isSysboxInstalled = false;
throw ValidationException::withMessages(['dockerMode' => 'Sysbox is not installed on this server. Install it, then save again.']);
}
@@ -258,25 +252,6 @@ class GithubRunners extends Component
}
}
public function checkSysbox(): void
{
$this->authorize('view', $this->server);
$this->isSysboxInstalled = InstallSysbox::isInstalled($this->server);
}
public function installSysbox()
{
$this->authorize('update', $this->server);
try {
$activity = InstallSysbox::run($this->server);
$this->dispatch('sysbox-install-started');
$this->dispatch('activityMonitor', $activity->id, 'sysboxInstalled');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
/**
* Creates the runner group when the App can run workflow jobs. An App that is not ready is saved anyway:
* the page lists what it misses, and the runner group is created when the first job is provisioned.
+28 -1
View File
@@ -850,8 +850,11 @@ class Show extends Component
{
try {
$this->authorize('update', $this->server);
$providerRefreshed = $this->refreshProviderStatus();
if (! $this->server->isFunctional()) {
$this->dispatch('error', 'Validate the server connection before fetching details.');
$providerRefreshed
? $this->dispatch('info', 'Provider status refreshed. Validate the server connection to fetch server details.')
: $this->dispatch('error', 'Validate the server connection before fetching details.');
return;
}
@@ -868,6 +871,30 @@ class Show extends Component
}
}
/**
* Refresh the state of the linked cloud provider server, if there is one.
*/
private function refreshProviderStatus(): bool
{
if (! $this->server->cloudProviderToken || $this->server->isTransferredAway()) {
return false;
}
$check = match (true) {
filled($this->server->hetzner_server_id) => $this->checkHetznerServerStatus(...),
filled($this->server->digitalocean_droplet_id) => $this->checkDigitalOceanDropletStatus(...),
filled($this->server->vultr_instance_id) => $this->checkVultrInstanceStatus(...),
filled($this->server->hostinger_virtual_machine_id) => $this->checkHostingerVirtualMachineStatus(...),
default => null,
};
if (! $check) {
return false;
}
$check();
return true;
}
public function submit()
{
try {
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Livewire\Server;
use App\Actions\Server\InstallSysbox;
use App\Models\Server;
use Illuminate\Contracts\View\View;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Lazy;
use Livewire\Component;
/**
* A separate component, so that its requests do not send the unsaved fields of the runner form.
* Otherwise the check would store a new Docker mode in the form snapshot and hide the unsaved bar.
*/
#[Lazy]
class SysboxStatus extends Component
{
use AuthorizesRequests;
public Server $server;
public bool $isInstalled = false;
protected $listeners = ['sysboxInstalled' => 'checkSysbox'];
public function mount(Server $server): void
{
$this->authorize('view', $server);
$this->server = $server;
$this->checkSysbox();
}
public function checkSysbox(): void
{
$this->authorize('view', $this->server);
$this->isInstalled = InstallSysbox::isInstalled($this->server);
}
public function installSysbox()
{
$this->authorize('update', $this->server);
try {
$activity = InstallSysbox::run($this->server);
$this->dispatch('sysbox-install-started');
$this->dispatch('activityMonitor', $activity->id, 'sysboxInstalled');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function placeholder(): View
{
return view('livewire.server.sysbox-status-placeholder');
}
public function render(): View
{
return view('livewire.server.sysbox-status');
}
}
+1 -1
View File
@@ -18391,7 +18391,7 @@
"Servers"
],
"summary": "Check a registry login on a server",
"description": "Check that the saved login of the server for a registry still works. Runs docker login with the saved credentials on the server.",
"description": "Check that the saved login of the server for a registry still works. Runs docker login with the saved credentials on the server. Needs a token with the write and deploy permissions.",
"operationId": "check-server-registry-login",
"parameters": [
{
+1 -1
View File
@@ -11705,7 +11705,7 @@ paths:
tags:
- Servers
summary: 'Check a registry login on a server'
description: 'Check that the saved login of the server for a registry still works. Runs docker login with the saved credentials on the server.'
description: 'Check that the saved login of the server for a registry still works. Runs docker login with the saved credentials on the server. Needs a token with the write and deploy permissions.'
operationId: check-server-registry-login
parameters:
-
@@ -6,6 +6,7 @@
'dynamic' => false,
'pulse' => false, // ping the dot for a state that is still in progress
'size' => 'md', // sm fits inline next to 12px table text without growing the row
'spinning' => false, // show the loading spinner instead of the dot, e.g. for a running deployment
])
@php
@@ -29,12 +30,19 @@
@if ($dynamic)
{{ $slot }}
@else
@if ($spinning)
<svg class="loading-indicator size-3 shrink-0 animate-spin text-neutral-500 dark:text-fg-dim" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-20" cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" />
<path class="opacity-80" d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
@else
<span class="relative flex size-1.5 shrink-0">
@if ($pulse)
<span class="absolute inline-flex size-full rounded-full opacity-75 motion-safe:animate-ping {{ $dotClasses[$type] ?? $dotClasses['neutral'] }}"></span>
@endif
<span class="relative inline-flex size-1.5 rounded-full {{ $dotClasses[$type] ?? $dotClasses['neutral'] }}"></span>
</span>
@endif
<span class="truncate">{{ collect([$label, $status])->filter()->join(' ') }}</span>
@endif
</button>
@@ -43,12 +51,19 @@
@if ($dynamic)
{{ $slot }}
@else
@if ($spinning)
<svg class="loading-indicator size-3 shrink-0 animate-spin text-neutral-500 dark:text-fg-dim" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-20" cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" />
<path class="opacity-80" d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
@else
<span class="relative flex size-1.5 shrink-0">
@if ($pulse)
<span class="absolute inline-flex size-full rounded-full opacity-75 motion-safe:animate-ping {{ $dotClasses[$type] ?? $dotClasses['neutral'] }}"></span>
@endif
<span class="relative inline-flex size-1.5 rounded-full {{ $dotClasses[$type] ?? $dotClasses['neutral'] }}"></span>
</span>
@endif
<span class="truncate">{{ collect([$label, $status])->filter()->join(' ') }}</span>
@endif
</a>
@@ -57,12 +72,19 @@
@if ($dynamic)
{{ $slot }}
@else
@if ($spinning)
<svg class="loading-indicator size-3 shrink-0 animate-spin text-neutral-500 dark:text-fg-dim" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-20" cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2" />
<path class="opacity-80" d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
@else
<span class="relative flex size-1.5 shrink-0">
@if ($pulse)
<span class="absolute inline-flex size-full rounded-full opacity-75 motion-safe:animate-ping {{ $dotClasses[$type] ?? $dotClasses['neutral'] }}"></span>
@endif
<span class="relative inline-flex size-1.5 rounded-full {{ $dotClasses[$type] ?? $dotClasses['neutral'] }}"></span>
</span>
@endif
<span class="truncate">{{ collect([$label, $status])->filter()->join(' ') }}</span>
@endif
</span>
@@ -81,14 +81,14 @@
{{ $deployment->server_name ?: '-' }}
</p>
<span class="hidden @2xl:block">
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" />
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" :spinning="$deployment->status === 'in_progress'" />
</span>
<p class="hidden truncate text-[12px] text-neutral-500 @2xl:block dark:text-fg-dim">
{{ $deployment->created_at?->diffForHumans() ?? '-' }}
</p>
<div class="mt-3 flex min-w-0 items-center gap-3 @2xl:hidden">
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" />
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" :spinning="$deployment->status === 'in_progress'" />
<p class="min-w-0 truncate text-[11px] text-neutral-500 dark:text-fg-faint">
{{ $environmentPath ?: '-' }}
<span class="px-1 text-neutral-300 dark:text-white/15">·</span>
@@ -141,14 +141,14 @@
{{ $deployment->server_name ?: '-' }}
</p>
<span class="hidden @2xl:block">
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" />
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" :spinning="$deployment->status === 'in_progress'" />
</span>
<p class="hidden truncate text-[12px] text-neutral-500 @2xl:block dark:text-fg-dim">
{{ $deployment->created_at?->diffForHumans() ?? '-' }}
</p>
<div class="mt-3 flex min-w-0 items-center gap-3 @2xl:hidden">
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" />
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" :spinning="$deployment->status === 'in_progress'" />
<p class="min-w-0 truncate text-[11px] text-neutral-500 dark:text-fg-faint">
{{ $environmentPath ?: '-' }}
<span class="px-1 text-neutral-300 dark:text-white/15">·</span>
@@ -48,7 +48,7 @@
<p class="truncate text-[13px] font-semibold text-black dark:text-fg">
{{ $deployment->application_name }}
</p>
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType"
<x-status-badge :status="$deploymentStatus" :type="$deploymentStatusType" :spinning="$deployment->status === 'in_progress'"
class="shrink-0" />
</div>
@@ -279,6 +279,7 @@
@unless ($embedded)
<span class="min-w-0">
<x-status-badge as="button" :status="$statusLabel" :type="$statusType"
:spinning="$deploymentStatus === 'in_progress'"
class="table-filter-value table-filter-badge" title="Filter by {{ $statusLabel }}"
wire:click="toggleDeploymentFilter('status:{{ $deploymentStatus }}')" />
</span>
@@ -336,6 +337,10 @@
<span>{{ \Carbon\Carbon::parse(data_get($deployment, 'created_at'))->diffForHumans(syntax: \Carbon\CarbonInterface::DIFF_ABSOLUTE, short: true) }}</span>
<x-loading compact x-cloak x-show="selecting === '{{ data_get($deployment, 'deployment_uuid') }}'"
aria-label="Loading deployment logs..." />
@if ($deploymentStatus === 'in_progress')
<x-loading compact x-show="selecting !== '{{ data_get($deployment, 'deployment_uuid') }}'"
aria-label="Deployment in progress" />
@else
<span x-show="selecting !== '{{ data_get($deployment, 'deployment_uuid') }}'" @class([
'size-2 shrink-0 rounded-full',
'bg-emerald-500' => $statusType === 'success',
@@ -343,6 +348,7 @@
'bg-red-500' => $statusType === 'error',
'bg-neutral-400 dark:bg-neutral-500' => $statusType === 'neutral',
])></span>
@endif
</a>
@endif
<a href="{{ $deploymentUrl }}" {!! $deploymentLinkAttributes !!} class="deployment-row-link absolute inset-0"
@@ -336,6 +336,9 @@
</button>
</div>
<span class="flex shrink-0 items-center gap-1.5 text-xs font-medium text-neutral-600 dark:text-fg-dim">
@if ($deploymentStatus === 'in_progress')
<x-loading compact />
@else
<span @class([
'size-2 shrink-0 rounded-full',
'bg-emerald-500' => $deploymentStatusType === 'success',
@@ -343,6 +346,7 @@
'bg-red-500' => $deploymentStatusType === 'error',
'bg-neutral-400 dark:bg-neutral-500' => $deploymentStatusType === 'neutral',
])></span>
@endif
{{ $deploymentStatusLabel }}
</span>
</div>
@@ -14,7 +14,7 @@
:confirmWithText="false" :confirmWithPassword="false">
<x-slot:trigger>
<button type="button"
class="button whitespace-nowrap text-error! hover:text-error! dark:text-error!">
class="button button-error whitespace-nowrap">
<span class="max-sm:hidden">Delete unused keys</span>
<span class="sm:hidden">Delete unused</span>
</button>
@@ -171,32 +171,8 @@
:placeholder="config('constants.github_runner.image')"
helper="Empty uses the latest official runner image, which Coolify pulls for every runner. Set a tag, for example ghcr.io/actions/actions-runner:2.337.0, to pin a version, or use a custom image based on the official one to add tools. GitHub stops sending jobs to runners that are more than 30 days old." />
</div>
<div x-cloak x-show="$wire.dockerMode === 'sysbox'" class="mt-4"
x-effect="if ($wire.dockerMode === 'sysbox' && $wire.isSysboxInstalled === null) $wire.checkSysbox()">
@if ($isSysboxInstalled === true)
<x-callout type="success" title="Sysbox is installed">
Jobs get a private Docker daemon in an unprivileged container.
</x-callout>
@elseif ($isSysboxInstalled === false)
<x-callout type="warning" title="Sysbox is not installed">
Isolated Docker needs the Sysbox runtime. Coolify installs Sysbox
{{ config('constants.github_runner.sysbox.version') }} from the official package
(Debian and Ubuntu, kernel 5.12 or newer). Docker reloads its configuration, and
running containers are not restarted.
@can('update', $server)
<div class="mt-3">
<x-forms.button type="button" wire:click="installSysbox"
wire:loading.attr="disabled" wire:target="installSysbox">
Install Sysbox
</x-forms.button>
</div>
@endcan
</x-callout>
@else
<x-callout type="info" title="Checking Sysbox">
Coolify checks if Sysbox is installed on this server.
</x-callout>
@endif
<div x-cloak x-show="$wire.dockerMode === 'sysbox'" class="mt-4">
<livewire:server.sysbox-status :server="$server" />
</div>
</x-application.settings-section>
+9 -29
View File
@@ -28,6 +28,7 @@
'Hostinger' => $hostingerVirtualMachineStatus,
default => null,
};
$canRefreshProvider = $provider && $server->cloudProviderToken && ! $server->isTransferredAway();
$providerStatusType = match (true) {
in_array($providerStatus, ['running', 'active']) => 'success',
in_array($providerStatus, ['starting', 'initializing', 'pending', 'new', 'initial', 'creating', 'unsuspending', 'recreating', 'restoring']) => 'warning',
@@ -265,35 +266,14 @@
<x-application.settings-section id="server-overview-section" title="Server overview"
helper="Provider state, operating system, and hardware details.">
<x-slot:actions>
@if ($provider)
@if ($provider)
<x-slot:badges>
<x-status-badge :label="$provider . ($providerStatus ? ' · ' . ucfirst($providerStatus) : '')"
:type="$providerStatusType" />
@if ($provider === 'Hetzner')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkHetznerServerStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'DigitalOcean')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkDigitalOceanDropletStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'Vultr')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkVultrInstanceStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@elseif ($provider === 'Hostinger')
<x-forms.button type="button" class="size-8! px-0!"
wire:click.prevent="checkHostingerVirtualMachineStatus(true)"
title="Refresh provider status">
<x-reicon name="refresh" class="size-3.5" />
</x-forms.button>
@endif
</x-slot:badges>
@endif
<x-slot:actions>
@if ($provider)
@if ($server->cloudProviderToken && ! $server->isTransferredAway())
@if ($provider === 'Hetzner' && !$server->isFunctional() && $hetznerServerStatus === 'off')
<x-forms.button type="button" wire:click.prevent="startHetznerServer" isHighlighted
@@ -314,8 +294,8 @@
@endif
@endif
<x-forms.button type="button" wire:click="refreshServerMetadata"
:disabled="! $server->isFunctional()"
:tooltip="$server->isFunctional() ? null : 'Validate the connection first.'">
:disabled="! $server->isFunctional() && ! $canRefreshProvider"
:tooltip="$server->isFunctional() || $canRefreshProvider ? null : 'Validate the connection first.'">
<x-reicon name="refresh" class="size-3.5" />
{{ $server->server_metadata ? 'Refresh details' : 'Fetch server details' }}
</x-forms.button>
@@ -0,0 +1,5 @@
<div>
<x-callout type="info" title="Checking Sysbox">
Coolify checks if Sysbox is installed on this server.
</x-callout>
</div>
@@ -0,0 +1,22 @@
<div>
@if ($isInstalled)
<x-callout type="success" title="Sysbox is installed">
Jobs get a private Docker daemon in an unprivileged container.
</x-callout>
@else
<x-callout type="warning" title="Sysbox is not installed">
Isolated Docker needs the Sysbox runtime. Coolify installs Sysbox
{{ config('constants.github_runner.sysbox.version') }} from the official package
(Debian and Ubuntu, kernel 5.12 or newer). Docker reloads its configuration, and
running containers are not restarted.
@can('update', $server)
<div class="mt-3">
<x-forms.button type="button" wire:click="installSysbox"
wire:loading.attr="disabled" wire:target="installSysbox">
Install Sysbox
</x-forms.button>
</div>
@endcan
</x-callout>
@endif
</div>
+1 -1
View File
@@ -201,7 +201,7 @@ Route::group([
Route::get('/servers/{uuid}/registries', [ServerDockerRegistriesController::class, 'index'])->middleware(['api.ability:read:sensitive']);
Route::post('/servers/{uuid}/registries', [ServerDockerRegistriesController::class, 'login'])->middleware(['api.ability:write', 'api.ability:deploy']);
Route::post('/servers/{uuid}/registries/{registry}/check', [ServerDockerRegistriesController::class, 'check'])->middleware(['api.ability:write']);
Route::post('/servers/{uuid}/registries/{registry}/check', [ServerDockerRegistriesController::class, 'check'])->middleware(['api.ability:write', 'api.ability:deploy']);
Route::delete('/servers/{uuid}/registries/{registry}', [ServerDockerRegistriesController::class, 'logout'])->middleware(['api.ability:delete']);
Route::get('/servers/{uuid}/proxy', [ServerProxyController::class, 'show'])->middleware(['api.ability:read']);
+17 -16
View File
@@ -8,6 +8,7 @@ use App\Jobs\ProvisionGithubRunnerJob;
use App\Livewire\Server\GithubRunnerExecutions;
use App\Livewire\Server\GithubRunners;
use App\Livewire\Server\Show;
use App\Livewire\Server\SysboxStatus;
use App\Livewire\Source\Github\Change;
use App\Livewire\Source\Github\Create;
use App\Models\GithubApp;
@@ -446,8 +447,7 @@ describe('isolated Docker with Sysbox', function () {
->set('githubAppId', $this->githubApp->id)
->set('dockerMode', 'sysbox')
->call('submit')
->assertHasErrors('dockerMode')
->assertSet('isSysboxInstalled', false);
->assertHasErrors('dockerMode');
expect(GithubRunnerConfig::count())->toBe(0);
});
@@ -469,29 +469,29 @@ describe('isolated Docker with Sysbox', function () {
it('checks that Sysbox can mount its FUSE file system', function () {
Process::fake(['*' => Process::result(output: '')]);
Livewire::actingAs($this->owner)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
->call('checkSysbox');
Livewire::withoutLazyLoading()
->actingAs($this->owner)
->test(SysboxStatus::class, ['server' => $this->server]);
Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), 'test -x /usr/bin/fusermount3'));
});
it('shows the install button when Sysbox is missing', function () {
Process::fake(['*' => Process::result(output: '')]);
GithubRunnerConfig::create(['server_id' => $this->server->id, 'github_app_id' => $this->githubApp->id, 'labels' => ['coolify']]);
Livewire::actingAs($this->owner)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
->call('checkSysbox')
->assertSet('isSysboxInstalled', false)
Livewire::withoutLazyLoading()
->actingAs($this->owner)
->test(SysboxStatus::class, ['server' => $this->server])
->assertSet('isInstalled', false)
->assertSee('Install Sysbox');
});
it('stops before the installation on servers without apt', function () {
Process::fake(['*' => Process::result(output: '')]);
Livewire::actingAs($this->owner)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
Livewire::withoutLazyLoading()
->actingAs($this->owner)
->test(SysboxStatus::class, ['server' => $this->server])
->call('installSysbox')
->assertNotDispatched('activityMonitor');
@@ -499,14 +499,15 @@ describe('isolated Docker with Sysbox', function () {
});
it('does not let members install Sysbox', function () {
Process::fake();
Process::fake(['*' => Process::result(output: '')]);
Livewire::actingAs($this->member)
->test(GithubRunners::class, ['server_uuid' => $this->server->uuid])
Livewire::withoutLazyLoading()
->actingAs($this->member)
->test(SysboxStatus::class, ['server' => $this->server])
->call('installSysbox')
->assertForbidden();
Process::assertNothingRan();
Process::assertDidntRun(fn ($process) => str_contains(remoteProcessScript($process), 'apt-get'));
});
});
+13 -3
View File
@@ -407,7 +407,7 @@ it('starts a stopped Hostinger VPS from the server page', function () {
expect($server->fresh()->hostinger_virtual_machine_status)->toBe('starting');
});
it('labels the Hostinger status refresh button on the server page', function () {
it('refreshes the Hostinger status from the server details action before the server is validated', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
@@ -416,10 +416,20 @@ it('labels the Hostinger status refresh button on the server page', function ()
'hostinger_virtual_machine_status' => 'running',
]);
Http::fake([
'https://developers.hostinger.com/api/vps/v1/virtual-machines/17923' => Http::response([
'id' => 17923,
'state' => 'stopped',
'ipv4' => [['address' => $server->ip]],
]),
]);
Livewire::test(Show::class, ['server_uuid' => $server->uuid])
->assertSee('Hostinger · Running')
->assertSee('Refresh provider status')
->assertSeeHtml('wire:click.prevent="checkHostingerVirtualMachineStatus(true)"');
->call('refreshServerMetadata')
->assertSet('hostingerVirtualMachineStatus', 'stopped')
->assertDispatched('info', 'Provider status refreshed. Validate the server connection to fetch server details.')
->assertSee('Hostinger · Stopped');
});
it('does not validate a stopped Hostinger VPS over SSH', function () {