test: schedule db

This commit is contained in:
Nicolas Meienberger 2026-02-03 22:16:17 +01:00
parent ff78a5470a
commit 2ea5acd256
8 changed files with 210 additions and 11 deletions

View file

@ -1,3 +1,4 @@
import waitForExpect from "wait-for-expect";
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
import { backupsService } from "../backups.service";
import { backupsExecutionService } from "../backups.execution";
@ -122,7 +123,11 @@ describe("stop backup", () => {
});
void backupsExecutionService.executeBackup(schedule.id);
await new Promise((resolve) => setTimeout(resolve, 50));
await waitForExpect(async () => {
const runningSchedule = await backupsService.getScheduleById(schedule.id);
expect(runningSchedule.lastBackupStatus).toBe("in_progress");
});
// act
await backupsExecutionService.stopBackup(schedule.id);
@ -335,7 +340,9 @@ describe("mirror operations", () => {
// act
await backupsExecutionService.copyToMirrors(schedule.id, sourceRepository, schedule.retentionPolicy);
await new Promise((resolve) => setTimeout(resolve, 100));
await waitForExpect(() => {
expect(resticCopyMock).toHaveBeenCalled();
});
// assert
expect(resticForgetMock).toHaveBeenCalledWith(
@ -363,7 +370,9 @@ describe("mirror operations", () => {
// act
await backupsExecutionService.copyToMirrors(schedule.id, sourceRepository, schedule.retentionPolicy);
await new Promise((resolve) => setTimeout(resolve, 100));
await waitForExpect(() => {
expect(resticCopyMock).toHaveBeenCalled();
});
// assert
expect(resticForgetMock).not.toHaveBeenCalled();

View file

@ -0,0 +1,178 @@
import { test, describe, expect, beforeEach } from "bun:test";
import { scheduleQueries } from "../backups.queries";
import { createTestBackupSchedule } from "~/test/helpers/backup";
import { createTestVolume } from "~/test/helpers/volume";
import { createTestRepository } from "~/test/helpers/repository";
import { TEST_ORG_ID } from "~/test/helpers/organization";
import { faker } from "@faker-js/faker";
describe("scheduleQueries.findExecutable", () => {
let volume: { id: number };
let repository: { id: string };
beforeEach(async () => {
volume = await createTestVolume();
repository = await createTestRepository();
});
test("should return enabled schedules with null nextBackupAt", async () => {
// arrange
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: null,
lastBackupStatus: null,
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).toContain(schedule.id);
});
test("should return enabled schedules with past nextBackupAt", async () => {
// arrange
const pastTime = faker.date.past().getTime();
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: pastTime,
lastBackupStatus: null,
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).toContain(schedule.id);
});
test("should not return schedules with future nextBackupAt", async () => {
// arrange
const futureTime = faker.date.future().getTime();
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: futureTime,
lastBackupStatus: null,
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).not.toContain(schedule.id);
});
test("should not return disabled schedules", async () => {
// arrange
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: false,
nextBackupAt: null,
lastBackupStatus: null,
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).not.toContain(schedule.id);
});
test("should not return schedules with in_progress status", async () => {
// arrange
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: null,
lastBackupStatus: "in_progress",
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).not.toContain(schedule.id);
});
test("should return schedules with success status and past nextBackupAt", async () => {
// arrange
const pastTime = faker.date.past().getTime();
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: pastTime,
lastBackupStatus: "success",
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).toContain(schedule.id);
});
test("should return schedules with error status and null nextBackupAt", async () => {
// arrange
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: null,
lastBackupStatus: "error",
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).toContain(schedule.id);
});
test("should not return schedules from other organizations", async () => {
// arrange
const otherOrgId = faker.string.uuid();
const schedule = await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: null,
lastBackupStatus: null,
organizationId: otherOrgId,
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result).not.toContain(schedule.id);
});
test("should only return schedule IDs", async () => {
// arrange
await createTestBackupSchedule({
volumeId: volume.id,
repositoryId: repository.id,
enabled: true,
nextBackupAt: null,
lastBackupStatus: null,
});
// act
const result = await scheduleQueries.findExecutable(TEST_ORG_ID);
// assert
expect(result.length).toBeGreaterThan(0);
for (const id of result) {
expect(typeof id).toBe("number");
}
});
});

View file

@ -1,3 +1,4 @@
import waitForExpect from "wait-for-expect";
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
import { backupsService } from "../backups.service";
import { createTestVolume } from "~/test/helpers/volume";
@ -106,7 +107,11 @@ describe("execute backup", () => {
// act
void backupsExecutionService.executeBackup(schedule.id);
await new Promise((resolve) => setTimeout(resolve, 10));
await waitForExpect(() => {
expect(resticBackupMock).toHaveBeenCalledTimes(1);
});
await backupsExecutionService.executeBackup(schedule.id);
// assert

View file

@ -23,7 +23,10 @@ export const processPattern = (pattern: string, volumePath: string) => {
const isNegated = pattern.startsWith("!");
const p = isNegated ? pattern.slice(1) : pattern;
if (p.startsWith(volumePath) || !p.startsWith("/")) {
const relative = path.relative(volumePath, p);
const isInsideVolume = !relative.startsWith("..") && !path.isAbsolute(relative);
if (isInsideVolume || !p.startsWith("/")) {
return pattern;
}

View file

@ -43,6 +43,7 @@ import {
import { notificationsService } from "../notifications/notifications.service";
import { requireAuth } from "../auth/auth.middleware";
import { backupsExecutionService } from "./backups.execution";
import { logger } from "~/server/utils/logger";
export const backupScheduleController = new Hono()
.use(requireAuth)
@ -95,7 +96,7 @@ export const backupScheduleController = new Hono()
}
backupsExecutionService.executeBackup(Number(scheduleId), true).catch((err) => {
console.error(`Error executing manual backup for schedule ${scheduleId}:`, err);
logger.error(`Error executing manual backup for schedule ${scheduleId}:`, err);
});
return c.json<RunBackupNowDto>({ success: true }, 200);

View file

@ -19,7 +19,7 @@ export const scheduleQueries = {
where: {
AND: [
{ enabled: true },
{ OR: [{ lastBackupStatus: { NOT: "in_progress" } }, { lastBackupStatus: { isNull: true } }] },
{ OR: [{ lastBackupStatus: { ne: "in_progress" } }, { lastBackupStatus: { isNull: true } }] },
{ organizationId },
],
},
@ -47,11 +47,10 @@ export const scheduleQueries = {
export const mirrorQueries = {
findEnabledBySchedule: async (scheduleId: number) => {
const mirrors = await db.query.backupScheduleMirrorsTable.findMany({
where: { scheduleId },
return db.query.backupScheduleMirrorsTable.findMany({
where: { scheduleId, enabled: true },
with: { repository: true },
});
return mirrors.filter((m) => m.enabled);
},
updateStatus: async (

View file

@ -98,6 +98,7 @@
"vite-bundle-analyzer": "^1.2.3",
"vite-plugin-babel": "^1.4.1",
"vite-tsconfig-paths": "^6.0.5",
"wait-for-expect": "^4.0.0",
},
},
},
@ -1655,6 +1656,8 @@
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="],
"wait-for-expect": ["wait-for-expect@4.0.0", "", {}, "sha512-mcH2HYUUHhdFGHVJkgwkBxRihZO4VSuPyh6xhYHz7LEnYkcaLbTAEEsTpYiFw4UY45XdTZYYIaquuMucw9wWMw=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],

View file

@ -117,7 +117,8 @@
"vite": "^7.3.1",
"vite-bundle-analyzer": "^1.2.3",
"vite-plugin-babel": "^1.4.1",
"vite-tsconfig-paths": "^6.0.5"
"vite-tsconfig-paths": "^6.0.5",
"wait-for-expect": "^4.0.0"
},
"overrides": {
"esbuild": "^0.27.2"