| |
| |
| |
| |
| |
|
|
| import { describe, it, expect } from 'vitest'; |
| import { customDeepMerge } from './deepMerge.js'; |
| import { MergeStrategy } from '../config/settingsSchema.js'; |
|
|
| describe('customDeepMerge', () => { |
| it('should merge simple objects', () => { |
| const target = { a: 1, b: 2 }; |
| const source = { b: 3, c: 4 }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: 1, b: 3, c: 4 }); |
| }); |
|
|
| it('should merge nested objects', () => { |
| const target = { a: { x: 1 }, b: 2 }; |
| const source = { a: { y: 2 }, c: 3 }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: { x: 1, y: 2 }, b: 2, c: 3 }); |
| }); |
|
|
| it('should replace arrays by default', () => { |
| const target = { a: [1, 2] }; |
| const source = { a: [3, 4] }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: [3, 4] }); |
| }); |
|
|
| it('should concatenate arrays with CONCAT strategy', () => { |
| const target = { a: [1, 2] }; |
| const source = { a: [3, 4] }; |
| const getMergeStrategy = (path: string[]) => |
| path.join('.') === 'a' ? MergeStrategy.CONCAT : undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: [1, 2, 3, 4] }); |
| }); |
|
|
| it('should union arrays with UNION strategy', () => { |
| const target = { a: [1, 2, 3] }; |
| const source = { a: [3, 4, 5] }; |
| const getMergeStrategy = (path: string[]) => |
| path.join('.') === 'a' ? MergeStrategy.UNION : undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: [1, 2, 3, 4, 5] }); |
| }); |
|
|
| it('should shallow merge objects with SHALLOW_MERGE strategy', () => { |
| const target = { a: { x: 1, y: 1 } }; |
| const source = { a: { y: 2, z: 2 } }; |
| const getMergeStrategy = (path: string[]) => |
| path.join('.') === 'a' ? MergeStrategy.SHALLOW_MERGE : undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| |
| expect(result).toEqual({ a: { x: 1, y: 2, z: 2 } }); |
| }); |
|
|
| it('should handle multiple source objects', () => { |
| const target = { a: 1 }; |
| const source1 = { b: 2 }; |
| const source2 = { c: 3 }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source1, source2); |
| expect(result).toEqual({ a: 1, b: 2, c: 3 }); |
| }); |
|
|
| it('should return an empty object if no sources are provided', () => { |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy); |
| expect(result).toEqual({}); |
| }); |
|
|
| it('should return a deep copy of the first source if only one is provided', () => { |
| const target = { a: { b: 1 } }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target); |
| expect(result).toEqual(target); |
| expect(result).not.toBe(target); |
| }); |
|
|
| it('should not mutate the original source objects', () => { |
| const target = { a: { x: 1 }, b: [1, 2] }; |
| const source = { a: { y: 2 }, b: [3, 4] }; |
| const originalTarget = JSON.parse(JSON.stringify(target)); |
| const originalSource = JSON.parse(JSON.stringify(source)); |
| const getMergeStrategy = () => undefined; |
|
|
| customDeepMerge(getMergeStrategy, target, source); |
|
|
| expect(target).toEqual(originalTarget); |
| expect(source).toEqual(originalSource); |
| }); |
|
|
| it('should not mutate sources when merging multiple levels deep', () => { |
| const s1 = { data: { common: { val: 'from s1' }, s1_only: true } }; |
| const s2 = { data: { common: { val: 'from s2' }, s2_only: true } }; |
| const s1_original = JSON.parse(JSON.stringify(s1)); |
| const s2_original = JSON.parse(JSON.stringify(s2)); |
|
|
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, s1, s2); |
|
|
| expect(s1).toEqual(s1_original); |
| expect(s2).toEqual(s2_original); |
| expect(result).toEqual({ |
| data: { |
| common: { val: 'from s2' }, |
| s1_only: true, |
| s2_only: true, |
| }, |
| }); |
| }); |
|
|
| it('should handle complex nested strategies', () => { |
| const target = { |
| level1: { |
| arr1: [1, 2], |
| arr2: [1, 2], |
| obj1: { a: 1 }, |
| }, |
| }; |
| const source = { |
| level1: { |
| arr1: [3, 4], |
| arr2: [2, 3], |
| obj1: { b: 2 }, |
| }, |
| }; |
| const getMergeStrategy = (path: string[]) => { |
| const p = path.join('.'); |
| if (p === 'level1.arr1') return MergeStrategy.CONCAT; |
| if (p === 'level1.arr2') return MergeStrategy.UNION; |
| if (p === 'level1.obj1') return MergeStrategy.SHALLOW_MERGE; |
| return undefined; |
| }; |
|
|
| const result = customDeepMerge(getMergeStrategy, target, source); |
|
|
| expect(result).toEqual({ |
| level1: { |
| arr1: [1, 2, 3, 4], |
| arr2: [1, 2, 3], |
| obj1: { a: 1, b: 2 }, |
| }, |
| }); |
| }); |
|
|
| it('should not pollute the prototype', () => { |
| const maliciousSource = JSON.parse('{"__proto__": {"polluted1": "true"}}'); |
| const getMergeStrategy = () => undefined; |
| let result = customDeepMerge(getMergeStrategy, {}, maliciousSource); |
|
|
| expect(result).toEqual({}); |
| |
| expect(({} as any).polluted1).toBeUndefined(); |
|
|
| const maliciousSource2 = JSON.parse( |
| '{"constructor": {"prototype": {"polluted2": "true"}}}', |
| ); |
| result = customDeepMerge(getMergeStrategy, {}, maliciousSource2); |
| expect(result).toEqual({}); |
| |
| expect(({} as any).polluted2).toBeUndefined(); |
|
|
| const maliciousSource3 = JSON.parse('{"prototype": {"polluted3": "true"}}'); |
| result = customDeepMerge(getMergeStrategy, {}, maliciousSource3); |
| expect(result).toEqual({}); |
| |
| expect(({} as any).polluted3).toBeUndefined(); |
| }); |
|
|
| it('should use additionalProperties merge strategy for dynamic properties', () => { |
| |
| const target = { |
| hooks: { |
| BeforeTool: [{ command: 'user-hook-1' }, { command: 'user-hook-2' }], |
| disabled: ['hook-a'], |
| }, |
| }; |
| const source = { |
| hooks: { |
| BeforeTool: [{ command: 'workspace-hook-1' }], |
| disabled: ['hook-b'], |
| }, |
| }; |
|
|
| |
| const getMergeStrategy = (path: string[]) => { |
| const p = path.join('.'); |
| |
| if (p === 'hooks.disabled') return MergeStrategy.UNION; |
| |
| if (p === 'hooks.BeforeTool') return MergeStrategy.CONCAT; |
| return undefined; |
| }; |
|
|
| const result = customDeepMerge(getMergeStrategy, target, source); |
|
|
| |
| |
| expect((result as any)['hooks']['BeforeTool']).toEqual([ |
| { command: 'user-hook-1' }, |
| { command: 'user-hook-2' }, |
| { command: 'workspace-hook-1' }, |
| ]); |
| |
| |
| expect((result as any)['hooks']['disabled']).toEqual(['hook-a', 'hook-b']); |
| }); |
|
|
| it('should overwrite primitive with object', () => { |
| const target = { a: 1 }; |
| const source = { a: { b: 2 } }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: { b: 2 } }); |
| }); |
|
|
| it('should overwrite object with primitive', () => { |
| const target = { a: { b: 2 } }; |
| const source = { a: 1 }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: 1 }); |
| }); |
|
|
| it('should not overwrite with undefined', () => { |
| const target = { a: 1 }; |
| const source = { a: undefined }; |
| const getMergeStrategy = () => undefined; |
| const result = customDeepMerge(getMergeStrategy, target, source); |
| expect(result).toEqual({ a: 1 }); |
| }); |
| }); |
|
|