fix(repositories): append short id when using the default path for local repos (#536)
This commit is contained in:
parent
0bbdbc85ad
commit
ebfafa4143
5 changed files with 98 additions and 16 deletions
|
|
@ -13,6 +13,7 @@ import * as spawnModule from "~/server/utils/spawn";
|
|||
import { restic } from "~/server/utils/restic";
|
||||
import { NotFoundError, BadRequestError } from "http-errors-enhanced";
|
||||
import { scheduleQueries } from "../backups.queries";
|
||||
import { fromAny } from "@total-typescript/shoehorn";
|
||||
|
||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: generateBackupOutput(), error: "" }));
|
||||
const resticForgetMock = mock(() => Promise.resolve({ success: true, data: null }));
|
||||
|
|
@ -66,10 +67,10 @@ describe("backup execution - validation failures", () => {
|
|||
const hydratedSchedule = await scheduleQueries.findById(schedule.id, TEST_ORG_ID);
|
||||
expect(hydratedSchedule).toBeDefined();
|
||||
const scheduleWithoutVolume = {
|
||||
...hydratedSchedule!,
|
||||
...hydratedSchedule,
|
||||
volume: null,
|
||||
} as unknown as NonNullable<Awaited<ReturnType<typeof scheduleQueries.findById>>>;
|
||||
spyOn(scheduleQueries, "findById").mockResolvedValueOnce(scheduleWithoutVolume);
|
||||
};
|
||||
spyOn(scheduleQueries, "findById").mockResolvedValueOnce(fromAny(scheduleWithoutVolume));
|
||||
|
||||
// act
|
||||
const result = await backupsExecutionService.validateBackupExecution(schedule.id);
|
||||
|
|
@ -95,10 +96,10 @@ describe("backup execution - validation failures", () => {
|
|||
const hydratedSchedule = await scheduleQueries.findById(schedule.id, TEST_ORG_ID);
|
||||
expect(hydratedSchedule).toBeDefined();
|
||||
const scheduleWithoutRepository = {
|
||||
...hydratedSchedule!,
|
||||
...hydratedSchedule,
|
||||
repository: null,
|
||||
} as unknown as NonNullable<Awaited<ReturnType<typeof scheduleQueries.findById>>>;
|
||||
spyOn(scheduleQueries, "findById").mockResolvedValueOnce(scheduleWithoutRepository);
|
||||
};
|
||||
spyOn(scheduleQueries, "findById").mockResolvedValueOnce(fromAny(scheduleWithoutRepository));
|
||||
|
||||
// act
|
||||
const result = await backupsExecutionService.validateBackupExecution(schedule.id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
||||
import type { RepositoryConfig } from "~/schemas/restic";
|
||||
import { REPOSITORY_BASE } from "~/server/core/constants";
|
||||
import { withContext } from "~/server/core/request-context";
|
||||
import { db } from "~/server/db/db";
|
||||
import { restic } from "~/server/utils/restic";
|
||||
import { createTestSession } from "~/test/helpers/auth";
|
||||
import { repositoriesService } from "../repositories.service";
|
||||
|
||||
describe("repositoriesService.createRepository", () => {
|
||||
const initMock = mock(() => Promise.resolve({ success: true, error: null }));
|
||||
|
||||
beforeEach(() => {
|
||||
initMock.mockClear();
|
||||
spyOn(restic, "init").mockImplementation(initMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
test("creates a shortId-scoped repository path when using the repository base directory", async () => {
|
||||
// arrange
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const config: RepositoryConfig = { backend: "local", path: REPOSITORY_BASE };
|
||||
|
||||
// act
|
||||
const result = await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.createRepository("main repo", config),
|
||||
);
|
||||
|
||||
const created = await db.query.repositoriesTable.findFirst({
|
||||
where: {
|
||||
id: result.repository.id,
|
||||
},
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(created).toBeTruthy();
|
||||
if (!created) {
|
||||
throw new Error("Repository should be created");
|
||||
}
|
||||
|
||||
const savedConfig = created.config as Extract<RepositoryConfig, { backend: "local" }>;
|
||||
|
||||
expect(savedConfig.path).toBe(`${REPOSITORY_BASE}/${created.shortId}`);
|
||||
expect(savedConfig.path).not.toBe(REPOSITORY_BASE);
|
||||
expect(created.status).toBe("healthy");
|
||||
});
|
||||
|
||||
test("keeps an explicit local repository path unchanged", async () => {
|
||||
// arrange
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const explicitPath = `${REPOSITORY_BASE}/custom-${randomUUID()}`;
|
||||
const config: RepositoryConfig = { backend: "local", path: explicitPath };
|
||||
|
||||
// act
|
||||
const result = await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.createRepository("custom repo", config),
|
||||
);
|
||||
|
||||
const created = await db.query.repositoriesTable.findFirst({
|
||||
where: {
|
||||
id: result.repository.id,
|
||||
},
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(created).toBeTruthy();
|
||||
if (!created) {
|
||||
throw new Error("Repository should be created");
|
||||
}
|
||||
|
||||
const savedConfig = created.config as Extract<RepositoryConfig, { backend: "local" }>;
|
||||
expect(savedConfig.path).toBe(explicitPath);
|
||||
expect(created.status).toBe("healthy");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +1,7 @@
|
|||
import crypto from "node:crypto";
|
||||
import { type } from "arktype";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
BadRequestError,
|
||||
ConflictError,
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
} from "http-errors-enhanced";
|
||||
import { BadRequestError, ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import {
|
||||
type CompressionMode,
|
||||
type OverwriteMode,
|
||||
|
|
@ -29,6 +24,7 @@ import { safeSpawn } from "../../utils/spawn";
|
|||
import { backupsService } from "../backups/backups.service";
|
||||
import type { UpdateRepositoryBody } from "./repositories.dto";
|
||||
import { executeDoctor } from "./doctor";
|
||||
import { REPOSITORY_BASE } from "~/server/core/constants";
|
||||
|
||||
const runningDoctors = new Map<string, AbortController>();
|
||||
|
||||
|
|
@ -130,8 +126,11 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||
const id = crypto.randomUUID();
|
||||
const shortId = generateShortId();
|
||||
|
||||
let processedConfig = config;
|
||||
const encryptedConfig = await encryptConfig(processedConfig);
|
||||
if (config.backend === "local" && config.path === REPOSITORY_BASE) {
|
||||
config.path = `${REPOSITORY_BASE}/${shortId}`;
|
||||
}
|
||||
|
||||
const encryptedConfig = await encryptConfig(config);
|
||||
|
||||
const [created] = await db
|
||||
.insert(repositoriesTable)
|
||||
|
|
@ -635,8 +634,7 @@ const updateRepository = async (id: string, updates: UpdateRepositoryBody) => {
|
|||
}
|
||||
|
||||
const decryptedExisting = existingConfig ? await decryptConfig(existingConfig) : null;
|
||||
const configChanged =
|
||||
updates.config && JSON.stringify(decryptedExisting) !== JSON.stringify(parsedConfig);
|
||||
const configChanged = updates.config && JSON.stringify(decryptedExisting) !== JSON.stringify(parsedConfig);
|
||||
const encryptedConfig = updates.config ? await encryptConfig(parsedConfig) : existingConfig;
|
||||
|
||||
const updatedAt = Date.now();
|
||||
|
|
|
|||
3
bun.lock
3
bun.lock
|
|
@ -78,6 +78,7 @@
|
|||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@total-typescript/shoehorn": "^0.1.2",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^19.2.14",
|
||||
|
|
@ -809,6 +810,8 @@
|
|||
|
||||
"@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
|
||||
|
||||
"@total-typescript/shoehorn": ["@total-typescript/shoehorn@0.1.2", "", {}, "sha512-p7nNZbOZIofpDNyP0u1BctFbjxD44Qc+oO5jufgQdFdGIXJLc33QRloJpq7k5T59CTgLWfQSUxsuqLcmeurYRw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@total-typescript/shoehorn": "^0.1.2",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^19.2.14",
|
||||
|
|
|
|||
Loading…
Reference in a new issue