Files
MBN_STOCK_WEBVIEW/tests/Web/comparison-import-workflow.test.cjs

172 lines
6.9 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const comparison = require("../../Web/comparison-workflow.js");
const workflow = require("../../Web/comparison-import-workflow.js");
const digest = "A".repeat(64);
const retrievedAt = "2026-07-12T08:00:00+09:00";
const samsung = Object.freeze({
kind: "krx-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930"
});
const nxtSamsung = Object.freeze({
kind: "nxt-stock", market: "kospi", stockName: "삼성전자", stockCode: "005930"
});
const nvidia = Object.freeze({
kind: "world-stock", inputName: "엔비디아", symbol: "NAS@NVDA", nation: "US"
});
function response(pairs = [
{ rowNumber: 1, first: samsung, second: nxtSamsung },
{
rowNumber: 2,
first: { kind: "market-target", target: "Kosdaq" },
second: nvidia
}
]) {
return {
requestId: "import-request-1",
sourceSha256: digest,
sourceRowCount: pairs.length,
retrievedAt,
pairs
};
}
test("strict import response normalizes all four trusted target shapes", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
assert.ok(normalized);
assert.equal(normalized.sourceRowCount, 2);
assert.equal(normalized.pairs[0].pair[0].displayName, "삼성전자");
assert.equal(normalized.pairs[0].pair[1].displayName, "삼성전자(NXT)");
assert.equal(normalized.pairs[1].pair[0], comparison.getMarketTarget("Kosdaq"));
assert.deepEqual(normalized.pairs[1].pair[1], {
...nvidia,
displayName: "엔비디아"
});
assert.ok(Object.isFrozen(normalized));
assert.ok(Object.isFrozen(normalized.pairs));
});
test("response validation rejects correlation, property, digest, row and target tampering", () => {
const valid = response();
assert.equal(workflow.normalizeImportResponse(valid, "wrong-request"), null);
assert.equal(workflow.normalizeImportResponse({ ...valid, extra: true }, valid.requestId), null);
assert.equal(workflow.normalizeImportResponse({ ...valid, sourceSha256: digest.toLowerCase() }, valid.requestId), null);
assert.equal(workflow.normalizeImportResponse({ ...valid, sourceRowCount: 3 }, valid.requestId), null);
assert.equal(workflow.normalizeImportResponse({
...valid,
pairs: [{ ...valid.pairs[0], rowNumber: 2 }, valid.pairs[1]]
}, valid.requestId), null);
assert.equal(workflow.normalizeImportResponse({
...valid,
pairs: [{ ...valid.pairs[0], first: { ...samsung, displayName: "삼성전자" } }, valid.pairs[1]]
}, valid.requestId), null);
assert.equal(workflow.normalizeImportResponse({
...valid,
pairs: [valid.pairs[0], { ...valid.pairs[1], second: { ...nvidia, nation: "KR" } }]
}, valid.requestId), null);
});
test("duplicate source pairs fail closed before local persistence", () => {
const duplicate = response([
{ rowNumber: 1, first: samsung, second: nxtSamsung },
{ rowNumber: 2, first: samsung, second: nxtSamsung }
]);
assert.equal(workflow.normalizeImportResponse(duplicate, duplicate.requestId), null);
});
test("merge appends deterministic records without replacing current local pairs", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
let current = comparison.deleteAllPairs();
current = comparison.appendPair(
current,
"existing",
[comparison.getMarketTarget("Kospi"), samsung]);
const merged = workflow.mergeImport(current, normalized);
assert.deepEqual(merged.pairs.map(value => value.id), [
"existing",
"legacy-compare-aaaaaaaaaaaa-1",
"legacy-compare-aaaaaaaaaaaa-2"
]);
assert.equal(current.pairs.length, 1);
assert.equal(merged.pairs[2].pair[1].symbol, "NAS@NVDA");
});
test("merge rejects duplicate existing pairs and the 500-pair boundary atomically", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
let duplicate = comparison.deleteAllPairs();
duplicate = comparison.appendPair(duplicate, "already-present", normalized.pairs[0].pair);
assert.throws(() => workflow.mergeImport(duplicate, normalized), /existing pair/i);
assert.equal(duplicate.pairs.length, 1);
let full = comparison.deleteAllPairs();
for (let index = 0; index < 499; index += 1) {
full = comparison.appendPair(full, `pair-${index}`, [
comparison.getMarketTarget(index % 2 ? "Kospi" : "Kosdaq"),
{ ...samsung, stockCode: String(index).padStart(6, "0") }
]);
}
assert.throws(() => workflow.mergeImport(full, normalized), /cannot fit/i);
assert.equal(full.pairs.length, 499);
});
test("merge independently rejects unnormalized count and row-order input", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
const empty = comparison.deleteAllPairs();
assert.throws(() => workflow.mergeImport(empty, {
...normalized,
sourceRowCount: 1
}), /cannot fit/i);
assert.throws(() => workflow.mergeImport(empty, {
...normalized,
pairs: [{ ...normalized.pairs[0], rowNumber: 2 }, normalized.pairs[1]]
}), /row order/i);
});
test("one-time marker is schema-versioned, strict and digest-bound", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
const marker = workflow.createMarker(normalized, "2026-07-12T00:00:00.000Z");
assert.deepEqual(marker, {
schemaVersion: 1,
sourceSha256: digest,
importedPairCount: 2,
importedAt: "2026-07-12T00:00:00.000Z"
});
assert.deepEqual(workflow.normalizeMarker(JSON.parse(JSON.stringify(marker))), marker);
assert.equal(workflow.normalizeMarker({ ...marker, schemaVersion: 2 }), null);
assert.equal(workflow.normalizeMarker({ ...marker, sourceSha256: "0".repeat(63) }), null);
assert.equal(workflow.normalizeMarker({ ...marker, extra: true }), null);
});
test("pair list and one-time marker form one strict crash-consistent envelope", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
const merged = workflow.mergeImport(comparison.deleteAllPairs(), normalized);
const marker = workflow.createMarker(normalized, "2026-07-12T00:00:00.000Z");
const envelope = workflow.createStorageEnvelope(merged, marker);
assert.ok(envelope);
assert.equal(envelope.schemaVersion, workflow.storageEnvelopeSchemaVersion);
assert.equal(envelope.pairList.pairs.length, 2);
assert.equal(envelope.legacyImportMarker.sourceSha256, digest);
assert.deepEqual(
workflow.normalizeStorageEnvelope(JSON.parse(JSON.stringify(envelope))),
envelope);
});
test("a legacy-import record without its marker is fail-closed as a split commit", () => {
const normalized = workflow.normalizeImportResponse(response(), "import-request-1");
const merged = workflow.mergeImport(comparison.deleteAllPairs(), normalized);
assert.equal(workflow.createStorageEnvelope(merged, null), null);
assert.equal(workflow.normalizeStorageEnvelope({
schemaVersion: workflow.storageEnvelopeSchemaVersion,
pairList: merged,
legacyImportMarker: null
}), null);
});