// 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, getBackupFileName, getLocalDateKey, isFileSystemAccessSupported, 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("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("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); });