Files
C.E.L_Slide_test2/Front/client/tests/user_overrides_endpoint.test.ts

810 lines
28 KiB
TypeScript

// IMP-52 u3/u4 — vitest coverage for the vite `/api/user-overrides/:key`
// GET and PUT endpoints and their supporting helpers.
//
// Scope:
// u3 (read path):
// 1) isValidUserOverridesKey: accept MDX-stem keys (03, 03__DX_BIM,
// a-b.c), reject empty / leading-dot / `..` / `/` / `\` /
// disallowed chars. Mirrors src/user_overrides_io.validate_key so
// backend (u2) and frontend endpoint (u3) agree on every key.
// 2) userOverridesPath: returns <root>/data/user_overrides/<key>.json.
// 3) handleGetUserOverrides: method != GET → false (next chained for
// PUT); invalid key → 400; missing file → 200 {}; corrupt JSON /
// non-object root → 200 {} (graceful degrade per u1 load contract);
// valid object JSON → 200 with parsed payload echoed back.
//
// u4 (write path):
// 4) mergeUserOverrides: only KNOWN_USER_OVERRIDES_AXES mutated;
// foreign top-level keys preserved; null clears axis; non-axis
// partial keys dropped (allowlist).
// 5) atomicWriteUserOverrides: tmp + rename; parent dir auto-created.
// 6) handlePutUserOverrides: method != PUT → false (next chained);
// invalid key → 400; invalid JSON → 400; non-object body → 400;
// success → 200 with merged result; partial-merge preserves axes
// not in payload; foreign-key preserve on disk; allowlist drops
// unknown payload keys; explicit null clears; corrupt existing →
// recover to clean state.
//
// Tests exercise the pure handlers with mock req/res — no real vite server.
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
KNOWN_USER_OVERRIDES_AXES,
USER_OVERRIDES_KEY_RE,
atomicWriteUserOverrides,
handleGetUserOverrides,
handlePutUserOverrides,
isValidUserOverridesKey,
mergeUserOverrides,
userOverridesPath,
} from "../../vite.config";
// ---------------------------------------------------------------------------
// mock res helper — captures writeHead(status, headers) + end(body) so the
// handler can be invoked synchronously without spawning a TCP socket.
// ---------------------------------------------------------------------------
function makeMockRes() {
const state = {
statusCode: 0,
headers: {} as Record<string, string>,
body: "",
ended: false,
};
return {
state,
res: {
writeHead(status: number, headers?: Record<string, string>) {
state.statusCode = status;
if (headers) state.headers = headers;
},
end(body?: string) {
state.body = body ?? "";
state.ended = true;
},
},
};
}
describe("USER_OVERRIDES_KEY_RE (IMP-52 u3)", () => {
it("matches Python validate_key regex literally", () => {
// The pattern locked in src/user_overrides_io.py:_KEY_RE — any drift here
// means backend pipeline fallback (u2) and the vite endpoint disagree on
// which keys are routable, which is the single failure mode that would
// silently lose persisted overrides.
expect(USER_OVERRIDES_KEY_RE.source).toBe(
"^[A-Za-z0-9_][A-Za-z0-9_.\\-]*$",
);
});
});
describe("isValidUserOverridesKey (IMP-52 u3)", () => {
it("accepts MDX-stem-style keys actually used in samples/mdx/", () => {
// 03 / 04 / 05 are the wired sample MDXs (vite.config.ts:SAMPLE_MDX_MAP).
expect(isValidUserOverridesKey("03")).toBe(true);
expect(isValidUserOverridesKey("04")).toBe(true);
expect(isValidUserOverridesKey("05")).toBe(true);
// Stage 1 EVIDENCE references 03__DX_BIM... — must round-trip.
expect(isValidUserOverridesKey("03__DX_BIM")).toBe(true);
expect(isValidUserOverridesKey("a-b.c")).toBe(true);
expect(isValidUserOverridesKey("a")).toBe(true);
expect(isValidUserOverridesKey("_leading_underscore")).toBe(true);
expect(isValidUserOverridesKey("9starts_with_digit")).toBe(true);
});
it("rejects empty and whitespace-only keys", () => {
expect(isValidUserOverridesKey("")).toBe(false);
});
it("rejects path-traversal substrings", () => {
// `..` rejected explicitly even if the rest of the regex would allow it
// — `a..b` would otherwise pass the char class.
expect(isValidUserOverridesKey("..")).toBe(false);
expect(isValidUserOverridesKey("a..b")).toBe(false);
expect(isValidUserOverridesKey("../escape")).toBe(false);
});
it("rejects path separators", () => {
expect(isValidUserOverridesKey("a/b")).toBe(false);
expect(isValidUserOverridesKey("a\\b")).toBe(false);
expect(isValidUserOverridesKey("/")).toBe(false);
expect(isValidUserOverridesKey("\\")).toBe(false);
});
it("rejects keys starting with a non-word character", () => {
expect(isValidUserOverridesKey(".hidden")).toBe(false);
expect(isValidUserOverridesKey("-leading-dash")).toBe(false);
});
it("rejects characters outside [A-Za-z0-9_.-]", () => {
expect(isValidUserOverridesKey("a b")).toBe(false);
expect(isValidUserOverridesKey("a:b")).toBe(false);
expect(isValidUserOverridesKey("a*b")).toBe(false);
expect(isValidUserOverridesKey("a%2Fb")).toBe(false);
});
});
describe("userOverridesPath (IMP-52 u3)", () => {
it("resolves <root>/data/user_overrides/<key>.json regardless of OS sep", () => {
const root = path.join("X:", "design_agent");
const got = userOverridesPath(root, "03");
expect(got).toBe(path.join(root, "data", "user_overrides", "03.json"));
});
});
describe("handleGetUserOverrides (IMP-52 u3)", () => {
let tmpRoot: string;
let overridesDir: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "imp52-u3-"));
overridesDir = path.join(tmpRoot, "data", "user_overrides");
fs.mkdirSync(overridesDir, { recursive: true });
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
it("returns false (next chained) when method != GET", () => {
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "PUT", url: "/03" },
res,
tmpRoot,
);
expect(handled).toBe(false);
// Crucial for u4: PUT must reach its own middleware unobstructed.
expect(state.ended).toBe(false);
expect(state.statusCode).toBe(0);
});
it("returns 400 on invalid key (path traversal)", () => {
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "GET", url: "/../escape" },
res,
tmpRoot,
);
expect(handled).toBe(true);
expect(state.statusCode).toBe(400);
expect(JSON.parse(state.body)).toEqual({ error: "invalid key" });
});
it("returns 400 on invalid key (missing key segment)", () => {
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "GET", url: "/" },
res,
tmpRoot,
);
expect(handled).toBe(true);
expect(state.statusCode).toBe(400);
});
it("returns 200 {} on missing file (graceful degrade)", () => {
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "GET", url: "/03" },
res,
tmpRoot,
);
expect(handled).toBe(true);
expect(state.statusCode).toBe(200);
expect(state.body).toBe("{}");
});
it("returns 200 {} on corrupt JSON (graceful degrade)", () => {
fs.writeFileSync(path.join(overridesDir, "03.json"), "{not json", "utf-8");
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "GET", url: "/03" },
res,
tmpRoot,
);
expect(handled).toBe(true);
expect(state.statusCode).toBe(200);
expect(state.body).toBe("{}");
});
it("returns 200 {} when JSON root is not an object", () => {
// Mirrors u1 load() which treats non-object roots as corrupt — covers
// both arrays and primitives so the frontend never receives a shape
// the typed service (u5) can't deserialize.
fs.writeFileSync(
path.join(overridesDir, "arr.json"),
JSON.stringify([1, 2, 3]),
"utf-8",
);
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "GET", url: "/arr" },
res,
tmpRoot,
);
expect(handled).toBe(true);
expect(state.statusCode).toBe(200);
expect(state.body).toBe("{}");
fs.writeFileSync(path.join(overridesDir, "num.json"), "42", "utf-8");
const { res: res2, state: state2 } = makeMockRes();
handleGetUserOverrides({ method: "GET", url: "/num" }, res2, tmpRoot);
expect(state2.statusCode).toBe(200);
expect(state2.body).toBe("{}");
});
it("returns 200 with parsed JSON object on hit", () => {
const payload = {
layout: "two_zone_split",
frames: { "03-1+03-2": "frame_07" },
zone_geometries: {
top: { x: 0.05, y: 0.1, w: 0.9, h: 0.3 },
},
zone_sections: { top: ["03-1", "03-2"] },
};
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify(payload),
"utf-8",
);
const { res, state } = makeMockRes();
const handled = handleGetUserOverrides(
{ method: "GET", url: "/03" },
res,
tmpRoot,
);
expect(handled).toBe(true);
expect(state.statusCode).toBe(200);
expect(state.headers["Content-Type"]).toBe(
"application/json; charset=utf-8",
);
expect(JSON.parse(state.body)).toEqual(payload);
});
it("preserves foreign top-level keys in the response", () => {
// Forward-compat with future axes (e.g., zone_sizes, image_overrides).
// u1 save() preserves them on the disk side; u3 GET must surface them
// so the frontend service (u5) can decide whether to act on them.
const payload = {
layout: "single_zone",
zone_sizes: { top: 0.42 }, // not part of KNOWN_AXES yet
custom_extension: { foo: "bar" },
};
fs.writeFileSync(
path.join(overridesDir, "future.json"),
JSON.stringify(payload),
"utf-8",
);
const { res, state } = makeMockRes();
handleGetUserOverrides({ method: "GET", url: "/future" }, res, tmpRoot);
expect(state.statusCode).toBe(200);
expect(JSON.parse(state.body)).toEqual(payload);
});
it("strips the leading slash and ignores query string when keying", () => {
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify({ layout: "x" }),
"utf-8",
);
const { res, state } = makeMockRes();
handleGetUserOverrides(
{ method: "GET", url: "/03?ts=1747884800" },
res,
tmpRoot,
);
expect(state.statusCode).toBe(200);
expect(JSON.parse(state.body)).toEqual({ layout: "x" });
});
});
// ---------------------------------------------------------------------------
// IMP-52 u4 — PUT endpoint coverage
// ---------------------------------------------------------------------------
describe("KNOWN_USER_OVERRIDES_AXES (IMP-52 u4)", () => {
it("matches the Python KNOWN_AXES tuple in src/user_overrides_io.py", () => {
// The on-disk schema is shared with backend pipeline fallback (u2).
// Any drift here means a PUT could write an axis that the Python
// load() ignores, or vice-versa, silently losing user overrides.
expect(KNOWN_USER_OVERRIDES_AXES).toEqual([
"layout",
"zone_geometries",
"zone_sections",
"frames",
"image_overrides",
]);
});
});
describe("mergeUserOverrides (IMP-52 u4)", () => {
it("only mutates KNOWN_AXES present in partial", () => {
const existing = {
layout: "old",
frames: { "03-1": "frame_01" },
zone_geometries: { top: { x: 0, y: 0, w: 1, h: 0.5 } },
zone_sections: { top: ["03-1"] },
};
const merged = mergeUserOverrides(existing, { layout: "new" });
expect(merged.layout).toBe("new");
// axes not in partial are preserved
expect(merged.frames).toEqual({ "03-1": "frame_01" });
expect(merged.zone_geometries).toEqual({
top: { x: 0, y: 0, w: 1, h: 0.5 },
});
expect(merged.zone_sections).toEqual({ top: ["03-1"] });
});
it("preserves foreign top-level keys in existing", () => {
// Forward-compat: future axes (zone_sizes, schema_version, etc.) on
// disk must survive PUT writes that only touch the 5 in-scope axes.
// `image_overrides` is no longer a foreign key after IMP-51 #79 u2 —
// it joined KNOWN_USER_OVERRIDES_AXES — so we probe with axes that
// are still NOT in the allowlist.
const existing = {
layout: "old",
zone_sizes: { top: 0.42 },
schema_version: 2,
};
const merged = mergeUserOverrides(existing, { layout: "new" });
expect(merged.zone_sizes).toEqual({ top: 0.42 });
expect(merged.schema_version).toBe(2);
});
it("clears axis when partial value is null (explicit clear)", () => {
const existing = { layout: "x", frames: { "03-1": "f01" } };
const merged = mergeUserOverrides(existing, { layout: null });
expect("layout" in merged).toBe(false);
expect(merged.frames).toEqual({ "03-1": "f01" });
});
it("drops non-axis keys in partial (allowlist)", () => {
// PUT payload may carry junk fields (typo, malicious key); allowlist
// ensures only the 5 axes can be written to disk.
const merged = mergeUserOverrides(
{},
{ layout: "x", random_key: "evil", __proto__: "x" } as Record<
string,
unknown
>,
);
expect(merged.layout).toBe("x");
expect("random_key" in merged).toBe(false);
});
it("merges all 5 axes when present in partial", () => {
const merged = mergeUserOverrides(
{},
{
layout: "two_zone_split",
frames: { "03-1+03-2": "frame_07" },
zone_geometries: { top: { x: 0, y: 0, w: 1, h: 0.5 } },
zone_sections: { top: ["03-1", "03-2"] },
image_overrides: { "img-1": { x: 0.1, y: 0.2, w: 0.3, h: 0.25 } },
},
);
expect(Object.keys(merged).sort()).toEqual([
"frames",
"image_overrides",
"layout",
"zone_geometries",
"zone_sections",
]);
});
it("preserves image_overrides when absent from partial (5th axis IMP-51 #79 u2)", () => {
// Sibling axis of layout/frames/zone_geometries/zone_sections: a PUT
// that touches only layout must NOT erase the image_overrides map
// already on disk. Mirrors the partial-merge invariant for the 4
// pre-existing axes.
const existing = {
layout: "old",
image_overrides: { "img-1": { x: 0.1, y: 0.2, w: 0.3, h: 0.25 } },
};
const merged = mergeUserOverrides(existing, { layout: "new" });
expect(merged.image_overrides).toEqual({
"img-1": { x: 0.1, y: 0.2, w: 0.3, h: 0.25 },
});
expect(merged.layout).toBe("new");
});
it("clears image_overrides when partial value is null (explicit clear)", () => {
// Same null-sentinel contract as the 4 sibling axes — `null` removes
// the axis from disk so the next render reverts to baseline (no
// user image position/size override).
const existing = {
layout: "x",
image_overrides: { "img-1": { x: 0.1, y: 0.2, w: 0.3, h: 0.25 } },
};
const merged = mergeUserOverrides(existing, { image_overrides: null });
expect("image_overrides" in merged).toBe(false);
expect(merged.layout).toBe("x");
});
it("does not mutate the existing input", () => {
const existing = { layout: "old", frames: { a: "b" } };
const snapshot = JSON.parse(JSON.stringify(existing));
mergeUserOverrides(existing, { layout: "new", layout_evil: "x" } as Record<
string,
unknown
>);
expect(existing).toEqual(snapshot);
});
});
describe("atomicWriteUserOverrides (IMP-52 u4)", () => {
let tmpRoot: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "imp52-u4-aw-"));
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
it("creates parent dir if missing and writes JSON content", () => {
const filePath = path.join(tmpRoot, "data", "user_overrides", "03.json");
expect(fs.existsSync(path.dirname(filePath))).toBe(false);
atomicWriteUserOverrides(filePath, { layout: "x" });
expect(fs.existsSync(filePath)).toBe(true);
expect(JSON.parse(fs.readFileSync(filePath, "utf-8"))).toEqual({
layout: "x",
});
});
it("leaves no .tmp residue after a successful write", () => {
const filePath = path.join(tmpRoot, "data", "user_overrides", "03.json");
atomicWriteUserOverrides(filePath, { layout: "x" });
const dirContents = fs.readdirSync(path.dirname(filePath));
expect(dirContents).toEqual(["03.json"]);
});
it("overwrites an existing file atomically", () => {
const filePath = path.join(tmpRoot, "data", "user_overrides", "03.json");
atomicWriteUserOverrides(filePath, { layout: "v1" });
atomicWriteUserOverrides(filePath, { layout: "v2" });
expect(JSON.parse(fs.readFileSync(filePath, "utf-8"))).toEqual({
layout: "v2",
});
});
});
// req mock — EventEmitter with method/url + a `send(body)` helper that
// emits the data chunk and then `end`, mirroring the node IncomingMessage
// flow used by vite's dev middlewares.
function makeMockReq(opts: {
method?: string;
url?: string;
}): EventEmitter & { method?: string; url?: string; send: (body: string) => void } {
const ee = new EventEmitter() as EventEmitter & {
method?: string;
url?: string;
send: (body: string) => void;
};
ee.method = opts.method;
ee.url = opts.url;
ee.send = (body: string) => {
if (body.length > 0) ee.emit("data", Buffer.from(body, "utf-8"));
ee.emit("end");
};
return ee;
}
describe("handlePutUserOverrides (IMP-52 u4)", () => {
let tmpRoot: string;
let overridesDir: string;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "imp52-u4-"));
overridesDir = path.join(tmpRoot, "data", "user_overrides");
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
it("returns false (next chained) when method != PUT", () => {
const req = makeMockReq({ method: "GET", url: "/03" });
const { res, state } = makeMockRes();
const handled = handlePutUserOverrides(req, res, tmpRoot);
expect(handled).toBe(false);
expect(state.ended).toBe(false);
});
it("returns 400 on invalid key", () => {
const req = makeMockReq({ method: "PUT", url: "/../escape" });
const { res, state } = makeMockRes();
const handled = handlePutUserOverrides(req, res, tmpRoot);
expect(handled).toBe(true);
expect(state.statusCode).toBe(400);
expect(JSON.parse(state.body)).toEqual({ error: "invalid key" });
});
it("returns 400 on invalid JSON body", () => {
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send("{not json");
expect(state.statusCode).toBe(400);
expect(JSON.parse(state.body)).toEqual({ error: "invalid JSON" });
// file MUST NOT have been created on parse failure
expect(fs.existsSync(path.join(overridesDir, "03.json"))).toBe(false);
});
it("returns 400 when JSON body is an array", () => {
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify([1, 2, 3]));
expect(state.statusCode).toBe(400);
expect(JSON.parse(state.body)).toEqual({
error: "body must be a JSON object",
});
});
it("returns 400 when JSON body is a primitive", () => {
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send("42");
expect(state.statusCode).toBe(400);
expect(JSON.parse(state.body)).toEqual({
error: "body must be a JSON object",
});
});
it("creates the override file on first PUT and returns merged body", () => {
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
const payload = { layout: "two_zone_split" };
req.send(JSON.stringify(payload));
expect(state.statusCode).toBe(200);
expect(state.headers["Content-Type"]).toBe(
"application/json; charset=utf-8",
);
expect(JSON.parse(state.body)).toEqual({ layout: "two_zone_split" });
const filePath = path.join(overridesDir, "03.json");
expect(fs.existsSync(filePath)).toBe(true);
expect(JSON.parse(fs.readFileSync(filePath, "utf-8"))).toEqual({
layout: "two_zone_split",
});
});
it("partial-merges: axes absent from payload are preserved on disk", () => {
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify({
layout: "old",
frames: { "03-1": "frame_01" },
zone_sections: { top: ["03-1"] },
}),
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify({ layout: "new" }));
expect(state.statusCode).toBe(200);
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect(onDisk).toEqual({
layout: "new",
frames: { "03-1": "frame_01" },
zone_sections: { top: ["03-1"] },
});
});
it("preserves foreign top-level keys on disk (forward-compat)", () => {
// `image_overrides` is no longer a foreign key after IMP-51 #79 u2;
// probe with axes that are still NOT in KNOWN_USER_OVERRIDES_AXES.
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "future.json"),
JSON.stringify({
layout: "old",
zone_sizes: { top: 0.42 },
schema_version: 2,
}),
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/future" });
const { res } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify({ layout: "new" }));
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "future.json"), "utf-8"),
);
expect(onDisk.zone_sizes).toEqual({ top: 0.42 });
expect(onDisk.schema_version).toBe(2);
expect(onDisk.layout).toBe("new");
});
it("persists image_overrides partial-merge and preserves sibling axes (IMP-51 #79 u2)", () => {
// 5th axis end-to-end PUT round-trip: writing only image_overrides
// must NOT touch the 4 sibling axes already on disk. Mirrors the
// existing partial-merge test for layout above.
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify({
layout: "two_zone_split",
frames: { "03-1": "frame_01" },
zone_geometries: { top: { x: 0, y: 0, w: 1, h: 0.5 } },
zone_sections: { top: ["03-1"] },
}),
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(
JSON.stringify({
image_overrides: { "img-1": { x: 0.1, y: 0.2, w: 0.3, h: 0.25 } },
}),
);
expect(state.statusCode).toBe(200);
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect(onDisk).toEqual({
layout: "two_zone_split",
frames: { "03-1": "frame_01" },
zone_geometries: { top: { x: 0, y: 0, w: 1, h: 0.5 } },
zone_sections: { top: ["03-1"] },
image_overrides: { "img-1": { x: 0.1, y: 0.2, w: 0.3, h: 0.25 } },
});
});
it("drops non-axis payload keys (allowlist enforced at write)", () => {
fs.mkdirSync(overridesDir, { recursive: true });
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(
JSON.stringify({
layout: "two_zone_split",
random_evil_key: "should not persist",
}),
);
expect(state.statusCode).toBe(200);
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect(onDisk).toEqual({ layout: "two_zone_split" });
expect("random_evil_key" in onDisk).toBe(false);
});
it("clears an axis when payload sets it to null", () => {
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify({ layout: "old", frames: { "03-1": "f01" } }),
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify({ layout: null }));
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect("layout" in onDisk).toBe(false);
expect(onDisk.frames).toEqual({ "03-1": "f01" });
});
it("recovers from corrupt existing file (graceful degrade)", () => {
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "03.json"),
"{this is not JSON",
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify({ layout: "recovered" }));
expect(state.statusCode).toBe(200);
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect(onDisk).toEqual({ layout: "recovered" });
});
it("treats array-rooted existing file as empty (graceful degrade)", () => {
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify(["not", "an", "object"]),
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify({ layout: "recovered" }));
expect(state.statusCode).toBe(200);
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect(onDisk).toEqual({ layout: "recovered" });
});
it("strips the leading slash and ignores query string when keying", () => {
const req = makeMockReq({
method: "PUT",
url: "/03?ts=1747884800",
});
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send(JSON.stringify({ layout: "x" }));
expect(state.statusCode).toBe(200);
expect(fs.existsSync(path.join(overridesDir, "03.json"))).toBe(true);
});
it("accepts an empty body as a no-op partial (no axes mutated)", () => {
fs.mkdirSync(overridesDir, { recursive: true });
fs.writeFileSync(
path.join(overridesDir, "03.json"),
JSON.stringify({ layout: "kept" }),
"utf-8",
);
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
req.send("");
expect(state.statusCode).toBe(200);
const onDisk = JSON.parse(
fs.readFileSync(path.join(overridesDir, "03.json"), "utf-8"),
);
expect(onDisk).toEqual({ layout: "kept" });
});
it("accepts a chunked PUT body (concatenates data events)", () => {
const req = makeMockReq({ method: "PUT", url: "/03" });
const { res, state } = makeMockRes();
expect(handlePutUserOverrides(req, res, tmpRoot)).toBe(true);
const body = JSON.stringify({
layout: "two_zone_split",
frames: { "03-1": "frame_01" },
});
// Emit in two halves to simulate a fragmented HTTP body.
const half = Math.floor(body.length / 2);
req.emit("data", Buffer.from(body.slice(0, half), "utf-8"));
req.emit("data", Buffer.from(body.slice(half), "utf-8"));
req.emit("end");
expect(state.statusCode).toBe(200);
expect(JSON.parse(state.body)).toEqual({
layout: "two_zone_split",
frames: { "03-1": "frame_01" },
});
});
});