Skip to content

Commit 7461dee

Browse files
committed
test(commons): add lodash merge parity tests for deepMerge
Cross-referenced against lodash _.merge test suite to close coverage gaps: multi-source array-of-objects merging, indirect prototype pollution via toString.constructor.prototype, function-replaced-by-object across sources, and self-merge safety.
1 parent 7e99c3d commit 7461dee

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

packages/commons/tests/unit/deepMerge.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ describe('Function: deepMerge', () => {
146146
});
147147
});
148148

149+
describe('Multi-source merging', () => {
150+
it('merges three sources with arrays of objects', () => {
151+
// Prepare
152+
const names = { characters: [{ name: 'barney' }, { name: 'fred' }] };
153+
const ages = { characters: [{ age: 36 }, { age: 40 }] };
154+
const heights = {
155+
characters: [{ height: '5\'4"' }, { height: '5\'5"' }],
156+
};
157+
158+
// Act
159+
const result = deepMerge({}, names, ages, heights);
160+
161+
// Assess
162+
expect(result).toEqual({
163+
characters: [
164+
{ name: 'barney', age: 36, height: '5\'4"' },
165+
{ name: 'fred', age: 40, height: '5\'5"' },
166+
],
167+
});
168+
});
169+
});
170+
149171
describe('Array merging (index-based)', () => {
150172
it('merges arrays by index', () => {
151173
// Prepare
@@ -238,6 +260,16 @@ describe('Function: deepMerge', () => {
238260
expect(result.constructor).toBe(Object);
239261
});
240262

263+
it('does not indirectly pollute via toString.constructor.prototype', () => {
264+
// Prepare & Act
265+
deepMerge({}, {
266+
toString: { constructor: { prototype: { polluted: true } } },
267+
} as Record<string, unknown>);
268+
269+
// Assess
270+
expect('polluted' in Function.prototype).toBe(false);
271+
});
272+
241273
it('handles nested __proto__ keys', () => {
242274
// Prepare
243275
const target = { nested: { a: 1 } };
@@ -692,5 +724,31 @@ describe('Function: deepMerge', () => {
692724
expect(result.fn).toBe(fn);
693725
expect((result.fn as () => number)()).toBe(42);
694726
});
727+
728+
it('replaces function target value with object from later source', () => {
729+
// Prepare
730+
const fn = () => 42;
731+
const source1 = { a: fn } as Record<string, unknown>;
732+
const source2 = { a: { b: 2 } };
733+
734+
// Act
735+
const result = deepMerge({}, source1, source2);
736+
737+
// Assess - object source overwrites function, not merged into it
738+
expect(result).toEqual({ a: { b: 2 } });
739+
expect('b' in (source1.a as object)).toBe(false);
740+
});
741+
742+
it('handles self-merge without infinite loop', () => {
743+
// Prepare
744+
const object: Record<string, unknown> = { a: 1, b: { c: 2 } };
745+
746+
// Act
747+
const result = deepMerge(object, object);
748+
749+
// Assess - should be a no-op
750+
expect(result).toBe(object);
751+
expect(result).toEqual({ a: 1, b: { c: 2 } });
752+
});
695753
});
696754
});

0 commit comments

Comments
 (0)