From 0b98402b263fc132a1e2f89b14b037491c6f0537 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 29 Apr 2026 23:36:09 +0200 Subject: [PATCH] chore: pr feedbacks --- .../components/create-schedule-form/types.ts | 8 ++++-- .../components/create-schedule-form/utils.ts | 7 +++-- .../src/backup-hooks/__tests__/hooks.test.ts | 28 +++++++++++++++++++ packages/core/src/backup-hooks/index.ts | 3 ++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/client/modules/backups/components/create-schedule-form/types.ts b/app/client/modules/backups/components/create-schedule-form/types.ts index 33581049..c4d1d9e8 100644 --- a/app/client/modules/backups/components/create-schedule-form/types.ts +++ b/app/client/modules/backups/components/create-schedule-form/types.ts @@ -7,8 +7,12 @@ const webhookHeadersSchema = z.string().refine( .split("\n") .map((header) => header.trim()) .filter(Boolean) - .every((header) => header.includes(":")), - { message: "Headers must use Key: Value format" }, + .every((header) => { + const [key, value] = header.split(":", 2); + + return /^[A-Za-z0-9-]+$/.test(key.trim()) && (value?.trim().length ?? 0) > 0; + }), + { message: "Headers must use non-empty Key: Value format with valid header names" }, ); export const internalFormSchema = z.object({ diff --git a/app/client/modules/backups/components/create-schedule-form/utils.ts b/app/client/modules/backups/components/create-schedule-form/utils.ts index 254205c0..314a8118 100644 --- a/app/client/modules/backups/components/create-schedule-form/utils.ts +++ b/app/client/modules/backups/components/create-schedule-form/utils.ts @@ -15,11 +15,14 @@ export const parseMultilineEntries = (value?: string) => { export const toWebhookConfig = (url?: string, headers?: string, body?: string) => { const trimmedUrl = url?.trim(); - const trimmedBody = body?.trim(); const parsedHeaders = parseMultilineEntries(headers); return trimmedUrl - ? { url: trimmedUrl, headers: parsedHeaders.length > 0 ? parsedHeaders : undefined, body: trimmedBody || undefined } + ? { + url: trimmedUrl, + headers: parsedHeaders.length > 0 ? parsedHeaders : undefined, + body: body === "" ? undefined : body, + } : null; }; diff --git a/packages/core/src/backup-hooks/__tests__/hooks.test.ts b/packages/core/src/backup-hooks/__tests__/hooks.test.ts index 6e826abd..6f0bea6b 100644 --- a/packages/core/src/backup-hooks/__tests__/hooks.test.ts +++ b/packages/core/src/backup-hooks/__tests__/hooks.test.ts @@ -353,3 +353,31 @@ test("cancels before the pre-backup webhook without running the backup", async ( expect(backupRan).toBe(false); expect(result).toEqual({ status: "cancelled", message: "Backup was cancelled" }); }); + +test("cancels after the pre-backup webhook without running the backup", async () => { + const abortController = new AbortController(); + let backupRan = false; + + server.use( + http.post("http://localhost:8080/pre", () => { + abortController.abort(new Error("Backup was cancelled")); + return new HttpResponse(null, { status: 204 }); + }), + ); + + const result = await runWithHooks({ + webhooks: { + pre: { url: "http://localhost:8080/pre" }, + post: null, + }, + signal: abortController.signal, + runBackup: () => + Effect.sync(() => { + backupRan = true; + return { exitCode: 0, result: null, warningDetails: null }; + }), + }); + + expect(backupRan).toBe(false); + expect(result).toEqual({ status: "cancelled", message: "Backup was cancelled" }); +}); diff --git a/packages/core/src/backup-hooks/index.ts b/packages/core/src/backup-hooks/index.ts index 477d4fe0..3ef95a69 100644 --- a/packages/core/src/backup-hooks/index.ts +++ b/packages/core/src/backup-hooks/index.ts @@ -220,6 +220,9 @@ export const runBackupLifecycle = ({ return { status: "failed", error: preHookError }; } + if (signal.aborted) { + return { status: "cancelled", message: formatError(signal.reason) }; + } const backupResult = yield* Effect.suspend(() => restic.backup(repositoryConfig, sourcePath, { ...options, organizationId, signal, onProgress }),