From e41dbde46bd76578a3316c14c206aa56148af9e0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:34:37 +0100 Subject: [PATCH] chore: prepare for PR --- app/Actions/Docker/GetContainersStatus.php | 12 +++ app/Jobs/PushServerUpdateJob.php | 4 + .../PushServerUpdateJobLastOnlineTest.php | 101 ++++++++++++++++++ ...inersStatusEmptyContainerSafeguardTest.php | 54 ++++++++++ 4 files changed, 171 insertions(+) create mode 100644 tests/Feature/PushServerUpdateJobLastOnlineTest.php create mode 100644 tests/Unit/GetContainersStatusEmptyContainerSafeguardTest.php diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 6c9a54f77..5966876c6 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -327,6 +327,12 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti if (str($exitedService->status)->startsWith('exited')) { continue; } + + // Only protection: If no containers at all, Docker query might have failed + if ($this->containers->isEmpty()) { + continue; + } + $name = data_get($exitedService, 'name'); $fqdn = data_get($exitedService, 'fqdn'); if ($name) { @@ -406,6 +412,12 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti if (str($database->status)->startsWith('exited')) { continue; } + + // Only protection: If no containers at all, Docker query might have failed + if ($this->containers->isEmpty()) { + continue; + } + // Reset restart tracking when database exits completely $database->update([ 'status' => 'exited', diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index 85684ff19..5e598cecd 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -399,6 +399,8 @@ private function updateApplicationStatus(string $applicationId, string $containe if ($application->status !== $containerStatus) { $application->status = $containerStatus; $application->save(); + } else { + $application->update(['last_online_at' => now()]); } } @@ -508,6 +510,8 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta if ($database->status !== $containerStatus) { $database->status = $containerStatus; $database->save(); + } else { + $database->update(['last_online_at' => now()]); } if ($this->isRunning($containerStatus) && $tcpProxy) { $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { diff --git a/tests/Feature/PushServerUpdateJobLastOnlineTest.php b/tests/Feature/PushServerUpdateJobLastOnlineTest.php new file mode 100644 index 000000000..5d2fd6c6a --- /dev/null +++ b/tests/Feature/PushServerUpdateJobLastOnlineTest.php @@ -0,0 +1,101 @@ +create(); + $database = StandalonePostgresql::factory()->create([ + 'team_id' => $team->id, + 'status' => 'running:healthy', + 'last_online_at' => now()->subMinutes(5), + ]); + + $server = $database->destination->server; + + $data = [ + 'containers' => [ + [ + 'name' => $database->uuid, + 'state' => 'running', + 'health_status' => 'healthy', + 'labels' => [ + 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'com.docker.compose.service' => $database->uuid, + ], + ], + ], + ]; + + $oldLastOnline = $database->last_online_at; + + $job = new PushServerUpdateJob($server, $data); + $job->handle(); + + $database->refresh(); + + // last_online_at should be updated even though status didn't change + expect($database->last_online_at->greaterThan($oldLastOnline))->toBeTrue(); + expect($database->status)->toBe('running:healthy'); +}); + +test('database status is updated when container status changes', function () { + $team = Team::factory()->create(); + $database = StandalonePostgresql::factory()->create([ + 'team_id' => $team->id, + 'status' => 'exited', + ]); + + $server = $database->destination->server; + + $data = [ + 'containers' => [ + [ + 'name' => $database->uuid, + 'state' => 'running', + 'health_status' => 'healthy', + 'labels' => [ + 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'com.docker.compose.service' => $database->uuid, + ], + ], + ], + ]; + + $job = new PushServerUpdateJob($server, $data); + $job->handle(); + + $database->refresh(); + + expect($database->status)->toBe('running:healthy'); +}); + +test('database is not marked exited when containers list is empty', function () { + $team = Team::factory()->create(); + $database = StandalonePostgresql::factory()->create([ + 'team_id' => $team->id, + 'status' => 'running:healthy', + ]); + + $server = $database->destination->server; + + // Empty containers = Sentinel might have failed, should NOT mark as exited + $data = [ + 'containers' => [], + ]; + + $job = new PushServerUpdateJob($server, $data); + $job->handle(); + + $database->refresh(); + + // Status should remain running, NOT be set to exited + expect($database->status)->toBe('running:healthy'); +}); diff --git a/tests/Unit/GetContainersStatusEmptyContainerSafeguardTest.php b/tests/Unit/GetContainersStatusEmptyContainerSafeguardTest.php new file mode 100644 index 000000000..d4271d3ee --- /dev/null +++ b/tests/Unit/GetContainersStatusEmptyContainerSafeguardTest.php @@ -0,0 +1,54 @@ +toContain('$notRunningApplications = $this->applications->pluck(\'id\')->diff($foundApplications);'); + + // Count occurrences of the safeguard pattern in the not-found sections + $safeguardPattern = '// Only protection: If no containers at all, Docker query might have failed'; + $safeguardCount = substr_count($actionFile, $safeguardPattern); + + // Should appear at least 4 times: applications, previews, databases, services + expect($safeguardCount)->toBeGreaterThanOrEqual(4); +}); + +it('has empty container safeguard for databases', function () { + $actionFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php'); + + // Extract the database not-found section + $databaseSectionStart = strpos($actionFile, '$notRunningDatabases = $databases->pluck(\'id\')->diff($foundDatabases);'); + expect($databaseSectionStart)->not->toBeFalse('Database not-found section should exist'); + + // Get the code between database section start and the next major section + $databaseSection = substr($actionFile, $databaseSectionStart, 500); + + // The empty container safeguard must exist in the database section + expect($databaseSection)->toContain('$this->containers->isEmpty()'); +}); + +it('has empty container safeguard for services', function () { + $actionFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php'); + + // Extract the service exited section + $serviceSectionStart = strpos($actionFile, '$exitedServices = $exitedServices->unique(\'uuid\');'); + expect($serviceSectionStart)->not->toBeFalse('Service exited section should exist'); + + // Get the code in the service exited loop + $serviceSection = substr($actionFile, $serviceSectionStart, 500); + + // The empty container safeguard must exist in the service section + expect($serviceSection)->toContain('$this->containers->isEmpty()'); +});