fix(backup): drop missing databases from edited plans

Closes #6057
This commit is contained in:
t8y2 committed 2026-08-13 03:41:03 +00:00
1 parent ee08f356e9
commit cf57df35da
2 files changed
+90 -2

No files matched your search

@@ -144,7 +144,10 @@ async function loadDatabases(connectionId: string, preserveSelection: boolean) {
const config = connectionStore.getConfig(connectionId); const config = connectionStore.getConfig(connectionId);
const names = config?.db_type === "dameng" ? await fetchNamespaceOptionsForConnection(connectionId, config) : (await api.listDatabases(connectionId)).map((database) => database.name); const names = config?.db_type === "dameng" ? await fetchNamespaceOptionsForConnection(connectionId, config) : (await api.listDatabases(connectionId)).map((database) => database.name);
databaseOptions.value = names; databaseOptions.value = names;
if (!preserveSelection) { if (preserveSelection) {
const selected = new Set(selectedDatabases.value);
selectedDatabases.value = names.filter((database) => selected.has(database));
} else {
selectedDatabases.value = []; selectedDatabases.value = [];
allDatabases.value = true; allDatabases.value = true;
draft.value.tableFilterMode = "all"; draft.value.tableFilterMode = "all";
@@ -4,9 +4,11 @@ import { createApp, nextTick, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n"; import i18n from "@/i18n";
import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue"; import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue";
import type { DatabaseBackupSchedule } from "@/lib/backup/scheduledDatabaseBackup";
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
connections: [] as Array<{ id: string; name: string; db_type: string }>, connections: [] as Array<{ id: string; name: string; db_type: string }>,
schedules: [] as DatabaseBackupSchedule[],
ensureConnected: vi.fn(async () => {}), ensureConnected: vi.fn(async () => {}),
listDatabases: vi.fn(async () => [{ name: "app" }]), listDatabases: vi.fn(async () => [{ name: "app" }]),
toast: vi.fn(), toast: vi.fn(),
@@ -29,7 +31,7 @@ vi.mock("@/stores/connectionStore", () => ({
vi.mock("@/composables/useScheduledDatabaseBackups", () => ({ vi.mock("@/composables/useScheduledDatabaseBackups", () => ({
useScheduledDatabaseBackups: () => ({ useScheduledDatabaseBackups: () => ({
schedules: { __v_isRef: true, value: [] }, schedules: { __v_isRef: true, value: mocks.schedules },
runs: { __v_isRef: true, value: [] }, runs: { __v_isRef: true, value: [] },
activeScheduleIds: new Set<string>(), activeScheduleIds: new Set<string>(),
activeRunIds: new Set<string>(), activeRunIds: new Set<string>(),
@@ -78,13 +80,55 @@ function addScheduleButton(): HTMLButtonElement {
return button; return button;
} }
function schedule(overrides: Partial<DatabaseBackupSchedule> = {}): DatabaseBackupSchedule {
return {
id: "schedule-1",
name: "Nightly backup",
enabled: true,
connectionId: "mysql-1",
databases: ["app"],
tableFilterMode: "all",
tablePatterns: [],
destinationDirectory: "/backups",
frequency: "daily",
intervalHours: 6,
timeOfDay: "02:00",
weekday: 1,
includeStructure: true,
includeData: true,
includeObjects: true,
dropTableIfExists: false,
retentionCount: 10,
createdAt: "2026-08-12T00:00:00.000Z",
updatedAt: "2026-08-12T00:00:00.000Z",
nextRunAt: "2026-08-13T02:00:00.000Z",
...overrides,
};
}
function buttonWithTitle(title: string): HTMLButtonElement {
const button = Array.from(document.body.querySelectorAll("button")).find((item) => item.title === title);
if (!button) throw new Error(`Button not found: ${title}`);
return button;
}
function saveScheduleButton(): HTMLButtonElement {
const dialog = document.body.querySelector('[data-slot="dialog-content"]');
const label = String(i18n.global.t("common.save"));
const button = Array.from(dialog?.querySelectorAll("button") ?? []).find((item) => item.textContent?.trim() === label);
if (!button) throw new Error("Save schedule button not found");
return button;
}
afterEach(() => { afterEach(() => {
for (const app of mountedApps.splice(0)) app.unmount(); for (const app of mountedApps.splice(0)) app.unmount();
document.body.innerHTML = ""; document.body.innerHTML = "";
mocks.connections.splice(0); mocks.connections.splice(0);
mocks.schedules.splice(0);
mocks.ensureConnected.mockClear(); mocks.ensureConnected.mockClear();
mocks.listDatabases.mockClear(); mocks.listDatabases.mockClear();
mocks.toast.mockClear(); mocks.toast.mockClear();
mocks.saveSchedule.mockClear();
}); });
describe("ScheduledDatabaseBackupSettings schedule dialog", () => { describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
@@ -112,4 +156,45 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
expect(addScheduleButton().disabled).toBe(true); expect(addScheduleButton().disabled).toBe(true);
expect(document.body.textContent).toContain(String(i18n.global.t("databaseBackup.noSupportedConnections"))); expect(document.body.textContent).toContain(String(i18n.global.t("databaseBackup.noSupportedConnections")));
}); });
it("removes unavailable databases from an edited schedule before saving", async () => {
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
mocks.schedules.push(schedule({ databases: ["app", "deleted"] }));
mocks.listDatabases.mockResolvedValueOnce([{ name: "app" }]);
await mountSettings();
buttonWithTitle(String(i18n.global.t("databaseBackup.edit"))).click();
await flush();
saveScheduleButton().click();
await flush();
expect(mocks.saveSchedule).toHaveBeenCalledWith(expect.objectContaining({ databases: ["app"] }));
});
it("does not allow saving when every selected database is unavailable", async () => {
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
mocks.schedules.push(schedule({ databases: ["deleted"] }));
mocks.listDatabases.mockResolvedValueOnce([{ name: "app" }]);
await mountSettings();
buttonWithTitle(String(i18n.global.t("databaseBackup.edit"))).click();
await flush();
expect(saveScheduleButton().disabled).toBe(true);
expect(mocks.saveSchedule).not.toHaveBeenCalled();
});
it("preserves configured databases when refreshing the list fails", async () => {
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
mocks.schedules.push(schedule({ databases: ["app", "archive"] }));
mocks.listDatabases.mockRejectedValueOnce(new Error("database list failed"));
await mountSettings();
buttonWithTitle(String(i18n.global.t("databaseBackup.edit"))).click();
await flush();
saveScheduleButton().click();
await flush();
expect(mocks.saveSchedule).toHaveBeenCalledWith(expect.objectContaining({ databases: ["app", "archive"] }));
});
}); });