Files
coolify/tests/Feature/ServerReachabilityNotificationTest.php
Andras Bacsai 99c840922c fix(notifications): throttle repeated notifications per resource
High disk usage notifications were sent every ~10 minutes on Coolify Cloud
because the cache rate limiter did not hold across Sentinel-triggered checks.

- Add a notification_throttles table and NotificationThrottle helper that
  atomically records when a notification was sent for a resource.
- Add the ThrottledNotification contract; Team::notify() skips a throttled
  notification that was already sent within its interval.
- High disk usage: configurable per-server interval
  (server_disk_usage_notification_interval_hours, default 24h) in
  Server > Advanced and the servers API.
- Container restarted (coolify-proxy, database TCP proxy): at most one
  notification per hour per server/database during a restart loop.
- Move the unreachable and missing-backup markers into the new table and
  migrate existing data (servers.unreachable_notification_sent and
  scheduled_database_backups.missing_backup_notification_sent_at).
- Regenerate OpenAPI files.
2026-09-29 11:29:39 +02:00

103 lines
3.4 KiB
PHP

<?php
use App\Events\ServerReachabilityChanged;
use App\Models\NotificationThrottle;
use App\Models\Server;
use App\Models\Team;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->team = Team::factory()->create();
$this->team->emailNotificationSettings()->update([
'use_instance_email_settings' => true,
'server_unreachable_email_notifications' => true,
'server_reachable_email_notifications' => true,
]);
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
Notification::fake();
});
it('sends Unreachable notification when threshold reached and not yet notified', function () {
$this->server->settings()->update(['is_reachable' => false]);
$this->server->forceFill([
'unreachable_count' => 2,
])->save();
ServerReachabilityChanged::dispatch($this->server->fresh());
Notification::assertSentTo($this->team, Unreachable::class);
expect($this->server->fresh()->unreachable_notification_sent)->toBeTrue();
});
it('does not send Unreachable on first transient failure (count=1)', function () {
$this->server->settings()->update(['is_reachable' => false]);
$this->server->forceFill([
'unreachable_count' => 1,
])->save();
ServerReachabilityChanged::dispatch($this->server->fresh());
Notification::assertNothingSent();
});
it('does not send Unreachable when already notified', function () {
$this->server->settings()->update(['is_reachable' => false]);
$this->server->forceFill([
'unreachable_count' => 5,
])->save();
NotificationThrottle::record($this->server, Unreachable::class);
ServerReachabilityChanged::dispatch($this->server->fresh());
Notification::assertNothingSent();
});
it('sends Reachable notification on recovery when previously notified', function () {
$this->server->settings()->update(['is_reachable' => true]);
$this->server->forceFill([
'unreachable_count' => 0,
])->save();
NotificationThrottle::record($this->server, Unreachable::class);
$fresh = $this->server->fresh();
expect($fresh->unreachable_notification_sent)->toBeTrue();
expect((bool) $fresh->settings->is_reachable)->toBeTrue();
ServerReachabilityChanged::dispatch($fresh);
Notification::assertSentTo($this->team, Reachable::class);
expect($this->server->fresh()->unreachable_notification_sent)->toBeFalse();
});
it('does not send Reachable when never notified', function () {
$this->server->settings()->update(['is_reachable' => true]);
$this->server->forceFill([
'unreachable_count' => 0,
])->save();
ServerReachabilityChanged::dispatch($this->server->fresh());
Notification::assertNothingSent();
});
it('routes Unreachable notification through EmailChannel when email toggle is on', function () {
$this->server->settings()->update(['is_reachable' => false]);
$this->server->forceFill([
'unreachable_count' => 2,
])->save();
ServerReachabilityChanged::dispatch($this->server->fresh());
Notification::assertSentTo($this->team, Unreachable::class, function ($notification, $channels) {
return in_array(EmailChannel::class, $channels);
});
});