mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-08 03:39:57 +00:00
162 lines
6.8 KiB
TypeScript
162 lines
6.8 KiB
TypeScript
import {describe, expect, it} from "vitest";
|
|
import {objectAssignDeep} from "../lib/util/objectAssignDeep";
|
|
|
|
/** Creates an object with a real own `__proto__`/`constructor`/`prototype` property, like a parsed YAML/JSON payload can. */
|
|
const parse = (json: string): Record<string, unknown> => JSON.parse(json);
|
|
|
|
describe("objectAssignDeep", () => {
|
|
it("Mutates and returns the target", () => {
|
|
const target = {a: 1};
|
|
const result = objectAssignDeep(target, {b: 2});
|
|
|
|
expect(result).toBe(target);
|
|
expect(result).toStrictEqual({a: 1, b: 2});
|
|
});
|
|
|
|
it("Applies sources in order, later ones win", () => {
|
|
expect(objectAssignDeep({}, {a: 1, b: 1}, {b: 2, c: 2})).toStrictEqual({a: 1, b: 2, c: 2});
|
|
});
|
|
|
|
it("Copies keys missing from the target", () => {
|
|
expect(objectAssignDeep({}, {nested: {deep: {value: 1}}})).toStrictEqual({nested: {deep: {value: 1}}});
|
|
});
|
|
|
|
it("Deep merges nested objects present in both", () => {
|
|
const target = {mqtt: {base_topic: "zigbee2mqtt", server: "old"}, advanced: {channel: 11}};
|
|
const result = objectAssignDeep(target, {mqtt: {server: "new"}});
|
|
|
|
expect(result).toStrictEqual({mqtt: {base_topic: "zigbee2mqtt", server: "new"}, advanced: {channel: 11}});
|
|
});
|
|
|
|
it("Replaces nested objects of the target instead of mutating them", () => {
|
|
const nested = {a: 1};
|
|
const target = {nested};
|
|
|
|
objectAssignDeep(target, {nested: {b: 2}});
|
|
|
|
expect(nested).toStrictEqual({a: 1});
|
|
expect(target.nested).not.toBe(nested);
|
|
expect(target.nested).toStrictEqual({a: 1, b: 2});
|
|
});
|
|
|
|
it("Replaces an existing non-object value with a clone of the source object", () => {
|
|
expect(objectAssignDeep({a: 5}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
|
|
expect(objectAssignDeep({a: "str"}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
|
|
expect(objectAssignDeep({a: [1, 2]}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
|
|
// `null` is not `undefined`, so it takes the "existing value" path but is not merged into
|
|
expect(objectAssignDeep({a: null}, {a: {b: 1}})).toStrictEqual({a: {b: 1}});
|
|
});
|
|
|
|
it("Overwrites with null and undefined", () => {
|
|
expect(objectAssignDeep({a: {b: 1}, c: 1}, {a: null, c: null})).toStrictEqual({a: null, c: null});
|
|
expect(objectAssignDeep({a: {b: 1}, c: 1}, {a: undefined, c: undefined})).toStrictEqual({a: undefined, c: undefined});
|
|
});
|
|
|
|
it("Replaces arrays instead of concatenating them", () => {
|
|
expect(objectAssignDeep({a: [1, 2, 3]}, {a: [4]})).toStrictEqual({a: [4]});
|
|
expect(objectAssignDeep({a: [1, 2, 3]}, {a: []})).toStrictEqual({a: []});
|
|
// no existing array either
|
|
expect(objectAssignDeep({a: 1}, {a: [4]})).toStrictEqual({a: [4]});
|
|
expect(objectAssignDeep({}, {a: [4]})).toStrictEqual({a: [4]});
|
|
});
|
|
|
|
it("Clones arrays and the objects nested inside them", () => {
|
|
const source = {a: [{b: 1}, [{c: 2}]]};
|
|
const result = objectAssignDeep({}, source) as typeof source;
|
|
|
|
expect(result).toStrictEqual(source);
|
|
expect(result.a).not.toBe(source.a);
|
|
expect(result.a[0]).not.toBe(source.a[0]);
|
|
expect((result.a[1] as {c: number}[])[0]).not.toBe((source.a[1] as {c: number}[])[0]);
|
|
});
|
|
|
|
it("Breaks all references to the sources", () => {
|
|
const source = {a: {b: {c: 1}}};
|
|
const result = objectAssignDeep({}, source) as typeof source;
|
|
|
|
source.a.b.c = 99;
|
|
|
|
expect(result.a.b.c).toStrictEqual(1);
|
|
});
|
|
|
|
it("Does not mutate the sources", () => {
|
|
const source = {a: {b: 1}};
|
|
|
|
objectAssignDeep({a: {c: 2}}, source);
|
|
|
|
expect(source).toStrictEqual({a: {b: 1}});
|
|
});
|
|
|
|
it("Copies functions and primitives by value/reference", () => {
|
|
const fn = (): number => 1;
|
|
const symbol = Symbol("s");
|
|
const result = objectAssignDeep({}, {fn, symbol, big: 1n, nan: Number.NaN});
|
|
|
|
expect(result.fn).toBe(fn);
|
|
expect(result.symbol).toBe(symbol);
|
|
expect(result.big).toStrictEqual(1n);
|
|
expect(result.nan).toBeNaN();
|
|
});
|
|
|
|
it("Reduces non-plain objects to their own enumerable properties", () => {
|
|
// documented (inherited) behaviour: only own enumerable properties survive, the prototype is lost
|
|
expect(objectAssignDeep({}, {date: new Date(0)})).toStrictEqual({date: {}});
|
|
expect(objectAssignDeep({}, {regexp: /abc/g})).toStrictEqual({regexp: {}});
|
|
expect(objectAssignDeep({}, {map: new Map([["k", 1]])})).toStrictEqual({map: {}});
|
|
expect(objectAssignDeep({}, {set: new Set([1])})).toStrictEqual({set: {}});
|
|
|
|
class Device {
|
|
id = 1;
|
|
get computed(): number {
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
const result = objectAssignDeep({}, {device: new Device()});
|
|
|
|
expect(result.device).toStrictEqual({id: 1});
|
|
expect(result.device).not.toBeInstanceOf(Device);
|
|
});
|
|
|
|
it("Merges deeply nested objects coming from multiple sources", () => {
|
|
const result = objectAssignDeep({}, {a: {b: {c: 1}}}, {a: {b: {d: 2}, e: 3}});
|
|
|
|
expect(result).toStrictEqual({a: {b: {c: 1, d: 2}, e: 3}});
|
|
});
|
|
|
|
it("Never copies keys that could tamper with the prototype chain", () => {
|
|
const result = objectAssignDeep({}, parse('{"__proto__": {"polluted": "yes"}, "constructor": {"x": 1}, "prototype": {"y": 2}, "safe": 1}'));
|
|
|
|
expect(result).toStrictEqual({safe: 1});
|
|
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
|
|
expect(({} as {polluted?: string}).polluted).toBeUndefined();
|
|
});
|
|
|
|
it("Never copies unsafe keys nested inside cloned objects", () => {
|
|
const result = objectAssignDeep({}, {nested: parse('{"__proto__": {"polluted": "yes"}, "constructor": 1, "prototype": 2, "safe": 1}')});
|
|
|
|
expect(result).toStrictEqual({nested: {safe: 1}});
|
|
expect(Object.getPrototypeOf(result.nested)).toBe(Object.prototype);
|
|
});
|
|
|
|
it("Never copies unsafe keys when merging into an existing object", () => {
|
|
const result = objectAssignDeep({nested: {safe: 1}}, {nested: parse('{"__proto__": {"polluted": "yes"}, "other": 2}')});
|
|
|
|
expect(result).toStrictEqual({nested: {safe: 1, other: 2}});
|
|
expect(Object.getPrototypeOf(result.nested)).toBe(Object.prototype);
|
|
});
|
|
|
|
it("Leaves every source untouched when given an empty target", () => {
|
|
const first = {a: {b: 1}};
|
|
const second = {a: {c: 2}};
|
|
const result = objectAssignDeep({}, first, second);
|
|
|
|
expect(result).toStrictEqual({a: {b: 1, c: 2}});
|
|
expect(result).not.toBe(first);
|
|
expect(result).not.toBe(second);
|
|
expect(result.a).not.toBe(first.a);
|
|
expect(first).toStrictEqual({a: {b: 1}});
|
|
expect(second).toStrictEqual({a: {c: 2}});
|
|
});
|
|
});
|