refactor: tests
This commit is contained in:
parent
4a812a884b
commit
c13e57e4ae
8 changed files with 364 additions and 420 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import waitForExpect from "wait-for-expect";
|
import waitForExpect from "wait-for-expect";
|
||||||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
import { test, describe, mock, expect, 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";
|
||||||
import { createTestVolume } from "~/test/helpers/volume";
|
import { createTestVolume } from "~/test/helpers/volume";
|
||||||
|
|
@ -15,19 +15,22 @@ import { NotFoundError, BadRequestError } from "http-errors-enhanced";
|
||||||
import { scheduleQueries } from "../backups.queries";
|
import { scheduleQueries } from "../backups.queries";
|
||||||
import { fromAny } from "@total-typescript/shoehorn";
|
import { fromAny } from "@total-typescript/shoehorn";
|
||||||
|
|
||||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: generateBackupOutput(), error: "" }));
|
const setup = () => {
|
||||||
const resticForgetMock = mock(() => Promise.resolve({ success: true, data: null }));
|
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: generateBackupOutput(), error: "" }));
|
||||||
const resticCopyMock = mock(() => Promise.resolve({ success: true, output: "" }));
|
const resticForgetMock = mock(() => Promise.resolve({ success: true, data: null }));
|
||||||
|
const resticCopyMock = mock(() => Promise.resolve({ success: true, output: "" }));
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
resticBackupMock.mockClear();
|
|
||||||
resticForgetMock.mockClear();
|
|
||||||
resticCopyMock.mockClear();
|
|
||||||
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
||||||
spyOn(restic, "forget").mockImplementation(resticForgetMock);
|
spyOn(restic, "forget").mockImplementation(resticForgetMock);
|
||||||
spyOn(restic, "copy").mockImplementation(resticCopyMock);
|
spyOn(restic, "copy").mockImplementation(resticCopyMock);
|
||||||
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
||||||
});
|
|
||||||
|
return {
|
||||||
|
resticBackupMock,
|
||||||
|
resticForgetMock,
|
||||||
|
resticCopyMock,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
|
|
@ -36,6 +39,7 @@ afterEach(() => {
|
||||||
describe("backup execution - validation failures", () => {
|
describe("backup execution - validation failures", () => {
|
||||||
test("should fail backup when volume is not mounted", async () => {
|
test("should fail backup when volume is not mounted", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume({ status: "unmounted" });
|
const volume = await createTestVolume({ status: "unmounted" });
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -57,6 +61,7 @@ describe("backup execution - validation failures", () => {
|
||||||
|
|
||||||
test("should fail backup when volume does not exist", async () => {
|
test("should fail backup when volume does not exist", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -86,6 +91,7 @@ describe("backup execution - validation failures", () => {
|
||||||
|
|
||||||
test("should fail backup when repository does not exist", async () => {
|
test("should fail backup when repository does not exist", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -115,6 +121,7 @@ describe("backup execution - validation failures", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should fail backup when schedule does not exist", async () => {
|
test("should fail backup when schedule does not exist", async () => {
|
||||||
|
setup();
|
||||||
// act
|
// act
|
||||||
const result = await backupsExecutionService.validateBackupExecution(99999);
|
const result = await backupsExecutionService.validateBackupExecution(99999);
|
||||||
|
|
||||||
|
|
@ -130,6 +137,7 @@ describe("backup execution - validation failures", () => {
|
||||||
describe("stop backup", () => {
|
describe("stop backup", () => {
|
||||||
test("should stop a running backup", async () => {
|
test("should stop a running backup", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -160,6 +168,7 @@ describe("stop backup", () => {
|
||||||
|
|
||||||
test("should throw ConflictError when trying to stop non-running backup", async () => {
|
test("should throw ConflictError when trying to stop non-running backup", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -174,6 +183,7 @@ describe("stop backup", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw NotFoundError when schedule does not exist", async () => {
|
test("should throw NotFoundError when schedule does not exist", async () => {
|
||||||
|
setup();
|
||||||
// act & assert
|
// act & assert
|
||||||
await expect(backupsExecutionService.stopBackup(99999)).rejects.toThrow("Backup schedule not found");
|
await expect(backupsExecutionService.stopBackup(99999)).rejects.toThrow("Backup schedule not found");
|
||||||
});
|
});
|
||||||
|
|
@ -182,6 +192,7 @@ describe("stop backup", () => {
|
||||||
describe("retention policy - runForget", () => {
|
describe("retention policy - runForget", () => {
|
||||||
test("should execute forget with retention policy", async () => {
|
test("should execute forget with retention policy", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticForgetMock } = setup();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
repositoryId: repository.id,
|
repositoryId: repository.id,
|
||||||
|
|
@ -216,6 +227,7 @@ describe("retention policy - runForget", () => {
|
||||||
|
|
||||||
test("should throw BadRequestError if no retention policy configured", async () => {
|
test("should throw BadRequestError if no retention policy configured", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
repositoryId: repository.id,
|
repositoryId: repository.id,
|
||||||
|
|
@ -229,12 +241,14 @@ describe("retention policy - runForget", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw NotFoundError when schedule does not exist", async () => {
|
test("should throw NotFoundError when schedule does not exist", async () => {
|
||||||
|
setup();
|
||||||
// act & assert
|
// act & assert
|
||||||
await expect(backupsExecutionService.runForget(99999)).rejects.toThrow("Backup schedule not found");
|
await expect(backupsExecutionService.runForget(99999)).rejects.toThrow("Backup schedule not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw NotFoundError when repository does not exist", async () => {
|
test("should throw NotFoundError when repository does not exist", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
retentionPolicy: {
|
retentionPolicy: {
|
||||||
keepHourly: 24,
|
keepHourly: 24,
|
||||||
|
|
@ -251,6 +265,7 @@ describe("retention policy - runForget", () => {
|
||||||
describe("mirror operations", () => {
|
describe("mirror operations", () => {
|
||||||
test("should copy snapshots to mirror repositories", async () => {
|
test("should copy snapshots to mirror repositories", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticCopyMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
@ -277,6 +292,7 @@ describe("mirror operations", () => {
|
||||||
|
|
||||||
test("should skip disabled mirrors", async () => {
|
test("should skip disabled mirrors", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticCopyMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
@ -296,6 +312,7 @@ describe("mirror operations", () => {
|
||||||
|
|
||||||
test("should update mirror status on success", async () => {
|
test("should update mirror status on success", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
@ -319,6 +336,7 @@ describe("mirror operations", () => {
|
||||||
|
|
||||||
test("should finalize mirror status when mirror settings are updated during copy", async () => {
|
test("should finalize mirror status when mirror settings are updated during copy", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticCopyMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
@ -350,6 +368,7 @@ describe("mirror operations", () => {
|
||||||
|
|
||||||
test("should update mirror status on failure", async () => {
|
test("should update mirror status on failure", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticCopyMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
@ -375,6 +394,7 @@ describe("mirror operations", () => {
|
||||||
|
|
||||||
test("should run forget on mirror after successful copy when retention policy exists", async () => {
|
test("should run forget on mirror after successful copy when retention policy exists", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticCopyMock, resticForgetMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
@ -406,6 +426,7 @@ describe("mirror operations", () => {
|
||||||
|
|
||||||
test("should not run forget on mirror when no retention policy", async () => {
|
test("should not run forget on mirror when no retention policy", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticCopyMock, resticForgetMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const sourceRepository = await createTestRepository();
|
const sourceRepository = await createTestRepository();
|
||||||
const mirrorRepository = await createTestRepository();
|
const mirrorRepository = await createTestRepository();
|
||||||
|
|
|
||||||
|
|
@ -1,141 +1,94 @@
|
||||||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
import { test, describe, expect } from "bun:test";
|
||||||
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 { getVolumePath } from "../../volumes/helpers";
|
|
||||||
import { restic } from "~/server/utils/restic";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
import { fromAny } from "@total-typescript/shoehorn";
|
||||||
import * as context from "~/server/core/request-context";
|
import { createBackupOptions, processPattern } from "../backup.helpers";
|
||||||
import { backupsExecutionService } from "../backups.execution";
|
|
||||||
|
|
||||||
const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));
|
type BackupScheduleInput = Parameters<typeof createBackupOptions>[0];
|
||||||
|
|
||||||
beforeEach(() => {
|
const createSchedule = (overrides: Partial<BackupScheduleInput> = {}): BackupScheduleInput =>
|
||||||
backupMock.mockClear();
|
fromAny({
|
||||||
spyOn(restic, "backup").mockImplementation(backupMock);
|
shortId: "sched-1234",
|
||||||
spyOn(restic, "forget").mockImplementation(mock(() => Promise.resolve({ success: true, data: null })));
|
oneFileSystem: false,
|
||||||
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
includePatterns: [],
|
||||||
});
|
excludePatterns: [],
|
||||||
|
excludeIfPresent: [],
|
||||||
afterEach(() => {
|
...overrides,
|
||||||
mock.restore();
|
}) as BackupScheduleInput;
|
||||||
});
|
|
||||||
|
|
||||||
describe("executeBackup - include / exclude patterns", () => {
|
describe("executeBackup - include / exclude patterns", () => {
|
||||||
test("should correctly build include and exclude patterns", async () => {
|
test("should correctly build include and exclude patterns", () => {
|
||||||
// arrange
|
// arrange
|
||||||
const volume = await createTestVolume();
|
const volumePath = "/var/lib/zerobyte/volumes/vol123/_data";
|
||||||
const repository = await createTestRepository();
|
const schedule = createSchedule({
|
||||||
const volumePath = getVolumePath(volume);
|
|
||||||
|
|
||||||
const schedule = await createTestBackupSchedule({
|
|
||||||
volumeId: volume.id,
|
|
||||||
repositoryId: repository.id,
|
|
||||||
includePatterns: ["*.zip", "/Photos", "!/Temp", "!*.log"],
|
includePatterns: ["*.zip", "/Photos", "!/Temp", "!*.log"],
|
||||||
excludePatterns: [".DS_Store", "/Config", "!/Important", "!*.tmp"],
|
excludePatterns: [".DS_Store", "/Config", "!/Important", "!*.tmp"],
|
||||||
excludeIfPresent: [".nobackup"],
|
excludeIfPresent: [".nobackup"],
|
||||||
});
|
});
|
||||||
|
const signal = new AbortController().signal;
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsExecutionService.executeBackup(schedule.id);
|
const options = createBackupOptions(schedule, volumePath, signal);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(options).toMatchObject({
|
||||||
expect.anything(),
|
include: ["*.zip", path.join(volumePath, "Photos"), `!${path.join(volumePath, "Temp")}`, "!*.log"],
|
||||||
volumePath,
|
exclude: [".DS_Store", path.join(volumePath, "Config"), `!${path.join(volumePath, "Important")}`, "!*.tmp"],
|
||||||
expect.objectContaining({
|
excludeIfPresent: [".nobackup"],
|
||||||
include: ["*.zip", path.join(volumePath, "Photos"), `!${path.join(volumePath, "Temp")}`, "!*.log"],
|
});
|
||||||
exclude: [".DS_Store", path.join(volumePath, "Config"), `!${path.join(volumePath, "Important")}`, "!*.tmp"],
|
|
||||||
excludeIfPresent: [".nobackup"],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle the case where a subfolder has the exact same name as the volume name", async () => {
|
test("should handle the case where a subfolder has the exact same name as the volume name", () => {
|
||||||
// arrange
|
// arrange
|
||||||
const volumeName = "SyncFolder";
|
const volumeName = "SyncFolder";
|
||||||
const volume = await createTestVolume({
|
const volumePath = `/${volumeName}`;
|
||||||
name: volumeName,
|
const selectedPath = `/${volumeName}`;
|
||||||
type: "directory",
|
const schedule = createSchedule({
|
||||||
config: { backend: "directory", path: `/${volumeName}` },
|
|
||||||
});
|
|
||||||
const volumePath = getVolumePath(volume);
|
|
||||||
const repository = await createTestRepository();
|
|
||||||
|
|
||||||
const selectedPath = `/${volumeName}`; // Selection of the folder inside the volume
|
|
||||||
|
|
||||||
const schedule = await createTestBackupSchedule({
|
|
||||||
volumeId: volume.id,
|
|
||||||
repositoryId: repository.id,
|
|
||||||
includePatterns: [selectedPath],
|
includePatterns: [selectedPath],
|
||||||
});
|
});
|
||||||
|
const signal = new AbortController().signal;
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsExecutionService.executeBackup(schedule.id);
|
const options = createBackupOptions(schedule, volumePath, signal);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(options.include).toEqual([path.join(volumePath, volumeName)]);
|
||||||
expect.anything(),
|
|
||||||
volumePath,
|
|
||||||
expect.objectContaining({
|
|
||||||
// Should produce /SyncFolder/SyncFolder and not just /SyncFolder
|
|
||||||
include: [path.join(volumePath, volumeName)],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should correctly mix relative and absolute patterns", async () => {
|
test("should correctly mix relative and absolute patterns", () => {
|
||||||
// arrange
|
// arrange
|
||||||
const volume = await createTestVolume();
|
const volumePath = "/var/lib/zerobyte/volumes/vol456/_data";
|
||||||
const volumePath = getVolumePath(volume);
|
|
||||||
const repository = await createTestRepository();
|
|
||||||
|
|
||||||
const relativeInclude = "relative/include";
|
const relativeInclude = "relative/include";
|
||||||
const anchoredInclude = "/anchored/include";
|
const anchoredInclude = "/anchored/include";
|
||||||
|
const schedule = createSchedule({
|
||||||
const schedule = await createTestBackupSchedule({
|
|
||||||
volumeId: volume.id,
|
|
||||||
repositoryId: repository.id,
|
|
||||||
includePatterns: [relativeInclude, anchoredInclude],
|
includePatterns: [relativeInclude, anchoredInclude],
|
||||||
});
|
});
|
||||||
|
const signal = new AbortController().signal;
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsExecutionService.executeBackup(schedule.id);
|
const options = createBackupOptions(schedule, volumePath, signal);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(options.include).toEqual([relativeInclude, path.join(volumePath, "anchored/include")]);
|
||||||
expect.anything(),
|
|
||||||
volumePath,
|
|
||||||
expect.objectContaining({
|
|
||||||
include: [relativeInclude, path.join(volumePath, "anchored/include")],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle empty include and exclude patterns", async () => {
|
test("should handle empty include and exclude patterns", () => {
|
||||||
// arrange
|
// arrange
|
||||||
const volume = await createTestVolume();
|
const schedule = createSchedule({
|
||||||
const repository = await createTestRepository();
|
|
||||||
const schedule = await createTestBackupSchedule({
|
|
||||||
volumeId: volume.id,
|
|
||||||
repositoryId: repository.id,
|
|
||||||
includePatterns: [],
|
includePatterns: [],
|
||||||
excludePatterns: [],
|
excludePatterns: [],
|
||||||
});
|
});
|
||||||
|
const signal = new AbortController().signal;
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsExecutionService.executeBackup(schedule.id);
|
const options = createBackupOptions(schedule, "/var/lib/zerobyte/volumes/vol999/_data", signal);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(options.include).toEqual([]);
|
||||||
expect.anything(),
|
expect(options.exclude).toEqual([]);
|
||||||
getVolumePath(volume),
|
});
|
||||||
expect.objectContaining({
|
|
||||||
include: [],
|
test("processPattern keeps relative and negated relative patterns unchanged", () => {
|
||||||
exclude: [],
|
expect(processPattern("relative/include", "/volume")).toBe("relative/include");
|
||||||
}),
|
expect(processPattern("!*.log", "/volume")).toBe("!*.log");
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, describe, expect, beforeEach } from "bun:test";
|
import { test, describe, expect } from "bun:test";
|
||||||
import { scheduleQueries } from "../backups.queries";
|
import { scheduleQueries } from "../backups.queries";
|
||||||
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
import { createTestBackupSchedule } from "~/test/helpers/backup";
|
||||||
import { createTestVolume } from "~/test/helpers/volume";
|
import { createTestVolume } from "~/test/helpers/volume";
|
||||||
|
|
@ -8,19 +8,22 @@ import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
|
|
||||||
describe("scheduleQueries.findExecutable", () => {
|
describe("scheduleQueries.findExecutable", () => {
|
||||||
let volume: { id: number };
|
const createScheduleDependencies = async () => {
|
||||||
let repository: { id: string };
|
const volume = await createTestVolume();
|
||||||
|
const repository = await createTestRepository();
|
||||||
|
|
||||||
beforeEach(async () => {
|
return {
|
||||||
volume = await createTestVolume();
|
volumeId: volume.id,
|
||||||
repository = await createTestRepository();
|
repositoryId: repository.id,
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
test("should return enabled schedules with null nextBackupAt", async () => {
|
test("should return enabled schedules with null nextBackupAt", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: null,
|
nextBackupAt: null,
|
||||||
lastBackupStatus: null,
|
lastBackupStatus: null,
|
||||||
|
|
@ -35,10 +38,11 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should return enabled schedules with past nextBackupAt", async () => {
|
test("should return enabled schedules with past nextBackupAt", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const pastTime = faker.date.past().getTime();
|
const pastTime = faker.date.past().getTime();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: pastTime,
|
nextBackupAt: pastTime,
|
||||||
lastBackupStatus: null,
|
lastBackupStatus: null,
|
||||||
|
|
@ -53,10 +57,11 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should not return schedules with future nextBackupAt", async () => {
|
test("should not return schedules with future nextBackupAt", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const futureTime = faker.date.future().getTime();
|
const futureTime = faker.date.future().getTime();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: futureTime,
|
nextBackupAt: futureTime,
|
||||||
lastBackupStatus: null,
|
lastBackupStatus: null,
|
||||||
|
|
@ -71,9 +76,10 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should not return disabled schedules", async () => {
|
test("should not return disabled schedules", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
nextBackupAt: null,
|
nextBackupAt: null,
|
||||||
lastBackupStatus: null,
|
lastBackupStatus: null,
|
||||||
|
|
@ -88,9 +94,10 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should not return schedules with in_progress status", async () => {
|
test("should not return schedules with in_progress status", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: null,
|
nextBackupAt: null,
|
||||||
lastBackupStatus: "in_progress",
|
lastBackupStatus: "in_progress",
|
||||||
|
|
@ -105,10 +112,11 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should return schedules with success status and past nextBackupAt", async () => {
|
test("should return schedules with success status and past nextBackupAt", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const pastTime = faker.date.past().getTime();
|
const pastTime = faker.date.past().getTime();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: pastTime,
|
nextBackupAt: pastTime,
|
||||||
lastBackupStatus: "success",
|
lastBackupStatus: "success",
|
||||||
|
|
@ -123,9 +131,10 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should return schedules with error status and null nextBackupAt", async () => {
|
test("should return schedules with error status and null nextBackupAt", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: null,
|
nextBackupAt: null,
|
||||||
lastBackupStatus: "error",
|
lastBackupStatus: "error",
|
||||||
|
|
@ -140,11 +149,12 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should not return schedules from other organizations", async () => {
|
test("should not return schedules from other organizations", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
const otherOrgId = faker.string.uuid();
|
const otherOrgId = faker.string.uuid();
|
||||||
await createTestOrganization({ id: otherOrgId });
|
await createTestOrganization({ id: otherOrgId });
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: null,
|
nextBackupAt: null,
|
||||||
lastBackupStatus: null,
|
lastBackupStatus: null,
|
||||||
|
|
@ -160,9 +170,10 @@ describe("scheduleQueries.findExecutable", () => {
|
||||||
|
|
||||||
test("should only return schedule IDs", async () => {
|
test("should only return schedule IDs", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { volumeId, repositoryId } = await createScheduleDependencies();
|
||||||
await createTestBackupSchedule({
|
await createTestBackupSchedule({
|
||||||
volumeId: volume.id,
|
volumeId,
|
||||||
repositoryId: repository.id,
|
repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
nextBackupAt: null,
|
nextBackupAt: null,
|
||||||
lastBackupStatus: null,
|
lastBackupStatus: null,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import waitForExpect from "wait-for-expect";
|
import waitForExpect from "wait-for-expect";
|
||||||
import { test, describe, mock, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
import { test, describe, mock, expect, afterEach, spyOn } from "bun:test";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { backupsService } from "../backups.service";
|
import { backupsService } from "../backups.service";
|
||||||
import { createTestVolume } from "~/test/helpers/volume";
|
import { createTestVolume } from "~/test/helpers/volume";
|
||||||
|
|
@ -14,13 +14,15 @@ import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||||
import * as context from "~/server/core/request-context";
|
import * as context from "~/server/core/request-context";
|
||||||
import { backupsExecutionService } from "../backups.execution";
|
import { backupsExecutionService } from "../backups.execution";
|
||||||
|
|
||||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
const setup = () => {
|
||||||
|
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
||||||
beforeEach(() => {
|
|
||||||
resticBackupMock.mockClear();
|
|
||||||
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock);
|
||||||
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID);
|
||||||
});
|
|
||||||
|
return {
|
||||||
|
resticBackupMock,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
|
|
@ -29,6 +31,7 @@ afterEach(() => {
|
||||||
describe("execute backup", () => {
|
describe("execute backup", () => {
|
||||||
test("should correctly set next backup time", async () => {
|
test("should correctly set next backup time", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -58,6 +61,7 @@ describe("execute backup", () => {
|
||||||
|
|
||||||
test("should skip backup if schedule is disabled", async () => {
|
test("should skip backup if schedule is disabled", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -75,6 +79,7 @@ describe("execute backup", () => {
|
||||||
|
|
||||||
test("should execute backup if schedule is disabled but the run is manual", async () => {
|
test("should execute backup if schedule is disabled but the run is manual", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -96,6 +101,7 @@ describe("execute backup", () => {
|
||||||
|
|
||||||
test("should skip the backup if the previous one is still running", async () => {
|
test("should skip the backup if the previous one is still running", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -123,6 +129,7 @@ describe("execute backup", () => {
|
||||||
|
|
||||||
test("should set the backup status to failed if restic returns a 3 exit code", async () => {
|
test("should set the backup status to failed if restic returns a 3 exit code", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -144,6 +151,7 @@ describe("execute backup", () => {
|
||||||
|
|
||||||
test("should set the backup status to failed if restic returns a non zero exit code", async () => {
|
test("should set the backup status to failed if restic returns a non zero exit code", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const { resticBackupMock } = setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -167,6 +175,7 @@ describe("execute backup", () => {
|
||||||
describe("getSchedulesToExecute", () => {
|
describe("getSchedulesToExecute", () => {
|
||||||
test("should return schedules with NULL lastBackupStatus", async () => {
|
test("should return schedules with NULL lastBackupStatus", async () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
|
|
||||||
|
|
@ -189,6 +198,7 @@ describe("getSchedulesToExecute", () => {
|
||||||
|
|
||||||
describe("getScheduleByIdOrShortId", () => {
|
describe("getScheduleByIdOrShortId", () => {
|
||||||
test("should resolve a schedule by numeric id string", async () => {
|
test("should resolve a schedule by numeric id string", async () => {
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -203,6 +213,7 @@ describe("getScheduleByIdOrShortId", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should resolve a schedule by short id", async () => {
|
test("should resolve a schedule by short id", async () => {
|
||||||
|
setup();
|
||||||
const volume = await createTestVolume();
|
const volume = await createTestVolume();
|
||||||
const repository = await createTestRepository();
|
const repository = await createTestRepository();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
|
|
@ -217,6 +228,7 @@ describe("getScheduleByIdOrShortId", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not return schedules from another organization", async () => {
|
test("should not return schedules from another organization", async () => {
|
||||||
|
setup();
|
||||||
const otherOrgId = faker.string.uuid();
|
const otherOrgId = faker.string.uuid();
|
||||||
const schedule = await createTestBackupSchedule({
|
const schedule = await createTestBackupSchedule({
|
||||||
organizationId: otherOrgId,
|
organizationId: otherOrgId,
|
||||||
|
|
@ -231,6 +243,7 @@ describe("getScheduleByIdOrShortId", () => {
|
||||||
|
|
||||||
describe("listSchedules", () => {
|
describe("listSchedules", () => {
|
||||||
test("should ignore schedules with missing relations", async () => {
|
test("should ignore schedules with missing relations", async () => {
|
||||||
|
setup();
|
||||||
const healthyVolume = await createTestVolume();
|
const healthyVolume = await createTestVolume();
|
||||||
const healthyRepository = await createTestRepository();
|
const healthyRepository = await createTestRepository();
|
||||||
const healthySchedule = await createTestBackupSchedule({
|
const healthySchedule = await createTestBackupSchedule({
|
||||||
|
|
@ -254,6 +267,7 @@ describe("listSchedules", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should ignore schedules with missing repository relation", async () => {
|
test("should ignore schedules with missing repository relation", async () => {
|
||||||
|
setup();
|
||||||
const healthyVolume = await createTestVolume();
|
const healthyVolume = await createTestVolume();
|
||||||
const healthyRepository = await createTestRepository();
|
const healthyRepository = await createTestRepository();
|
||||||
const healthySchedule = await createTestBackupSchedule({
|
const healthySchedule = await createTestBackupSchedule({
|
||||||
|
|
@ -279,6 +293,7 @@ describe("listSchedules", () => {
|
||||||
|
|
||||||
describe("cleanupOrphanedSchedules", () => {
|
describe("cleanupOrphanedSchedules", () => {
|
||||||
test("should return zero when cascades already removed orphaned schedules", async () => {
|
test("should return zero when cascades already removed orphaned schedules", async () => {
|
||||||
|
setup();
|
||||||
const healthyVolume = await createTestVolume();
|
const healthyVolume = await createTestVolume();
|
||||||
const healthyRepository = await createTestRepository();
|
const healthyRepository = await createTestRepository();
|
||||||
const healthySchedule = await createTestBackupSchedule({
|
const healthySchedule = await createTestBackupSchedule({
|
||||||
|
|
|
||||||
|
|
@ -140,10 +140,15 @@ describe("repositoriesService.dumpSnapshot", () => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("calls restic.dump with common-ancestor selector and stripped path", async () => {
|
const createDumpResult = () => ({
|
||||||
|
stream: Readable.from([]),
|
||||||
|
completion: Promise.resolve(),
|
||||||
|
abort: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupDumpSnapshotScenario = async ({ snapshotId, basePath }: { snapshotId: string; basePath: string }) => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, user } = await createTestSession();
|
||||||
const shortId = generateShortId();
|
const shortId = generateShortId();
|
||||||
const basePath = "/var/lib/zerobyte/volumes/vol123/_data";
|
|
||||||
|
|
||||||
await db.insert(repositoriesTable).values({
|
await db.insert(repositoriesTable).values({
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
|
|
@ -159,31 +164,36 @@ describe("repositoriesService.dumpSnapshot", () => {
|
||||||
organizationId,
|
organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotsMock = mock(() =>
|
spyOn(restic, "snapshots").mockResolvedValue([
|
||||||
Promise.resolve([
|
{
|
||||||
{
|
id: snapshotId,
|
||||||
id: "snapshot-123",
|
short_id: snapshotId,
|
||||||
short_id: "snapshot-123",
|
time: new Date().toISOString(),
|
||||||
time: new Date().toISOString(),
|
paths: [basePath],
|
||||||
tree: "tree-1",
|
hostname: "host",
|
||||||
paths: [basePath],
|
},
|
||||||
hostname: "host",
|
]);
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
spyOn(restic, "snapshots").mockImplementation(snapshotsMock as typeof restic.snapshots);
|
|
||||||
|
|
||||||
const dumpMock = mock(() =>
|
const dumpMock = mock(() => Promise.resolve(createDumpResult()));
|
||||||
Promise.resolve({
|
|
||||||
stream: Readable.from([]),
|
|
||||||
completion: Promise.resolve(),
|
|
||||||
abort: () => {},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
spyOn(restic, "dump").mockImplementation(dumpMock);
|
spyOn(restic, "dump").mockImplementation(dumpMock);
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizationId,
|
||||||
|
userId: user.id,
|
||||||
|
shortId,
|
||||||
|
basePath,
|
||||||
|
dumpMock,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
test("calls restic.dump with common-ancestor selector and stripped path", async () => {
|
||||||
|
const { organizationId, userId, shortId, basePath, dumpMock } = await setupDumpSnapshotScenario({
|
||||||
|
snapshotId: "snapshot-123",
|
||||||
|
basePath: "/var/lib/zerobyte/volumes/vol123/_data",
|
||||||
|
});
|
||||||
const emitSpy = spyOn(serverEvents, "emit");
|
const emitSpy = spyOn(serverEvents, "emit");
|
||||||
|
|
||||||
await withContext({ organizationId, userId: user.id }, () =>
|
await withContext({ organizationId, userId }, () =>
|
||||||
repositoriesService.dumpSnapshot(shortId, "snapshot-123", `${basePath}/documents`, "dir"),
|
repositoriesService.dumpSnapshot(shortId, "snapshot-123", `${basePath}/documents`, "dir"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -210,48 +220,12 @@ describe("repositoriesService.dumpSnapshot", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("streams a single file directly when selected path is a file", async () => {
|
test("streams a single file directly when selected path is a file", async () => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, userId, shortId, basePath, dumpMock } = await setupDumpSnapshotScenario({
|
||||||
const shortId = generateShortId();
|
snapshotId: "snapshot-file",
|
||||||
const basePath = "/var/lib/zerobyte/volumes/vol123/_data";
|
basePath: "/var/lib/zerobyte/volumes/vol123/_data",
|
||||||
|
|
||||||
await db.insert(repositoriesTable).values({
|
|
||||||
id: randomUUID(),
|
|
||||||
shortId,
|
|
||||||
name: `Repository-${randomUUID()}`,
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
backend: "local",
|
|
||||||
path: `/tmp/repository-${randomUUID()}`,
|
|
||||||
isExistingRepository: true,
|
|
||||||
},
|
|
||||||
compressionMode: "off",
|
|
||||||
organizationId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotsMock = mock(() =>
|
const result = await withContext({ organizationId, userId }, () =>
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
id: "snapshot-file",
|
|
||||||
short_id: "snapshot-file",
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
tree: "tree-file",
|
|
||||||
paths: [basePath],
|
|
||||||
hostname: "host",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
spyOn(restic, "snapshots").mockImplementation(snapshotsMock as typeof restic.snapshots);
|
|
||||||
|
|
||||||
const dumpMock = mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
stream: Readable.from([]),
|
|
||||||
completion: Promise.resolve(),
|
|
||||||
abort: () => {},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
spyOn(restic, "dump").mockImplementation(dumpMock);
|
|
||||||
|
|
||||||
const result = await withContext({ organizationId, userId: user.id }, () =>
|
|
||||||
repositoriesService.dumpSnapshot(shortId, "snapshot-file", `${basePath}/documents/report.txt`, "file"),
|
repositoriesService.dumpSnapshot(shortId, "snapshot-file", `${basePath}/documents/report.txt`, "file"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -265,90 +239,25 @@ describe("repositoriesService.dumpSnapshot", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects path downloads without a kind", async () => {
|
test("rejects path downloads without a kind", async () => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, userId, shortId, basePath } = await setupDumpSnapshotScenario({
|
||||||
const shortId = generateShortId();
|
snapshotId: "snapshot-no-kind",
|
||||||
const basePath = "/var/lib/zerobyte/volumes/vol123/_data";
|
basePath: "/var/lib/zerobyte/volumes/vol123/_data",
|
||||||
|
|
||||||
await db.insert(repositoriesTable).values({
|
|
||||||
id: randomUUID(),
|
|
||||||
shortId,
|
|
||||||
name: `Repository-${randomUUID()}`,
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
backend: "local",
|
|
||||||
path: `/tmp/repository-${randomUUID()}`,
|
|
||||||
isExistingRepository: true,
|
|
||||||
},
|
|
||||||
compressionMode: "off",
|
|
||||||
organizationId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotsMock = mock(() =>
|
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
id: "snapshot-no-kind",
|
|
||||||
short_id: "snapshot-no-kind",
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
tree: "tree-no-kind",
|
|
||||||
paths: [basePath],
|
|
||||||
hostname: "host",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
spyOn(restic, "snapshots").mockImplementation(snapshotsMock as typeof restic.snapshots);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
withContext({ organizationId, userId: user.id }, () =>
|
withContext({ organizationId, userId }, () =>
|
||||||
repositoriesService.dumpSnapshot(shortId, "snapshot-no-kind", `${basePath}/documents/report.txt`),
|
repositoriesService.dumpSnapshot(shortId, "snapshot-no-kind", `${basePath}/documents/report.txt`),
|
||||||
),
|
),
|
||||||
).rejects.toThrow("Path kind is required when downloading a specific snapshot path");
|
).rejects.toThrow("Path kind is required when downloading a specific snapshot path");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("downloads full snapshot relative to common ancestor when path is omitted", async () => {
|
test("downloads full snapshot relative to common ancestor when path is omitted", async () => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, userId, shortId, basePath, dumpMock } = await setupDumpSnapshotScenario({
|
||||||
const shortId = generateShortId();
|
snapshotId: "snapshot-999",
|
||||||
const basePath = "/var/lib/zerobyte/volumes/vol555/_data";
|
basePath: "/var/lib/zerobyte/volumes/vol555/_data",
|
||||||
|
|
||||||
await db.insert(repositoriesTable).values({
|
|
||||||
id: randomUUID(),
|
|
||||||
shortId,
|
|
||||||
name: `Repository-${randomUUID()}`,
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
backend: "local",
|
|
||||||
path: `/tmp/repository-${randomUUID()}`,
|
|
||||||
isExistingRepository: true,
|
|
||||||
},
|
|
||||||
compressionMode: "off",
|
|
||||||
organizationId,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotsMock = mock(() =>
|
await withContext({ organizationId, userId }, () => repositoriesService.dumpSnapshot(shortId, "snapshot-999"));
|
||||||
Promise.resolve([
|
|
||||||
{
|
|
||||||
id: "snapshot-999",
|
|
||||||
short_id: "snapshot-999",
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
tree: "tree-9",
|
|
||||||
paths: [basePath],
|
|
||||||
hostname: "host",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
spyOn(restic, "snapshots").mockImplementation(snapshotsMock as typeof restic.snapshots);
|
|
||||||
|
|
||||||
const dumpMock = mock(() =>
|
|
||||||
Promise.resolve({
|
|
||||||
stream: Readable.from([]),
|
|
||||||
completion: Promise.resolve(),
|
|
||||||
abort: () => {},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
spyOn(restic, "dump").mockImplementation(dumpMock);
|
|
||||||
|
|
||||||
await withContext({ organizationId, userId: user.id }, () =>
|
|
||||||
repositoriesService.dumpSnapshot(shortId, "snapshot-999"),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(dumpMock).toHaveBeenCalledWith(expect.anything(), `snapshot-999:${basePath}`, {
|
expect(dumpMock).toHaveBeenCalledWith(expect.anything(), `snapshot-999:${basePath}`, {
|
||||||
organizationId,
|
organizationId,
|
||||||
|
|
|
||||||
|
|
@ -10,121 +10,117 @@ import { createTestSession } from "~/test/helpers/auth";
|
||||||
import { withContext } from "~/server/core/request-context";
|
import { withContext } from "~/server/core/request-context";
|
||||||
import { asShortId } from "~/server/utils/branded";
|
import { asShortId } from "~/server/utils/branded";
|
||||||
|
|
||||||
describe("volumeService", () => {
|
describe("volumeService.getVolume", () => {
|
||||||
describe("findVolume", () => {
|
test("should find volume by shortId", async () => {
|
||||||
test("should find volume by shortId", async () => {
|
const { organizationId, user } = await createTestSession();
|
||||||
const { organizationId, user } = await createTestSession();
|
|
||||||
|
|
||||||
const [volume] = await db
|
const [volume] = await db
|
||||||
.insert(volumesTable)
|
.insert(volumesTable)
|
||||||
.values({
|
.values({
|
||||||
shortId: asShortId(randomUUID().slice(0, 8)),
|
shortId: asShortId(randomUUID().slice(0, 8)),
|
||||||
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
||||||
type: "directory",
|
type: "directory",
|
||||||
status: "mounted",
|
status: "mounted",
|
||||||
config: { backend: "directory", path: "/" },
|
config: { backend: "directory", path: "/" },
|
||||||
autoRemount: true,
|
autoRemount: true,
|
||||||
organizationId,
|
organizationId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await withContext({ organizationId, userId: user.id }, async () => {
|
await withContext({ organizationId, userId: user.id }, async () => {
|
||||||
const result = await volumeService.getVolume(volume.shortId);
|
const result = await volumeService.getVolume(volume.shortId);
|
||||||
expect(result.volume.id).toBe(volume.id);
|
expect(result.volume.id).toBe(volume.id);
|
||||||
expect(result.volume.shortId).toBe(volume.shortId);
|
expect(result.volume.shortId).toBe(volume.shortId);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should find volume by shortId from literal input", async () => {
|
test("should find volume by shortId from literal input", async () => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, user } = await createTestSession();
|
||||||
|
|
||||||
const [volume] = await db
|
const [volume] = await db
|
||||||
.insert(volumesTable)
|
.insert(volumesTable)
|
||||||
.values({
|
.values({
|
||||||
shortId: asShortId("test1234"),
|
shortId: asShortId("test1234"),
|
||||||
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
||||||
type: "directory",
|
type: "directory",
|
||||||
status: "mounted",
|
status: "mounted",
|
||||||
config: { backend: "directory", path: "/" },
|
config: { backend: "directory", path: "/" },
|
||||||
autoRemount: true,
|
autoRemount: true,
|
||||||
organizationId,
|
organizationId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await withContext({ organizationId, userId: user.id }, async () => {
|
await withContext({ organizationId, userId: user.id }, async () => {
|
||||||
const result = await volumeService.getVolume(volume.shortId);
|
const result = await volumeService.getVolume(volume.shortId);
|
||||||
expect(result.volume.id).toBe(volume.id);
|
expect(result.volume.id).toBe(volume.id);
|
||||||
expect(result.volume.shortId).toBe(volume.shortId);
|
expect(result.volume.shortId).toBe(volume.shortId);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should find volume by numeric-looking shortId", async () => {
|
test("should find volume by numeric-looking shortId", async () => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, user } = await createTestSession();
|
||||||
|
|
||||||
const [volume] = await db
|
const [volume] = await db
|
||||||
.insert(volumesTable)
|
.insert(volumesTable)
|
||||||
.values({
|
.values({
|
||||||
shortId: asShortId("499780"),
|
shortId: asShortId("499780"),
|
||||||
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
||||||
type: "directory",
|
type: "directory",
|
||||||
status: "mounted",
|
status: "mounted",
|
||||||
config: { backend: "directory", path: "/" },
|
config: { backend: "directory", path: "/" },
|
||||||
autoRemount: true,
|
autoRemount: true,
|
||||||
organizationId,
|
organizationId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await withContext({ organizationId, userId: user.id }, async () => {
|
await withContext({ organizationId, userId: user.id }, async () => {
|
||||||
const result = await volumeService.getVolume(asShortId("499780"));
|
const result = await volumeService.getVolume(asShortId("499780"));
|
||||||
expect(result.volume.id).toBe(volume.id);
|
expect(result.volume.id).toBe(volume.id);
|
||||||
expect(result.volume.shortId).toBe(asShortId("499780"));
|
expect(result.volume.shortId).toBe(asShortId("499780"));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should throw NotFoundError for non-existent volume", async () => {
|
test("should throw NotFoundError for non-existent volume", async () => {
|
||||||
const { organizationId, user } = await createTestSession();
|
const { organizationId, user } = await createTestSession();
|
||||||
|
|
||||||
await withContext({ organizationId, userId: user.id }, async () => {
|
await withContext({ organizationId, userId: user.id }, async () => {
|
||||||
await expect(volumeService.getVolume(asShortId("nonexistent"))).rejects.toThrow("Volume not found");
|
await expect(volumeService.getVolume(asShortId("nonexistent"))).rejects.toThrow("Volume not found");
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("volumeService security", () => {
|
describe("volumeService.listFiles security", () => {
|
||||||
describe("path traversal", () => {
|
test("should reject traversal outside the volume root in listFiles", async () => {
|
||||||
test("should reject traversal outside the volume root in listFiles", async () => {
|
const { organizationId, user } = await createTestSession();
|
||||||
const { organizationId, user } = await createTestSession();
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-vol-svc-"));
|
||||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-vol-svc-"));
|
const volumePath = path.join(tempRoot, "vol");
|
||||||
const volumePath = path.join(tempRoot, "vol");
|
const secretPath = path.join(tempRoot, "volume-secret");
|
||||||
const secretPath = path.join(tempRoot, "volume-secret");
|
|
||||||
|
|
||||||
await fs.mkdir(volumePath, { recursive: true });
|
await fs.mkdir(volumePath, { recursive: true });
|
||||||
await fs.mkdir(secretPath, { recursive: true });
|
await fs.mkdir(secretPath, { recursive: true });
|
||||||
await fs.writeFile(path.join(secretPath, "secret.txt"), "top secret", "utf-8");
|
await fs.writeFile(path.join(secretPath, "secret.txt"), "top secret", "utf-8");
|
||||||
|
|
||||||
const [volume] = await db
|
const [volume] = await db
|
||||||
.insert(volumesTable)
|
.insert(volumesTable)
|
||||||
.values({
|
.values({
|
||||||
shortId: asShortId(randomUUID().slice(0, 8)),
|
shortId: asShortId(randomUUID().slice(0, 8)),
|
||||||
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
name: `test-vol-${randomUUID().slice(0, 8)}`,
|
||||||
type: "directory",
|
type: "directory",
|
||||||
status: "mounted",
|
status: "mounted",
|
||||||
config: { backend: "directory", path: volumePath },
|
config: { backend: "directory", path: volumePath },
|
||||||
autoRemount: true,
|
autoRemount: true,
|
||||||
organizationId,
|
organizationId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withContext({ organizationId, userId: user.id }, async () => {
|
await withContext({ organizationId, userId: user.id }, async () => {
|
||||||
const traversalPath = `../${path.basename(secretPath)}`;
|
const traversalPath = `../${path.basename(secretPath)}`;
|
||||||
|
|
||||||
await expect(volumeService.listFiles(volume.shortId, traversalPath)).rejects.toThrow("Invalid path");
|
await expect(volumeService.listFiles(volume.shortId, traversalPath)).rejects.toThrow("Invalid path");
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -75,34 +75,30 @@ describe("safeExec", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("shell injection protection", () => {
|
describe("shell injection protection", () => {
|
||||||
test("treats semicolon-separated commands as a single literal argument", async () => {
|
test.each([
|
||||||
|
{
|
||||||
|
name: "treats semicolon-separated commands as a single literal argument",
|
||||||
|
argument: "safe; echo injected",
|
||||||
|
expected: "safe; echo injected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not evaluate command substitution syntax",
|
||||||
|
argument: "$(echo injected)",
|
||||||
|
expected: "$(echo injected)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not expand glob patterns",
|
||||||
|
argument: "*.ts",
|
||||||
|
expected: "*.ts",
|
||||||
|
},
|
||||||
|
])("$name", async ({ argument, expected }) => {
|
||||||
const result = await safeExec({
|
const result = await safeExec({
|
||||||
command: "echo",
|
command: "echo",
|
||||||
args: ["safe; echo injected"],
|
args: [argument],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.stdout.trim()).toBe("safe; echo injected");
|
expect(result.stdout.trim()).toBe(expected);
|
||||||
});
|
|
||||||
|
|
||||||
test("does not evaluate command substitution syntax", async () => {
|
|
||||||
const result = await safeExec({
|
|
||||||
command: "echo",
|
|
||||||
args: ["$(echo injected)"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
expect(result.stdout.trim()).toBe("$(echo injected)");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not expand glob patterns", async () => {
|
|
||||||
const result = await safeExec({
|
|
||||||
command: "echo",
|
|
||||||
args: ["*.ts"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
expect(result.stdout.trim()).toBe("*.ts");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -268,40 +264,32 @@ describe("safeSpawn", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("shell injection protection", () => {
|
describe("shell injection protection", () => {
|
||||||
test("treats semicolon-separated commands as a single literal argument", async () => {
|
test.each([
|
||||||
|
{
|
||||||
|
name: "treats semicolon-separated commands as a single literal argument",
|
||||||
|
argument: "safe; echo injected",
|
||||||
|
expected: "safe; echo injected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not evaluate command substitution syntax",
|
||||||
|
argument: "$(echo injected)",
|
||||||
|
expected: "$(echo injected)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not expand glob patterns",
|
||||||
|
argument: "*.ts",
|
||||||
|
expected: "*.ts",
|
||||||
|
},
|
||||||
|
])("$name", async ({ argument, expected }) => {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
await safeSpawn({
|
await safeSpawn({
|
||||||
command: "echo",
|
command: "echo",
|
||||||
args: ["safe; echo injected"],
|
args: [argument],
|
||||||
onStdout: (line) => lines.push(line),
|
onStdout: (line) => lines.push(line),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(lines).toEqual(["safe; echo injected"]);
|
expect(lines).toEqual([expected]);
|
||||||
});
|
|
||||||
|
|
||||||
test("does not evaluate command substitution syntax", async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
await safeSpawn({
|
|
||||||
command: "echo",
|
|
||||||
args: ["$(echo injected)"],
|
|
||||||
onStdout: (line) => lines.push(line),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lines).toEqual(["$(echo injected)"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not expand glob patterns", async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
await safeSpawn({
|
|
||||||
command: "echo",
|
|
||||||
args: ["*.ts"],
|
|
||||||
onStdout: (line) => lines.push(line),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lines).toEqual(["*.ts"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { describe, expect, test } from "bun:test";
|
import fs from "node:fs/promises";
|
||||||
|
import { afterEach, describe, expect, test } from "bun:test";
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { organization } from "~/server/db/schema";
|
import { organization } from "~/server/db/schema";
|
||||||
import { RESTIC_CACHE_DIR } from "~/server/core/constants";
|
import { RESTIC_CACHE_DIR } from "~/server/core/constants";
|
||||||
|
|
@ -27,6 +28,23 @@ const createTestOrg = async (overrides: Partial<typeof organization.$inferInsert
|
||||||
const PLAIN_PRIVATE_KEY =
|
const PLAIN_PRIVATE_KEY =
|
||||||
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----";
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----";
|
||||||
|
|
||||||
|
const tempFiles = new Set<string>();
|
||||||
|
|
||||||
|
const trackTempFile = (filePath: string | undefined) => {
|
||||||
|
if (filePath) {
|
||||||
|
tempFiles.add(filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
[...tempFiles].map(async (filePath) => {
|
||||||
|
await fs.rm(filePath, { force: true });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
tempFiles.clear();
|
||||||
|
});
|
||||||
|
|
||||||
describe("buildEnv", () => {
|
describe("buildEnv", () => {
|
||||||
describe("base environment", () => {
|
describe("base environment", () => {
|
||||||
test("always sets RESTIC_CACHE_DIR", async () => {
|
test("always sets RESTIC_CACHE_DIR", async () => {
|
||||||
|
|
@ -49,7 +67,15 @@ describe("buildEnv", () => {
|
||||||
"org-1",
|
"org-1",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(env.RESTIC_PASSWORD_FILE).toMatch(/^\/tmp\/zerobyte-pass-.+\.txt$/);
|
const passwordFilePath = env.RESTIC_PASSWORD_FILE;
|
||||||
|
expect(passwordFilePath).toBeDefined();
|
||||||
|
if (!passwordFilePath) {
|
||||||
|
throw new Error("Expected password file path to be defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
trackTempFile(passwordFilePath);
|
||||||
|
const fileContent = await fs.readFile(passwordFilePath, "utf-8");
|
||||||
|
expect(fileContent).toBe("my-secret");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("writes a password file from the organization's resticPassword when no customPassword is given", async () => {
|
test("writes a password file from the organization's resticPassword when no customPassword is given", async () => {
|
||||||
|
|
@ -57,7 +83,15 @@ describe("buildEnv", () => {
|
||||||
|
|
||||||
const env = await buildEnv({ backend: "local" as const, path: "/tmp/repo" }, orgId);
|
const env = await buildEnv({ backend: "local" as const, path: "/tmp/repo" }, orgId);
|
||||||
|
|
||||||
expect(env.RESTIC_PASSWORD_FILE).toMatch(/^\/tmp\/zerobyte-pass-.+\.txt$/);
|
const passwordFilePath = env.RESTIC_PASSWORD_FILE;
|
||||||
|
expect(passwordFilePath).toBeDefined();
|
||||||
|
if (!passwordFilePath) {
|
||||||
|
throw new Error("Expected password file path to be defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
trackTempFile(passwordFilePath);
|
||||||
|
const fileContent = await fs.readFile(passwordFilePath, "utf-8");
|
||||||
|
expect(fileContent).toBe("org-restic-password");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws when the organization does not exist", async () => {
|
test("throws when the organization does not exist", async () => {
|
||||||
|
|
@ -137,7 +171,16 @@ describe("buildEnv", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(env.GOOGLE_PROJECT_ID).toBe("my-gcp-project");
|
expect(env.GOOGLE_PROJECT_ID).toBe("my-gcp-project");
|
||||||
expect(env.GOOGLE_APPLICATION_CREDENTIALS).toMatch(/^\/tmp\/zerobyte-gcs-.+\.json$/);
|
|
||||||
|
const credentialsPath = env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||||
|
expect(credentialsPath).toBeDefined();
|
||||||
|
if (!credentialsPath) {
|
||||||
|
throw new Error("Expected credentials path to be defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
trackTempFile(credentialsPath);
|
||||||
|
const fileContent = await fs.readFile(credentialsPath, "utf-8");
|
||||||
|
expect(fileContent).toBe('{"type":"service_account"}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -285,7 +328,15 @@ describe("buildEnv", () => {
|
||||||
"org-1",
|
"org-1",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(env.RESTIC_CACERT).toMatch(/^\/tmp\/zerobyte-cacert-.+\.pem$/);
|
const certPath = env.RESTIC_CACERT;
|
||||||
|
expect(certPath).toBeDefined();
|
||||||
|
if (!certPath) {
|
||||||
|
throw new Error("Expected certificate path to be defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
trackTempFile(certPath);
|
||||||
|
const fileContent = await fs.readFile(certPath, "utf-8");
|
||||||
|
expect(fileContent).toBe("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does not set RESTIC_CACERT when cacert is absent", async () => {
|
test("does not set RESTIC_CACERT when cacert is absent", async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue