* test: backups service * refactor: create hono app in a separate file To avoid side effects like db migration or startup scripts when testing test(backups): add security tests to the backups controller * ci: run typechecks, build and tests on PR * test: controllers security tests * chore: update lock file * refactor: pr feedbacks
151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
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");
|
|
});
|
|
});
|