Add local automatic backups and Aniimo regions lists
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-09-24 09:39:33 +02:00
parent 2255de94a7
commit 0a3bb4dffd
24 changed files with 908 additions and 16 deletions

128
tests/auto-backup.test.mjs Normal file
View file

@ -0,0 +1,128 @@
// 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("permission failure prevents export and cleanup", async () => {
const directory = new FakeDirectory();
directory.permission = "prompt";
let generated = false;
const result = await saveBackup({ directoryHandle: directory, generateJson: async () => { generated = true; return "{}"; } });
assert.equal(result.reason, "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);
});