@novo-learning/novo-sdk
Version:
SDK for the Novolanguage Speech Analysis API
151 lines (145 loc) • 3.7 kB
text/typescript
import { RecursivePartial } from '../type-helpers';
import { mergeDeep } from './merge-deep';
const mergeSource = Object.freeze({
level1: true,
sibling: 10,
inner: {
level2: true,
sibling: 100,
},
});
type MergeSource = {
level1: boolean;
sibling: number;
extra?: boolean;
inner: { level2: boolean; sibling: number; extra?: boolean };
};
describe('Merge deep merges properties', () => {
const cases: [{ source: MergeSource; changes: RecursivePartial<MergeSource>[] }, MergeSource][] = [
[{ source: mergeSource, changes: [] }, mergeSource],
[{ source: mergeSource, changes: [{}] }, mergeSource],
[
{ source: mergeSource, changes: [{ sibling: 20 }] },
{
level1: true,
sibling: 20,
inner: {
level2: true,
sibling: 100,
},
},
],
[
{ source: mergeSource, changes: [{ inner: { sibling: 200 } }] },
{
level1: true,
sibling: 10,
inner: {
level2: true,
sibling: 200,
},
},
],
[
{ source: mergeSource, changes: [{ inner: { sibling: 200 }, sibling: 20 }] },
{
level1: true,
sibling: 20,
inner: {
level2: true,
sibling: 200,
},
},
],
[
{ source: mergeSource, changes: [{ inner: { sibling: 300 } }, { sibling: 30 }] },
{
level1: true,
sibling: 30,
inner: {
level2: true,
sibling: 300,
},
},
],
[
{ source: mergeSource, changes: [{ extra: true }] },
{
...mergeSource,
extra: true,
},
],
[
{ source: mergeSource, changes: [{ inner: { extra: true } }] },
{
...mergeSource,
inner: { ...mergeSource.inner, extra: true },
},
],
];
test.each(cases)('merge deep', ({ source, changes }, expectedResult) => {
const target = structuredClone(source);
const result = mergeDeep(target, ...(changes as Partial<MergeSource>[]));
expect(result).toEqual(expectedResult);
expect(target).toEqual(expectedResult);
});
// Tests against prototype pollution
// eslint-disable-next-line @typescript-eslint/ban-types
const casesPrototype: [{ source: MergeSource; changes: unknown[] }, MergeSource][] = [
[
{
source: mergeSource,
changes: [
{
constructor: (): void => {
console.error('');
},
},
],
},
mergeSource,
],
[{ source: mergeSource, changes: [{ __proto__: { test: true } }] }, mergeSource],
[{ source: mergeSource, changes: [{ prototype: { test: true } }] }, mergeSource],
[
{ source: mergeSource, changes: [{ sibling: 20, __proto__: { test: true } }] },
{
level1: true,
sibling: 20,
inner: {
level2: true,
sibling: 100,
},
},
],
[
{
source: mergeSource,
changes: [
{
sibling: 20,
inner: {
constructor: (): void => {
console.error('');
},
},
},
],
},
{
level1: true,
sibling: 20,
inner: {
level2: true,
sibling: 100,
},
},
],
];
test.each(casesPrototype)('merge deep does not pollute prototype', ({ source, changes }, expectedResult) => {
const target = structuredClone(source);
const result = mergeDeep(target, ...(changes as Partial<MergeSource>[]));
expect(result).toEqual(expectedResult);
expect(target).toEqual(expectedResult);
});
});