Files
dbx/apps/desktop/src/stores/migrationStore.spec.ts
T

195 lines
9.3 KiB
TypeScript
Vendored

import { describe, expect, it, vi } from "vitest";
import { useMigrationStore } from "./migrationStore";
import type { MigrationPreflight, MigrationReport } from "@/lib/backend/migration";
const pending = (): MigrationPreflight => ({
migrationId: "secret-store-v1",
state: "pending",
needsMigration: true,
keyProviderAvailable: true,
keyCreationAllowed: false,
databasePlaintextCount: 2,
connectionCount: 1,
pluginSecretCount: 0,
aiSecretCount: 0,
tunnelSecretCount: 0,
syncCredentialCount: 0,
legacyJsonFiles: [],
backupRequired: true,
});
const done = (): MigrationPreflight => ({ ...pending(), state: "succeeded", needsMigration: false, databasePlaintextCount: 0, backupPath: "/tmp/backup" });
const report: MigrationReport = { migrationId: "secret-store-v1", state: "succeeded", backupPath: "/tmp/backup", databasePlaintextCount: 2, legacyJsonFiles: [], verifiedSecretCount: 2 };
describe("migration store", () => {
it("keeps the wizard blocked until explicit entry after a successful migration", async () => {
const status = vi.fn().mockResolvedValueOnce(pending()).mockResolvedValue(done());
const store = useMigrationStore({ migrationStatus: status, migrationStart: vi.fn().mockResolvedValue(report), migrationRetry: vi.fn(), migrationCleanupBackups: vi.fn() });
await store.initialize();
expect(store.blocking.value).toBe(true);
await store.start();
expect(store.state.report).toEqual(report);
expect(store.blocking.value).toBe(true);
store.enter();
expect(store.blocking.value).toBe(false);
});
it("preserves the report and status when cleanup fails", async () => {
const cleanup = vi.fn().mockRejectedValue(new Error("failed"));
const store = useMigrationStore({ migrationStatus: vi.fn().mockResolvedValue(done()), migrationStart: vi.fn(), migrationRetry: vi.fn(), migrationCleanupBackups: cleanup });
await store.initialize();
store.state.report = report;
await store.cleanup();
expect(store.state.error).toBe("cleanupFailed");
expect(store.state.report).toEqual(report);
});
it("allows a completed migration with a retained backup to enter after restart", async () => {
const store = useMigrationStore({ migrationStatus: vi.fn().mockResolvedValue(done()), migrationStart: vi.fn(), migrationRetry: vi.fn(), migrationCleanupBackups: vi.fn() });
await store.initialize();
expect(store.completed.value).toBe(true);
expect(store.blocking.value).toBe(false);
expect(store.state.entered).toBe(false);
});
it("does not block a restart when migration is not required", async () => {
const store = useMigrationStore({
migrationStatus: vi.fn().mockResolvedValue({ ...done(), state: "not_required" }),
migrationStart: vi.fn(),
migrationRetry: vi.fn(),
migrationCleanupBackups: vi.fn(),
});
await store.initialize();
expect(store.completed.value).toBe(true);
expect(store.blocking.value).toBe(false);
expect(store.state.entered).toBe(false);
});
it("keeps status failures on the status retry path", async () => {
const status = vi
.fn()
.mockRejectedValueOnce(Object.assign(new Error("unavailable"), { backendError: { code: "KEY_PROVIDER_UNAVAILABLE", detail: "safe detail" } }))
.mockResolvedValue(done());
const store = useMigrationStore({ migrationStatus: status, migrationStart: vi.fn(), migrationRetry: vi.fn(), migrationCleanupBackups: vi.fn() });
await store.initialize();
expect(store.state.error).toBe("statusFailed");
expect(store.state.errorCode).toBeNull();
expect(store.state.errorMessage).toBeNull();
await store.initialize();
expect(store.state.error).toBeNull();
expect(store.completed.value).toBe(true);
});
it("discards arbitrary transport exception values in state and diagnostics", async () => {
const secret = "password=DO_NOT_EXPOSE";
const failure = Object.assign(new Error(secret), { backendError: { code: secret, detail: secret } });
const status = vi.fn().mockRejectedValue(failure);
const store = useMigrationStore({ migrationStatus: status, migrationStart: vi.fn().mockRejectedValue(failure), migrationRetry: vi.fn().mockRejectedValue(failure), migrationCleanupBackups: vi.fn().mockRejectedValue(failure) });
for (const action of [store.initialize, store.start, store.retry, store.cleanup]) {
await action();
expect(JSON.stringify(store.state)).not.toContain(secret);
expect(JSON.stringify(store.diagnostic())).not.toContain(secret);
}
});
it("shows safe preflight errors even when the migration is pending", async () => {
const store = useMigrationStore({ migrationStatus: vi.fn().mockResolvedValue({ ...pending(), errorCode: "MISSING_PERSISTENT_KEY", errorMessage: "Configure a persistent key." }), migrationStart: vi.fn(), migrationRetry: vi.fn(), migrationCleanupBackups: vi.fn() });
await store.initialize();
expect(store.state.error).toBe("migrationFailed");
expect(store.state.errorCode).toBe("MISSING_PERSISTENT_KEY");
expect(store.state.errorMessage).toBe("Configure a persistent key.");
});
it("does not show a failure for a managed key created by migration", async () => {
const store = useMigrationStore({
migrationStatus: vi.fn().mockResolvedValue({
...pending(),
keyProviderAvailable: false,
keyCreationAllowed: true,
errorCode: "MISSING_MANAGED_KEY",
errorMessage: "A managed data-directory secret key will be created when migration starts",
}),
migrationStart: vi.fn(),
migrationRetry: vi.fn(),
migrationCleanupBackups: vi.fn(),
});
await store.initialize();
expect(store.state.error).toBeNull();
expect(store.state.errorCode).toBe("MISSING_MANAGED_KEY");
});
it("keeps completion through a failed cleanup and successful cleanup retry", async () => {
let finishCleanup!: () => void;
const cleanup = vi
.fn()
.mockRejectedValueOnce(new Error("password=secret"))
.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
finishCleanup = resolve;
}),
);
const retry = vi.fn();
const store = useMigrationStore({ migrationStatus: vi.fn().mockResolvedValue(done()), migrationStart: vi.fn(), migrationRetry: retry, migrationCleanupBackups: cleanup });
await store.initialize();
await store.cleanup();
expect(store.state.error).toBe("cleanupFailed");
expect(store.state.step).toBe(3);
expect(store.completed.value).toBe(true);
expect(store.state.status?.backupPath).toBe("/tmp/backup");
const cleanupAttempt = store.cleanup();
expect(store.state.busy).toBe(true);
await store.cleanup();
expect(cleanup).toHaveBeenCalledTimes(2);
finishCleanup();
await cleanupAttempt;
store.enter();
expect(store.state.entered).toBe(true);
expect(store.state.busy).toBe(false);
expect(store.state.step).toBe(3);
expect(store.state.error).toBeNull();
expect(store.completed.value).toBe(true);
expect(store.state.status?.backupPath).toBeNull();
expect(retry).not.toHaveBeenCalled();
});
it("recovers when a lost action response is followed by verified completion", async () => {
const store = useMigrationStore({ migrationStatus: vi.fn().mockResolvedValueOnce(pending()).mockResolvedValue(done()), migrationStart: vi.fn().mockRejectedValue(new Error("response lost")), migrationRetry: vi.fn(), migrationCleanupBackups: vi.fn() });
await store.initialize();
await store.start();
expect(store.state.step).toBe(3);
expect(store.state.error).toBeNull();
expect(store.state.busy).toBe(false);
expect(store.state.report).toBeNull();
expect(store.blocking.value).toBe(true);
store.enter();
expect(store.blocking.value).toBe(false);
});
it("keeps the success page after retrying a failed status check in the same session", async () => {
const status = vi.fn().mockResolvedValueOnce(pending()).mockRejectedValueOnce(new Error("status unavailable")).mockResolvedValueOnce(done());
const store = useMigrationStore({ migrationStatus: status, migrationStart: vi.fn().mockRejectedValue(new Error("response lost")), migrationRetry: vi.fn(), migrationCleanupBackups: vi.fn() });
await store.initialize();
await store.start();
expect(store.state.error).toBe("statusFailed");
await store.initialize();
expect(store.completed.value).toBe(true);
expect(store.state.report).toBeNull();
expect(store.blocking.value).toBe(true);
store.enter();
expect(store.blocking.value).toBe(false);
});
it("retries a failed migration and completes without losing the error report", async () => {
const failed = { ...pending(), state: "failed" as const, errorCode: "BACKUP_FAILED", errorMessage: "Cannot create backup" };
const retry = vi.fn().mockResolvedValue(report);
const store = useMigrationStore({ migrationStatus: vi.fn().mockResolvedValueOnce(failed).mockResolvedValue(done()), migrationStart: vi.fn().mockRejectedValue(new Error("failure")), migrationRetry: retry, migrationCleanupBackups: vi.fn() });
await store.start();
expect(store.state.errorCode).toBe("BACKUP_FAILED");
expect(store.diagnostic().actionError).toBe("migrationFailed");
await store.retry();
expect(retry).toHaveBeenCalledOnce();
expect(store.state.step).toBe(3);
expect(store.state.error).toBeNull();
});
});