twenty / packages /twenty-shared /src /utils /__tests__ /applyDiff.test.ts
Jules
Initial clean CRM deployment with prebuilt assets
d9494a5
Raw
History Blame Contribute Delete
17.5 kB
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' },
],
});
});
});
});