import { test, describe, mock, expect } from "bun:test"; import { backupsService } from "../backups.service"; import { createTestVolume } from "~/test/helpers/volume"; import { createTestBackupSchedule } from "~/test/helpers/backup"; import { createTestRepository } from "~/test/helpers/repository"; import { generateBackupOutput } from "~/test/helpers/restic"; import { beforeEach } from "bun:test"; const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0 })); mock.module("~/server/utils/spawn", () => ({ safeSpawn: resticBackupMock, })); beforeEach(() => { resticBackupMock.mockClear(); }); describe("execute backup", () => { test("should correctly set next backup time", async () => { // arrange const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, cronExpression: "*/5 * * * *", }); expect(schedule.nextBackupAt).toBeNull(); resticBackupMock.mockImplementationOnce(() => Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }), ); // act await backupsService.executeBackup(schedule.id); // assert const updatedSchedule = await backupsService.getSchedule(schedule.id); expect(updatedSchedule.nextBackupAt).not.toBeNull(); const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0); const now = new Date(); expect(nextBackupAt.getTime()).toBeGreaterThanOrEqual(now.getTime()); expect(nextBackupAt.getTime() - now.getTime()).toBeLessThanOrEqual(5 * 60 * 1000); }); test("should skip backup if schedule is disabled", async () => { // arrange const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, enabled: false, }); // act await backupsService.executeBackup(schedule.id); // assert expect(resticBackupMock).not.toHaveBeenCalled(); }); test("should execute backup if schedule is disabled but the run is manual", async () => { // arrange const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, enabled: false, }); resticBackupMock.mockImplementationOnce(() => Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }), ); // act await backupsService.executeBackup(schedule.id, true); // assert expect(resticBackupMock).toHaveBeenCalled(); }); test("should skip the backup if the previous one is still running", async () => { // arrange const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, }); resticBackupMock.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return Promise.resolve({ exitCode: 0, stdout: generateBackupOutput(), stderr: "" }); }); // act backupsService.executeBackup(schedule.id); await new Promise((resolve) => setTimeout(resolve, 10)); await backupsService.executeBackup(schedule.id); // assert expect(resticBackupMock).toHaveBeenCalledTimes(1); }); test("should set the backup status to failed if restic returns a 3 exit code", async () => { // arrange const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, }); resticBackupMock.mockImplementationOnce(() => Promise.resolve({ exitCode: 3, stdout: generateBackupOutput(), stderr: "Some error occurred" }), ); // act await backupsService.executeBackup(schedule.id); // assert const updatedSchedule = await backupsService.getSchedule(schedule.id); expect(updatedSchedule.lastBackupStatus).toBe("warning"); }); test("should set the backup status to failed if restic returns a non zero exit code", async () => { // arrange const volume = await createTestVolume(); const repository = await createTestRepository(); const schedule = await createTestBackupSchedule({ volumeId: volume.id, repositoryId: repository.id, }); resticBackupMock.mockImplementationOnce(() => Promise.resolve({ exitCode: 1, stdout: generateBackupOutput(), stderr: "Some error occurred" }), ); // act await backupsService.executeBackup(schedule.id); // assert const updatedSchedule = await backupsService.getSchedule(schedule.id); expect(updatedSchedule.lastBackupStatus).toBe("error"); }); });