/** * 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 = { greeting: "Hola" }; const source: Record = { 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 = { a: 1 }; const source: Record = { 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 = { localeKey: "Hola" }; const source: Record = { 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 = { namespace1: { localeKey: "Hola" } }; const source: Record = { 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).localeKey, "Hola"); // fallback key added inside existing namespace assert.equal((result.namespace1 as Record).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 = { a: { b: { locale: "es value", }, }, }; const source: Record = { a: { b: { locale: "en value", fallback: "en fallback", }, c: "only in en", }, }; const result = deepMergeFallback(target, source); const a = result.a as Record; const b = a.b as Record; 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 = { l1: { l2: { l3: { key: "locale" } } }, }; const source: Record = { l1: { l2: { l3: { key: "fallback", extra: "extra-en" }, l2extra: "l2extra-en" } }, }; const result = deepMergeFallback(target, source); const l3 = (((result.l1 as Record).l2 as Record).l3 as Record); assert.equal(l3.key, "locale"); assert.equal(l3.extra, "extra-en"); const l2 = ((result.l1 as Record).l2 as Record); 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 = { items: ["es-a", "es-b"] }; const source: Record = { 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 = {}; const source: Record = { 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 = { data: { nested: "locale" } }; const source: Record = { 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 = { list: ["es-item"] }; const source: Record = { 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 = {}; const source: Record = { 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 = { section: null }; const source: Record = { 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 = {}; const source: Record = { 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 = { x: "locale-x" }; const source: Record = {}; 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 = { namespace1: { localeKey: "Hola", // fallbackKey is absent — will come from EN }, // namespace2 is absent — will come from EN }; const enFallback: Record = { 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; 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 = { 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 = { common: { save: "Salvar" }, // 'common.cancel' is missing — should come from EN }; const enFallback: Record = { common: { save: "Save", cancel: "Cancel" }, extra: { key: "Extra EN" }, }; const messages = deepMergeFallback({ ...ptBrLocale }, enFallback); const common = messages.common as Record; 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 = {}; // 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; deepMergeFallback(target, malicious); assert.equal( ({} as Record).polluted, undefined, "Object.prototype must not be polluted" ); assert.equal((target as Record).polluted, undefined); assert.equal(target.safe, "ok", "legitimate keys still merge through"); });