From f11e3b7f823340a70017fdc0c924d011196ca72e Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 4 Apr 2026 21:07:38 +0200 Subject: [PATCH] fix(schedule): disable if saving with manual-only mode --- .../routes/__tests__/edit-backup.test.tsx | 148 +++++++++++++++++- .../modules/backups/routes/edit-backup.tsx | 1 + 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/app/client/modules/backups/routes/__tests__/edit-backup.test.tsx b/app/client/modules/backups/routes/__tests__/edit-backup.test.tsx index 5d46dd16..9cc66b98 100644 --- a/app/client/modules/backups/routes/__tests__/edit-backup.test.tsx +++ b/app/client/modules/backups/routes/__tests__/edit-backup.test.tsx @@ -1,6 +1,6 @@ import { afterEach, expect, test, vi } from "vitest"; import { HttpResponse, http, server } from "~/test/msw/server"; -import { cleanup, render, screen, userEvent } from "~/test/test-utils"; +import { cleanup, fireEvent, render, screen, userEvent } from "~/test/test-utils"; const navigateMock = vi.fn(async () => {}); @@ -27,6 +27,7 @@ test("submits the computed cron expression when saving a daily schedule", async return HttpResponse.json({ shortId: "backup-1", name: "Backup 1", + enabled: true, repository: { shortId: "repo-1", name: "Repo 1", type: "local" }, volume: { id: "volume-1", @@ -82,6 +83,151 @@ test("submits the computed cron expression when saving a daily schedule", async await expect(submittedBody).resolves.toMatchObject({ frequency: "daily", + enabled: true, + cronExpression: "00 02 * * *", + }); +}); + +test("disables the schedule when switching an enabled custom cron schedule to manual only", async () => { + const submittedBody = new Promise>((resolve) => { + server.use( + http.get("/api/v1/backups/:shortId", () => { + return HttpResponse.json({ + shortId: "backup-1", + name: "Backup 1", + enabled: true, + repository: { shortId: "repo-1", name: "Repo 1", type: "local" }, + volume: { + id: "volume-1", + shortId: "vol-1", + name: "Volume 1", + config: { backend: "directory", path: "/mnt" }, + }, + cronExpression: "*/13 * * * *", + retentionPolicy: null, + includePaths: ["/project"], + includePatterns: [], + excludePatterns: [], + excludeIfPresent: [], + oneFileSystem: false, + customResticParams: [], + }); + }), + http.get("/api/v1/repositories", () => { + return HttpResponse.json([{ shortId: "repo-1", name: "Repo 1", type: "local" }]); + }), + http.get("/api/v1/volumes/:shortId/files", () => { + return HttpResponse.json({ + files: [{ name: "project", path: "/project", type: "directory" }], + path: "/", + offset: 0, + limit: 100, + total: 1, + hasMore: false, + }); + }), + http.patch("/api/v1/backups/:shortId", async ({ request }) => { + const body = (await request.json()) as Record; + resolve(body); + + return HttpResponse.json({ + shortId: "backup-1", + volume: { + id: "volume-1", + shortId: "vol-1", + name: "Volume 1", + config: { backend: "directory", path: "/mnt" }, + }, + repository: { shortId: "repo-1", name: "Repo 1", type: "local" }, + ...body, + }); + }), + ); + }); + + render(, { withSuspense: true }); + + const nativeFrequencySelect = (await screen.findAllByRole("combobox")) + .at(1) + ?.parentElement?.querySelector('select[aria-hidden="true"]'); + if (!(nativeFrequencySelect instanceof HTMLSelectElement)) { + throw new Error("Expected hidden native select for frequency field"); + } + + fireEvent.change(nativeFrequencySelect, { target: { value: "manual" } }); + await userEvent.click(screen.getByRole("button", { name: "Update schedule" })); + + await expect(submittedBody).resolves.toMatchObject({ + frequency: "manual", + enabled: false, + cronExpression: "", + }); +}); + +test("preserves a disabled schedule when saving a non-manual frequency", async () => { + const submittedBody = new Promise>((resolve) => { + server.use( + http.get("/api/v1/backups/:shortId", () => { + return HttpResponse.json({ + shortId: "backup-1", + name: "Backup 1", + enabled: false, + repository: { shortId: "repo-1", name: "Repo 1", type: "local" }, + volume: { + id: "volume-1", + shortId: "vol-1", + name: "Volume 1", + config: { backend: "directory", path: "/mnt" }, + }, + cronExpression: "0 2 * * *", + retentionPolicy: null, + includePaths: ["/project"], + includePatterns: [], + excludePatterns: [], + excludeIfPresent: [], + oneFileSystem: false, + customResticParams: [], + }); + }), + http.get("/api/v1/repositories", () => { + return HttpResponse.json([{ shortId: "repo-1", name: "Repo 1", type: "local" }]); + }), + http.get("/api/v1/volumes/:shortId/files", () => { + return HttpResponse.json({ + files: [{ name: "project", path: "/project", type: "directory" }], + path: "/", + offset: 0, + limit: 100, + total: 1, + hasMore: false, + }); + }), + http.patch("/api/v1/backups/:shortId", async ({ request }) => { + const body = (await request.json()) as Record; + resolve(body); + + return HttpResponse.json({ + shortId: "backup-1", + volume: { + id: "volume-1", + shortId: "vol-1", + name: "Volume 1", + config: { backend: "directory", path: "/mnt" }, + }, + repository: { shortId: "repo-1", name: "Repo 1", type: "local" }, + ...body, + }); + }), + ); + }); + + render(, { withSuspense: true }); + + await userEvent.click(await screen.findByRole("button", { name: "Update schedule" })); + + await expect(submittedBody).resolves.toMatchObject({ + frequency: "daily", + enabled: false, cronExpression: "00 02 * * *", }); }); diff --git a/app/client/modules/backups/routes/edit-backup.tsx b/app/client/modules/backups/routes/edit-backup.tsx index 8aae42c7..108f90cf 100644 --- a/app/client/modules/backups/routes/edit-backup.tsx +++ b/app/client/modules/backups/routes/edit-backup.tsx @@ -51,6 +51,7 @@ export function EditBackupPage({ backupId }: { backupId: string }) { path: { shortId: schedule.shortId }, body: { ...formValues, + enabled: formValues.frequency === "manual" ? false : schedule.enabled, cronExpression, retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined, },