test: schedule db
This commit is contained in:
parent
ff78a5470a
commit
2ea5acd256
8 changed files with 210 additions and 11 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import waitForExpect from "wait-for-expect";
|
||||||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||||
import { backupsService } from "../backups.service";
|
import { backupsService } from "../backups.service";
|
||||||
import { backupsExecutionService } from "../backups.execution";
|
import { backupsExecutionService } from "../backups.execution";
|
||||||
|
|
@ -122,7 +123,11 @@ describe("stop backup", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
void backupsExecutionService.executeBackup(schedule.id);
|
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
|
// act
|
||||||
await backupsExecutionService.stopBackup(schedule.id);
|
await backupsExecutionService.stopBackup(schedule.id);
|
||||||
|
|
@ -335,7 +340,9 @@ describe("mirror operations", () => {
|
||||||
// act
|
// act
|
||||||
await backupsExecutionService.copyToMirrors(schedule.id, sourceRepository, schedule.retentionPolicy);
|
await backupsExecutionService.copyToMirrors(schedule.id, sourceRepository, schedule.retentionPolicy);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await waitForExpect(() => {
|
||||||
|
expect(resticCopyMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticForgetMock).toHaveBeenCalledWith(
|
expect(resticForgetMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -363,7 +370,9 @@ describe("mirror operations", () => {
|
||||||
// act
|
// act
|
||||||
await backupsExecutionService.copyToMirrors(schedule.id, sourceRepository, schedule.retentionPolicy);
|
await backupsExecutionService.copyToMirrors(schedule.id, sourceRepository, schedule.retentionPolicy);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await waitForExpect(() => {
|
||||||
|
expect(resticCopyMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticForgetMock).not.toHaveBeenCalled();
|
expect(resticForgetMock).not.toHaveBeenCalled();
|
||||||
|
|
|
||||||
178
app/server/modules/backups/__tests__/backups.queries.test.ts
Normal file
178
app/server/modules/backups/__tests__/backups.queries.test.ts
Normal 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import waitForExpect from "wait-for-expect";
|
||||||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||||
import { backupsService } from "../backups.service";
|
import { backupsService } from "../backups.service";
|
||||||
import { createTestVolume } from "~/test/helpers/volume";
|
import { createTestVolume } from "~/test/helpers/volume";
|
||||||
|
|
@ -106,7 +107,11 @@ describe("execute backup", () => {
|
||||||
|
|
||||||
// act
|
// act
|
||||||
void backupsExecutionService.executeBackup(schedule.id);
|
void backupsExecutionService.executeBackup(schedule.id);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
||||||
|
await waitForExpect(() => {
|
||||||
|
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
await backupsExecutionService.executeBackup(schedule.id);
|
await backupsExecutionService.executeBackup(schedule.id);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ export const processPattern = (pattern: string, volumePath: string) => {
|
||||||
const isNegated = pattern.startsWith("!");
|
const isNegated = pattern.startsWith("!");
|
||||||
const p = isNegated ? pattern.slice(1) : pattern;
|
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;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import {
|
||||||
import { notificationsService } from "../notifications/notifications.service";
|
import { notificationsService } from "../notifications/notifications.service";
|
||||||
import { requireAuth } from "../auth/auth.middleware";
|
import { requireAuth } from "../auth/auth.middleware";
|
||||||
import { backupsExecutionService } from "./backups.execution";
|
import { backupsExecutionService } from "./backups.execution";
|
||||||
|
import { logger } from "~/server/utils/logger";
|
||||||
|
|
||||||
export const backupScheduleController = new Hono()
|
export const backupScheduleController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
|
|
@ -95,7 +96,7 @@ export const backupScheduleController = new Hono()
|
||||||
}
|
}
|
||||||
|
|
||||||
backupsExecutionService.executeBackup(Number(scheduleId), true).catch((err) => {
|
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);
|
return c.json<RunBackupNowDto>({ success: true }, 200);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export const scheduleQueries = {
|
||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
{ enabled: true },
|
{ enabled: true },
|
||||||
{ OR: [{ lastBackupStatus: { NOT: "in_progress" } }, { lastBackupStatus: { isNull: true } }] },
|
{ OR: [{ lastBackupStatus: { ne: "in_progress" } }, { lastBackupStatus: { isNull: true } }] },
|
||||||
{ organizationId },
|
{ organizationId },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -47,11 +47,10 @@ export const scheduleQueries = {
|
||||||
|
|
||||||
export const mirrorQueries = {
|
export const mirrorQueries = {
|
||||||
findEnabledBySchedule: async (scheduleId: number) => {
|
findEnabledBySchedule: async (scheduleId: number) => {
|
||||||
const mirrors = await db.query.backupScheduleMirrorsTable.findMany({
|
return db.query.backupScheduleMirrorsTable.findMany({
|
||||||
where: { scheduleId },
|
where: { scheduleId, enabled: true },
|
||||||
with: { repository: true },
|
with: { repository: true },
|
||||||
});
|
});
|
||||||
return mirrors.filter((m) => m.enabled);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStatus: async (
|
updateStatus: async (
|
||||||
|
|
|
||||||
3
bun.lock
3
bun.lock
|
|
@ -98,6 +98,7 @@
|
||||||
"vite-bundle-analyzer": "^1.2.3",
|
"vite-bundle-analyzer": "^1.2.3",
|
||||||
"vite-plugin-babel": "^1.4.1",
|
"vite-plugin-babel": "^1.4.1",
|
||||||
"vite-tsconfig-paths": "^6.0.5",
|
"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=="],
|
"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=="],
|
"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=="],
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,8 @@
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-bundle-analyzer": "^1.2.3",
|
"vite-bundle-analyzer": "^1.2.3",
|
||||||
"vite-plugin-babel": "^1.4.1",
|
"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": {
|
"overrides": {
|
||||||
"esbuild": "^0.27.2"
|
"esbuild": "^0.27.2"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue