From bd6b98928047a6ce0d1e4af8c63fb24822c98a81 Mon Sep 17 00:00:00 2001 From: Mats Estensen Date: Mon, 9 Mar 2026 21:43:25 +0100 Subject: [PATCH] fix(notifications): more consistent levels for server patching notifications --- app/Notifications/Server/ServerPatchCheck.php | 107 +++++---- .../ServerPatchCheckNotificationTest.php | 219 ++++++++++++++++++ 2 files changed, 280 insertions(+), 46 deletions(-) diff --git a/app/Notifications/Server/ServerPatchCheck.php b/app/Notifications/Server/ServerPatchCheck.php index ba6cd4982..68066cda5 100644 --- a/app/Notifications/Server/ServerPatchCheck.php +++ b/app/Notifications/Server/ServerPatchCheck.php @@ -83,6 +83,16 @@ class ServerPatchCheck extends CustomEmailNotification $osId = $this->patchData['osId'] ?? 'unknown'; $packageManager = $this->patchData['package_manager'] ?? 'unknown'; + // Check for critical packages + $criticalPackages = collect($updates)->filter(function ($update) { + return str_contains(strtolower($update['package']), 'docker') || + str_contains(strtolower($update['package']), 'kernel') || + str_contains(strtolower($update['package']), 'openssh') || + str_contains(strtolower($update['package']), 'ssl'); + }); + + $hasCriticalPackages = $criticalPackages->count() > 0; + $description = "**{$totalUpdates} package updates** available for server {$this->server->name}\n\n"; $description .= "**Summary:**\n"; $description .= '• OS: '.ucfirst($osId)."\n"; @@ -91,7 +101,7 @@ class ServerPatchCheck extends CustomEmailNotification // Show first few packages if (count($updates) > 0) { - $description .= "**Sample Updates:**\n"; + $description .= "**Updates:**\n"; $sampleUpdates = array_slice($updates, 0, 5); foreach ($sampleUpdates as $update) { $description .= "• {$update['package']}: {$update['current_version']} → {$update['new_version']}\n"; @@ -100,24 +110,20 @@ class ServerPatchCheck extends CustomEmailNotification $description .= '• ... and '.(count($updates) - 5)." more packages\n"; } - // Check for critical packages - $criticalPackages = collect($updates)->filter(function ($update) { - return str_contains(strtolower($update['package']), 'docker') || - str_contains(strtolower($update['package']), 'kernel') || - str_contains(strtolower($update['package']), 'openssh') || - str_contains(strtolower($update['package']), 'ssl'); - }); - - if ($criticalPackages->count() > 0) { + if ($hasCriticalPackages) { $description .= "\n **Critical packages detected** ({$criticalPackages->count()} packages may require restarts)"; } $description .= "\n [Manage Server Patches]($this->serverUrl)"; } + // Use warning color for critical packages, info color otherwise + $color = $hasCriticalPackages ? DiscordMessage::warningColor() : DiscordMessage::infoColor(); + $icon = $hasCriticalPackages ? ':warning:' : ':information_source:'; + return new DiscordMessage( - title: ':warning: Coolify: [ACTION REQUIRED] Server patches available on '.$this->server->name, + title: "{$icon} Coolify: [ACTION REQUIRED] Server patches available on ".$this->server->name, description: $description, - color: DiscordMessage::errorColor(), + color: $color, ); } @@ -152,14 +158,27 @@ class ServerPatchCheck extends CustomEmailNotification $osId = $this->patchData['osId'] ?? 'unknown'; $packageManager = $this->patchData['package_manager'] ?? 'unknown'; - $message = "šŸ”§ Coolify: [ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n"; + // Check for critical packages + $criticalPackages = collect($updates)->filter(function ($update) { + return str_contains(strtolower($update['package']), 'docker') || + str_contains(strtolower($update['package']), 'kernel') || + str_contains(strtolower($update['package']), 'openssh') || + str_contains(strtolower($update['package']), 'ssl'); + }); + + $hasCriticalPackages = $criticalPackages->count() > 0; + + // Use warning emoji for critical packages, info emoji otherwise + $icon = $hasCriticalPackages ? 'āš ļø' : 'ā„¹ļø'; + + $message = "{$icon} Coolify: [ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n"; $message .= "šŸ“Š Summary:\n"; $message .= '• OS: '.ucfirst($osId)."\n"; $message .= "• Package Manager: {$packageManager}\n"; $message .= "• Total Updates: {$totalUpdates}\n\n"; if (count($updates) > 0) { - $message .= "šŸ“¦ Sample Updates:\n"; + $message .= "šŸ“¦ Updates:\n"; $sampleUpdates = array_slice($updates, 0, 5); foreach ($sampleUpdates as $update) { $message .= "• {$update['package']}: {$update['current_version']} → {$update['new_version']}\n"; @@ -168,15 +187,7 @@ class ServerPatchCheck extends CustomEmailNotification $message .= '• ... and '.(count($updates) - 5)." more packages\n"; } - // Check for critical packages - $criticalPackages = collect($updates)->filter(function ($update) { - return str_contains(strtolower($update['package']), 'docker') || - str_contains(strtolower($update['package']), 'kernel') || - str_contains(strtolower($update['package']), 'openssh') || - str_contains(strtolower($update['package']), 'ssl'); - }); - - if ($criticalPackages->count() > 0) { + if ($hasCriticalPackages) { $message .= "\nāš ļø Critical packages detected: {$criticalPackages->count()} packages may require restarts\n"; foreach ($criticalPackages->take(3) as $package) { $message .= "• {$package['package']}: {$package['current_version']} → {$package['new_version']}\n"; @@ -230,6 +241,16 @@ class ServerPatchCheck extends CustomEmailNotification $osId = $this->patchData['osId'] ?? 'unknown'; $packageManager = $this->patchData['package_manager'] ?? 'unknown'; + // Check for critical packages + $criticalPackages = collect($updates)->filter(function ($update) { + return str_contains(strtolower($update['package']), 'docker') || + str_contains(strtolower($update['package']), 'kernel') || + str_contains(strtolower($update['package']), 'openssh') || + str_contains(strtolower($update['package']), 'ssl'); + }); + + $hasCriticalPackages = $criticalPackages->count() > 0; + $message = "[ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n"; $message .= "Summary:\n"; $message .= '• OS: '.ucfirst($osId)."\n"; @@ -237,7 +258,7 @@ class ServerPatchCheck extends CustomEmailNotification $message .= "• Total Updates: {$totalUpdates}\n\n"; if (count($updates) > 0) { - $message .= "Sample Updates:\n"; + $message .= "Updates:\n"; $sampleUpdates = array_slice($updates, 0, 3); foreach ($sampleUpdates as $update) { $message .= "• {$update['package']}: {$update['current_version']} → {$update['new_version']}\n"; @@ -246,22 +267,14 @@ class ServerPatchCheck extends CustomEmailNotification $message .= '• ... and '.(count($updates) - 3)." more packages\n"; } - // Check for critical packages - $criticalPackages = collect($updates)->filter(function ($update) { - return str_contains(strtolower($update['package']), 'docker') || - str_contains(strtolower($update['package']), 'kernel') || - str_contains(strtolower($update['package']), 'openssh') || - str_contains(strtolower($update['package']), 'ssl'); - }); - - if ($criticalPackages->count() > 0) { + if ($hasCriticalPackages) { $message .= "\nCritical packages detected: {$criticalPackages->count()} may require restarts"; } } return new PushoverMessage( title: 'Server patches available', - level: 'error', + level: $hasCriticalPackages ? 'warning' : 'info', message: $message, buttons: [ [ @@ -299,6 +312,16 @@ class ServerPatchCheck extends CustomEmailNotification $osId = $this->patchData['osId'] ?? 'unknown'; $packageManager = $this->patchData['package_manager'] ?? 'unknown'; + // Check for critical packages + $criticalPackages = collect($updates)->filter(function ($update) { + return str_contains(strtolower($update['package']), 'docker') || + str_contains(strtolower($update['package']), 'kernel') || + str_contains(strtolower($update['package']), 'openssh') || + str_contains(strtolower($update['package']), 'ssl'); + }); + + $hasCriticalPackages = $criticalPackages->count() > 0; + $description = "{$totalUpdates} server patches available on '{$this->server->name}'!\n\n"; $description .= "*Summary:*\n"; $description .= '• OS: '.ucfirst($osId)."\n"; @@ -306,7 +329,7 @@ class ServerPatchCheck extends CustomEmailNotification $description .= "• Total Updates: {$totalUpdates}\n\n"; if (count($updates) > 0) { - $description .= "*Sample Updates:*\n"; + $description .= "*Updates:*\n"; $sampleUpdates = array_slice($updates, 0, 5); foreach ($sampleUpdates as $update) { $description .= "• `{$update['package']}`: {$update['current_version']} → {$update['new_version']}\n"; @@ -315,15 +338,7 @@ class ServerPatchCheck extends CustomEmailNotification $description .= '• ... and '.(count($updates) - 5)." more packages\n"; } - // Check for critical packages - $criticalPackages = collect($updates)->filter(function ($update) { - return str_contains(strtolower($update['package']), 'docker') || - str_contains(strtolower($update['package']), 'kernel') || - str_contains(strtolower($update['package']), 'openssh') || - str_contains(strtolower($update['package']), 'ssl'); - }); - - if ($criticalPackages->count() > 0) { + if ($hasCriticalPackages) { $description .= "\n:warning: *Critical packages detected:* {$criticalPackages->count()} packages may require restarts\n"; foreach ($criticalPackages->take(3) as $package) { $description .= "• `{$package['package']}`: {$package['current_version']} → {$package['new_version']}\n"; @@ -339,7 +354,7 @@ class ServerPatchCheck extends CustomEmailNotification return new SlackMessage( title: 'Coolify: [ACTION REQUIRED] Server patches available', description: $description, - color: SlackMessage::errorColor() + color: $hasCriticalPackages ? SlackMessage::warningColor() : SlackMessage::infoColor() ); } @@ -372,7 +387,7 @@ class ServerPatchCheck extends CustomEmailNotification }); return [ - 'success' => false, + 'success' => true, 'message' => 'Server patches available', 'event' => 'server_patch_check', 'server_name' => $this->server->name, diff --git a/tests/Feature/ServerPatchCheckNotificationTest.php b/tests/Feature/ServerPatchCheckNotificationTest.php index dd8901e82..2abbc19bc 100644 --- a/tests/Feature/ServerPatchCheckNotificationTest.php +++ b/tests/Feature/ServerPatchCheckNotificationTest.php @@ -144,3 +144,222 @@ it('uses correct url in error notifications', function () { expect($webhook['url'])->toBe('https://coolify.production.com/server/error-server-uuid/security/patches') ->and($webhook['event'])->toBe('server_patch_check_error'); }); + +it('uses correct level for patch notifications in Discord', function () { + ($this->setInstanceSettings)('https://coolify.test'); + + $mockServer = ($this->createMockServer)('test-uuid', 'Test Server'); + + // Regular patches (no critical packages) + $regularPatchData = [ + 'total_updates' => 5, + 'updates' => [ + ['package' => 'nginx', 'current_version' => '1.18', 'new_version' => '1.20', 'architecture' => 'amd64', 'repository' => 'main'], + ['package' => 'curl', 'current_version' => '7.68', 'new_version' => '7.70', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $regularNotification = new ServerPatchCheck($mockServer, $regularPatchData); + $regularDiscord = $regularNotification->toDiscord(); + + expect($regularDiscord->color)->toBe(\App\Notifications\Dto\DiscordMessage::infoColor()) + ->and($regularDiscord->title)->toContain(':information_source:'); + + // Critical packages (docker, kernel, openssh, ssl) + $criticalPatchData = [ + 'total_updates' => 3, + 'updates' => [ + ['package' => 'docker-ce', 'current_version' => '20.10', 'new_version' => '20.11', 'architecture' => 'amd64', 'repository' => 'main'], + ['package' => 'nginx', 'current_version' => '1.18', 'new_version' => '1.20', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $criticalNotification = new ServerPatchCheck($mockServer, $criticalPatchData); + $criticalDiscord = $criticalNotification->toDiscord(); + + expect($criticalDiscord->color)->toBe(\App\Notifications\Dto\DiscordMessage::warningColor()) + ->and($criticalDiscord->title)->toContain(':warning:'); +}); + +it('uses correct level for patch notifications in Slack', function () { + ($this->setInstanceSettings)('https://coolify.test'); + + $mockServer = ($this->createMockServer)('test-uuid', 'Test Server'); + + // Regular patches + $regularPatchData = [ + 'total_updates' => 5, + 'updates' => [ + ['package' => 'nginx', 'current_version' => '1.18', 'new_version' => '1.20', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $regularNotification = new ServerPatchCheck($mockServer, $regularPatchData); + $regularSlack = $regularNotification->toSlack(); + + expect($regularSlack->color)->toBe(\App\Notifications\Dto\SlackMessage::infoColor()); + + // Critical packages + $criticalPatchData = [ + 'total_updates' => 2, + 'updates' => [ + ['package' => 'linux-kernel', 'current_version' => '5.4', 'new_version' => '5.5', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $criticalNotification = new ServerPatchCheck($mockServer, $criticalPatchData); + $criticalSlack = $criticalNotification->toSlack(); + + expect($criticalSlack->color)->toBe(\App\Notifications\Dto\SlackMessage::warningColor()); +}); + +it('uses correct level for patch notifications in Pushover', function () { + ($this->setInstanceSettings)('https://coolify.test'); + + $mockServer = ($this->createMockServer)('test-uuid', 'Test Server'); + + // Regular patches + $regularPatchData = [ + 'total_updates' => 5, + 'updates' => [ + ['package' => 'nginx', 'current_version' => '1.18', 'new_version' => '1.20', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $regularNotification = new ServerPatchCheck($mockServer, $regularPatchData); + $regularPushover = $regularNotification->toPushover(); + + expect($regularPushover->level)->toBe('info'); + + // Critical packages + $criticalPatchData = [ + 'total_updates' => 2, + 'updates' => [ + ['package' => 'openssh-server', 'current_version' => '8.2', 'new_version' => '8.3', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $criticalNotification = new ServerPatchCheck($mockServer, $criticalPatchData); + $criticalPushover = $criticalNotification->toPushover(); + + expect($criticalPushover->level)->toBe('warning'); +}); + +it('uses correct icon for patch notifications in Telegram', function () { + ($this->setInstanceSettings)('https://coolify.test'); + + $mockServer = ($this->createMockServer)('test-uuid', 'Test Server'); + + // Regular patches + $regularPatchData = [ + 'total_updates' => 5, + 'updates' => [ + ['package' => 'nginx', 'current_version' => '1.18', 'new_version' => '1.20', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $regularNotification = new ServerPatchCheck($mockServer, $regularPatchData); + $regularTelegram = $regularNotification->toTelegram(); + + expect($regularTelegram['message'])->toContain('ā„¹ļø'); + + // Critical packages + $criticalPatchData = [ + 'total_updates' => 2, + 'updates' => [ + ['package' => 'libssl1.1', 'current_version' => '1.1.1', 'new_version' => '1.1.2', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $criticalNotification = new ServerPatchCheck($mockServer, $criticalPatchData); + $criticalTelegram = $criticalNotification->toTelegram(); + + expect($criticalTelegram['message'])->toContain('āš ļø'); +}); + +it('returns success true for regular patches and false for errors in webhook', function () { + ($this->setInstanceSettings)('https://coolify.test'); + + $mockServer = ($this->createMockServer)('test-uuid', 'Test Server'); + + // Regular patches + $regularPatchData = [ + 'total_updates' => 5, + 'updates' => [ + ['package' => 'nginx', 'current_version' => '1.18', 'new_version' => '1.20', 'architecture' => 'amd64', 'repository' => 'main'], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $regularNotification = new ServerPatchCheck($mockServer, $regularPatchData); + $regularWebhook = $regularNotification->toWebhook(); + + expect($regularWebhook['success'])->toBeTrue() + ->and($regularWebhook['message'])->toBe('Server patches available') + ->and($regularWebhook['event'])->toBe('server_patch_check'); + + // Error case + $errorPatchData = [ + 'error' => 'Failed to connect to package manager', + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $errorNotification = new ServerPatchCheck($mockServer, $errorPatchData); + $errorWebhook = $errorNotification->toWebhook(); + + expect($errorWebhook['success'])->toBeFalse() + ->and($errorWebhook['message'])->toBe('Failed to check patches') + ->and($errorWebhook['event'])->toBe('server_patch_check_error'); +}); + +it('uses error level for actual errors in all channels', function () { + ($this->setInstanceSettings)('https://coolify.test'); + + $mockServer = ($this->createMockServer)('test-uuid', 'Test Server'); + + $errorPatchData = [ + 'error' => 'Connection refused', + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $notification = new ServerPatchCheck($mockServer, $errorPatchData); + + // Discord should use error color + $discord = $notification->toDiscord(); + expect($discord->color)->toBe(\App\Notifications\Dto\DiscordMessage::errorColor()); + + // Slack should use error color + $slack = $notification->toSlack(); + expect($slack->color)->toBe(\App\Notifications\Dto\SlackMessage::errorColor()); + + // Pushover should use error level + $pushover = $notification->toPushover(); + expect($pushover->level)->toBe('error'); + + // Telegram should use error icon + $telegram = $notification->toTelegram(); + expect($telegram['message'])->toContain('āŒ'); + + // Webhook should return success false + $webhook = $notification->toWebhook(); + expect($webhook['success'])->toBeFalse(); +});