92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { rm, access } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import {
|
|
MapNameResolver, nameUuidIndexFromEntries, saveNameUuidIndex, loadNameUuidIndex,
|
|
type NameUuidIndex,
|
|
} from "../src/resolver.js";
|
|
|
|
const TMP = "/tmp/test-resolver-out";
|
|
|
|
describe("nameUuidIndexFromEntries", () => {
|
|
it("builds both directions, first occurrence wins", () => {
|
|
const idx = nameUuidIndexFromEntries([
|
|
{ name: "Fenris", uuid: "JournalEntry.aaa" },
|
|
{ name: "Joron", uuid: "JournalEntry.bbb" },
|
|
{ name: "Fenris", uuid: "JournalEntry.ccc" }, // dup name -> name keeps first uuid
|
|
{ name: "", uuid: "JournalEntry.ddd" }, // empty name -> ignored
|
|
]);
|
|
expect(idx.nameToUuid["Fenris"]).toBe("JournalEntry.aaa"); // first wins for the name
|
|
expect(idx.nameToUuid["Joron"]).toBe("JournalEntry.bbb");
|
|
expect(idx.uuidToName["JournalEntry.aaa"]).toBe("Fenris");
|
|
expect(idx.uuidToName["JournalEntry.bbb"]).toBe("Joron");
|
|
expect(idx.uuidToName["JournalEntry.ccc"]).toBe("Fenris"); // distinct uuid still recorded
|
|
expect(idx.nameToUuid[""]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("MapNameResolver", () => {
|
|
const idx: NameUuidIndex = {
|
|
nameToUuid: { Fenris: "JournalEntry.aaa" },
|
|
uuidToName: { "JournalEntry.aaa": "Fenris" },
|
|
};
|
|
|
|
it("resolves name->uuid and uuid->name", () => {
|
|
const r = new MapNameResolver(idx);
|
|
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
|
|
expect(r.nameOf("JournalEntry.aaa")).toBe("Fenris");
|
|
});
|
|
|
|
it("returns undefined for unknown names/uuids", () => {
|
|
const r = new MapNameResolver(idx);
|
|
expect(r.uuidOf("Nobody")).toBeUndefined();
|
|
expect(r.nameOf("JournalEntry.zzz")).toBeUndefined();
|
|
});
|
|
|
|
it("does not return Object.prototype members for prototype-colliding names", () => {
|
|
// A JSON.parse-loaded index carries Object.prototype, so a name like
|
|
// "constructor" must NOT resolve to Object.prototype.constructor (a truthy
|
|
// function) — that would corrupt pushed Foundry @UUID links. Regression
|
|
// guard for the Map.get → record-access switch.
|
|
const r = new MapNameResolver(JSON.parse('{"nameToUuid":{"Fenris":"JournalEntry.aaa"},"uuidToName":{"JournalEntry.aaa":"Fenris"}}') as NameUuidIndex);
|
|
expect(r.uuidOf("constructor")).toBeUndefined();
|
|
expect(r.uuidOf("toString")).toBeUndefined();
|
|
expect(r.uuidOf("hasOwnProperty")).toBeUndefined();
|
|
expect(r.nameOf("constructor")).toBeUndefined();
|
|
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
|
|
});
|
|
|
|
it("stores a name literally named __proto__ as an own key", () => {
|
|
const idx = nameUuidIndexFromEntries([{ name: "__proto__", uuid: "JournalEntry.zzz" }]);
|
|
expect(idx.nameToUuid["__proto__"]).toBe("JournalEntry.zzz");
|
|
expect(new MapNameResolver(idx).uuidOf("__proto__")).toBe("JournalEntry.zzz");
|
|
});
|
|
|
|
it("satisfies the NameResolver interface (duck-types like JournalDb)", () => {
|
|
const r: { uuidOf(n: string): string | undefined; nameOf(u: string): string | undefined } = new MapNameResolver(idx);
|
|
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
|
|
});
|
|
});
|
|
|
|
describe("save/load name-uuid index", () => {
|
|
it("round-trips through disk", async () => {
|
|
const idx = nameUuidIndexFromEntries([
|
|
{ name: "Fenris", uuid: "JournalEntry.aaa" },
|
|
{ name: "Joron", uuid: "JournalEntry.bbb" },
|
|
]);
|
|
const path = join(TMP, "name-uuid.json");
|
|
await saveNameUuidIndex(idx, path);
|
|
const r = await loadNameUuidIndex(path);
|
|
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
|
|
expect(r.nameOf("JournalEntry.bbb")).toBe("Joron");
|
|
});
|
|
|
|
it("saveNameUuidIndex creates the parent dir", async () => {
|
|
await rm(TMP, { recursive: true, force: true });
|
|
await expect(access(TMP)).rejects.toThrow();
|
|
const path = join(TMP, "nested", "name-uuid.json");
|
|
await saveNameUuidIndex(nameUuidIndexFromEntries([]), path);
|
|
const r = await loadNameUuidIndex(path);
|
|
expect(r.uuidOf("x")).toBeUndefined();
|
|
});
|
|
}); |