diff --git a/app/Notifications/ApiTokenExpiringNotification.php b/app/Notifications/ApiTokenExpiringNotification.php index 451dd312a1..c5567a3a64 100644 --- a/app/Notifications/ApiTokenExpiringNotification.php +++ b/app/Notifications/ApiTokenExpiringNotification.php @@ -100,4 +100,16 @@ class ApiTokenExpiringNotification extends CustomEmailNotification color: SlackMessage::warningColor(), ); } + + public function toWebhook(): array + { + return [ + 'success' => false, + 'message' => "API token '{$this->tokenName}' expires on {$this->expiresAt}. Rotate this token before it expires to avoid API outages.", + 'event' => 'api_token_expiring', + 'token_name' => $this->tokenName, + 'expires_at' => $this->expiresAt, + 'url' => $this->manageUrl, + ]; + } } diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 1d23672100..52e986ed6e 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -58,4 +58,14 @@ class GeneralNotification extends Notification implements ShouldQueue color: SlackMessage::infoColor(), ); } + + public function toWebhook(): array + { + return [ + 'success' => true, + 'message' => $this->message, + 'event' => 'general', + 'url' => base_url(), + ]; + } } diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 4b56f5860b..2d2ebabaf0 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -74,4 +74,16 @@ class ForceDisabled extends CustomEmailNotification color: SlackMessage::errorColor() ); } + + public function toWebhook(): array + { + return [ + 'success' => false, + 'message' => "Server ({$this->server->name}) disabled because it is not paid! All automations and integrations are stopped.", + 'event' => 'server_force_disabled', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'url' => base_url().'/server/'.$this->server->uuid, + ]; + } } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 36dad3c60f..61022d36b5 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -65,4 +65,16 @@ class ForceEnabled extends CustomEmailNotification color: SlackMessage::successColor() ); } + + public function toWebhook(): array + { + return [ + 'success' => true, + 'message' => "Server ({$this->server->name}) enabled again!", + 'event' => 'server_force_enabled', + 'server_name' => $this->server->name, + 'server_uuid' => $this->server->uuid, + 'url' => base_url().'/server/'.$this->server->uuid, + ]; + } } diff --git a/app/Notifications/SslExpirationNotification.php b/app/Notifications/SslExpirationNotification.php index 78e1e8be9c..73c7a665d3 100644 --- a/app/Notifications/SslExpirationNotification.php +++ b/app/Notifications/SslExpirationNotification.php @@ -148,4 +148,18 @@ class SslExpirationNotification extends CustomEmailNotification color: SlackMessage::warningColor() ); } + + public function toWebhook(): array + { + $resourceNames = $this->resources->pluck('name'); + + return [ + 'success' => false, + 'message' => "SSL certificates have been renewed for: {$resourceNames->join(', ')}. These resources need to be redeployed manually for the new SSL certificates to take effect.", + 'event' => 'ssl_certificate_renewal', + 'resources' => $resourceNames->values()->all(), + 'urls' => $this->urls, + 'url' => base_url(), + ]; + } } diff --git a/tests/Unit/Notifications/Channels/WebhookChannelTest.php b/tests/Unit/Notifications/Channels/WebhookChannelTest.php new file mode 100644 index 0000000000..783159e69e --- /dev/null +++ b/tests/Unit/Notifications/Channels/WebhookChannelTest.php @@ -0,0 +1,113 @@ + InstanceSettings::query()->updateOrCreate(['id' => 0], [ + 'fqdn' => 'https://coolify.example.com', + ])); + Queue::fake(); + + $this->team = Team::create([ + 'name' => 'Webhook Channel Team', + 'personal_team' => false, + 'show_boarding' => false, + ]); + // Assign through the model so the `encrypted` cast on webhook_url is applied. + $settings = $this->team->webhookNotificationSettings; + $settings->webhook_enabled = true; + $settings->webhook_url = 'https://webhook.example.com/coolify'; + $settings->save(); + $this->team->refresh(); +}); + +/** + * Send a notification through the webhook channel and return the dispatched payload. + * + * @return array + */ +function deliverOverWebhook(Team $team, Notification $notification): array +{ + expect($notification->via($team))->toContain(WebhookChannel::class); + + (new WebhookChannel)->send($team, $notification); + + $payload = null; + Queue::assertPushed(SendWebhookJob::class, function (SendWebhookJob $job) use (&$payload) { + $payload = $job->payload; + + return true; + }); + + expect($payload)->toBeArray() + ->and($payload['success'])->toBeBool() + ->and($payload['message'])->toBeString()->not->toBeEmpty(); + + return $payload; +} + +it('delivers ssl certificate renewal notifications over the webhook channel', function () { + $payload = deliverOverWebhook( + $this->team, + new SslExpirationNotification([(object) ['name' => 'my-application']]) + ); + + expect($payload['event'])->toBe('ssl_certificate_renewal') + ->and($payload['resources'])->toBe(['my-application']); +}); + +it('delivers api token expiring notifications over the webhook channel', function () { + $token = new PersonalAccessToken([ + 'name' => 'ci-token', + 'expires_at' => now()->addDay(), + ]); + + $payload = deliverOverWebhook($this->team, new ApiTokenExpiringNotification($token)); + + expect($payload['event'])->toBe('api_token_expiring') + ->and($payload['token_name'])->toBe('ci-token'); +}); + +it('delivers server force enabled notifications over the webhook channel', function () { + $server = Server::factory()->create(['team_id' => $this->team->id]); + + $payload = deliverOverWebhook($this->team, new ForceEnabled($server)); + + expect($payload['event'])->toBe('server_force_enabled') + ->and($payload['success'])->toBeTrue() + ->and($payload['server_uuid'])->toBe($server->uuid); +}); + +it('delivers server force disabled notifications over the webhook channel', function () { + $server = Server::factory()->create(['team_id' => $this->team->id]); + + $payload = deliverOverWebhook($this->team, new ForceDisabled($server)); + + expect($payload['event'])->toBe('server_force_disabled') + ->and($payload['success'])->toBeFalse() + ->and($payload['server_uuid'])->toBe($server->uuid); +}); + +it('delivers general notifications over the webhook channel', function () { + $payload = deliverOverWebhook($this->team, new GeneralNotification('Something happened')); + + expect($payload['event'])->toBe('general') + ->and($payload['message'])->toBe('Something happened'); +});