sokko-g/tests/auto-backup.test.mjs
Shinuwa ee823d2c37
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
Improve local backup permissions and change tracking
2026-09-25 10:45:12 +02:00

160 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Rôle : vérifie les règles de nommage, d’écriture et de rétention des sauvegardes locales.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
backupContentsEqual,
getBackupContentSignature,
getBackupFileName,
getLocalDateKey,
hasBackupDataChanged,
isFileSystemAccessSupported,
normalizeAutoBackupConfig,
saveBackup
} from "../website/src/features/toolboxes/storage/autoBackup.js";
class FakeDirectory {
constructor(names = []) {
this.files = new Map(names.map((name) => [name, "old"]));
this.directories = new Set();
this.permission = "granted";
this.failCleanup = false;
this.failWrite = false;
}
async queryPermission() { return this.permission; }
async *values() {
for (const name of this.files.keys()) yield { kind: "file", name };
for (const name of this.directories) yield { kind: "directory", name };
}
async getFileHandle(name, { create = false } = {}) {
if (create && !this.files.has(name)) this.files.set(name, "");
const directory = this;
return {
async getFile() {
return { text: async () => directory.files.get(name) || "" };
},
async createWritable() {
if (directory.failWrite) throw new Error("write failed");
return {
async write(contents) { directory.files.set(name, contents); },
async close() {},
async abort() {}
};
}
};
}
async removeEntry(name) {
if (this.failCleanup) throw new Error("cleanup failed");
this.files.delete(name);
}
}
test("feature detection does not rely on the user agent", () => {
assert.equal(isFileSystemAccessSupported({ showDirectoryPicker() {} }), true);
assert.equal(isFileSystemAccessSupported({}), false);
});
test("legacy enabled configurations start dirty while explicit backup state is preserved", () => {
assert.equal(normalizeAutoBackupConfig().hasUnsavedChanges, false);
assert.equal(normalizeAutoBackupConfig({ enabled: true, directoryHandle: {} }).hasUnsavedChanges, true);
assert.equal(normalizeAutoBackupConfig({ enabled: true, directoryHandle: {}, hasUnsavedChanges: false }).hasUnsavedChanges, false);
});
test("backup names use the browser local date", () => {
const date = new Date(2026, 8, 24, 0, 5);
assert.equal(getLocalDateKey(date), "2026-09-24");
assert.equal(getBackupFileName(date), "sokkog-global-2026-09-24.json");
});
test("automatic backup skips an existing day and creates the next day", async () => {
const directory = new FakeDirectory(["sokkog-global-2026-09-23.json"]);
let generated = 0;
const generateJson = async () => { generated += 1; return "{}"; };
const first = await saveBackup({ directoryHandle: directory, generateJson, date: new Date(2026, 8, 23) });
const second = await saveBackup({ directoryHandle: directory, generateJson, date: new Date(2026, 8, 24) });
assert.equal(first.created, false);
assert.equal(second.created, true);
assert.equal(generated, 1);
assert.ok(directory.files.has("sokkog-global-2026-09-24.json"));
});
test("automatic backup skips a new day when the export is unchanged", async () => {
const previous = JSON.stringify({ version: 1, exportedAt: "2026-09-23T08:00:00.000Z", toolboxes: [], modules: {}, links: {} });
const directory = new FakeDirectory(["sokkog-global-2026-09-23.json"]);
directory.files.set("sokkog-global-2026-09-23.json", previous);
const current = JSON.stringify({ version: 1, exportedAt: "2026-09-24T08:00:00.000Z", toolboxes: [], modules: {}, links: {} });
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => current, date: new Date(2026, 8, 24) });
assert.equal(backupContentsEqual(previous, current), true);
assert.equal(result.ok, true);
assert.equal(result.created, false);
assert.equal(result.unchanged, true);
assert.equal(directory.files.has("sokkog-global-2026-09-24.json"), false);
});
test("content signatures ignore the export timestamp and detect toolbox changes", async () => {
const previous = JSON.stringify({ version: 1, exportedAt: "2026-09-23T08:00:00.000Z", toolboxes: [{ id: "one", name: "Avant" }], modules: {}, links: {} });
const sameData = JSON.stringify({ version: 1, exportedAt: "2026-09-24T08:00:00.000Z", toolboxes: [{ id: "one", name: "Avant" }], modules: {}, links: {} });
const changedData = JSON.stringify({ version: 1, exportedAt: "2026-09-24T08:00:00.000Z", toolboxes: [{ id: "one", name: "Après" }], modules: {}, links: {} });
const previousSignature = await getBackupContentSignature(previous, {});
const sameSignature = await getBackupContentSignature(sameData, {});
const changedSignature = await getBackupContentSignature(changedData, {});
assert.equal(previousSignature, sameSignature);
assert.equal(hasBackupDataChanged({ lastBackupSignature: previousSignature, currentSignature: sameSignature }), false);
assert.equal(hasBackupDataChanged({ lastBackupSignature: previousSignature, currentSignature: changedSignature }), true);
});
test("manual backup replaces the day and retains ten non-consecutive versions", async () => {
const names = [
"sokkog-global-2024-01-01.json", "sokkog-global-2024-06-01.json", "sokkog-global-2025-01-01.json",
"sokkog-global-2025-03-01.json", "sokkog-global-2025-09-01.json", "sokkog-global-2026-01-01.json",
"sokkog-global-2026-03-01.json", "sokkog-global-2026-06-01.json", "sokkog-global-2026-07-01.json",
"sokkog-global-2026-08-01.json", "sokkog-global-2026-09-24.json", "notes.json"
];
const directory = new FakeDirectory(names);
const result = await saveBackup({ directoryHandle: directory, force: true, date: new Date(2026, 8, 24), generateJson: async () => "new" });
assert.equal(result.ok, true);
assert.equal(directory.files.get("sokkog-global-2026-09-24.json"), "new");
assert.equal([...directory.files.keys()].filter((name) => /^sokkog-global-\d{4}-\d{2}-\d{2}\.json$/.test(name)).length, 10);
assert.equal(directory.files.get("notes.json"), "old");
});
test("missing permission never opens a prompt or starts an export", async () => {
for (const permission of ["prompt", "denied"]) {
const directory = new FakeDirectory();
directory.permission = permission;
directory.requestPermission = () => { throw new Error("Permission popup must require user interaction"); };
let generated = false;
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => { generated = true; return "{}"; } });
assert.equal(result.reason, "permission");
assert.equal(result.permission, permission);
assert.equal(generated, false);
}
});
test("cleanup failure does not invalidate a successfully written backup", async () => {
const directory = new FakeDirectory(Array.from({ length: 10 }, (_, index) => `sokkog-global-2020-01-${String(index + 1).padStart(2, "0")}.json`));
directory.failCleanup = true;
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => "{}", date: new Date(2026, 8, 24) });
assert.equal(result.ok, true);
assert.match(result.cleanupError, /cleanup failed/);
assert.equal(directory.files.get("sokkog-global-2026-09-24.json"), "{}");
});
test("failed write does not leave a newly created backup", async () => {
const directory = new FakeDirectory();
directory.failWrite = true;
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => "{}", date: new Date(2026, 8, 24) });
assert.equal(result.ok, false);
assert.equal(directory.files.has("sokkog-global-2026-09-24.json"), false);
});
test("export failure does not create a partial backup", async () => {
const directory = new FakeDirectory();
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => { throw new Error("export failed"); }, date: new Date(2026, 8, 24) });
assert.equal(result.ok, false);
assert.equal(directory.files.has("sokkog-global-2026-09-24.json"), false);
});