From e29b517fef37aeac1797b45aebf915fac87f12d9 Mon Sep 17 00:00:00 2001 From: Cinzya Date: Sun, 26 Oct 2025 02:50:41 +0200 Subject: [PATCH 1/4] fix: server URL generation in ServerPatchCheck notification --- app/Notifications/Server/ServerPatchCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Notifications/Server/ServerPatchCheck.php b/app/Notifications/Server/ServerPatchCheck.php index 4d3053569..bfca19e40 100644 --- a/app/Notifications/Server/ServerPatchCheck.php +++ b/app/Notifications/Server/ServerPatchCheck.php @@ -16,7 +16,7 @@ class ServerPatchCheck extends CustomEmailNotification public function __construct(public Server $server, public array $patchData) { $this->onQueue('high'); - $this->serverUrl = route('server.security.patches', ['server_uuid' => $this->server->uuid]); + $this->serverUrl = base_url().'/server/'.$this->server->uuid.'/security/patches'; if (isDev()) { $this->serverUrl = 'https://staging-but-dev.coolify.io/server/'.$this->server->uuid.'/security/patches'; } From c16844db7fa426a4737d9ccc370799b601f1d0e4 Mon Sep 17 00:00:00 2001 From: Cinzya Date: Sun, 26 Oct 2025 02:00:26 +0100 Subject: [PATCH 2/4] test: add unit tests for ServerPatchCheck notification URL generation --- .../Unit/ServerPatchCheckNotificationTest.php | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/Unit/ServerPatchCheckNotificationTest.php diff --git a/tests/Unit/ServerPatchCheckNotificationTest.php b/tests/Unit/ServerPatchCheckNotificationTest.php new file mode 100644 index 000000000..da6441c35 --- /dev/null +++ b/tests/Unit/ServerPatchCheckNotificationTest.php @@ -0,0 +1,158 @@ +andReturn((object) [ + 'fqdn' => 'https://coolify.example.com', + 'public_ipv4' => null, + 'public_ipv6' => null, + ]); + + $mockServer = Mockery::mock(Server::class); + $mockServer->shouldReceive('getAttribute') + ->with('uuid') + ->andReturn('test-server-uuid'); + $mockServer->uuid = 'test-server-uuid'; + + $patchData = [ + 'total_updates' => 5, + 'updates' => [], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $notification = new ServerPatchCheck($mockServer, $patchData); + + // The URL should use the FQDN from InstanceSettings, not APP_URL + expect($notification->serverUrl)->toBe('https://coolify.example.com/server/test-server-uuid/security/patches'); +}); + +it('falls back to public_ipv4 with port when fqdn is not set', function () { + // Mock InstanceSettings to return public IPv4 + InstanceSettings::shouldReceive('get') + ->andReturn((object) [ + 'fqdn' => null, + 'public_ipv4' => '192.168.1.100', + 'public_ipv6' => null, + ]); + + $mockServer = Mockery::mock(Server::class); + $mockServer->shouldReceive('getAttribute') + ->with('uuid') + ->andReturn('test-server-uuid'); + $mockServer->uuid = 'test-server-uuid'; + + $patchData = [ + 'total_updates' => 3, + 'updates' => [], + 'osId' => 'debian', + 'package_manager' => 'apt', + ]; + + $notification = new ServerPatchCheck($mockServer, $patchData); + + // The URL should use public IPv4 with default port 8000 + expect($notification->serverUrl)->toBe('http://192.168.1.100:8000/server/test-server-uuid/security/patches'); +}); + +it('includes server url in all notification channels', function () { + InstanceSettings::shouldReceive('get') + ->andReturn((object) [ + 'fqdn' => 'https://coolify.test', + 'public_ipv4' => null, + 'public_ipv6' => null, + ]); + + $mockServer = Mockery::mock(Server::class); + $mockServer->shouldReceive('getAttribute') + ->with('uuid') + ->andReturn('abc-123'); + $mockServer->shouldReceive('getAttribute') + ->with('name') + ->andReturn('Test Server'); + $mockServer->uuid = 'abc-123'; + $mockServer->name = 'Test Server'; + + $patchData = [ + 'total_updates' => 10, + 'updates' => [ + [ + 'package' => 'nginx', + 'current_version' => '1.18', + 'new_version' => '1.20', + 'architecture' => 'amd64', + 'repository' => 'main', + ], + ], + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $notification = new ServerPatchCheck($mockServer, $patchData); + + // Check Discord + $discord = $notification->toDiscord(); + expect($discord->description)->toContain('https://coolify.test/server/abc-123/security/patches'); + + // Check Telegram + $telegram = $notification->toTelegram(); + expect($telegram['buttons'][0]['url'])->toBe('https://coolify.test/server/abc-123/security/patches'); + + // Check Pushover + $pushover = $notification->toPushover(); + expect($pushover->buttons[0]['url'])->toBe('https://coolify.test/server/abc-123/security/patches'); + + // Check Slack + $slack = $notification->toSlack(); + expect($slack->description)->toContain('https://coolify.test/server/abc-123/security/patches'); + + // Check Webhook + $webhook = $notification->toWebhook(); + expect($webhook['url'])->toBe('https://coolify.test/server/abc-123/security/patches'); +}); + +it('uses correct url in error notifications', function () { + InstanceSettings::shouldReceive('get') + ->andReturn((object) [ + 'fqdn' => 'https://coolify.production.com', + 'public_ipv4' => null, + 'public_ipv6' => null, + ]); + + $mockServer = Mockery::mock(Server::class); + $mockServer->shouldReceive('getAttribute') + ->with('uuid') + ->andReturn('error-server-uuid'); + $mockServer->shouldReceive('getAttribute') + ->with('name') + ->andReturn('Error Server'); + $mockServer->uuid = 'error-server-uuid'; + $mockServer->name = 'Error Server'; + + $patchData = [ + 'error' => 'Failed to connect to package manager', + 'osId' => 'ubuntu', + 'package_manager' => 'apt', + ]; + + $notification = new ServerPatchCheck($mockServer, $patchData); + + // Check error Discord notification + $discord = $notification->toDiscord(); + expect($discord->description)->toContain('https://coolify.production.com/server/error-server-uuid/security/patches'); + + // Check error webhook + $webhook = $notification->toWebhook(); + expect($webhook['url'])->toBe('https://coolify.production.com/server/error-server-uuid/security/patches') + ->and($webhook['event'])->toBe('server_patch_check_error'); +}); From bceef418c8af68693d2b682db02b6c29ff42f901 Mon Sep 17 00:00:00 2001 From: Cinzya Date: Sun, 26 Oct 2025 21:52:51 +0100 Subject: [PATCH 3/4] refactor: remove staging URL logic from ServerPatchCheck constructor --- app/Notifications/Server/ServerPatchCheck.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/Notifications/Server/ServerPatchCheck.php b/app/Notifications/Server/ServerPatchCheck.php index bfca19e40..ba6cd4982 100644 --- a/app/Notifications/Server/ServerPatchCheck.php +++ b/app/Notifications/Server/ServerPatchCheck.php @@ -17,9 +17,6 @@ public function __construct(public Server $server, public array $patchData) { $this->onQueue('high'); $this->serverUrl = base_url().'/server/'.$this->server->uuid.'/security/patches'; - if (isDev()) { - $this->serverUrl = 'https://staging-but-dev.coolify.io/server/'.$this->server->uuid.'/security/patches'; - } } public function via(object $notifiable): array From 35b104477ad38225df57430a8603e1c9445e4ad3 Mon Sep 17 00:00:00 2001 From: Cinzya Date: Sun, 26 Oct 2025 21:54:55 +0100 Subject: [PATCH 4/4] test: fix ServerPatchCheckNotification tests to avoid global state pollution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved ServerPatchCheckNotificationTest from Unit to Feature tests and replaced Mockery alias mocking with real database records to prevent global state pollution. The original implementation used Mockery::mock('alias:InstanceSettings::class) which creates a global class alias that persists across all tests, causing other tests to fail when they try to use the real InstanceSettings model. Changes: - Moved test from tests/Unit/ to tests/Feature/ (requires database access) - Replaced Mockery alias mocking with RefreshDatabase and real InstanceSettings records - Tests now create actual InstanceSettings records in the test database - Preserved Server mocking with Mockery for non-database dependencies All 4 tests pass individually and when run via php artisan test without polluting global state or affecting other tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../ServerPatchCheckNotificationTest.php | 98 ++++++++----------- 1 file changed, 43 insertions(+), 55 deletions(-) rename tests/{Unit => Feature}/ServerPatchCheckNotificationTest.php (61%) diff --git a/tests/Unit/ServerPatchCheckNotificationTest.php b/tests/Feature/ServerPatchCheckNotificationTest.php similarity index 61% rename from tests/Unit/ServerPatchCheckNotificationTest.php rename to tests/Feature/ServerPatchCheckNotificationTest.php index da6441c35..dd8901e82 100644 --- a/tests/Unit/ServerPatchCheckNotificationTest.php +++ b/tests/Feature/ServerPatchCheckNotificationTest.php @@ -3,26 +3,49 @@ use App\Models\InstanceSettings; use App\Models\Server; use App\Notifications\Server\ServerPatchCheck; -use Mockery; +use Illuminate\Foundation\Testing\RefreshDatabase; + +uses(RefreshDatabase::class); + +beforeEach(function () { + // Create a real InstanceSettings record in the test database + // This avoids Mockery alias/overload issues that pollute global state + $this->setInstanceSettings = function ($fqdn = null, $publicIpv4 = null, $publicIpv6 = null) { + InstanceSettings::query()->delete(); + InstanceSettings::create([ + 'id' => 0, + 'fqdn' => $fqdn, + 'public_ipv4' => $publicIpv4, + 'public_ipv6' => $publicIpv6, + ]); + }; + + $this->createMockServer = function ($uuid, $name = 'Test Server') { + $mockServer = Mockery::mock(Server::class); + $mockServer->shouldReceive('getAttribute') + ->with('uuid') + ->andReturn($uuid); + $mockServer->shouldReceive('getAttribute') + ->with('name') + ->andReturn($name); + $mockServer->shouldReceive('setAttribute')->andReturnSelf(); + $mockServer->shouldReceive('getSchemalessAttributes')->andReturn([]); + $mockServer->uuid = $uuid; + $mockServer->name = $name; + + return $mockServer; + }; +}); afterEach(function () { Mockery::close(); }); it('generates url using base_url instead of APP_URL', function () { - // Mock InstanceSettings to return a specific FQDN - InstanceSettings::shouldReceive('get') - ->andReturn((object) [ - 'fqdn' => 'https://coolify.example.com', - 'public_ipv4' => null, - 'public_ipv6' => null, - ]); + // Set InstanceSettings to return a specific FQDN + ($this->setInstanceSettings)('https://coolify.example.com'); - $mockServer = Mockery::mock(Server::class); - $mockServer->shouldReceive('getAttribute') - ->with('uuid') - ->andReturn('test-server-uuid'); - $mockServer->uuid = 'test-server-uuid'; + $mockServer = ($this->createMockServer)('test-server-uuid'); $patchData = [ 'total_updates' => 5, @@ -38,19 +61,10 @@ }); it('falls back to public_ipv4 with port when fqdn is not set', function () { - // Mock InstanceSettings to return public IPv4 - InstanceSettings::shouldReceive('get') - ->andReturn((object) [ - 'fqdn' => null, - 'public_ipv4' => '192.168.1.100', - 'public_ipv6' => null, - ]); + // Set InstanceSettings to return public IPv4 + ($this->setInstanceSettings)(null, '192.168.1.100'); - $mockServer = Mockery::mock(Server::class); - $mockServer->shouldReceive('getAttribute') - ->with('uuid') - ->andReturn('test-server-uuid'); - $mockServer->uuid = 'test-server-uuid'; + $mockServer = ($this->createMockServer)('test-server-uuid'); $patchData = [ 'total_updates' => 3, @@ -66,22 +80,9 @@ }); it('includes server url in all notification channels', function () { - InstanceSettings::shouldReceive('get') - ->andReturn((object) [ - 'fqdn' => 'https://coolify.test', - 'public_ipv4' => null, - 'public_ipv6' => null, - ]); + ($this->setInstanceSettings)('https://coolify.test'); - $mockServer = Mockery::mock(Server::class); - $mockServer->shouldReceive('getAttribute') - ->with('uuid') - ->andReturn('abc-123'); - $mockServer->shouldReceive('getAttribute') - ->with('name') - ->andReturn('Test Server'); - $mockServer->uuid = 'abc-123'; - $mockServer->name = 'Test Server'; + $mockServer = ($this->createMockServer)('abc-123', 'Test Server'); $patchData = [ 'total_updates' => 10, @@ -122,22 +123,9 @@ }); it('uses correct url in error notifications', function () { - InstanceSettings::shouldReceive('get') - ->andReturn((object) [ - 'fqdn' => 'https://coolify.production.com', - 'public_ipv4' => null, - 'public_ipv6' => null, - ]); + ($this->setInstanceSettings)('https://coolify.production.com'); - $mockServer = Mockery::mock(Server::class); - $mockServer->shouldReceive('getAttribute') - ->with('uuid') - ->andReturn('error-server-uuid'); - $mockServer->shouldReceive('getAttribute') - ->with('name') - ->andReturn('Error Server'); - $mockServer->uuid = 'error-server-uuid'; - $mockServer->name = 'Error Server'; + $mockServer = ($this->createMockServer)('error-server-uuid', 'Error Server'); $patchData = [ 'error' => 'Failed to connect to package manager',