From 2ce225f7d1d6714a6ed9ecfdc8c2373dd8fb24d3 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 25 Feb 2026 18:15:36 +0100 Subject: [PATCH] fix(repositories): throw proper errors when deleting snapshots --- .../__tests__/repositories.controller.test.ts | 27 ++++++++++++ .../__tests__/repositories.service.test.ts | 44 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/app/server/modules/repositories/__tests__/repositories.controller.test.ts b/app/server/modules/repositories/__tests__/repositories.controller.test.ts index 40c6c706..653935be 100644 --- a/app/server/modules/repositories/__tests__/repositories.controller.test.ts +++ b/app/server/modules/repositories/__tests__/repositories.controller.test.ts @@ -240,4 +240,31 @@ describe("repositories updates", () => { expect(res.status).toBe(400); }); + + describe("delete snapshot", () => { + test("should return 500 when restic deleteSnapshot throws ResticError", async () => { + const { token, organizationId } = await createTestSession(); + const repository = await createRepositoryRecord(organizationId); + + const { restic } = await import("~/server/utils/restic"); + const { ResticError } = await import("~/server/utils/errors"); + const originalDeleteSnapshot = restic.deleteSnapshot; + + // Mock it to throw an error + restic.deleteSnapshot = async () => { + throw new ResticError(1, "Fatal: unexpected HTTP response (403): 403 Forbidden"); + }; + + const res = await app.request(`/api/v1/repositories/${repository.shortId}/snapshots/snap123`, { + method: "DELETE", + headers: getAuthHeaders(token), + }); + + restic.deleteSnapshot = originalDeleteSnapshot; + + expect(res.status).toBe(500); + const body = await res.json(); + expect(body.message).toContain("Command failed"); + }); + }); }); diff --git a/app/server/modules/repositories/__tests__/repositories.service.test.ts b/app/server/modules/repositories/__tests__/repositories.service.test.ts index 730372e7..6ce8bf66 100644 --- a/app/server/modules/repositories/__tests__/repositories.service.test.ts +++ b/app/server/modules/repositories/__tests__/repositories.service.test.ts @@ -12,8 +12,28 @@ import { restic } from "~/server/utils/restic"; import { createTestSession } from "~/test/helpers/auth"; import { createTestBackupSchedule } from "~/test/helpers/backup"; import { cache, cacheKeys } from "~/server/utils/cache"; +import { ResticError } from "~/server/utils/errors"; import { repositoriesService } from "../repositories.service"; +const createTestRepository = async (organizationId: string) => { + const id = randomUUID(); + const shortId = generateShortId(); + const [repository] = await db + .insert(repositoriesTable) + .values({ + id, + shortId, + name: `Test-${randomUUID()}`, + type: "local", + config: { backend: "local", path: "/tmp" }, + compressionMode: "auto", + status: "healthy", + organizationId, + }) + .returning(); + return repository; +}; + describe("repositoriesService.createRepository", () => { const initMock = mock(() => Promise.resolve({ success: true, error: null })); @@ -371,3 +391,27 @@ describe("repositoriesService.getRetentionCategories", () => { expect(forgetSpy).toHaveBeenCalledTimes(2); }); }); + +describe("repositoriesService.deleteSnapshot", () => { + afterEach(() => { + mock.restore(); + }); + + test("should throw original error when restic deleteSnapshot fails", async () => { + const { organizationId, user } = await createTestSession(); + const repository = await createTestRepository(organizationId); + + const originalDeleteSnapshot = restic.deleteSnapshot; + restic.deleteSnapshot = mock(async () => { + throw new ResticError(1, "Fatal: unexpected HTTP response (403): 403 Forbidden"); + }); + + await expect( + withContext({ organizationId, userId: user.id }, () => + repositoriesService.deleteSnapshot(repository.shortId, "snap123"), + ), + ).rejects.toThrow("Fatal: unexpected HTTP response"); + + restic.deleteSnapshot = originalDeleteSnapshot; + }); +});