Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
e5c75db84a
13 changed files with 349 additions and 4 deletions
|
|
@ -5,6 +5,7 @@
|
|||
use App\Jobs\ApiTokenExpirationWarningJob;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CheckMissingDatabaseBackupsJob;
|
||||
use App\Jobs\CheckTraefikVersionJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupOrphanedPreviewContainersJob;
|
||||
|
|
@ -53,6 +54,7 @@ protected function schedule(Schedule $schedule): void
|
|||
->runInBackground();
|
||||
$this->scheduleInstance->command('sanctum:prune-expired --hours=1')->hourly()->onOneServer();
|
||||
$this->scheduleInstance->job(new ApiTokenExpirationWarningJob)->hourly()->onOneServer();
|
||||
$this->scheduleInstance->job(new CheckMissingDatabaseBackupsJob)->hourly()->onOneServer();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
|
|
|
|||
|
|
@ -769,6 +769,7 @@ public function update_by_uuid(Request $request)
|
|||
'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Number of days to retain backups in S3'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'number', 'description' => 'Max storage (GB) for S3 backups'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600],
|
||||
'missing_backup_notification_days' => ['type' => 'integer', 'description' => 'Alert after this many days without an execution; 0 disables alerts', 'minimum' => 0, 'maximum' => 365, 'default' => 0],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -805,7 +806,7 @@ public function update_by_uuid(Request $request)
|
|||
)]
|
||||
public function create_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout'];
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout', 'missing_backup_notification_days'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
|
|
@ -833,6 +834,7 @@ public function create_backup(Request $request)
|
|||
'database_backup_retention_days_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'numeric|min:0',
|
||||
'timeout' => 'integer|min:60|max:36000',
|
||||
'missing_backup_notification_days' => 'integer|min:0|max:365',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
|
@ -1025,6 +1027,7 @@ public function create_backup(Request $request)
|
|||
'database_backup_retention_days_s3' => ['type' => 'integer', 'description' => 'Retention days of the backup in s3'],
|
||||
'database_backup_retention_max_storage_s3' => ['type' => 'number', 'description' => 'Max storage of the backup in S3'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'Backup job timeout in seconds (min: 60, max: 36000)', 'default' => 3600],
|
||||
'missing_backup_notification_days' => ['type' => 'integer', 'description' => 'Alert after this many days without an execution; 0 disables alerts', 'minimum' => 0, 'maximum' => 365],
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -1054,7 +1057,7 @@ public function create_backup(Request $request)
|
|||
)]
|
||||
public function update_backup(Request $request)
|
||||
{
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout'];
|
||||
$backupConfigFields = ['save_s3', 'enabled', 'dump_all', 'frequency', 'databases_to_backup', 'database_backup_retention_amount_locally', 'database_backup_retention_days_locally', 'database_backup_retention_max_storage_locally', 'database_backup_retention_amount_s3', 'database_backup_retention_days_s3', 'database_backup_retention_max_storage_s3', 's3_storage_uuid', 'timeout', 'missing_backup_notification_days'];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
|
|
@ -1080,6 +1083,7 @@ public function update_backup(Request $request)
|
|||
'database_backup_retention_days_s3' => 'integer|min:0',
|
||||
'database_backup_retention_max_storage_s3' => 'numeric|min:0',
|
||||
'timeout' => 'integer|min:60|max:36000',
|
||||
'missing_backup_notification_days' => 'integer|min:0|max:365',
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
|
|
@ -4885,6 +4889,8 @@ function () use ($database) {
|
|||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_execution_at',
|
||||
'missing_backup_notification_sent_at',
|
||||
])->fill([
|
||||
'uuid' => new_public_id(),
|
||||
'database_id' => $newDatabase->id,
|
||||
|
|
|
|||
59
app/Jobs/CheckMissingDatabaseBackupsJob.php
Normal file
59
app/Jobs/CheckMissingDatabaseBackupsJob.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Notifications\Database\BackupMissing;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckMissingDatabaseBackupsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
ScheduledDatabaseBackup::query()
|
||||
->with(['team', 'database', 'latest_log'])
|
||||
->where('enabled', true)
|
||||
->where('missing_backup_notification_days', '>', 0)
|
||||
->chunkById(100, function ($backups): void {
|
||||
foreach ($backups as $backup) {
|
||||
$this->notifyIfMissing($backup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function notifyIfMissing(ScheduledDatabaseBackup $backup): void
|
||||
{
|
||||
$lastExecutionAt = $backup->last_execution_at ?? $backup->latest_log?->created_at;
|
||||
$lastActivityAt = $lastExecutionAt ?? $backup->created_at;
|
||||
|
||||
if (! $lastActivityAt || $lastActivityAt->isAfter(now()->subDays($backup->missing_backup_notification_days))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($backup->missing_backup_notification_sent_at?->greaterThanOrEqualTo($lastActivityAt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $backup->team) {
|
||||
Log::warning("Cannot send missing backup notification for backup {$backup->id}: team not found");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($backup->team->getEnabledChannels('backup_failure') === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backup->team->notify(new BackupMissing($backup, $lastExecutionAt));
|
||||
$backup->forceFill(['missing_backup_notification_sent_at' => now()])->save();
|
||||
}
|
||||
}
|
||||
|
|
@ -85,6 +85,9 @@ class BackupEdit extends Component
|
|||
#[Validate(['required', 'int', 'min:60', 'max:36000'])]
|
||||
public int|string $timeout = 3600;
|
||||
|
||||
#[Validate(['required', 'integer', 'min:0', 'max:365'])]
|
||||
public int $missingBackupNotificationDays = 0;
|
||||
|
||||
public function getListeners(): array
|
||||
{
|
||||
// Keep "Backup Now" in sync when the database starts/stops without a full page refresh.
|
||||
|
|
@ -152,6 +155,7 @@ private function syncData(bool $toModel = false): void
|
|||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
$this->backup->dump_all = $this->dumpAll;
|
||||
$this->backup->timeout = $this->timeout;
|
||||
$this->backup->missing_backup_notification_days = $this->missingBackupNotificationDays;
|
||||
$this->customValidate();
|
||||
$this->backup->save();
|
||||
} else {
|
||||
|
|
@ -170,6 +174,7 @@ private function syncData(bool $toModel = false): void
|
|||
$this->databasesToBackup = $this->backup->databases_to_backup;
|
||||
$this->dumpAll = $this->backup->dump_all;
|
||||
$this->timeout = $this->backup->timeout;
|
||||
$this->missingBackupNotificationDays = $this->backup->missing_backup_notification_days;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ protected function casts(): array
|
|||
'dump_all' => 'boolean',
|
||||
'database_backup_retention_max_storage_locally' => 'float',
|
||||
'database_backup_retention_max_storage_s3' => 'float',
|
||||
'missing_backup_notification_days' => 'integer',
|
||||
'missing_backup_notification_sent_at' => 'datetime',
|
||||
'last_execution_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +40,7 @@ protected function casts(): array
|
|||
'database_backup_retention_max_storage_s3',
|
||||
'timeout',
|
||||
'disable_local_backup',
|
||||
'missing_backup_notification_days',
|
||||
];
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@
|
|||
|
||||
class ScheduledDatabaseBackupExecution extends BaseModel
|
||||
{
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function (ScheduledDatabaseBackupExecution $execution): void {
|
||||
$execution->scheduledDatabaseBackup()->update(['last_execution_at' => $execution->created_at ?? now()]);
|
||||
});
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'scheduled_database_backup_id',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Notifications\Application\StatusChanged;
|
||||
use App\Notifications\Container\ContainerRestarted;
|
||||
use App\Notifications\Database\BackupFailed;
|
||||
use App\Notifications\Database\BackupMissing;
|
||||
use App\Notifications\Database\BackupSuccess;
|
||||
use App\Notifications\ScheduledTask\TaskFailed;
|
||||
use App\Notifications\ScheduledTask\TaskSuccess;
|
||||
|
|
@ -40,7 +41,8 @@ public function send($notifiable, $notification): void
|
|||
RestartLimitReached::class => $settings->telegram_notifications_restart_limit_reached_thread_id,
|
||||
|
||||
BackupSuccess::class => $settings->telegram_notifications_backup_success_thread_id,
|
||||
BackupFailed::class => $settings->telegram_notifications_backup_failure_thread_id,
|
||||
BackupFailed::class,
|
||||
BackupMissing::class => $settings->telegram_notifications_backup_failure_thread_id,
|
||||
|
||||
TaskSuccess::class => $settings->telegram_notifications_scheduled_task_success_thread_id,
|
||||
TaskFailed::class => $settings->telegram_notifications_scheduled_task_failure_thread_id,
|
||||
|
|
|
|||
83
app/Notifications/Database/BackupMissing.php
Normal file
83
app/Notifications/Database/BackupMissing.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Notifications\CustomEmailNotification;
|
||||
use App\Notifications\Dto\DiscordMessage;
|
||||
use App\Notifications\Dto\PushoverMessage;
|
||||
use App\Notifications\Dto\SlackMessage;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class BackupMissing extends CustomEmailNotification
|
||||
{
|
||||
public string $databaseName;
|
||||
|
||||
public function __construct(public ScheduledDatabaseBackup $backup, public ?Carbon $lastExecutionAt)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->databaseName = $backup->database?->name ?? $backup->description ?? $backup->uuid;
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return $notifiable->getEnabledChannels('backup_failure');
|
||||
}
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject("Coolify: [ACTION REQUIRED] No recent backup for {$this->databaseName}")
|
||||
->view('emails.backup-missing', $this->messageData());
|
||||
}
|
||||
|
||||
public function toDiscord(): DiscordMessage
|
||||
{
|
||||
return new DiscordMessage(
|
||||
title: ':warning: Scheduled database backup missing',
|
||||
description: $this->description(),
|
||||
color: DiscordMessage::errorColor(),
|
||||
isCritical: true,
|
||||
);
|
||||
}
|
||||
|
||||
public function toTelegram(): array
|
||||
{
|
||||
return ['message' => 'Coolify: '.$this->description()];
|
||||
}
|
||||
|
||||
public function toPushover(): PushoverMessage
|
||||
{
|
||||
return new PushoverMessage(title: 'Scheduled database backup missing', level: 'error', message: $this->description());
|
||||
}
|
||||
|
||||
public function toSlack(): SlackMessage
|
||||
{
|
||||
return new SlackMessage(title: 'Scheduled database backup missing', description: $this->description(), color: SlackMessage::errorColor());
|
||||
}
|
||||
|
||||
public function toWebhook(): array
|
||||
{
|
||||
return array_merge($this->messageData(), [
|
||||
'success' => false,
|
||||
'message' => 'Scheduled database backup missing',
|
||||
'event' => 'backup_missing',
|
||||
'backup_uuid' => $this->backup->uuid,
|
||||
]);
|
||||
}
|
||||
|
||||
private function description(): string
|
||||
{
|
||||
return "The enabled backup schedule for {$this->databaseName} has produced no executions in the last {$this->backup->missing_backup_notification_days} day(s).";
|
||||
}
|
||||
|
||||
private function messageData(): array
|
||||
{
|
||||
return [
|
||||
'database_name' => $this->databaseName,
|
||||
'days' => $this->backup->missing_backup_notification_days,
|
||||
'last_execution_at' => $this->lastExecutionAt?->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||
$table->unsignedInteger('missing_backup_notification_days')->default(0);
|
||||
$table->timestamp('missing_backup_notification_sent_at')->nullable();
|
||||
$table->timestamp('last_execution_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'missing_backup_notification_days',
|
||||
'missing_backup_notification_sent_at',
|
||||
'last_execution_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
7
resources/views/emails/backup-missing.blade.php
Normal file
7
resources/views/emails/backup-missing.blade.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
The enabled backup schedule for {{ $database_name }} has produced no executions in the last {{ $days }} day(s).
|
||||
|
||||
@if ($last_execution_at)
|
||||
The last execution was at {{ $last_execution_at }}.
|
||||
@else
|
||||
This schedule has never produced an execution.
|
||||
@endif
|
||||
|
|
@ -91,6 +91,9 @@ class="chip-remove"
|
|||
required />
|
||||
<x-forms.input label="Timeout" id="timeout" type="number" min="60"
|
||||
helper="Maximum backup runtime in seconds." required />
|
||||
<x-forms.input label="Missing backup alert after" id="missingBackupNotificationDays" type="number"
|
||||
min="0" max="365" suffix="days" canGate="manageBackups" :canResource="$backup->database"
|
||||
helper="Notify through backup failure channels after this many days without an execution. Use 0 to disable." required />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -138,6 +138,16 @@
|
|||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
$backup = $database->scheduledBackups()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'enabled' => true,
|
||||
'frequency' => '0 0 * * *',
|
||||
'save_s3' => false,
|
||||
]);
|
||||
$backup->forceFill([
|
||||
'last_execution_at' => now()->subDay(),
|
||||
'missing_backup_notification_sent_at' => now(),
|
||||
])->save();
|
||||
|
||||
$response = $this->withHeaders($this->headers)
|
||||
->postJson("/api/v1/databases/{$database->uuid}/clone", [
|
||||
|
|
@ -149,11 +159,14 @@
|
|||
->assertJsonPath('message', 'Database cloned.');
|
||||
|
||||
$cloned = StandalonePostgresql::where('uuid', $response->json('uuid'))->first();
|
||||
$clonedBackup = $cloned->scheduledBackups()->sole();
|
||||
expect($cloned)->not->toBeNull()
|
||||
->and($cloned->name)->toBe('cloned-db')
|
||||
->and($cloned->environment_id)->toBe($database->environment_id)
|
||||
->and($cloned->destination_id)->toBe($this->destination->id)
|
||||
->and(str($cloned->status)->startsWith('exited'))->toBeTrue();
|
||||
->and(str($cloned->status)->startsWith('exited'))->toBeTrue()
|
||||
->and($clonedBackup->last_execution_at)->toBeNull()
|
||||
->and($clonedBackup->missing_backup_notification_sent_at)->toBeNull();
|
||||
});
|
||||
|
||||
test('creates renamed volumes when cloning a database with clone_volumes', function () {
|
||||
|
|
|
|||
126
tests/Feature/MissingDatabaseBackupNotificationTest.php
Normal file
126
tests/Feature/MissingDatabaseBackupNotificationTest.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
use App\Jobs\CheckMissingDatabaseBackupsJob;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\Database\BackupMissing;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
afterEach(fn () => Carbon::setTestNow());
|
||||
|
||||
function missingBackupSchedule(Team $team, array $attributes = []): ScheduledDatabaseBackup
|
||||
{
|
||||
$backup = ScheduledDatabaseBackup::create(array_merge([
|
||||
'enabled' => true,
|
||||
'frequency' => '0 0 * * *',
|
||||
'save_s3' => false,
|
||||
'database_type' => 'App\\Models\\StandalonePostgresql',
|
||||
'database_id' => 999,
|
||||
'team_id' => $team->id,
|
||||
'missing_backup_notification_days' => 2,
|
||||
], $attributes));
|
||||
|
||||
ScheduledDatabaseBackup::whereKey($backup->id)->update(['created_at' => now()->subDays(3)]);
|
||||
|
||||
return $backup->refresh();
|
||||
}
|
||||
|
||||
function teamWithBackupFailureNotifications(): Team
|
||||
{
|
||||
$team = Team::create(['name' => 'Test team']);
|
||||
$team->emailNotificationSettings->update([
|
||||
'smtp_enabled' => true,
|
||||
'backup_failure_email_notifications' => true,
|
||||
]);
|
||||
|
||||
return $team;
|
||||
}
|
||||
|
||||
it('notifies the team when an enabled backup has no executions for the configured days', function () {
|
||||
Notification::fake();
|
||||
$team = teamWithBackupFailureNotifications();
|
||||
$backup = missingBackupSchedule($team);
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
|
||||
Notification::assertSentTo($team, BackupMissing::class, fn (BackupMissing $notification) => $notification->backup->is($backup));
|
||||
expect($backup->fresh()->missing_backup_notification_sent_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('does not notify for recent disabled or unconfigured backup schedules', function () {
|
||||
Notification::fake();
|
||||
$team = teamWithBackupFailureNotifications();
|
||||
missingBackupSchedule($team, ['enabled' => false]);
|
||||
missingBackupSchedule($team, ['missing_backup_notification_days' => 0]);
|
||||
$recent = missingBackupSchedule($team);
|
||||
ScheduledDatabaseBackup::whereKey($recent->id)->update(['created_at' => now()->subDay()]);
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
it('notifies once per period without executions and rearms after another execution', function () {
|
||||
Notification::fake();
|
||||
$team = teamWithBackupFailureNotifications();
|
||||
$backup = missingBackupSchedule($team);
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
|
||||
Notification::assertSentToTimes($team, BackupMissing::class, 1);
|
||||
|
||||
Carbon::setTestNow(now()->addMinute());
|
||||
$execution = ScheduledDatabaseBackupExecution::create([
|
||||
'scheduled_database_backup_id' => $backup->id,
|
||||
'status' => 'success',
|
||||
'database_name' => 'app',
|
||||
]);
|
||||
Carbon::setTestNow(now()->addDays(3));
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
|
||||
Notification::assertSentToTimes($team, BackupMissing::class, 2);
|
||||
});
|
||||
|
||||
it('preserves the last execution checkpoint when execution history is deleted', function () {
|
||||
Notification::fake();
|
||||
$team = teamWithBackupFailureNotifications();
|
||||
$backup = missingBackupSchedule($team);
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
Carbon::setTestNow(now()->addMinute());
|
||||
$execution = ScheduledDatabaseBackupExecution::create([
|
||||
'scheduled_database_backup_id' => $backup->id,
|
||||
'status' => 'success',
|
||||
'database_name' => 'app',
|
||||
]);
|
||||
$execution->delete();
|
||||
Carbon::setTestNow(now()->addDays(3));
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
|
||||
Notification::assertSentToTimes($team, BackupMissing::class, 2);
|
||||
});
|
||||
|
||||
it('waits to mark an incident sent until a notification channel is enabled', function () {
|
||||
Notification::fake();
|
||||
$team = Team::create(['name' => 'Test team']);
|
||||
$backup = missingBackupSchedule($team);
|
||||
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
expect($backup->fresh()->missing_backup_notification_sent_at)->toBeNull();
|
||||
|
||||
$team->emailNotificationSettings->update([
|
||||
'smtp_enabled' => true,
|
||||
'backup_failure_email_notifications' => true,
|
||||
]);
|
||||
(new CheckMissingDatabaseBackupsJob)->handle();
|
||||
|
||||
Notification::assertSentTo($team, BackupMissing::class);
|
||||
});
|
||||
Loading…
Reference in a new issue