Files
2026-07-13 13:39:12 +08:00

263 lines
12 KiB
TypeScript

/**
* Tests for G1 — i18n deep-merge EN fallback (src/i18n/request.ts).
*
* Strategy (A): test `deepMergeFallback` directly via its named export.
* This avoids mocking next/headers, next-intl, and dynamic imports while
* achieving ≥90% line coverage of the merge function itself.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { deepMergeFallback } from "../../src/i18n/request.ts";
// ---------------------------------------------------------------------------
// 1. deepMergeFallback — locale-specific key wins (target wins)
// ---------------------------------------------------------------------------
test("deepMergeFallback: locale-specific key is preserved when source has the same key", () => {
const target: Record<string, unknown> = { greeting: "Hola" };
const source: Record<string, unknown> = { greeting: "Hello" };
const result = deepMergeFallback(target, source);
assert.equal(result.greeting, "Hola", "target value must survive when both target and source have the key");
});
test("deepMergeFallback: returns the same target reference (mutates in-place)", () => {
const target: Record<string, unknown> = { a: 1 };
const source: Record<string, unknown> = { b: 2 };
const result = deepMergeFallback(target, source);
assert.equal(result, target, "must return the same object reference");
});
// ---------------------------------------------------------------------------
// 2. deepMergeFallback — missing keys are added from source (fallback wins)
// ---------------------------------------------------------------------------
test("deepMergeFallback: missing key in target is filled from source", () => {
const target: Record<string, unknown> = { localeKey: "Hola" };
const source: Record<string, unknown> = { localeKey: "Hello", fallbackKey: "Fallback EN" };
const result = deepMergeFallback(target, source);
assert.equal(result.localeKey, "Hola", "locale key must win");
assert.equal(result.fallbackKey, "Fallback EN", "fallback key must be added");
});
test("deepMergeFallback: entire namespace missing in target is filled from source", () => {
const target: Record<string, unknown> = { namespace1: { localeKey: "Hola" } };
const source: Record<string, unknown> = {
namespace1: { localeKey: "Hello", fallbackKey: "Fallback EN" },
namespace2: { onlyEn: "Only EN" },
};
const result = deepMergeFallback(target, source);
// locale key wins inside existing namespace
assert.equal((result.namespace1 as Record<string, unknown>).localeKey, "Hola");
// fallback key added inside existing namespace
assert.equal((result.namespace1 as Record<string, unknown>).fallbackKey, "Fallback EN");
// entire namespace from source added to target
assert.deepEqual(result.namespace2, { onlyEn: "Only EN" });
});
// ---------------------------------------------------------------------------
// 3. deepMergeFallback — deep merge on nested objects
// ---------------------------------------------------------------------------
test("deepMergeFallback: deep merge recurses into nested objects", () => {
const target: Record<string, unknown> = {
a: {
b: {
locale: "es value",
},
},
};
const source: Record<string, unknown> = {
a: {
b: {
locale: "en value",
fallback: "en fallback",
},
c: "only in en",
},
};
const result = deepMergeFallback(target, source);
const a = result.a as Record<string, unknown>;
const b = a.b as Record<string, unknown>;
assert.equal(b.locale, "es value", "deeply nested locale key must win");
assert.equal(b.fallback, "en fallback", "deeply nested missing key must be filled from fallback");
assert.equal(a.c, "only in en", "sibling key missing in target must be filled from source");
});
test("deepMergeFallback: three levels deep — target wins at all levels", () => {
const target: Record<string, unknown> = {
l1: { l2: { l3: { key: "locale" } } },
};
const source: Record<string, unknown> = {
l1: { l2: { l3: { key: "fallback", extra: "extra-en" }, l2extra: "l2extra-en" } },
};
const result = deepMergeFallback(target, source);
const l3 = (((result.l1 as Record<string, unknown>).l2 as Record<string, unknown>).l3 as Record<string, unknown>);
assert.equal(l3.key, "locale");
assert.equal(l3.extra, "extra-en");
const l2 = ((result.l1 as Record<string, unknown>).l2 as Record<string, unknown>);
assert.equal(l2.l2extra, "l2extra-en");
});
// ---------------------------------------------------------------------------
// 4. deepMergeFallback — arrays are NOT deep-merged (scalar replacement)
// ---------------------------------------------------------------------------
test("deepMergeFallback: arrays in target are preserved as-is (not merged with source)", () => {
const target: Record<string, unknown> = { items: ["es-a", "es-b"] };
const source: Record<string, unknown> = { items: ["en-a", "en-b", "en-c"] };
const result = deepMergeFallback(target, source);
// Target already has the key, so it wins — array from source is ignored.
assert.deepEqual(result.items, ["es-a", "es-b"]);
});
test("deepMergeFallback: array missing in target is filled from source (not merged)", () => {
const target: Record<string, unknown> = {};
const source: Record<string, unknown> = { tags: ["en-tag-1", "en-tag-2"] };
const result = deepMergeFallback(target, source);
assert.deepEqual(result.tags, ["en-tag-1", "en-tag-2"]);
});
test("deepMergeFallback: source array does NOT overwrite existing object in target", () => {
// Source has an array where target has an object — target wins (existing value kept).
const target: Record<string, unknown> = { data: { nested: "locale" } };
const source: Record<string, unknown> = { data: ["en-1", "en-2"] };
const result = deepMergeFallback(target, source);
// target has "data" defined (as object), so source's array is ignored
assert.deepEqual(result.data, { nested: "locale" });
});
test("deepMergeFallback: source object does NOT overwrite existing array in target", () => {
// Source has an object where target has an array — target array wins (existing value kept).
const target: Record<string, unknown> = { list: ["es-item"] };
const source: Record<string, unknown> = { list: { key: "en-val" } };
const result = deepMergeFallback(target, source);
// target has "list" defined (as array), source is an object — since target[key] !== undefined
// the else-if branch is skipped, so target.list remains the array.
assert.deepEqual(result.list, ["es-item"]);
});
// ---------------------------------------------------------------------------
// 5. deepMergeFallback — null values in source / target
// ---------------------------------------------------------------------------
test("deepMergeFallback: null in source is treated as scalar (fills missing target key)", () => {
const target: Record<string, unknown> = {};
const source: Record<string, unknown> = { nullable: null };
const result = deepMergeFallback(target, source);
assert.equal(result.nullable, null);
});
test("deepMergeFallback: null in target preserves null (source object does not recurse into null)", () => {
const target: Record<string, unknown> = { section: null };
const source: Record<string, unknown> = { section: { key: "en-val" } };
const result = deepMergeFallback(target, source);
// target has "section" defined (as null — not undefined), so source's value is NOT applied.
assert.equal(result.section, null);
});
// ---------------------------------------------------------------------------
// 6. deepMergeFallback — empty objects
// ---------------------------------------------------------------------------
test("deepMergeFallback: empty target gets all keys from source", () => {
const target: Record<string, unknown> = {};
const source: Record<string, unknown> = { a: "A", b: { c: "C" } };
const result = deepMergeFallback(target, source);
assert.equal(result.a, "A");
assert.deepEqual(result.b, { c: "C" });
});
test("deepMergeFallback: empty source leaves target unchanged", () => {
const target: Record<string, unknown> = { x: "locale-x" };
const source: Record<string, unknown> = {};
const result = deepMergeFallback(target, source);
assert.equal(result.x, "locale-x");
assert.equal(Object.keys(result).length, 1);
});
// ---------------------------------------------------------------------------
// 7. Scenario: realistic i18n shape — simulate en.json as fallback
// ---------------------------------------------------------------------------
test("realistic i18n: es locale with partial translations falls back to EN for missing keys", () => {
// Simulates what getRequestConfig does:
// localeMessages = es.json content (partial)
// fallbackMessages = en.json content (complete)
// messages = deepMergeFallback({ ...localeMessages }, fallbackMessages)
const esLocale: Record<string, unknown> = {
namespace1: {
localeKey: "Hola",
// fallbackKey is absent — will come from EN
},
// namespace2 is absent — will come from EN
};
const enFallback: Record<string, unknown> = {
namespace1: {
localeKey: "Hello",
fallbackKey: "Fallback EN",
},
namespace2: {
onlyEn: "Only EN",
},
};
// Simulate what the factory does: shallow copy first so we don't mutate the import cache
const messages = deepMergeFallback({ ...esLocale }, enFallback);
assert.equal(messages.namespace1, esLocale.namespace1, "namespace1 object is the same reference (mutated in-place)");
const ns1 = messages.namespace1 as Record<string, unknown>;
assert.equal(ns1.localeKey, "Hola", "locale-specific key wins");
assert.equal(ns1.fallbackKey, "Fallback EN", "missing key filled from EN fallback");
assert.deepEqual(messages.namespace2, { onlyEn: "Only EN" }, "entirely missing namespace filled from EN");
});
test("realistic i18n: en locale — shallow copy means no mutation of original en object", () => {
// When locale === 'en', the factory returns localeMessages as-is (no merge).
// This test verifies the shallow copy pattern does not mutate the original.
const enLocale: Record<string, unknown> = { key: "EN value" };
const copy = { ...enLocale };
deepMergeFallback(copy, {}); // noop — empty source
assert.equal(enLocale.key, "EN value", "original object must not be mutated");
});
test("realistic i18n: locale invalid → DEFAULT_LOCALE applies; merge still works for non-EN default", () => {
// If DEFAULT_LOCALE were "pt-BR" (not EN), we'd merge pt-BR with EN fallback.
// Simulate this by merging a pt-BR partial object with EN.
const ptBrLocale: Record<string, unknown> = {
common: { save: "Salvar" },
// 'common.cancel' is missing — should come from EN
};
const enFallback: Record<string, unknown> = {
common: { save: "Save", cancel: "Cancel" },
extra: { key: "Extra EN" },
};
const messages = deepMergeFallback({ ...ptBrLocale }, enFallback);
const common = messages.common as Record<string, unknown>;
assert.equal(common.save, "Salvar", "locale key wins in default locale");
assert.equal(common.cancel, "Cancel", "missing key filled from EN");
assert.deepEqual(messages.extra, { key: "Extra EN" }, "missing namespace filled from EN");
});
// ---------------------------------------------------------------------------
// 8. Prototype-pollution guard (js/prototype-pollution-utility regression)
// ---------------------------------------------------------------------------
test("deepMergeFallback: ignores __proto__ / constructor / prototype keys (no prototype pollution)", () => {
const target: Record<string, unknown> = {};
// JSON.parse produces a real own-enumerable __proto__ key (an object literal would
// not), so Object.entries iterates it — exactly the attack vector the guard blocks.
const malicious = JSON.parse(
'{"__proto__":{"polluted":"yes"},"constructor":{"bad":1},"safe":"ok"}'
) as Record<string, unknown>;
deepMergeFallback(target, malicious);
assert.equal(
({} as Record<string, unknown>).polluted,
undefined,
"Object.prototype must not be polluted"
);
assert.equal((target as Record<string, unknown>).polluted, undefined);
assert.equal(target.safe, "ok", "legitimate keys still merge through");
});