Spaces:
Build error
Build error
| import { type Difference } from 'microdiff'; | |
| import { applyDiff } from '@/utils/applyDiff'; | |
| describe('applyDiff', () => { | |
| describe('input validation', () => { | |
| it('should throw error for non-array diffs', () => { | |
| const obj = { test: 'value' }; | |
| expect(() => applyDiff(obj, null as any)).toThrow( | |
| 'Diffs must be an array', | |
| ); | |
| expect(() => applyDiff(obj, 'invalid' as any)).toThrow( | |
| 'Diffs must be an array', | |
| ); | |
| }); | |
| it('should handle empty diffs array', () => { | |
| const obj = { test: 'value' }; | |
| const result = applyDiff(obj, []); | |
| expect(result).toEqual({ test: 'value' }); | |
| expect(result).not.toBe(obj); // Should return a copy | |
| }); | |
| it('should skip invalid diffs', () => { | |
| const obj = { test: 'value' }; | |
| const diffs: Difference[] = [ | |
| null as any, | |
| { type: 'CREATE', path: [], value: 'test' } as any, | |
| { type: 'CREATE', path: ['test'], value: 'updated' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ test: 'updated' }); | |
| }); | |
| }); | |
| describe('CREATE operations', () => { | |
| it('should create new properties in objects', () => { | |
| const obj = { existing: 'value' }; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: ['newProp'], value: 'newValue' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| existing: 'value', | |
| newProp: 'newValue', | |
| }); | |
| }); | |
| it('should create nested properties', () => { | |
| const obj = { level1: { existing: 'value' } }; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: ['level1', 'newProp'], value: 'newValue' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| level1: { | |
| existing: 'value', | |
| newProp: 'newValue', | |
| }, | |
| }); | |
| }); | |
| it('should create array elements', () => { | |
| const obj = ['existing']; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: [1], value: 'newElement' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual(['existing', 'newElement']); | |
| }); | |
| it('should create deeply nested structures with existing intermediate objects', () => { | |
| const obj = { level1: { level2: {} } }; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: ['level1', 'level2', 'prop'], value: 'deep' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| level1: { | |
| level2: { | |
| prop: 'deep', | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('should build deeply nested structures with multiple CREATE operations', () => { | |
| const obj = {}; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: ['level1'], value: {} }, | |
| { type: 'CREATE', path: ['level1', 'level2'], value: {} }, | |
| { type: 'CREATE', path: ['level1', 'level2', 'prop'], value: 'deep' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| level1: { | |
| level2: { | |
| prop: 'deep', | |
| }, | |
| }, | |
| }); | |
| }); | |
| }); | |
| describe('CHANGE operations', () => { | |
| it('should change existing properties', () => { | |
| const obj = { prop: 'oldValue' }; | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['prop'], | |
| oldValue: 'oldValue', | |
| value: 'newValue', | |
| }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ prop: 'newValue' }); | |
| }); | |
| it('should change nested properties', () => { | |
| const obj = { level1: { prop: 'oldValue' } }; | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['level1', 'prop'], | |
| oldValue: 'oldValue', | |
| value: 'newValue', | |
| }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| level1: { prop: 'newValue' }, | |
| }); | |
| }); | |
| it('should change array elements', () => { | |
| const obj = ['old', 'values']; | |
| const diffs: Difference[] = [ | |
| { type: 'CHANGE', path: [0], oldValue: 'old', value: 'new' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual(['new', 'values']); | |
| }); | |
| it('should change complex nested structures', () => { | |
| const obj = { | |
| users: [ | |
| { id: 1, name: 'John' }, | |
| { id: 2, name: 'Jane' }, | |
| ], | |
| }; | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['users', 0, 'name'], | |
| oldValue: 'John', | |
| value: 'Johnny', | |
| }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| users: [ | |
| { id: 1, name: 'Johnny' }, | |
| { id: 2, name: 'Jane' }, | |
| ], | |
| }); | |
| }); | |
| }); | |
| describe('REMOVE operations', () => { | |
| it('should remove properties from objects', () => { | |
| const obj = { keep: 'value', remove: 'toDelete' }; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: ['remove'], oldValue: 'toDelete' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ keep: 'value' }); | |
| }); | |
| it('should remove nested properties', () => { | |
| const obj = { | |
| level1: { | |
| keep: 'value', | |
| remove: 'toDelete', | |
| }, | |
| }; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: ['level1', 'remove'], oldValue: 'toDelete' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| level1: { keep: 'value' }, | |
| }); | |
| }); | |
| it('should remove single array element', () => { | |
| const obj = ['keep1', 'remove', 'keep2']; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: [1], oldValue: 'remove' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual(['keep1', 'keep2']); | |
| }); | |
| it('should remove multiple array elements in correct order', () => { | |
| const obj = ['a', 'b', 'c', 'd', 'e']; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: [1], oldValue: 'b' }, // Remove 'b' | |
| { type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd' | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual(['a', 'c', 'e']); | |
| }); | |
| it('should handle complex array removal scenarios', () => { | |
| const obj = ['a', 'b', 'c', 'd', 'e', 'f']; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: [0], oldValue: 'a' }, // Remove 'a' | |
| { type: 'REMOVE', path: [2], oldValue: 'c' }, // Remove 'c' | |
| { type: 'REMOVE', path: [4], oldValue: 'e' }, // Remove 'e' | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual(['b', 'd', 'f']); | |
| }); | |
| it('should remove from nested arrays', () => { | |
| const obj = { | |
| items: [ | |
| { id: 1, tags: ['tag1', 'tag2', 'tag3'] }, | |
| { id: 2, tags: ['tag4', 'tag5'] }, | |
| ], | |
| }; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: ['items', 0, 'tags', 1], oldValue: 'tag2' }, // Remove 'tag2' | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| items: [ | |
| { id: 1, tags: ['tag1', 'tag3'] }, | |
| { id: 2, tags: ['tag4', 'tag5'] }, | |
| ], | |
| }); | |
| }); | |
| it('should throw error for non-numeric array index', () => { | |
| const obj = ['a', 'b', 'c']; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: ['invalid'], oldValue: 'invalid' }, | |
| ]; | |
| expect(() => applyDiff(obj, diffs)).toThrow( | |
| 'Expected numeric index for array removal, got string', | |
| ); | |
| }); | |
| }); | |
| describe('mixed operations', () => { | |
| it('should apply multiple different operations', () => { | |
| const obj = { | |
| keep: 'value', | |
| change: 'oldValue', | |
| remove: 'toDelete', | |
| nested: { | |
| array: ['a', 'b', 'c'], | |
| }, | |
| }; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: ['newProp'], value: 'newValue' }, | |
| { | |
| type: 'CHANGE', | |
| path: ['change'], | |
| oldValue: 'oldValue', | |
| value: 'newValue', | |
| }, | |
| { type: 'REMOVE', path: ['remove'], oldValue: 'toDelete' }, | |
| { type: 'REMOVE', path: ['nested', 'array', 1], oldValue: 'b' }, // Remove 'b' | |
| { type: 'CREATE', path: ['nested', 'newArray'], value: [1, 2, 3] }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| keep: 'value', | |
| change: 'newValue', | |
| newProp: 'newValue', | |
| nested: { | |
| array: ['a', 'c'], | |
| newArray: [1, 2, 3], | |
| }, | |
| }); | |
| }); | |
| it('should handle operations on the same array', () => { | |
| const obj = ['a', 'b', 'c', 'd']; | |
| const diffs: Difference[] = [ | |
| { type: 'CHANGE', path: [0], oldValue: 'a', value: 'A' }, // Change 'a' to 'A' | |
| { type: 'REMOVE', path: [1], oldValue: 'b' }, // Remove 'b' | |
| { type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd' | |
| { type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd' | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual(['A', 'c']); | |
| }); | |
| }); | |
| describe('error handling', () => { | |
| it('should throw error for invalid diff type', () => { | |
| const obj = { test: 'value' }; | |
| const diffs: Difference[] = [ | |
| { type: 'INVALID' as any, path: ['test'], value: 'newValue' }, | |
| ]; | |
| expect(() => applyDiff(obj, diffs)).toThrow( | |
| 'Unsupported diff type: INVALID', | |
| ); | |
| }); | |
| it('should throw error with path information for invalid operations', () => { | |
| const obj = { test: 'value' }; | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['nonExistent', 'deep', 'path'], | |
| oldValue: 'value', | |
| value: 'value', | |
| }, | |
| ]; | |
| expect(() => applyDiff(obj, diffs)).toThrow( | |
| 'Failed to apply diff at path nonExistent.deep.path', | |
| ); | |
| }); | |
| }); | |
| describe('FORBIDDEN_OBJECT_KEYS protection', () => { | |
| it('should throw error when trying to CREATE forbidden property keys', () => { | |
| const obj = { safe: 'value' }; | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: ['__proto__'], value: 'malicious' }, | |
| ]; | |
| expect(() => applyDiff(obj, diffs)).toThrow( | |
| "Refusing to set forbidden property key '__proto__' on object (prototype pollution protection)", | |
| ); | |
| }); | |
| it('should prevent Unicode escape bypasses of forbidden property keys', () => { | |
| const obj = { safe: 'value' }; | |
| const unicodeBypassAttempts = [ | |
| // __proto__ with Unicode escapes | |
| '__\u0070roto__', // \u0070 = 'p' | |
| '__\u{70}roto__', // ES6 syntax | |
| '__pr\u006fto__', // \u006f = 'o' | |
| '__proto\u005f\u005f', // \u005f = '_' | |
| // constructor with Unicode escapes | |
| 'construc\u0074or', // \u0074 = 't' | |
| 'constr\u0075ctor', // \u0075 = 'u' | |
| '\u0063onstructor', // \u0063 = 'c' | |
| // prototype with Unicode escapes | |
| 'proto\u0074ype', // \u0074 = 't' | |
| 'prototy\u0070e', // \u0070 = 'p' | |
| '\u0070rototype', // \u0070 = 'p' | |
| ]; | |
| unicodeBypassAttempts.forEach((maliciousKey) => { | |
| const diffs: Difference[] = [ | |
| { type: 'CREATE', path: [maliciousKey], value: 'malicious' }, | |
| ]; | |
| expect(() => applyDiff(obj, diffs)).toThrow( | |
| new RegExp( | |
| `Refusing to set forbidden property key.*prototype pollution protection`, | |
| ), | |
| ); | |
| }); | |
| }); | |
| it('should throw error when trying to CHANGE forbidden property keys', () => { | |
| const obj = { safe: 'value' }; | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['constructor'], | |
| oldValue: 'old', | |
| value: 'malicious', | |
| }, | |
| ]; | |
| expect(() => applyDiff(obj, diffs)).toThrow( | |
| "Refusing to set forbidden property key 'constructor' on object (prototype pollution protection)", | |
| ); | |
| }); | |
| it('should silently skip removal of forbidden property keys', () => { | |
| const obj = { | |
| safe: 'value', | |
| normalProp: 'normal', | |
| }; | |
| const diffs: Difference[] = [ | |
| // Try to remove forbidden keys (these should be silently skipped) | |
| { type: 'REMOVE', path: ['__proto__'], oldValue: 'anything' }, | |
| { type: 'REMOVE', path: ['constructor'], oldValue: 'anything' }, | |
| { type: 'REMOVE', path: ['prototype'], oldValue: 'anything' }, | |
| // Remove a normal property (this should work) | |
| { type: 'REMOVE', path: ['safe'], oldValue: 'value' }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| // Only the safe property should be removed, normalProp should remain | |
| expect(result).toEqual({ | |
| normalProp: 'normal', | |
| }); | |
| // Verify safe property was actually removed | |
| expect(result).not.toHaveProperty('safe'); | |
| }); | |
| }); | |
| describe('immutability', () => { | |
| it('should not modify the original object', () => { | |
| const obj = { prop: 'value', nested: { deep: 'value' } }; | |
| const originalObj = JSON.parse(JSON.stringify(obj)); | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['prop'], | |
| oldValue: 'value', | |
| value: 'newValue', | |
| }, | |
| { | |
| type: 'CHANGE', | |
| path: ['nested', 'deep'], | |
| oldValue: 'value', | |
| value: 'newDeepValue', | |
| }, | |
| ]; | |
| applyDiff(obj, diffs); | |
| expect(obj).toEqual(originalObj); | |
| }); | |
| it('should not modify the original array', () => { | |
| const obj = ['a', 'b', 'c']; | |
| const originalObj = [...obj]; | |
| const diffs: Difference[] = [ | |
| { type: 'REMOVE', path: [1], oldValue: 'b' }, | |
| ]; | |
| applyDiff(obj, diffs); | |
| expect(obj).toEqual(originalObj); | |
| }); | |
| it('should handle frozen objects', () => { | |
| const obj = Object.freeze({ prop: 'value' }); | |
| const diffs: Difference[] = [ | |
| { | |
| type: 'CHANGE', | |
| path: ['prop'], | |
| oldValue: 'value', | |
| value: 'newValue', | |
| }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ prop: 'newValue' }); | |
| expect(obj.prop).toBe('value'); // Original unchanged | |
| }); | |
| }); | |
| describe('complex scenarios', () => { | |
| it('should handle workflow-like data structures', () => { | |
| const obj = { | |
| trigger: { | |
| type: 'DATABASE_EVENT', | |
| settings: { table: 'users' }, | |
| }, | |
| steps: [ | |
| { | |
| id: '1', | |
| type: 'CODE', | |
| settings: { code: 'console.log("hello")' }, | |
| }, | |
| { | |
| id: '2', | |
| type: 'EMAIL', | |
| settings: { to: 'test@example.com' }, | |
| }, | |
| ], | |
| }; | |
| const diffs: Difference[] = [ | |
| // Update trigger settings | |
| { | |
| type: 'CHANGE', | |
| path: ['trigger', 'settings', 'table'], | |
| oldValue: 'users', | |
| value: 'contacts', | |
| }, | |
| // Remove first step | |
| { type: 'REMOVE', path: ['steps', 0], oldValue: '1' }, | |
| // Update remaining step | |
| { | |
| type: 'CHANGE', | |
| path: ['steps', 1, 'settings', 'to'], | |
| oldValue: 'test@example.com', | |
| value: 'new@example.com', | |
| }, | |
| // Add new step | |
| { | |
| type: 'CREATE', | |
| path: ['steps', 2], | |
| value: { | |
| id: '3', | |
| type: 'WEBHOOK', | |
| settings: { url: 'https://api.example.com/webhook' }, | |
| }, | |
| }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| trigger: { | |
| type: 'DATABASE_EVENT', | |
| settings: { table: 'contacts' }, | |
| }, | |
| steps: [ | |
| { | |
| id: '2', | |
| type: 'EMAIL', | |
| settings: { to: 'new@example.com' }, | |
| }, | |
| { | |
| id: '3', | |
| type: 'WEBHOOK', | |
| settings: { url: 'https://api.example.com/webhook' }, | |
| }, | |
| ], | |
| }); | |
| }); | |
| it('should handle array with multiple removals and additions', () => { | |
| const obj = { | |
| items: [ | |
| { id: 1, name: 'Item 1' }, | |
| { id: 2, name: 'Item 2' }, | |
| { id: 3, name: 'Item 3' }, | |
| { id: 4, name: 'Item 4' }, | |
| { id: 5, name: 'Item 5' }, | |
| ], | |
| }; | |
| const diffs: Difference[] = [ | |
| // Remove items at indices 1 and 3 (Item 2 and Item 4) | |
| { type: 'REMOVE', path: ['items', 1], oldValue: 'Item 2' }, | |
| { type: 'REMOVE', path: ['items', 3], oldValue: 'Item 4' }, | |
| { type: 'REMOVE', path: ['items', 3], oldValue: 'Item 4' }, | |
| // Update remaining item | |
| { | |
| type: 'CHANGE', | |
| path: ['items', 0, 'name'], | |
| oldValue: 'Item 1', | |
| value: 'Updated Item 1', | |
| }, | |
| // Add new item | |
| { | |
| type: 'CREATE', | |
| path: ['items', 5], | |
| value: { id: 6, name: 'New Item' }, | |
| }, | |
| ]; | |
| const result = applyDiff(obj, diffs); | |
| expect(result).toEqual({ | |
| items: [ | |
| { id: 1, name: 'Updated Item 1' }, | |
| { id: 3, name: 'Item 3' }, | |
| { id: 5, name: 'Item 5' }, | |
| { id: 6, name: 'New Item' }, | |
| ], | |
| }); | |
| }); | |
| }); | |
| }); | |