Files
MBN_STOCK_WEBVIEW/tests/Web/playlist-entry-edit-safety-app-integration.test.cjs

181 lines
6.5 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = path.resolve(__dirname, "..", "..");
const app = fs.readFileSync(path.join(root, "Web", "app.js"), "utf8");
function functionSource(name, nextName) {
const start = app.indexOf(`function ${name}(`);
const end = app.indexOf(`function ${nextName}(`, start + 1);
assert.ok(start >= 0 && end > start, `${name} must precede ${nextName}`);
return app.slice(start, end).trim();
}
function evaluateFunction(name, nextName, dependencies = {}) {
const names = Object.keys(dependencies);
return new Function(...names,
`return (${functionSource(name, nextName)});`)(...names.map(key => dependencies[key]));
}
function stateStub() {
return {
namedPlaylist: { guardByEntryId: new Map() },
manualFinancialRestore: { entries: new Map() }
};
}
test("every restored workflow boundary blocks direct scene and DB-row edits", () => {
const state = stateStub();
const blockReason = evaluateFunction(
"playlistEntryManualEditBlockReason",
"normalizeManualSceneSelection",
{ state });
const protectedEntries = [
["krx", "legacy-theme-workflow", { namedKrxRestore: {} }],
["nxt", "legacy-theme-workflow", { namedNxtRestore: {} }],
["foreign-stock", "legacy-named-foreign-stock-workflow", {}],
["foreign-index", "legacy-foreign-index-candle-workflow", {}],
["manual-financial", "manual-financial-workflow", {}],
["manual-lists", "legacy-manual-lists-workflow", {}]
];
for (const [id, source, proof] of protectedEntries) {
const entry = Object.freeze({
id,
enabled: true,
operator: Object.freeze({ source, ...proof })
});
assert.equal(blockReason(entry), "trusted-workflow-entry", id);
}
const guarded = Object.freeze({ id: "guarded", enabled: true });
state.namedPlaylist.guardByEntryId.set(guarded.id, Object.freeze({ trusted: false }));
assert.equal(blockReason(guarded), "named-playlist-entry");
const pending = Object.freeze({ id: "pending", enabled: true });
state.manualFinancialRestore.entries.set(pending.id, Object.freeze({ status: "load" }));
assert.equal(blockReason(pending), "manual-restore-entry");
});
test("a frozen ordinary entry receives scene values through an immutable replacement", () => {
const state = stateStub();
const blockReason = evaluateFunction(
"playlistEntryManualEditBlockReason",
"normalizeManualSceneSelection",
{ state });
const normalize = evaluateFunction(
"normalizeManualSceneSelection",
"recreatePlaylistEntryWithSceneSelection");
const sceneAliases = item => item.aliases;
const recreate = evaluateFunction(
"recreatePlaylistEntryWithSceneSelection",
"recreatePlaylistEntryWithLiveSelection",
{
playlistEntryManualEditBlockReason: blockReason,
normalizeManualSceneSelection: normalize,
sceneAliases
});
const entry = Object.freeze({
id: "ordinary",
code: "5001",
aliases: Object.freeze(["5001", "N5001"]),
enabled: true,
fadeDuration: 6,
selection: Object.freeze({
groupCode: "KOSPI",
subject: "OLD",
graphicType: "",
subtype: "",
dataCode: ""
})
});
const replacement = recreate(entry, {
groupCode: " NXT_KOSPI ",
subject: " 삼성전자 ",
graphicType: "CURRENT",
subtype: "",
dataCode: "005930"
}, "N5001", 4);
assert.notEqual(replacement, entry);
assert.equal(entry.code, "5001");
assert.equal(entry.fadeDuration, 6);
assert.equal(entry.selection.subject, "OLD");
assert.equal(replacement.code, "N5001");
assert.equal(replacement.fadeDuration, 4);
assert.equal(replacement.selection.groupCode, "NXT_KOSPI");
assert.equal(replacement.selection.subject, "삼성전자");
assert.equal(Object.isFrozen(replacement.selection), true);
assert.equal(recreate(entry, replacement.selection, "invalid", 4), null);
});
test("live DB selection also replaces only an unprotected ordinary entry", () => {
const state = stateStub();
const blockReason = evaluateFunction(
"playlistEntryManualEditBlockReason",
"normalizeManualSceneSelection",
{ state });
const normalize = evaluateFunction(
"normalizeManualSceneSelection",
"recreatePlaylistEntryWithSceneSelection");
const recreate = evaluateFunction(
"recreatePlaylistEntryWithLiveSelection",
"replacePlaylistEntryAt",
{
playlistEntryManualEditBlockReason: blockReason,
normalizeManualSceneSelection: normalize
});
const ordinary = Object.freeze({
id: "ordinary-live",
code: "5001",
enabled: true,
selection: Object.freeze({
groupCode: "KOSPI_STOCK",
subject: "OLD",
graphicType: "CURRENT",
subtype: "",
dataCode: "000001"
})
});
const selection = {
groupCode: "KOSPI_STOCK",
subject: "삼성전자",
graphicType: "CURRENT",
subtype: "",
dataCode: "005930"
};
const replacement = recreate(ordinary, selection);
assert.notEqual(replacement, ordinary);
assert.equal(ordinary.selection.subject, "OLD");
assert.equal(replacement.selection.subject, "삼성전자");
assert.equal(replacement.subject, "삼성전자");
assert.equal(replacement.dataCode, "005930");
const protectedEntry = Object.freeze({
...ordinary,
id: "protected-live",
operator: Object.freeze({ source: "legacy-stock-workflow" })
});
assert.equal(recreate(protectedEntry, selection), null);
});
test("both edit handlers keep snapshot locking and contain no playlist-entry assignment", () => {
const render = functionSource("renderSceneSelectionForm", "applySceneSelection");
const scene = functionSource("applySceneSelection", "applyCutAliasPreset");
const live = functionSource("applyLiveRowToCurrentCut", "renderLiveTables");
assert.match(render, /playlistEntryManualEditBlockReason\(item\)/u);
assert.match(scene, /if \(!requirePlaylistEditable\(\)\) return;/u);
assert.match(scene, /playlistEntryManualEditBlockReason\(item\)/u);
assert.match(scene, /recreatePlaylistEntryWithSceneSelection/u);
assert.match(scene, /replacePlaylistEntryAt\(state\.currentIndex, replacement\)/u);
assert.match(live, /if \(!requirePlaylistEditable\(\)\) return;/u);
assert.match(live, /playlistEntryManualEditBlockReason\(item\)/u);
assert.match(live, /recreatePlaylistEntryWithLiveSelection/u);
assert.match(live, /replacePlaylistEntryAt\(state\.currentIndex, replacement\)/u);
assert.doesNotMatch(app,
/\bitem\.[A-Za-z_$][A-Za-z0-9_$]*\s*=(?!=)/u);
});