diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 54426ca2..49bcabb9 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,14 +1,13 @@ name: ci on: - push: - branches: - - master - pull_request: - workflow_dispatch: + push: + branches: + - master + - next + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@master - with: - min-node-version: 14 + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9d..e0b41f66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage **/.DS_Store **/._* @@ -11,3 +11,4 @@ coverage.* **/.vs **/.vscode **/.idea +dist diff --git a/API.md b/API.md index ae0a7c68..4f9b16ea 100755 --- a/API.md +++ b/API.md @@ -1,12 +1,12 @@ - ### Object **hoek** provides several helpful methods for objects and arrays. #### clone(obj, [options]) -Clones an object or an array. A *deep copy* is made (duplicates everything, including values that are +Clones an object or an array. A _deep copy_ is made (duplicates everything, including values that are objects, as well as non-enumerable properties) where: + - `obj` - the object to be cloned. - `options` - optional settings: - `symbols` - clone symbol properties. Defaults to `true`. @@ -15,54 +15,53 @@ objects, as well as non-enumerable properties) where: - `true` to shallow copy all object properties. Used to shallow copy an object with non-enumerable properties and prototype; ```javascript - const nestedObj = { - w: /^something$/ig, - x: { - a: [1, 2, 3], - b: 123456, - c: new Date() - }, - y: 'y', - z: new Date() - }; + w: /^something$/gi, + x: { + a: [1, 2, 3], + b: 123456, + c: new Date(), + }, + y: 'y', + z: new Date(), +}; const copy = Hoek.clone(nestedObj); copy.x.b = 100; -console.log(copy.y); // results in 'y' +console.log(copy.y); // results in 'y' console.log(nestedObj.x.b); // results in 123456 -console.log(copy.x.b); // results in 100 +console.log(copy.x.b); // results in 100 ``` Clones an object or array excluding some keys which are shallow copied: ```javascript - const nestedObj = { - w: /^something$/ig, - x: { - a: [1, 2, 3], - b: 123456, - c: new Date() - }, - y: 'y', - z: new Date() - }; + w: /^something$/gi, + x: { + a: [1, 2, 3], + b: 123456, + c: new Date(), + }, + y: 'y', + z: new Date(), +}; const copy = Hoek.clone(nestedObj, { shallow: ['x'] }); copy.x.b = 100; -console.log(copy.y); // results in 'y' +console.log(copy.y); // results in 'y' console.log(nestedObj.x.b); // results in 100 -console.log(copy.x.b); // results in 100 +console.log(copy.x.b); // results in 100 ``` #### merge(target, source, [options]) Merge all the properties of source into target where: + - `target` - the object onto which the properties of `source` are copied to. - `source` - the object copied onto `target`. - `options` - optional settings: @@ -75,27 +74,26 @@ Merge all the properties of source into target where: Note that source wins in conflict, and by default null and undefined from source are applied. Merge is destructive where the target is modified. For non destructive merge, use `applyToDefaults`. - ```javascript +const target = { a: 1, b: 2 }; +const source = { a: 0, c: 5 }; +const source2 = { a: null, c: 5 }; -const target = {a: 1, b : 2}; -const source = {a: 0, c: 5}; -const source2 = {a: null, c: 5}; - -Hoek.merge(target, source); // results in {a: 0, b: 2, c: 5} -Hoek.merge(target, source2); // results in {a: null, b: 2, c: 5} -Hoek.merge(target, source2, { nullOverride: false} ); // results in {a: 1, b: 2, c: 5} +Hoek.merge(target, source); // results in {a: 0, b: 2, c: 5} +Hoek.merge(target, source2); // results in {a: null, b: 2, c: 5} +Hoek.merge(target, source2, { nullOverride: false }); // results in {a: 1, b: 2, c: 5} const targetArray = [1, 2, 3]; const sourceArray = [4, 5]; -Hoek.merge(targetArray, sourceArray); // results in [1, 2, 3, 4, 5] +Hoek.merge(targetArray, sourceArray); // results in [1, 2, 3, 4, 5] Hoek.merge(targetArray, sourceArray, { mergeArrays: false }); // results in [4, 5] ``` #### applyToDefaults(defaults, source, [options]) Apply source to a copy of the defaults where: + - `defaults` - the default object to clone and then apply `source` onto. - `source` - the object applied to the `defaults`. - `options` - optional settings: @@ -104,8 +102,7 @@ Apply source to a copy of the defaults where: - `shallow` - an array of dot-separated or array-based key paths to shallow copy values in `source`. ```javascript - -const defaults = { host: "localhost", port: 8000 }; +const defaults = { host: 'localhost', port: 8000 }; const source = { port: 8080 }; const config = Hoek.applyToDefaults(defaults, source); // results in { host: "localhost", port: 8080 } @@ -114,8 +111,7 @@ const config = Hoek.applyToDefaults(defaults, source); // results in { host: "lo Apply source with a null value to a copy of the defaults ```javascript - -const defaults = { host: "localhost", port: 8000 }; +const defaults = { host: 'localhost', port: 8000 }; const source = { host: null, port: 8080 }; const config = Hoek.applyToDefaults(defaults, source, { nullOverride: true }); // results in { host: null, port: 8080 } @@ -124,27 +120,27 @@ const config = Hoek.applyToDefaults(defaults, source, { nullOverride: true }); / Apply source to a copy of the defaults where the shallow keys specified in the last parameter are shallow copied from source instead of merged ```javascript - const defaults = { db: { server: { - host: "localhost", - port: 8000 + host: 'localhost', + port: 8000, }, - name: 'example' - } + name: 'example', + }, }; const source = { db: { server: { port: 8080 } } }; -const config = Hoek.applyToDefaults(defaults, source, { shallow: ['db.server'] }); // results in { db: { server: { port: 8080 }, name: 'example' } } -const config = Hoek.applyToDefaults(defaults, source, { shallow: [['db', 'server']] }); // results in { db: { server: { port: 8080 }, name: 'example' } } +const config = Hoek.applyToDefaults(defaults, source, { shallow: ['db.server'] }); // results in { db: { server: { port: 8080 }, name: 'example' } } +const config = Hoek.applyToDefaults(defaults, source, { shallow: [['db', 'server']] }); // results in { db: { server: { port: 8080 }, name: 'example' } } ``` #### deepEqual(a, b, [options]) Performs a deep comparison of the two values including support for circular dependencies, prototype, and enumerable properties, where: + - `a` - the first value. - `b` - the second value. - `options` - optional settings: @@ -168,13 +164,13 @@ Hoek.deepEqual(Object.create(null), {}); //results in false #### intersect(array1, array2, [options]) Find the common unique items betwee two arrays where: + - `array1` - the first array. - `array2` - the second array. - `options` - optional settings: - `first` - if `true`, return only the first intersecting item. Defaults to `false`. ```javascript - const array1 = [1, 2, 3]; const array2 = [1, 4, 5]; @@ -184,6 +180,7 @@ const newArray = Hoek.intersect(array1, array2); // results in [1] #### contain(ref, values, [options]) Tests if the reference value contains the provided values where: + - `ref` - the reference string, array, or object. - `values` - a single or array of values to find within the `ref` value. If `ref` is an object, `values` can be a key name, an array of key names, or an object with key-value pairs to compare. @@ -198,9 +195,9 @@ Note: comparing a string to overlapping values will result in failed comparison Also, if an object key's value does not match the provided value, `false` is returned even when `part` is specified. ```javascript -Hoek.contain('aaa', 'a', { only: true }); // true -Hoek.contain([{ a: 1 }], [{ a: 1 }], { deep: true }); // true -Hoek.contain([1, 2, 2], [1, 2], { once: true }); // false +Hoek.contain('aaa', 'a', { only: true }); // true +Hoek.contain([{ a: 1 }], [{ a: 1 }], { deep: true }); // true +Hoek.contain([1, 2, 2], [1, 2], { once: true }); // false Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 4 }, { part: true }); // true ``` @@ -209,7 +206,6 @@ Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 4 }, { part: true }); // true Flatten an array ```javascript - const array = [1, [2, 3]]; const flattenedArray = Hoek.flatten(array); // results in [1, 2, 3] @@ -243,14 +239,13 @@ A chain including negative numbers will work like negative indices on an array. If chain is `null`, `undefined` or `false`, the object itself will be returned. ```javascript - const chain = 'a.b.c'; -const obj = {a : {b : { c : 1}}}; +const obj = { a: { b: { c: 1 } } }; Hoek.reach(obj, chain); // returns 1 const chain = ['a', 'b', -1]; -const obj = {a : {b : [2,3,6]}}; +const obj = { a: { b: [2, 3, 6] } }; Hoek.reach(obj, chain); // returns 6 ``` @@ -280,7 +275,7 @@ error message) without the need to worry about invalid conversion. ```javascript const a = {}; a.b = a; -Hoek.stringify(a); // Returns '[Cannot display object: Converting circular structure to JSON]' +Hoek.stringify(a); // Returns '[Cannot display object: Converting circular structure to JSON]' ``` ### Bench @@ -293,21 +288,19 @@ human-readable timestamps. More accurate for benchmarking or internal timers. **hoek** provides convenient methods for escaping html characters. The escaped characters are as followed: ```javascript - internals.htmlEscaped = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', - '`': '`' + '`': '`', }; ``` #### escapeHtml(string) ```javascript - const string = ' hey '; const escapedString = Hoek.escapeHtml(string); // returns <html> hey </html> ``` @@ -317,8 +310,7 @@ const escapedString = Hoek.escapeHtml(string); // returns <html> hey </ Escape attribute value for use in HTTP header ```javascript - -const a = Hoek.escapeHeaderAttribute('I said "go w\\o me"'); //returns I said \"go w\\o me\" +const a = Hoek.escapeHeaderAttribute('I said "go w\\o me"'); //returns I said \"go w\\o me\" ``` #### escapeJson(string) @@ -326,9 +318,8 @@ const a = Hoek.escapeHeaderAttribute('I said "go w\\o me"'); //returns I said \ Unicode escapes the characters `<`, `>`, and `&` to prevent mime-sniffing older browsers mistaking JSON as HTML, and escapes line and paragraph separators for JSONP and script contexts. ```javascript - const lineSeparator = String.fromCharCode(0x2028); -const a = Hoek.escapeJson('I said '); - expect(encoded).to.equal('<script>alert(1)</script>'); + expect(encoded).toEqual('<script>alert(1)</script>'); }); it('encodes < and > as named characters', () => { - const encoded = Hoek.escapeHtml(''); - expect(encoded).to.equal('\\u003cscript\\u003ealert(1)\\u003c/script\\u003e'); + expect(encoded).toEqual('\\u003cscript\\u003ealert(1)\\u003c/script\\u003e'); }); }); diff --git a/test/exports.js b/test/exports.js deleted file mode 100644 index 4cdf1f9f..00000000 --- a/test/exports.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict'; - -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); -const Hoek = require('..'); - - -const internals = {}; - - -const { before, describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; - - -internals.uncapitalize = (str) => { - - // Used capitalized classes like "Bench" and "Error" - - return str[0].toLowerCase() + str.slice(1); -}; - - -describe('ESM', () => { - - let esmHoek; - - before(async () => { - - esmHoek = await import('@hapi/hoek'); - }); - - it('exposes all methods and classes as named imports', () => { - - const rlist = Object.keys(Hoek).sort(); - const mlist = Object.keys(esmHoek).sort(); - - expect(mlist).to.equal(rlist); - expect(esmHoek).to.equal(Hoek); - expect(esmHoek).to.not.shallow.equal(Hoek); - }); - - it('exposes all methods and classes as direct imports', async () => { - - const exported = await Promise.all(Object.keys(Hoek).map((item) => import(`@hapi/hoek/${internals.uncapitalize(item)}`))); - const defaulted = exported.map((item) => item.default); - - expect(defaulted).to.equal(Object.values(Hoek)); - }); - - it('can call an exported destructured method', () => { - - const { assert } = esmHoek; - - expect(() => assert(false, 'oops')).to.throw('oops'); - }); - - it('exports identical classes as CJS', async () => { - - const error = (await import(`@hapi/hoek/assertError`)).default; - - expect(error).to.shallow.equal(Hoek.AssertError); - expect(error).to.shallow.equal(esmHoek.AssertError); - }); -}); - - -describe('package', () => { - - it('exports all methods and classes', () => { - - const exported = Object.keys(Hoek).map((item) => require(`@hapi/hoek/${internals.uncapitalize(item)}`)); - - expect(exported).to.equal(Object.values(Hoek)); - }); - - it('can call an exported method', () => { - - const assert = require(`@hapi/hoek/assert`); - - expect(() => assert(false, 'oops')).to.throw('oops'); - }); - - it('does not export unlisted modules', () => { - - expect(() => require('@hapi/hoek/types')).to.throw(); - expect(() => require('@hapi/hoek/utils')).to.throw(); - }); -}); diff --git a/test/index.js b/test/index.js deleted file mode 100755 index 4b45b842..00000000 --- a/test/index.js +++ /dev/null @@ -1,2523 +0,0 @@ -'use strict'; - -const Util = require('util'); - -const Code = require('@hapi/code'); -const Hoek = require('..'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; - - -const nestedObj = { - v: [7, 8, 9], - w: /^something$/igm, - x: { - a: [1, 2, 3], - b: 123456, - c: new Date(), - d: /hi/igm, - e: /hello/ - }, - y: 'y', - z: new Date(1378775452757) -}; - - -describe('merge()', () => { - - it('deep copies source items', () => { - - const sym1 = Symbol('1'); - const sym2 = Symbol('2'); - const sym3 = Symbol('3'); - - const target = { - b: 3, - d: [], - [sym1]: true, - [sym3]: true - }; - - const source = { - c: { - d: 1 - }, - d: [{ e: 1 }], - [sym2]: true, - [sym3]: false - }; - - Hoek.merge(target, source); - expect(target.c).to.not.shallow.equal(source.c); - expect(target.c).to.equal(source.c); - expect(target.d).to.not.shallow.equal(source.d); - expect(target.d[0]).to.not.shallow.equal(source.d[0]); - expect(target.d).to.equal(source.d); - - expect(target[sym1]).to.be.true(); - expect(target[sym2]).to.be.true(); - expect(target[sym3]).to.be.false(); - }); - - it('deep copies source items without symbols', () => { - - const sym1 = Symbol('1'); - const sym2 = Symbol('2'); - const sym3 = Symbol('3'); - - const target = { - b: 3, - d: [], - [sym1]: true, - [sym3]: true - }; - - const source = { - c: { - d: 1 - }, - d: [{ e: 1 }], - [sym2]: true, - [sym3]: false - }; - - Hoek.merge(target, source, { symbols: false }); - expect(target.c).to.not.shallow.equal(source.c); - expect(target.c).to.equal(source.c); - expect(target.d).to.not.shallow.equal(source.d); - expect(target.d[0]).to.not.shallow.equal(source.d[0]); - expect(target.d).to.equal(source.d); - - expect(target[sym1]).to.be.true(); - expect(target[sym2]).to.not.exist(); - expect(target[sym3]).to.be.true(); - }); - - it('merges array over an object', () => { - - const a = { - x: ['n', 'm'] - }; - - const b = { - x: { - n: '1', - m: '2' - } - }; - - Hoek.merge(b, a); - expect(a.x[0]).to.equal('n'); - expect(a.x.n).to.not.exist(); - }); - - it('merges object over an array', () => { - - const a = { - x: ['n', 'm'] - }; - - const b = { - x: { - n: '1', - m: '2' - } - }; - - Hoek.merge(a, b); - expect(a.x.n).to.equal('1'); - expect(a.x[0]).to.not.exist(); - }); - - it('merges from null prototype objects', () => { - - const a = {}; - - const b = Object.create(null); - b.x = true; - - Hoek.merge(a, b); - expect(a.x).to.be.true(); - }); - - it('skips non-enumerable properties', () => { - - const a = { x: 0 }; - - const b = {}; - Object.defineProperty(b, 'x', { - enumerable: false, - value: 1 - }); - - Hoek.merge(a, b); - expect(a.x).to.equal(0); - }); - - it('does not throw if source is null', () => { - - const a = {}; - const b = null; - let c = null; - - expect(() => { - - c = Hoek.merge(a, b); - }).to.not.throw(); - - expect(c).to.equal(a); - }); - - it('does not throw if source is undefined', () => { - - const a = {}; - const b = undefined; - let c = null; - - expect(() => { - - c = Hoek.merge(a, b); - }).to.not.throw(); - - expect(c).to.equal(a); - }); - - it('throws if source is not an object', () => { - - expect(() => { - - const a = {}; - const b = 0; - - Hoek.merge(a, b); - }).to.throw('Invalid source value: must be null, undefined, or an object'); - }); - - it('throws if target is not an object', () => { - - expect(() => { - - const a = 0; - const b = {}; - - Hoek.merge(a, b); - }).to.throw('Invalid target value: must be an object'); - }); - - it('throws if target is not an array and source is', () => { - - expect(() => { - - const a = {}; - const b = [1, 2]; - - Hoek.merge(a, b); - }).to.throw('Cannot merge array onto an object'); - }); - - it('returns the same object when merging arrays', () => { - - const a = []; - const b = [1, 2]; - - expect(Hoek.merge(a, b)).to.equal(a); - }); - - it('combines an empty object with a non-empty object', () => { - - const a = {}; - const b = nestedObj; - - const c = Hoek.merge(a, b); - expect(a).to.equal(b); - expect(c).to.equal(b); - }); - - it('overrides values in target', () => { - - const a = { x: 1, y: 2, z: 3, v: 5, t: 'test', s: 1, m: 'abc' }; - const b = { x: null, z: 4, v: 0, t: { u: 6 }, s: undefined, m: '123' }; - - const c = Hoek.merge(a, b); - expect(c.x).to.equal(null); - expect(c.y).to.equal(2); - expect(c.z).to.equal(4); - expect(c.v).to.equal(0); - expect(c.m).to.equal('123'); - expect(c.t).to.equal({ u: 6 }); - expect(c.s).to.equal(undefined); - }); - - it('overrides values in target (flip)', () => { - - const a = { x: 1, y: 2, z: 3, v: 5, t: 'test', s: 1, m: 'abc' }; - const b = { x: null, z: 4, v: 0, t: { u: 6 }, s: undefined, m: '123' }; - - const d = Hoek.merge(b, a); - expect(d.x).to.equal(1); - expect(d.y).to.equal(2); - expect(d.z).to.equal(3); - expect(d.v).to.equal(5); - expect(d.m).to.equal('abc'); - expect(d.t).to.equal('test'); - expect(d.s).to.equal(1); - }); - - it('retains Date properties', () => { - - const a = { x: new Date(1378776452757) }; - - const b = Hoek.merge({}, a); - expect(a.x.getTime()).to.equal(b.x.getTime()); - }); - - it('retains Date properties when merging keys', () => { - - const a = { x: new Date(1378776452757) }; - - const b = Hoek.merge({ x: {} }, a); - expect(a.x.getTime()).to.equal(b.x.getTime()); - }); - - it('overrides Buffer', () => { - - const a = { x: Buffer.from('abc') }; - - Hoek.merge({ x: {} }, a); - expect(a.x.toString()).to.equal('abc'); - }); - - it('overrides RegExp', () => { - - const a = { x: /test/ }; - - Hoek.merge({ x: {} }, a); - expect(a.x).to.equal(/test/); - }); - - it('overrides Symbol properties', () => { - - const sym = Symbol(); - const a = { [sym]: 1 }; - - Hoek.merge({ [sym]: {} }, a); - expect(a[sym]).to.equal(1); - }); - - it('skips __proto__', () => { - - const a = '{ "ok": "value", "__proto__": { "test": "value" } }'; - - const b = Hoek.merge({}, JSON.parse(a)); - expect(b).to.equal({ ok: 'value' }); - expect(b.test).to.equal(undefined); - }); -}); - -describe('applyToDefaults()', () => { - - it('throws when target is null', () => { - - expect(() => { - - Hoek.applyToDefaults(null, {}); - }).to.throw('Invalid defaults value: must be an object'); - }); - - it('throws when options are invalid', () => { - - expect(() => { - - Hoek.applyToDefaults({}, {}, false); - }).to.throw('Invalid options: must be an object'); - - expect(() => { - - Hoek.applyToDefaults({}, {}, 123); - }).to.throw('Invalid options: must be an object'); - }); - - it('returns null if source is false', () => { - - const defaults = { - a: 1, - b: 2, - c: { - d: 3, - e: [5, 6] - }, - f: 6, - g: 'test' - }; - - const result = Hoek.applyToDefaults(defaults, false); - expect(result).to.equal(null); - }); - - it('returns null if source is null', () => { - - const defaults = { - a: 1, - b: 2, - c: { - d: 3, - e: [5, 6] - }, - f: 6, - g: 'test' - }; - - const result = Hoek.applyToDefaults(defaults, null); - expect(result).to.equal(null); - }); - - it('returns null if source is undefined', () => { - - const defaults = { - a: 1, - b: 2, - c: { - d: 3, - e: [5, 6] - }, - f: 6, - g: 'test' - }; - - const result = Hoek.applyToDefaults(defaults, undefined); - expect(result).to.equal(null); - }); - - it('returns a copy of defaults if source is true', () => { - - const defaults = { - a: 1, - b: 2, - c: { - d: 3, - e: [5, 6] - }, - f: 6, - g: 'test' - }; - - const result = Hoek.applyToDefaults(defaults, true); - expect(result).to.equal(defaults); - }); - - it('applies object to defaults', () => { - - const defaults = { - a: 1, - b: 2, - c: { - d: 3, - e: [5, 6] - }, - f: 6, - g: 'test' - }; - - const obj = { - a: null, - c: { - e: [4] - }, - f: 0, - g: { - h: 5 - } - }; - - const result = Hoek.applyToDefaults(defaults, obj); - expect(result.c.e).to.equal([4]); - expect(result.a).to.equal(1); - expect(result.b).to.equal(2); - expect(result.f).to.equal(0); - expect(result.g).to.equal({ h: 5 }); - }); - - it('applies object to defaults with null', () => { - - const defaults = { - a: 1, - b: 2, - c: { - d: 3, - e: [5, 6] - }, - f: 6, - g: 'test' - }; - - const obj = { - a: null, - c: { - e: [4] - }, - f: 0, - g: { - h: 5 - } - }; - - const result = Hoek.applyToDefaults(defaults, obj, { nullOverride: true }); - expect(result.c.e).to.equal([4]); - expect(result.a).to.equal(null); - expect(result.b).to.equal(2); - expect(result.f).to.equal(0); - expect(result.g).to.equal({ h: 5 }); - }); - - it('shallow copies the listed keys from source without merging', () => { - - const defaults = { - a: { - b: 5, - e: 3 - }, - c: { - d: 7, - g: 1 - } - }; - - const source = { - a: { - b: 4 - }, - c: { - d: 6, - f: 7 - } - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['a'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { d: 6, g: 1, f: 7 } }); - expect(merged.a).to.shallow.equal(source.a); - expect(merged.a).to.not.equal(defaults.a); - expect(merged.c).to.not.equal(source.c); - expect(merged.c).to.not.equal(defaults.c); - }); - - it('shallow copies the nested keys (override)', () => { - - const defaults = { - a: { - b: 5 - }, - c: { - d: 7, - g: 1 - } - }; - - const source = { - a: { - b: 4 - }, - c: { - d: 6, - g: { - h: 8 - } - } - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { d: 6, g: { h: 8 } } }); - expect(merged.c.g).to.shallow.equal(source.c.g); - }); - - it('shallow copies the nested keys (missing)', () => { - - const defaults = { - a: { - b: 5 - } - }; - - const source = { - a: { - b: 4 - }, - c: { - g: { - h: 8 - } - } - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { g: { h: 8 } } }); - expect(merged.c.g).to.shallow.equal(source.c.g); - }); - - it('shallow copies the nested keys (override)', () => { - - const defaults = { - a: { - b: 5 - }, - c: { - g: { - d: 7 - } - } - }; - - const source = { - a: { - b: 4 - }, - c: { - g: { - h: 8 - } - } - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { g: { h: 8 } } }); - expect(merged.c.g).to.shallow.equal(source.c.g); - }); - - it('shallow copies the nested keys (deeper)', () => { - - const defaults = { - a: { - b: 5 - } - }; - - const source = { - a: { - b: 4 - }, - c: { - g: { - r: { - h: 8 - } - } - } - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g.r'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { g: { r: { h: 8 } } } }); - expect(merged.c.g.r).to.shallow.equal(source.c.g.r); - }); - - it('shallow copies the nested keys (not present)', () => { - - const defaults = { - a: { - b: 5 - } - }; - - const source = { - a: { - b: 4 - }, - c: { - g: { - r: { - h: 8 - } - } - } - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['x.y'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { g: { r: { h: 8 } } } }); - }); - - it('shallow copies the nested keys (non-object)', () => { - - const defaults = { - // All falsy values: - _undefined: { - a: 1 - }, - _null: { - a: 2 - }, - _false: { - a: 3 - }, - _emptyString: { - a: 4 - }, - _zero: { - a: 5 - }, - _NaN: { - a: 6 - }, - // Other non-object values: - _string: { - a: 7 - }, - _number: { - a: 8 - }, - _true: { - a: 9 - }, - _function: { - a: 10 - } - }; - - const source = { - _undefined: undefined, - _null: null, - _false: false, - _emptyString: '', - _zero: 0, - _NaN: NaN, - _string: 'foo', - _number: 42, - _true: true, - _function: () => {} - }; - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: [ - '_undefined.a', - '_null.a', - '_false.a', - '_emptyString.a', - '_zero.a', - '_NaN.a', - '_string.a', - '_number.a', - '_true.a', - '_function.a' - ] }); - expect(merged).to.equal({ - _undefined: { a: 1 }, - _null: { a: 2 }, - _false: false, - _emptyString: '', - _zero: 0, - _NaN: NaN, - _string: 'foo', - _number: 42, - _true: true, - _function: source._function - }); - }); - - it('shallow copies the listed keys in the defaults', () => { - - const defaults = { - a: { - b: 1 - } - }; - - const merged = Hoek.applyToDefaults(defaults, {}, { shallow: ['a'] }); - expect(merged.a).to.shallow.equal(defaults.a); - }); - - it('shallow copies the listed keys in the defaults (true)', () => { - - const defaults = { - a: { - b: 1 - } - }; - - const merged = Hoek.applyToDefaults(defaults, true, { shallow: ['a'] }); - expect(merged.a).to.shallow.equal(defaults.a); - }); - - it('returns null on false', () => { - - const defaults = { - a: { - b: 1 - } - }; - - const merged = Hoek.applyToDefaults(defaults, false, { shallow: ['a'] }); - expect(merged).to.equal(null); - }); - - it('handles missing shallow key in defaults', () => { - - const defaults = { - a: { - b: 1 - } - }; - - const options = { - a: { - b: 4 - }, - c: { - d: 2 - } - }; - - const merged = Hoek.applyToDefaults(defaults, options, { shallow: ['c'] }); - expect(merged).to.equal({ a: { b: 4 }, c: { d: 2 } }); - expect(merged.c).to.shallow.equal(options.c); - - expect(Hoek.applyToDefaults(defaults, true, { shallow: ['c'] })).to.equal({ a: { b: 1 } }); - }); - - it('throws on missing defaults', () => { - - expect(() => Hoek.applyToDefaults(null, {}, { shallow: ['a'] })).to.throw('Invalid defaults value: must be an object'); - }); - - it('throws on invalid defaults', () => { - - expect(() => Hoek.applyToDefaults('abc', {}, { shallow: ['a'] })).to.throw('Invalid defaults value: must be an object'); - }); - - it('throws on invalid source', () => { - - expect(() => Hoek.applyToDefaults({}, 'abc', { shallow: ['a'] })).to.throw('Invalid source value: must be true, falsy or an object'); - }); - - it('throws on missing keys', () => { - - expect(() => Hoek.applyToDefaults({}, true, { shallow: 123 })).to.throw('Invalid keys'); - }); - - it('handles array keys', () => { - - const sym = Symbol(); - - const defaults = { - a: { - b: 5, - e: 3 - }, - c: { - d: 7, - [sym]: { - f: 9 - } - } - }; - - const options = { - a: { - b: 4 - }, - c: { - d: 6, - [sym]: { - g: 1 - } - } - }; - - const merged = Hoek.applyToDefaults(defaults, options, { shallow: [['c', sym]] }); - expect(merged).to.equal({ a: { b: 4, e: 3 }, c: { d: 6, [sym]: { g: 1 } } }); - expect(merged.c[sym]).to.shallow.equal(options.c[sym]); - }); - - it('does not modify shallow entries in source', () => { - - const defaults = { - a: { - b: 5 - } - }; - - const source = {}; - - Object.defineProperty(source, 'a', { value: { b: 4 } }); - - const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['a'] }); - expect(merged).to.equal({ a: { b: 4 } }); - expect(merged.a).to.shallow.equal(source.a); - expect(merged.a).to.not.equal(defaults.a); - }); - - it('should respect nullOverride when shallow is used', () => { - - const defaults = { host: 'localhost', port: 8000 }; - const source = { host: null, port: 8080 }; - - const result = Hoek.applyToDefaults(defaults, source, { nullOverride: true, shallow: [] }); - expect(result.host).to.equal(null); - expect(result.port).to.equal(8080); - }); -}); - -describe('deepEqual()', () => { - - it('compares identical references', () => { - - const x = {}; - expect(Hoek.deepEqual(x, x)).to.be.true(); - }); - - it('compares simple values', () => { - - expect(Hoek.deepEqual('x', 'x')).to.be.true(); - expect(Hoek.deepEqual('x', 'y')).to.be.false(); - expect(Hoek.deepEqual('x1', 'x')).to.be.false(); - expect(Hoek.deepEqual(-0, +0)).to.be.false(); - expect(Hoek.deepEqual(-0, -0)).to.be.true(); - expect(Hoek.deepEqual(+0, +0)).to.be.true(); - expect(Hoek.deepEqual(+0, -0)).to.be.false(); - expect(Hoek.deepEqual(1, 1)).to.be.true(); - expect(Hoek.deepEqual(0, 0)).to.be.true(); - expect(Hoek.deepEqual(-1, 1)).to.be.false(); - expect(Hoek.deepEqual(NaN, 0)).to.be.false(); - expect(Hoek.deepEqual(NaN, NaN)).to.be.true(); - }); - - it('compares different types', () => { - - expect(Hoek.deepEqual([], 5, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(5, [], { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, null, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(null, {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual('abc', {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, 'abc', { prototype: false })).to.be.false(); - }); - - it('compares empty structures', () => { - - expect(Hoek.deepEqual([], [])).to.be.true(); - expect(Hoek.deepEqual({}, {})).to.be.true(); - expect(Hoek.deepEqual([], {})).to.be.false(); - expect(Hoek.deepEqual([], {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, [], { prototype: false })).to.be.false(); - }); - - it('compares empty arguments object', () => { - - const compare = function () { - - expect(Hoek.deepEqual([], arguments)).to.be.false(); // eslint-disable-line prefer-rest-params - }; - - compare(); - }); - - it('compares empty arguments objects', () => { - - const compare = function () { - - const arg1 = arguments; // eslint-disable-line prefer-rest-params - - const inner = function () { - - // callee is not supported in strict mode, was previously false becuse callee was different - expect(Hoek.deepEqual(arg1, arguments)).to.be.true(); // eslint-disable-line prefer-rest-params - }; - - inner(); - }; - - compare(); - }); - - it('compares symbol object properties', () => { - - const sym = Symbol(); - - const ne = {}; - Object.defineProperty(ne, sym, { value: true }); - - expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: { c: true } })).to.be.true(); - expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: { c: false } })).to.be.false(); - expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: true })).to.be.false(); - expect(Hoek.deepEqual({ [sym]: undefined }, { [sym]: undefined })).to.be.true(); - expect(Hoek.deepEqual({ [sym]: undefined }, {})).to.be.false(); - expect(Hoek.deepEqual({}, { [sym]: undefined })).to.be.false(); - - expect(Hoek.deepEqual({}, ne)).to.be.true(); - expect(Hoek.deepEqual(ne, {})).to.be.true(); - expect(Hoek.deepEqual({ [sym]: true }, ne)).to.be.false(); - expect(Hoek.deepEqual(ne, { [sym]: true })).to.be.false(); - expect(Hoek.deepEqual(ne, { [Symbol()]: undefined })).to.be.false(); - - expect(Hoek.deepEqual({ [sym]: true }, { [sym]: true })).to.be.true(); - expect(Hoek.deepEqual({ [sym]: true }, {})).to.be.false(); - expect(Hoek.deepEqual({ [sym]: true }, {}, { symbols: false })).to.be.true(); - }); - - it('compares dates', () => { - - expect(Hoek.deepEqual(new Date(2015, 1, 1), new Date('2015/02/01'))).to.be.true(); - expect(Hoek.deepEqual(new Date(100), new Date(101))).to.be.false(); - expect(Hoek.deepEqual(new Date(), {})).to.be.false(); - expect(Hoek.deepEqual(new Date(2015, 1, 1), new Date('2015/02/01'), { prototype: false })).to.be.true(); - expect(Hoek.deepEqual(new Date(), {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, new Date(), { prototype: false })).to.be.false(); - }); - - it('compares regular expressions', () => { - - expect(Hoek.deepEqual(/\s/, new RegExp('\\\s'))).to.be.true(); - expect(Hoek.deepEqual(/\s/g, /\s/g)).to.be.true(); - expect(Hoek.deepEqual(/a/, {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(/\s/g, /\s/i)).to.be.false(); - expect(Hoek.deepEqual(/a/g, /b/g)).to.be.false(); - }); - - it('compares errors', () => { - - expect(Hoek.deepEqual(new Error('a'), new Error('a'))).to.be.true(); - expect(Hoek.deepEqual(new Error('a'), new Error('b'))).to.be.false(); - - expect(Hoek.deepEqual(new Error('a'), new TypeError('a'))).to.be.false(); - expect(Hoek.deepEqual(new Error('a'), new TypeError('a'), { prototype: false })).to.be.false(); - - expect(Hoek.deepEqual(new Error(), {})).to.be.false(); - expect(Hoek.deepEqual(new Error(), {}, { prototype: false })).to.be.false(); - - expect(Hoek.deepEqual({}, new Error())).to.be.false(); - expect(Hoek.deepEqual({}, new Error(), { prototype: false })).to.be.false(); - - const error = new Error('a'); - expect(Hoek.deepEqual(Hoek.clone(error), error)).to.be.true(); - expect(Hoek.deepEqual(Hoek.clone(error), error, { prototype: false })).to.be.true(); - }); - - it('compares arrays', () => { - - expect(Hoek.deepEqual([[1]], [[1]])).to.be.true(); - expect(Hoek.deepEqual([1, 2, 3], [1, 2, 3])).to.be.true(); - expect(Hoek.deepEqual([1, 2, 3], [1, 3, 2])).to.be.false(); - expect(Hoek.deepEqual([1, 2, 3], [1, 2])).to.be.false(); - expect(Hoek.deepEqual([1], [1])).to.be.true(); - const item1 = { key: 'value1' }; - const item2 = { key: 'value2' }; - expect(Hoek.deepEqual([item1, item1], [item1, item2])).to.be.false(); - }); - - it('compares sets', () => { - - expect(Hoek.deepEqual(new Set(), new Set())).to.be.true(); - expect(Hoek.deepEqual(new Set([1]), new Set([1]))).to.be.true(); - expect(Hoek.deepEqual(new Set([]), new Set([]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([1, 2, 3]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([3, 2, 1]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([1, 2, 4]))).to.be.false(); - expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([1, 2]))).to.be.false(); - expect(Hoek.deepEqual(new Set([1, 2, 1]), new Set([1, 2]))).to.be.true(); - expect(Hoek.deepEqual(new Set([+0]), new Set([-0]))).to.be.true(); - expect(Hoek.deepEqual(new Set([NaN]), new Set([NaN]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, {}]), new Set([1, {}]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, {}]), new Set([{}, 1]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, {}, {}]), new Set([{}, 1, {}]))).to.be.true(); - expect(Hoek.deepEqual(new Set([1, { a: 1 }]), new Set([{ a: 0 }, 1]))).to.be.false(); - expect(Hoek.deepEqual(new WeakSet(), new WeakSet())).to.be.true(); - const obj = {}; - expect(Hoek.deepEqual(new WeakSet([obj]), new WeakSet())).to.be.true(); - expect(Hoek.deepEqual(new WeakSet(), new Set()), { prototype: false }).to.be.false(); - - const sets = [new Set(), new Set()].map((set) => { - - set.modified = true; - return set; - }); - expect(Hoek.deepEqual(sets[0], sets[1])).to.be.true(); - expect(Hoek.deepEqual(sets[0], new Set())).to.be.false(); - }); - - it('compares extended sets', () => { - - class PrivateSet extends Set { - - has() { - - throw new Error('not allowed'); - } - } - - const entries = ['a', undefined]; - expect(Hoek.deepEqual(new PrivateSet(), new PrivateSet())).to.be.true(); - expect(Hoek.deepEqual(new PrivateSet(entries), new PrivateSet(entries))).to.be.true(); - expect(Hoek.deepEqual(new PrivateSet(entries), new Set(entries), { prototype: false })).to.be.true(); - expect(Hoek.deepEqual(new PrivateSet(entries), new Set(entries), { prototype: true })).to.be.false(); - expect(Hoek.deepEqual(new PrivateSet(), new PrivateSet(entries))).to.be.false(); - expect(Hoek.deepEqual(new PrivateSet(entries), new PrivateSet())).to.be.false(); - - class LockableSet extends Set { - - constructor(values, locked = true) { - - super(values); - this.locked = locked; - } - - has() { - - if (this.locked) { - throw new Error('not allowed'); - } - } - } - - expect(Hoek.deepEqual(new LockableSet(), new LockableSet())).to.be.true(); - expect(Hoek.deepEqual(new LockableSet(entries), new LockableSet(entries))).to.be.true(); - expect(Hoek.deepEqual(new LockableSet(entries, false), new LockableSet(entries, false))).to.be.true(); - expect(Hoek.deepEqual(new LockableSet(entries, true), new LockableSet(entries, false))).to.be.false(); - expect(Hoek.deepEqual(new LockableSet(entries, false), new LockableSet(entries, true))).to.be.false(); - expect(Hoek.deepEqual(new LockableSet(entries), new Set(entries), { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(new LockableSet(entries), new PrivateSet(entries), { prototype: false })).to.be.false(); - }); - - it('compares maps', () => { - - const item1 = { key: 'value1' }; - const item2 = { key: 'value2' }; - expect(Hoek.deepEqual(new Map(), new Map())).to.be.true(); - expect(Hoek.deepEqual(new Map([[1, {}]]), new Map([[1, {}]]))).to.be.true(); - expect(Hoek.deepEqual(new Map([[1, item1]]), new Map([[1, item1]]))).to.be.true(); - expect(Hoek.deepEqual(new Map([[1, item1]]), new Map([[1, item2]]))).to.be.false(); - expect(Hoek.deepEqual(new Map([[1, undefined]]), new Map([[1, undefined]]))).to.be.true(); - expect(Hoek.deepEqual(new Map([[1, undefined]]), new Map([[2, undefined]]))).to.be.false(); - expect(Hoek.deepEqual(new Map([[1, {}]]), new Map([[1, {}], [2, {}]]))).to.be.false(); - expect(Hoek.deepEqual(new Map([[item1, 1]]), new Map([[item1, 1]]))).to.be.true(); - expect(Hoek.deepEqual(new Map([[{}, 1]]), new Map([[{}, 1]]))).to.be.false(); - expect(Hoek.deepEqual(new WeakMap(), new WeakMap())).to.be.true(); - expect(Hoek.deepEqual(new WeakMap([[item1, 1]]), new WeakMap())).to.be.true(); - expect(Hoek.deepEqual(new WeakMap(), new Map()), { prototype: false }).to.be.false(); - - const maps = [new Map(), new Map()].map((map) => { - - map.modified = true; - return map; - }); - expect(Hoek.deepEqual(maps[0], maps[1])).to.be.true(); - expect(Hoek.deepEqual(maps[0], new Map())).to.be.false(); - }); - - it('compares extended maps', () => { - - class PrivateMap extends Map { - - get() { - - throw new Error('not allowed'); - } - } - - const entries = [['a', 1], ['b', undefined]]; - expect(Hoek.deepEqual(new PrivateMap(), new PrivateMap())).to.be.true(); - expect(Hoek.deepEqual(new PrivateMap(entries), new PrivateMap(entries))).to.be.true(); - expect(Hoek.deepEqual(new PrivateMap(entries), new Map(entries), { prototype: false })).to.be.true(); - expect(Hoek.deepEqual(new PrivateMap(entries), new Map(entries), { prototype: true })).to.be.false(); - expect(Hoek.deepEqual(new PrivateMap(), new PrivateMap(entries))).to.be.false(); - expect(Hoek.deepEqual(new PrivateMap(entries), new PrivateMap())).to.be.false(); - - class LockableMap extends Map { - - constructor(kvs, locked = true) { - - super(kvs); - this.locked = locked; - } - - get() { - - if (this.locked) { - throw new Error('not allowed'); - } - } - } - - expect(Hoek.deepEqual(new LockableMap(), new LockableMap())).to.be.true(); - expect(Hoek.deepEqual(new LockableMap(entries), new LockableMap(entries))).to.be.true(); - expect(Hoek.deepEqual(new LockableMap(entries, false), new LockableMap(entries, false))).to.be.true(); - expect(Hoek.deepEqual(new LockableMap(entries, true), new LockableMap(entries, false))).to.be.false(); - expect(Hoek.deepEqual(new LockableMap(entries, false), new LockableMap(entries, true))).to.be.false(); - expect(Hoek.deepEqual(new LockableMap(entries), new Map(entries), { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(new LockableMap(entries), new PrivateMap(entries), { prototype: false })).to.be.false(); - }); - - it('compares promises', () => { - - const a = new Promise(() => { }); - - expect(Hoek.deepEqual(a, a)).to.be.true(); - expect(Hoek.deepEqual(a, new Promise(() => { }))).to.be.false(); - }); - - it('compares urls', () => { - - const a = new URL('https://hapi.dev/'); - - expect(Hoek.deepEqual(a, a)).to.be.true(); - expect(Hoek.deepEqual(a, new URL('https://hapi.dev/?new'))).to.be.false(); - expect(Hoek.deepEqual(a, {}, { prototype: false })).to.be.false(); - }); - - it('compares buffers', () => { - - expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2, 3]))).to.be.true(); - expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 3, 2]))).to.be.false(); - expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2]))).to.be.false(); - expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), {})).to.be.false(); - expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), [1, 2, 3])).to.be.false(); - }); - - it('compares string objects', () => { - - /* eslint-disable no-new-wrappers */ - expect(Hoek.deepEqual(new String('a'), new String('a'))).to.be.true(); - expect(Hoek.deepEqual(new String('a'), new String('b'))).to.be.false(); - expect(Hoek.deepEqual(new String(''), {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, new String(''), { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(new String('a'), 'a', { prototype: false })).to.be.false(); - expect(Hoek.deepEqual('a', new String('a'), { prototype: false })).to.be.false(); - /* eslint-enable no-new-wrappers */ - }); - - it('compares number objects', () => { - - /* eslint-disable no-new-wrappers */ - expect(Hoek.deepEqual(new Number(1), new Number(1))).to.be.true(); - expect(Hoek.deepEqual(new Number(1), new Number(2))).to.be.false(); - expect(Hoek.deepEqual(new Number(+0), new Number(-0))).to.be.false(); - expect(Hoek.deepEqual(new Number(NaN), new Number(NaN))).to.be.true(); - expect(Hoek.deepEqual(new Number(0), {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, new Number(0), { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(new Number(1), 1, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(1, new Number(1), { prototype: false })).to.be.false(); - /* eslint-enable no-new-wrappers */ - }); - - it('compares boolean objects', () => { - - /* eslint-disable no-new-wrappers */ - expect(Hoek.deepEqual(new Boolean(true), new Boolean(true))).to.be.true(); - expect(Hoek.deepEqual(new Boolean(true), new Boolean(false))).to.be.false(); - expect(Hoek.deepEqual(new Boolean(false), {}, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({}, new Boolean(false), { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(new Boolean(true), true, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual(true, new Boolean(true), { prototype: false })).to.be.false(); - /* eslint-enable no-new-wrappers */ - }); - - it('compares objects', () => { - - expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).to.be.true(); - expect(Hoek.deepEqual({ foo: 'bar' }, { foo: 'baz' })).to.be.false(); - expect(Hoek.deepEqual({ foo: { bar: 'foo' } }, { foo: { bar: 'baz' } })).to.be.false(); - expect(Hoek.deepEqual({ foo: undefined }, {})).to.be.false(); - expect(Hoek.deepEqual({}, { foo: undefined })).to.be.false(); - expect(Hoek.deepEqual({ foo: undefined }, { bar: undefined })).to.be.false(); - }); - - it('compares functions', () => { - - const f1 = () => 1; - const f2 = () => 2; - const f2a = () => 2; - - expect(Hoek.deepEqual({ f1 }, { f1 })).to.be.true(); - expect(Hoek.deepEqual({ f1 }, { f1: f2 })).to.be.false(); - expect(Hoek.deepEqual({ f2 }, { f2: f2a })).to.be.false(); - expect(Hoek.deepEqual({ f2 }, { f2: f2a }, { deepFunction: true })).to.be.true(); - expect(Hoek.deepEqual({ f2 }, { f2: f1 }, { deepFunction: true })).to.be.false(); - - const f3 = () => 3; - f3.x = 1; - - const f3a = () => 3; - f3a.x = 1; - - const f3b = () => 3; - f3b.x = 2; - - expect(Hoek.deepEqual({ f3 }, { f3: f3a }, { deepFunction: true })).to.be.true(); - expect(Hoek.deepEqual({ f3 }, { f3: f3b }, { deepFunction: true })).to.be.false(); - }); - - it('skips keys', () => { - - expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 })).to.be.false(); - expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 }, { skip: ['c'] })).to.be.true(); - - expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).to.be.false(); - expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }, { skip: ['c'] })).to.be.true(); - - const sym = Symbol('test'); - expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2, [sym]: 4 })).to.be.false(); - expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2, [sym]: 4 }, { skip: [sym] })).to.be.true(); - - expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2 })).to.be.false(); - expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2 }, { skip: [sym] })).to.be.true(); - expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3, [Symbol('other')]: true }, { a: 1, b: 2 }, { skip: [sym] })).to.be.false(); - - expect(Hoek.deepEqual({ a: 1, b: 2 }, { a: 1 }, { skip: ['a'] })).to.be.false(); - expect(Hoek.deepEqual({ a: 1 }, { a: 1, b: 2 }, { skip: ['a'] })).to.be.false(); - }); - - it('handles circular dependency', () => { - - const a = {}; - a.x = a; - - const b = Hoek.clone(a); - expect(Hoek.deepEqual(a, b)).to.be.true(); - }); - - it('handles obj only circular dependency', () => { - - const a = {}; - a.x = a; - - const b = { x: {} }; - expect(Hoek.deepEqual(a, b)).to.be.false(); - expect(Hoek.deepEqual(b, a)).to.be.false(); - }); - - it('handles irregular circular dependency', () => { - - const a = {}; - a.x = a; - - const b = { x: {} }; - b.x.x = b; - - const c = { x: { x: {} } }; - c.x.x.x = c; - expect(Hoek.deepEqual(a, b)).to.be.true(); - expect(Hoek.deepEqual(b, a)).to.be.true(); - expect(Hoek.deepEqual(a, c)).to.be.true(); - expect(Hoek.deepEqual(b, c)).to.be.true(); - expect(Hoek.deepEqual(c, a)).to.be.true(); - expect(Hoek.deepEqual(c, b)).to.be.true(); - b.x.y = 1; - expect(Hoek.deepEqual(a, b)).to.be.false(); - expect(Hoek.deepEqual(b, a)).to.be.false(); - }); - - it('handles cross circular dependency', () => { - - const a = {}; - const b = { x: {}, y: a }; - b.x.x = b; - b.x.y = b.x; - a.x = b; - a.y = a; - expect(Hoek.deepEqual(b, a)).to.be.true(); - expect(Hoek.deepEqual(a, b)).to.be.true(); - b.x.y = 1; - expect(Hoek.deepEqual(b, a)).to.be.false(); - expect(Hoek.deepEqual(a, b)).to.be.false(); - }); - - it('handles reuse of objects', () => { - - const date1 = { year: 2018, month: 1, day: 1 }; - const date2 = { year: 2000, month: 1, day: 1 }; - - expect(Hoek.deepEqual({ start: date1, end: date1 }, { start: date1, end: date2 })).to.be.false(); - }); - - it('handles valueOf() that throws', () => { - - const throwing = class { - - constructor(value) { - - this.value = value; - } - - valueOf() { - - throw new Error('failed'); - } - }; - - expect(Hoek.deepEqual(new throwing('a'), new throwing('a'))).to.be.true(); - expect(Hoek.deepEqual(new throwing('a'), new throwing('b'))).to.be.false(); - expect(Hoek.deepEqual(new throwing('a'), { value: 'a' }, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({ value: 'a' }, new throwing('a'), { prototype: false })).to.be.false(); - }); - - it('handles valueOf() that returns similar value', () => { - - const identity = class { - - constructor(value) { - - this.value = value; - } - - valueOf() { - - return { value: this.value }; - } - }; - - expect(Hoek.deepEqual(new identity('a'), new identity('a'))).to.be.true(); - expect(Hoek.deepEqual(new identity('a'), new identity('b'))).to.be.false(); - expect(Hoek.deepEqual(new identity('a'), { value: 'a' }, { prototype: true })).to.be.false(); - expect(Hoek.deepEqual(new identity('a'), { value: 'a' }, { prototype: false })).to.be.true(); - expect(Hoek.deepEqual({ value: 'a' }, new identity('a'), { prototype: true })).to.be.false(); - expect(Hoek.deepEqual({ value: 'a' }, new identity('a'), { prototype: false })).to.be.true(); - }); - - it('skips enumerable properties on prototype chain', () => { - - const base = function (value, surprice) { - - this.value = value; - if (surprice) { - this.surprice = surprice; - } - }; - - Object.defineProperty(base.prototype, 'enum', { - enumerable: true, - configurable: true, - value: true - }); - - expect('enum' in new base('a')).to.be.true(); - expect(Hoek.deepEqual(new base('a'), new base('a'))).to.be.true(); - expect(Hoek.deepEqual(new base('a'), new base('b'))).to.be.false(); - expect(Hoek.deepEqual(new base('a'), { value: 'a' }, { prototype: false })).to.be.true(); - expect(Hoek.deepEqual({ value: 'a' }, new base('a'), { prototype: false })).to.be.true(); - expect(Hoek.deepEqual(new base('a', 1), { value: 'a', enum: true }, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({ value: 'a', enum: true }, new base('a', 1), { prototype: false })).to.be.false(); - }); - - it('skips non-enumerable properties', () => { - - const base = function Base(value, surprice) { - - this.value = value; - if (surprice) { - this.surprice = surprice; - } - }; - - const createObj = (...args) => { - - const obj = new base(...args); - - Object.defineProperty(obj, 'hidden', { - enumerable: false, - configurable: true, - value: true - }); - - return obj; - }; - - expect(Hoek.deepEqual(createObj('a'), createObj('a'))).to.be.true(); - expect(Hoek.deepEqual(createObj('a'), createObj('b'))).to.be.false(); - expect(Hoek.deepEqual(createObj('a'), { value: 'a' }, { prototype: false })).to.be.true(); - expect(Hoek.deepEqual({ value: 'a' }, createObj('a'), { prototype: false })).to.be.true(); - expect(Hoek.deepEqual(createObj('a', 1), { value: 'a', hidden: true }, { prototype: false })).to.be.false(); - expect(Hoek.deepEqual({ value: 'a', hidden: true }, createObj('a', 1), { prototype: false })).to.be.false(); - }); - - it('compares an object with property getter while executing it', () => { - - const obj = {}; - const value = 1; - let execCount = 0; - - Object.defineProperty(obj, 'test', { - enumerable: true, - configurable: true, - get: function () { - - ++execCount; - return value; - } - }); - - const copy = Hoek.clone(obj); - expect(Hoek.deepEqual(obj, copy)).to.be.true(); - expect(execCount).to.equal(2); - expect(copy.test).to.equal(1); - expect(execCount).to.equal(3); - }); - - it('compares objects with property getters', () => { - - const obj = {}; - Object.defineProperty(obj, 'test', { - enumerable: true, - configurable: true, - get: function () { - - return 1; - } - }); - - const ref = {}; - Object.defineProperty(ref, 'test', { - enumerable: true, - configurable: true, - get: function () { - - return 2; - } - }); - - expect(Hoek.deepEqual(obj, ref)).to.be.false(); - }); - - it('compares object prototypes', () => { - - const Obj = function () { - - this.a = 5; - }; - - Obj.prototype.b = function () { - - return this.a; - }; - - const Ref = function () { - - this.a = 5; - }; - - Ref.prototype.b = function () { - - return this.a; - }; - - expect(Hoek.deepEqual(new Obj(), new Ref())).to.be.false(); - expect(Hoek.deepEqual(new Obj(), new Obj())).to.be.true(); - expect(Hoek.deepEqual(new Ref(), new Ref())).to.be.true(); - }); - - it('compares plain objects', () => { - - const a = Object.create(null); - const b = Object.create(null); - - a.b = 'c'; - b.b = 'c'; - - expect(Hoek.deepEqual(a, b)).to.be.true(); - expect(Hoek.deepEqual(a, { b: 'c' })).to.be.false(); - }); - - it('compares an object with an empty object', () => { - - const a = { a: 1, b: 2 }; - - expect(Hoek.deepEqual({}, a)).to.be.false(); - expect(Hoek.deepEqual(a, {})).to.be.false(); - }); - - it('compares an object ignoring the prototype', () => { - - const a = Object.create(null); - const b = {}; - - expect(Hoek.deepEqual(a, b, { prototype: false })).to.be.true(); - }); - - it('compares an object ignoring the prototype recursively', () => { - - const a = [Object.create(null)]; - const b = [{}]; - - expect(Hoek.deepEqual(a, b, { prototype: false })).to.be.true(); - }); -}); - -describe('intersect()', () => { - - it('returns the common objects of two arrays', () => { - - const array1 = [1, 2, 3, 4, 4, 5, 5]; - const array2 = [5, 4, 5, 6, 7]; - const common = Hoek.intersect(array1, array2); - expect(common).to.equal([5, 4]); - }); - - it('returns the common objects of array and set', () => { - - const array1 = new Set([1, 2, 3, 4, 4, 5, 5]); - const array2 = [5, 4, 5, 6, 7]; - const common = Hoek.intersect(array1, array2); - expect(common).to.equal([5, 4]); - }); - - it('returns the common objects of set and array', () => { - - const array1 = [1, 2, 3, 4, 4, 5, 5]; - const array2 = new Set([5, 4, 5, 6, 7]); - const common = Hoek.intersect(array1, array2); - expect(common).to.equal([5, 4]); - }); - - it('returns the common objects of two sets', () => { - - const array1 = new Set([1, 2, 3, 4, 4, 5, 5]); - const array2 = new Set([5, 4, 5, 6, 7]); - const common = Hoek.intersect(array1, array2); - expect(common).to.equal([5, 4]); - }); - - it('returns just the first common object of two arrays', () => { - - const array1 = [1, 2, 3, 4, 4, 5, 5]; - const array2 = [5, 4, 5, 6, 7]; - const common = Hoek.intersect(array1, array2, { first: true }); - expect(common).to.equal(5); - }); - - it('returns null when no common and returning just the first common object of two arrays', () => { - - const array1 = [1, 2, 3, 4, 4, 5, 5]; - const array2 = [6, 7]; - const common = Hoek.intersect(array1, array2, { first: true }); - expect(common).to.equal(null); - }); - - it('returns an empty array if either input is null', () => { - - expect(Hoek.intersect([1], null).length).to.equal(0); - expect(Hoek.intersect(null, [1]).length).to.equal(0); - expect(Hoek.intersect(null, [1], { first: true })).to.be.null(); - }); - - it('returns the common objects of object and array', () => { - - const array1 = { 1: true, 2: true, 3: true, 4: true, 5: true }; - const array2 = [5, 4, 5, 6, 7]; - const common = Hoek.intersect(array1, array2); - expect(common.length).to.equal(2); - }); -}); - -describe('contain()', () => { - - it('tests strings', () => { - - expect(Hoek.contain('abc', 'ab')).to.be.true(); - expect(Hoek.contain('abc', 'abc', { only: true })).to.be.true(); - expect(Hoek.contain('aaa', 'a', { only: true })).to.be.true(); - expect(Hoek.contain('aaa', 'a', { only: true, once: true })).to.be.false(); - expect(Hoek.contain('abc', 'b', { once: true })).to.be.true(); - expect(Hoek.contain('abc', ['a', 'c'])).to.be.true(); - expect(Hoek.contain('abc', ['a', 'd'], { part: true })).to.be.true(); - expect(Hoek.contain('aaa', ['a', 'a'], { only: true, once: true })).to.be.false(); - expect(Hoek.contain('aaa', ['a', 'a'], { only: true })).to.be.true(); - expect(Hoek.contain('aaa', ['a', 'a', 'a'], { only: true, once: true })).to.be.true(); - - expect(Hoek.contain('abc', 'ac')).to.be.false(); - expect(Hoek.contain('abcd', 'abc', { only: true })).to.be.false(); - expect(Hoek.contain('aab', 'a', { only: true })).to.be.false(); - expect(Hoek.contain('abb', 'b', { once: true })).to.be.false(); - expect(Hoek.contain('abc', ['a', 'd'])).to.be.false(); - expect(Hoek.contain('abc', ['ab', 'bc'])).to.be.false(); // Overlapping values not supported - - expect(Hoek.contain('', 'a')).to.be.false(); - expect(Hoek.contain('', 'a', { only: true })).to.be.false(); - - expect(Hoek.contain('', '')).to.be.true(); - expect(Hoek.contain('', ''), { only: true }).to.be.true(); - expect(Hoek.contain('', ''), { once: true }).to.be.true(); - expect(Hoek.contain('', ['', ''])).to.be.true(); - expect(Hoek.contain('', ['', ''], { only: true })).to.be.true(); - expect(Hoek.contain('', ['', ''], { once: true })).to.be.false(); - - expect(Hoek.contain('a', '')).to.be.true(); - expect(Hoek.contain('a', '', { only: true })).to.be.false(); - expect(Hoek.contain('a', '', { once: true })).to.be.false(); - expect(Hoek.contain('a', ['', ''])).to.be.true(); - expect(Hoek.contain('a', ['', ''], { only: true })).to.be.false(); - expect(Hoek.contain('a', ['', ''], { once: true })).to.be.false(); - - expect(Hoek.contain('ab', ['a', 'b', 'c'])).to.be.false(); - expect(Hoek.contain('ab', ['a', 'b', 'c'], { only: true })).to.be.false(); - expect(Hoek.contain('ab', ['a', 'b', 'c'], { only: true, once: true })).to.be.false(); - - expect(Hoek.contain('ab', ['c'], { part: true })).to.be.false(); - expect(Hoek.contain('ab', ['b'], { part: true })).to.be.true(); - }); - - it('tests arrays', () => { - - expect(Hoek.contain([1, 2, 3], 1)).to.be.true(); - expect(Hoek.contain([{ a: 1 }], { a: 1 }, { deep: true })).to.be.true(); - expect(Hoek.contain([1, 2, 3], [1, 2])).to.be.true(); - expect(Hoek.contain([{ a: 1 }], [{ a: 1 }], { deep: true })).to.be.true(); - expect(Hoek.contain([1, 1, 2], [1, 2], { only: true })).to.be.true(); - expect(Hoek.contain([1, 2], [1, 2], { once: true })).to.be.true(); - expect(Hoek.contain([1, 2, 3], [1, 4], { part: true })).to.be.true(); - expect(Hoek.contain([null, 2, 3], [null, 4], { part: true })).to.be.true(); - expect(Hoek.contain([null], null, { deep: true })).to.be.true(); - expect(Hoek.contain([[1], [2]], [[1]], { deep: true })).to.be.true(); - expect(Hoek.contain([[1], [2], 3], [[1]], { deep: true })).to.be.true(); - expect(Hoek.contain([[1, 2]], [[1]], { deep: true, part: true })).to.be.true(); - expect(Hoek.contain([[1, 2]], [[1], 2], { deep: true, part: true })).to.be.true(); - expect(Hoek.contain([1, 2, 1], [1, 1, 2], { only: true })).to.be.true(); - expect(Hoek.contain([1, 2, 1], [1, 1, 2], { only: true, once: true })).to.be.true(); - expect(Hoek.contain([1, 2, 1], [1, 2, 2], { only: true })).to.be.false(); - expect(Hoek.contain([1, 2, 1], [1, 2, 2], { only: true, part: true })).to.be.true(); - expect(Hoek.contain([1, 1, 1], [1, 1, 1, 1])).to.be.false(); - expect(Hoek.contain([1, 1, 1], [1, 1, 1, 1], { part: true })).to.be.true(); - - expect(Hoek.contain([1, 2, 3], 4)).to.be.false(); - expect(Hoek.contain([{ a: 1 }], { a: 2 }, { deep: true })).to.be.false(); - expect(Hoek.contain([{ a: 1 }, { a: 1 }], [{ a: 1 }, { a: 1 }], { deep: true, once: true, only: true })).to.be.true(); - expect(Hoek.contain([{ a: 1 }, { a: 1 }], [{ a: 1 }, { a: 2 }], { deep: true, once: true, only: true })).to.be.false(); - expect(Hoek.contain([{ a: 1 }], { a: 1 })).to.be.false(); - expect(Hoek.contain([1, 2, 3], [4, 5])).to.be.false(); - expect(Hoek.contain([[3], [2]], [[1]])).to.be.false(); - expect(Hoek.contain([[1], [2]], [[1]])).to.be.false(); - expect(Hoek.contain([[1, 2]], [[1]], { deep: true })).to.be.false(); - expect(Hoek.contain([{ a: 1 }], [{ a: 2 }], { deep: true })).to.be.false(); - expect(Hoek.contain([1, 3, 2], [1, 2], { only: true })).to.be.false(); - expect(Hoek.contain([1, 2, 2], [1, 2], { once: true })).to.be.false(); - expect(Hoek.contain([0, 2, 3], [1, 4], { part: true })).to.be.false(); - expect(Hoek.contain([1, 2, 1], [1, 2, 2], { only: true, once: true })).to.be.false(); - expect(Hoek.contain([1, 2, 1], [1, 2], { only: true, once: true })).to.be.false(); - - expect(Hoek.contain([], 1)).to.be.false(); - expect(Hoek.contain([], 1, { only: true })).to.be.false(); - - expect(Hoek.contain(['a', 'b'], ['a', 'b', 'c'])).to.be.false(); - expect(Hoek.contain(['a', 'b'], ['a', 'b', 'c'], { only: true })).to.be.false(); - expect(Hoek.contain(['a', 'b'], ['a', 'b', 'c'], { only: true, once: true })).to.be.false(); - - expect(Hoek.contain(['a', 'b'], ['c'], { part: true })).to.be.false(); - expect(Hoek.contain(['a', 'b'], ['b'], { part: true })).to.be.true(); - - expect(Hoek.contain([{ a: 1 }], [1], { deep: true })).to.be.false(); - }); - - it('tests objects', () => { - - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, 'a')).to.be.true(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, ['a', 'c'])).to.be.true(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, ['a', 'b', 'c'], { only: true })).to.be.true(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1 })).to.be.true(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, c: 3 })).to.be.true(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 4 }, { part: true })).to.be.true(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 }, { only: true })).to.be.true(); - expect(Hoek.contain({ a: [1], b: [2], c: [3] }, { a: [1], c: [3] }, { deep: true })).to.be.true(); - expect(Hoek.contain({ a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, { a: [{ b: 1 }, { d: 3 }] }, { deep: true })).to.be.false(); - expect(Hoek.contain({ a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, { a: [{ b: 1 }, { d: 3 }] }, { deep: true, part: true })).to.be.true(); - expect(Hoek.contain({ a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, { a: [{ b: 1 }, { d: 3 }] }, { deep: true, part: false })).to.be.false(); - expect(Hoek.contain({ a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, { a: [{ b: 1 }, { d: 3 }] }, { deep: true, only: true })).to.be.false(); - expect(Hoek.contain({ a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, { a: [{ b: 1 }, { d: 3 }] }, { deep: true, only: false })).to.be.true(); - expect(Hoek.contain({ a: [1, 2, 3] }, { a: [2, 4, 6] }, { deep: true, part: true })).to.be.true(); - - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, 'd')).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, ['a', 'd'])).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2, c: 3, d: 4 }, ['a', 'b', 'c'], { only: true })).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 2 })).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 2, b: 2 }, { part: true })).to.be.false(); // part does not ignore bad value - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 3 })).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 4 })).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }, { only: true })).to.be.false(); - expect(Hoek.contain({ a: [1], b: [2], c: [3] }, { a: [1], c: [3] })).to.be.false(); - expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } })).to.be.false(); - expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true })).to.be.false(); - expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, only: true })).to.be.false(); - expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, only: false })).to.be.true(); - expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, part: true })).to.be.true(); - expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, part: false })).to.be.false(); - expect(Hoek.contain({ a: [1, 2, 3] }, { a: [4, 5, 6] }, { deep: true, part: true })).to.be.false(); - - expect(Hoek.contain({}, 'a')).to.be.false(); - expect(Hoek.contain({}, 'a', { only: true })).to.be.false(); - - expect(Hoek.contain({ a: 'foo', b: 'bar' }, ['a', 'b', 'c'])).to.be.false(); - expect(Hoek.contain({ a: 'foo', b: 'bar' }, ['a', 'b', 'c'], { only: true })).to.be.false(); - expect(Hoek.contain({ a: 'foo', b: 'bar' }, { a: 'foo', b: 'bar', c: 'x' })).to.be.false(); - expect(Hoek.contain({ a: 'foo', b: 'bar' }, { a: 'foo', b: 'bar', c: 'x' }, { only: true })).to.be.false(); - - expect(Hoek.contain({ a: 1, b: 2 }, ['c'], { part: true })).to.be.false(); - expect(Hoek.contain({ a: 1, b: 2 }, ['b'], { part: true })).to.be.true(); - - // Getter check - - { - const Foo = function (bar) { - - this.bar = bar; - }; - - const getBar = function () { - - return this.bar; - }; - - const createFoo = (value) => { - - const foo = new Foo(value); - Object.defineProperty(foo, 'baz', { - enumerable: true, - get: getBar - }); - - return foo; - }; - - expect(Hoek.contain({ a: createFoo('b') }, { a: createFoo('b') }, { deep: true })).to.be.true(); - expect(Hoek.contain({ a: createFoo('b') }, { a: createFoo('b') }, { deep: true, part: true })).to.be.true(); - expect(Hoek.contain({ a: createFoo('b') }, { a: { bar: 'b', baz: 'b' } }, { deep: true })).to.be.true(); - expect(Hoek.contain({ a: createFoo('b') }, { a: { bar: 'b', baz: 'b' } }, { deep: true, only: true })).to.be.false(); - expect(Hoek.contain({ a: createFoo('b') }, { a: { baz: 'b' } }, { deep: true, part: false })).to.be.false(); - expect(Hoek.contain({ a: createFoo('b') }, { a: { baz: 'b' } }, { deep: true, part: true })).to.be.true(); - expect(Hoek.contain({ a: createFoo('b') }, { a: createFoo('b') }, { deep: true })).to.be.true(); - } - - // Properties on prototype not visible - - { - const Foo = function () { - - this.a = 1; - }; - - Object.defineProperty(Foo.prototype, 'b', { - enumerable: true, - value: 2 - }); - - const Bar = function () { - - Foo.call(this); - this.c = 3; - }; - - Util.inherits(Bar, Foo); - - expect((new Bar()).a).to.equal(1); - expect((new Bar()).b).to.equal(2); - expect((new Bar()).c).to.equal(3); - expect(Hoek.contain(new Bar(), { 'a': 1, 'c': 3 }, { only: true })).to.be.true(); - expect(Hoek.contain(new Bar(), 'b')).to.be.false(); - } - - // Non-Enumerable properties - - { - const foo = { a: 1, b: 2 }; - - Object.defineProperty(foo, 'c', { - enumerable: false, - value: 3 - }); - - expect(Hoek.contain(foo, 'c')).to.be.true(); - expect(Hoek.contain(foo, { 'c': 3 })).to.be.true(); - expect(Hoek.contain(foo, { 'a': 1, 'b': 2, 'c': 3 }, { only: true })).to.be.true(); - } - }); - - it('supports symbols', () => { - - const sym = Symbol(); - - expect(Hoek.contain([sym], sym)).to.be.true(); - expect(Hoek.contain({ [sym]: 1 }, sym)).to.be.true(); - expect(Hoek.contain({ [sym]: 1, a: 2 }, { [sym]: 1 })).to.be.true(); - - expect(Hoek.contain([sym], Symbol())).to.be.false(); - expect(Hoek.contain({ [sym]: 1 }, Symbol())).to.be.false(); - }); - - it('compares error keys', () => { - - const error = new Error('test'); - expect(Hoek.contain(error, { x: 1 })).to.be.false(); - expect(Hoek.contain(error, { x: 1 }, { part: true })).to.be.false(); - - error.x = 1; - - expect(Hoek.contain(error, { x: 1 })).to.be.true(); - expect(Hoek.contain(error, { x: 1 }, { part: true })).to.be.true(); - - expect(Hoek.contain(error, { x: 1, y: 2 })).to.be.false(); - expect(Hoek.contain(error, { x: 1, y: 2 }, { part: true })).to.be.true(); - }); -}); - -describe('flatten()', () => { - - it('returns a flat array', () => { - - const result = Hoek.flatten([1, 2, [3, 4, [5, 6], [7], 8], [9], [10, [11, 12]], 13]); - expect(result.length).to.equal(13); - expect(result).to.equal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); - }); -}); - -describe('reach()', () => { - - const sym = Symbol(); - const obj = { - a: { - b: { - c: { - d: 1, - e: 2 - }, - f: 'hello' - }, - g: { - h: 3 - }, - '-2': true, - [sym]: { - v: true - } - }, - i: function () { }, - j: null, - k: [4, 8, 9, 1] - }; - - obj.i.x = 5; - - it('returns object itself', () => { - - expect(Hoek.reach(obj, null)).to.equal(obj); - expect(Hoek.reach(obj, false)).to.equal(obj); - expect(Hoek.reach(obj)).to.equal(obj); - expect(Hoek.reach(obj, [])).to.equal(obj); - }); - - it('returns values of array', () => { - - expect(Hoek.reach(obj, 'k.0')).to.equal(4); - expect(Hoek.reach(obj, 'k.1')).to.equal(8); - }); - - it('returns last value of array using negative index', () => { - - expect(Hoek.reach(obj, 'k.-1')).to.equal(1); - expect(Hoek.reach(obj, 'k.-2')).to.equal(9); - }); - - it('returns object property with negative index for non-array', () => { - - expect(Hoek.reach(obj, 'a.-2')).to.be.equal(true); - }); - - it('returns a valid member', () => { - - expect(Hoek.reach(obj, 'a.b.c.d')).to.equal(1); - }); - - it('returns a valid member with separator override', () => { - - expect(Hoek.reach(obj, 'a/b/c/d', '/')).to.equal(1); - }); - - it('returns undefined on null object', () => { - - expect(Hoek.reach(null, 'a.b.c.d')).to.equal(undefined); - }); - - it('returns undefined on missing object member', () => { - - expect(Hoek.reach(obj, 'a.b.c.d.x')).to.equal(undefined); - }); - - it('returns undefined on missing function member', () => { - - expect(Hoek.reach(obj, 'i.y', { functions: true })).to.equal(undefined); - }); - - it('throws on missing member in strict mode', () => { - - expect(() => { - - Hoek.reach(obj, 'a.b.c.o.x', { strict: true }); - }).to.throw('Missing segment o in reach path a.b.c.o.x'); - - }); - - it('returns undefined on invalid member', () => { - - expect(Hoek.reach(obj, 'a.b.c.d-.x')).to.equal(undefined); - expect(Hoek.reach(obj, 'k.x')).to.equal(undefined); - expect(Hoek.reach(obj, 'k.1000')).to.equal(undefined); - expect(Hoek.reach(obj, 'k/0.5', '/')).to.equal(undefined); - }); - - it('returns function member', () => { - - expect(typeof Hoek.reach(obj, 'i')).to.equal('function'); - }); - - it('returns function property', () => { - - expect(Hoek.reach(obj, 'i.x')).to.equal(5); - }); - - it('returns null', () => { - - expect(Hoek.reach(obj, 'j')).to.equal(null); - }); - - it('throws on function property when functions not allowed', () => { - - expect(() => { - - Hoek.reach(obj, 'i.x', { functions: false }); - }).to.throw('Invalid segment x in reach path i.x'); - }); - - it('will return a default value if property is not found', () => { - - expect(Hoek.reach(obj, 'a.b.q', { default: 'defaultValue' })).to.equal('defaultValue'); - }); - - it('will return a default value if path is not found', () => { - - expect(Hoek.reach(obj, 'q', { default: 'defaultValue' })).to.equal('defaultValue'); - }); - - it('allows a falsey value to be used as the default value', () => { - - expect(Hoek.reach(obj, 'q', { default: '' })).to.equal(''); - }); - - it('allows array-based lookup', () => { - - expect(Hoek.reach(obj, ['a', 'b', 'c', 'd'])).to.equal(1); - expect(Hoek.reach(obj, ['k', '1'])).to.equal(8); - expect(Hoek.reach(obj, ['k', 1])).to.equal(8); - expect(Hoek.reach(obj, ['k', '-2'])).to.equal(9); - expect(Hoek.reach(obj, ['k', -2])).to.equal(9); - }); - - it('allows array-based lookup with symbols', () => { - - expect(Hoek.reach(obj, ['a', sym, 'v'])).to.equal(true); - expect(Hoek.reach(obj, ['a', Symbol(), 'v'])).to.equal(undefined); - }); - - it('returns character in string', () => { - - expect(Hoek.reach(['abc'], [0])).to.equal('abc'); - expect(Hoek.reach(['abc'], ['0'])).to.equal('abc'); - }); - - it('reaches sets and maps', () => { - - const value = { - a: { - b: new Set([ - { x: 1 }, - { x: 2 }, - { - y: new Map([ - ['v', 4], - ['w', 5] - ]) - } - ]) - } - }; - - expect(Hoek.reach(value, 'a.b.2.y.w')).to.not.exist(); - expect(Hoek.reach(value, 'a.b.2.y.w', { iterables: true })).to.equal(5); - }); -}); - -describe('reachTemplate()', () => { - - it('applies object to template', () => { - - const obj = { - a: { - b: { - c: { - d: 1 - } - } - }, - j: null, - k: [4, 8, 9, 1] - }; - - const template = '{k.0}:{k.-2}:{a.b.c.d}:{x.y}:{j}'; - - expect(Hoek.reachTemplate(obj, template)).to.equal('4:9:1::'); - }); - - it('applies object to template (options)', () => { - - const obj = { - a: { - b: { - c: { - d: 1 - } - } - }, - j: null, - k: [4, 8, 9, 1] - }; - - const template = '{k/0}:{k/-2}:{a/b/c/d}:{x/y}:{j}'; - - expect(Hoek.reachTemplate(obj, template, '/')).to.equal('4:9:1::'); - }); - - it('isn\'t prone to ReDoS given an adversarial template', () => { - - const sizes = [0, 1, 2, 3, 4]; // Should be evenly-spaced - const times = []; - const diffs = []; - - for (const size of sizes) { - const start = Date.now(); - Hoek.reachTemplate({}, '{'.repeat(size * 10000)); - times.push(Date.now() - start); - } - - for (let i = 1; i < times.length; ++i) { - diffs.push(times[i] - times[i - 1]); - } - - // Under ReDoS, as the size of the input increases the timing accelerates upwards, - // i.e. each timing diff would be greater than the last. - - const diffsMonotonic = diffs[0] < diffs[1] && diffs[1] < diffs[2] && diffs[2] < diffs[3]; - - expect(diffsMonotonic, 'Timing diffs monotonic').to.be.false(); - }); -}); - -describe('assert()', () => { - - it('throws an Error when using assert in a test', () => { - - expect(() => { - - Hoek.assert(false, 'my error message'); - }).to.throw(Hoek.AssertError, 'my error message'); - }); - - it('throws an Error when using assert in a test with no message', () => { - - expect(() => { - - Hoek.assert(false); - }).to.throw(Hoek.AssertError, 'Unknown error'); - }); - - it('throws an Error when using assert in a test with multipart message', () => { - - expect(() => { - - Hoek.assert(false, 'This', 'is', 'my message'); - }).to.throw(Hoek.AssertError, 'This is my message'); - }); - - it('throws an Error when using assert in a test with multipart message (empty)', () => { - - expect(() => { - - Hoek.assert(false, 'This', 'is', '', 'my message'); - }).to.throw(Hoek.AssertError, 'This is my message'); - }); - - it('throws an Error when using assert in a test with object message', () => { - - expect(() => { - - Hoek.assert(false, 'This', 'is', { spinal: 'tap' }); - }).to.throw(Hoek.AssertError, 'This is {"spinal":"tap"}'); - }); - - it('throws an Error when using assert in a test with multipart string and error messages', () => { - - expect(() => { - - Hoek.assert(false, new Error('This'), 'is', 'spinal', new Error('tap')); - }).to.throw(Hoek.AssertError, 'This is spinal tap'); - }); - - it('throws an Error when using assert in a test with error object message', () => { - - const err = new TypeError('This is spinal tap'); - const got = expect(() => Hoek.assert(false, err)).to.throw(TypeError, 'This is spinal tap'); - expect(got).to.shallow.equal(err); - }); - - it('throws the same Error that is passed to it if there is only one error passed', () => { - - const error = new Error('ruh roh'); - const error2 = new Error('ruh roh'); - - const fn = function () { - - Hoek.assert(false, error); - }; - - try { - fn(); - } - catch (err) { - expect(err).to.equal(error); // should be the same reference - expect(err).to.not.shallow.equal(error2); // error with the same message should not match - } - }); -}); - -describe('AssertError', () => { - - it('takes an optional message', () => { - - expect(new Hoek.AssertError().message).to.equal('Unknown error'); - expect(new Hoek.AssertError(null).message).to.equal('Unknown error'); - expect(new Hoek.AssertError('msg').message).to.equal('msg'); - }); - - it('has AssertError name property', () => { - - expect(new Hoek.AssertError().name).to.equal('AssertError'); - expect(new Hoek.AssertError('msg').name).to.equal('AssertError'); - }); - - it('uses ctor argument to hide stack', { skip: typeof Error.captureStackTrace !== 'function' }, () => { - - const parentFn = () => { - - throw new Hoek.AssertError('msg', parentFn); - }; - - const err = expect(parentFn).to.throw(Hoek.AssertError); - - expect(err.stack).to.not.contain('parentFn'); - }); -}); - -describe('Bench', () => { - - it('returns time elapsed', async () => { - - const timer = new Hoek.Bench(); - await Hoek.wait(12); - expect(timer.elapsed()).to.be.above(9); - }); -}); - -describe('escapeRegex()', () => { - - it('escapes all special regular expression characters', () => { - - const a = Hoek.escapeRegex('4^f$s.4*5+-_?%=#!:@|~\\/`"(>)[<]d{}s,'); - expect(a).to.equal('4\\^f\\$s\\.4\\*5\\+\\-_\\?%\\=#\\!\\:@\\|~\\\\\\/`"\\(>\\)\\[<\\]d\\{\\}s\\,'); - }); -}); - -describe('escapeHeaderAttribute()', () => { - - it('should not alter ascii values', () => { - - const a = Hoek.escapeHeaderAttribute('My Value'); - expect(a).to.equal('My Value'); - }); - - it('escapes all special HTTP header attribute characters', () => { - - const a = Hoek.escapeHeaderAttribute('I said go!!!#"' + String.fromCharCode(92)); - expect(a).to.equal('I said go!!!#\\"\\\\'); - }); - - it('throws on large unicode characters', () => { - - expect(() => { - - Hoek.escapeHeaderAttribute('this is a test' + String.fromCharCode(500) + String.fromCharCode(300)); - }).to.throw(Error); - }); - - it('throws on CRLF to prevent response splitting', () => { - - expect(() => { - - Hoek.escapeHeaderAttribute('this is a test\r\n'); - }).to.throw(Error); - }); -}); - -describe('escapeHtml()', () => { - - it('escapes all special HTML characters', () => { - - const a = Hoek.escapeHtml('&<>"\'`'); - expect(a).to.equal('&<>"'`'); - }); - - it('returns empty string on falsy input', () => { - - const a = Hoek.escapeHtml(''); - expect(a).to.equal(''); - }); - - it('returns unchanged string on no reserved input', () => { - - const a = Hoek.escapeHtml('abc'); - expect(a).to.equal('abc'); - }); -}); - -describe('once()', () => { - - it('allows function to only execute once', () => { - - let gen = 0; - let add = function (x) { - - gen += x; - }; - - add(5); - expect(gen).to.equal(5); - add = Hoek.once(add); - add(5); - expect(gen).to.equal(10); - add(5); - expect(gen).to.equal(10); - }); - - it('double once wraps one time', () => { - - let method = function () { }; - method = Hoek.once(method); - method.x = 1; - method = Hoek.once(method); - expect(method.x).to.equal(1); - }); -}); - -describe('ignore()', () => { - - it('exists', () => { - - expect(Hoek.ignore).to.exist(); - expect(typeof Hoek.ignore).to.equal('function'); - }); -}); - -describe('stringify()', () => { - - it('converts object to string', () => { - - const obj = { a: 1 }; - expect(Hoek.stringify(obj)).to.equal('{"a":1}'); - }); - - it('returns error in result string', () => { - - const obj = { a: 1 }; - obj.b = obj; - expect(Hoek.stringify(obj)).to.contain('Cannot display object'); - }); -}); - -describe('isPromise()', () => { - - it('determines if an object is a promise', async () => { - - expect(Hoek.isPromise({})).to.be.false(); - expect(Hoek.isPromise(null)).to.be.false(); - expect(Hoek.isPromise(false)).to.be.false(); - expect(Hoek.isPromise(0)).to.be.false(); - expect(Hoek.isPromise('')).to.be.false(); - expect(Hoek.isPromise({ then: 1 })).to.be.false(); - expect(Hoek.isPromise([])).to.be.false(); - - const items = [ - Promise.resolve(), - Promise.reject() - ]; - - expect(Hoek.isPromise(items[0])).to.be.true(); - expect(Hoek.isPromise(items[1])).to.be.true(); - expect(Hoek.isPromise(new Promise(Hoek.ignore))).to.be.true(); - expect(Hoek.isPromise({ then: Hoek.ignore })).to.be.true(); - - try { - await Promise.all(items); - } - catch (err) { } - }); -}); - -describe('wait()', () => { - - it('delays for timeout ms', async () => { - - const timeout = {}; - setTimeout(() => (timeout.before = true), 10); - const wait = Hoek.wait(10); - setTimeout(() => (timeout.after = true), 10); - - await wait; - - expect(timeout.before).to.be.true(); - expect(timeout.after).to.be.undefined(); - }); - - it('delays for timeout ms as bigint', async () => { - - const timeout = {}; - setTimeout(() => (timeout.before = true), 10); - const wait = Hoek.wait(10n); - setTimeout(() => (timeout.after = true), 10); - - await wait; - - expect(timeout.before).to.be.true(); - expect(timeout.after).to.be.undefined(); - }); - - it('handles timeouts >= 2^31', async () => { - - const flow = []; - let no = 0; - - const fakeTimeout = function (cb, time) { - - const timer = ++no; - - flow.push(`CALL(${timer}): ${time}`); - setImmediate(() => { - - flow.push(`PRE(${timer})`); - cb(); - flow.push(`POST(${timer})`); - }); - }; - - await Hoek.wait(2 ** 31, null, { setTimeout: fakeTimeout }); - flow.push('DONE1'); - await Hoek.wait(2 ** 32 + 2 ** 30, null, { setTimeout: fakeTimeout }); - flow.push('DONE2'); - - expect(flow).to.equal([ - 'CALL(1): 2147483647', - 'PRE(1)', - 'CALL(2): 1', - 'POST(1)', - 'PRE(2)', - 'POST(2)', - 'DONE1', - 'CALL(3): 2147483647', - 'PRE(3)', - 'CALL(4): 2147483647', - 'POST(3)', - 'PRE(4)', - 'CALL(5): 1073741826', - 'POST(4)', - 'PRE(5)', - 'POST(5)', - 'DONE2' - ]); - }); - - it('returns never resolving promise when timeout >= Number.MAX_SAFE_INTEGER', async () => { - - let calls = 0; - const fakeTimeout = function (cb) { - - ++calls; - process.nextTick(cb); - }; - - await Hoek.wait(2 ** 31 - 1, null, { setTimeout: fakeTimeout }); - expect(calls).to.equal(1); - - const waited = Symbol('waited'); - - const result = await Promise.race([ - Hoek.wait(1, waited), - Hoek.wait(Number.MAX_SAFE_INTEGER, null, { setTimeout: fakeTimeout }), - Hoek.wait(Infinity, null, { setTimeout: fakeTimeout }) - ]); - - expect(result).to.be.equal(waited); - expect(calls).to.equal(1); - }); - - it('handles a return value', async () => { - - const uniqueValue = {}; - const timeout = {}; - setTimeout(() => (timeout.before = true), 10); - const wait = Hoek.wait(10, uniqueValue); - setTimeout(() => (timeout.after = true), 10); - - expect(await wait).to.shallow.equal(uniqueValue); - expect(timeout.before).to.be.true(); - expect(timeout.after).to.be.undefined(); - }); - - it('undefined timeout resolves immediately', async () => { - - const waited = Symbol('waited'); - const result = await Promise.race([ - Hoek.wait(undefined, waited), - Hoek.wait(0) - ]); - - expect(result).to.equal(waited); - }); - - it('NaN timeout resolves immediately', async () => { - - const waited = Symbol('waited'); - const result = await Promise.race([ - Hoek.wait(Number.NaN, waited), - Hoek.wait(0) - ]); - - expect(result).to.equal(waited); - }); - - it('rejects on weird timeout values', async () => { - - await expect(() => Hoek.wait({})).to.throw(); - await expect(() => Hoek.wait(Symbol('hi'))).to.throw(); - }); -}); - -describe('block()', () => { - - it('returns a promise', () => { - - expect(Hoek.block()).to.be.instanceOf(Promise); - }); - - it('does not immediately reject or resolve', async () => { - - const promise = Hoek.block(); - const waited = Symbol('waited'); - - const result = await Promise.race([ - Hoek.wait(1, waited), - promise - ]); - - expect(result).to.be.equal(waited); - }); -}); diff --git a/test/index.ts b/test/index.ts index 322cc7da..d4310a1c 100755 --- a/test/index.ts +++ b/test/index.ts @@ -1,364 +1,2481 @@ -import * as Hoek from '..'; -import * as Lab from '@hapi/lab'; -import deepEqual = require('../lib/deepEqual'); - - -const { expect } = Lab.types; - -// Direct module import -expect.type(deepEqual); -expect.type(deepEqual(1, 2)); - -interface Foo { - a?: number; - b?: string; +// oxlint-disable unicorn/no-thenable + +import Util from 'util'; + +import { describe, it, expect } from 'vitest'; + +import * as Hoek from '../src/index.js'; +import { type MergeTypes } from '../src/merge.js'; + +const nestedObj = { + v: [7, 8, 9], + w: /^something$/gim, + x: { + a: [1, 2, 3], + b: 123456, + c: new Date(), + d: /hi/gim, + e: /hello/, + }, + y: 'y', + z: new Date(1378775452757), }; -interface Bar { - b?: string; - c?: boolean; -}; - - -// deepEqual() - -Hoek.deepEqual('some', 'some'); -Hoek.deepEqual('some', 3); -Hoek.deepEqual({}, {}); -Hoek.deepEqual({}, {}, { prototype: false, symbols: true, part: false, deepFunction: true }); -Hoek.deepEqual({}, {}, { skip: ['a', 'b', Symbol('test')] }); - -expect.type(Hoek.deepEqual(1, 2)); - -expect.error(Hoek.deepEqual()); -expect.error(Hoek.deepEqual(1, 2, {}, 'x')); -expect.error(Hoek.deepEqual({}, {}, { unknown: true })); -expect.error(Hoek.deepEqual({}, {}, { skip: [1] })) - - -// clone() - -Hoek.clone('string'); -Hoek.clone(123); -Hoek.clone({ a: 1 }); -Hoek.clone({ a: 1 }, { prototype: true, symbols: true }); -Hoek.clone({}, { shallow: [] }); -Hoek.clone(1, { shallow: [] }); -Hoek.clone(null, { shallow: [] }); -Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: true }); -Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: ['b'] }); -Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: [['b']] }); -Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: ['b'], prototype: true, symbols: true }); - -expect.type(Hoek.clone('string')); -expect.type(Hoek.clone({})); -expect.type(Hoek.clone({} as Bar)); -expect.type(Hoek.clone({ a: 1 } as Foo, { shallow: ['b'] })); - -expect.error(Hoek.clone({}, { unknown: true })); -expect.error(Hoek.clone({}, { shallow: [1] })); -expect.error(Hoek.clone({}, { shallow: 1 })); - - -// merge() - -Hoek.merge({ a: 1 } as Foo, { b: 'x' } as Bar); -Hoek.merge({ a: 1 }, { a: null }); -Hoek.merge({ a: 1 }, { a: null }, { mergeArrays: true, nullOverride: true, symbols: false }); - -expect.type(Hoek.merge({}, {})); -expect.type(Hoek.merge({ a: 1 } as Foo, { b: 'x' } as Bar)); - -expect.error(Hoek.merge(1, 2)); -expect.error(Hoek.merge({ a: 1 }, { a: null }, true)); -expect.error(Hoek.merge({ a: 1 }, { a: null }, { unknown: true })); - - -// applyToDefaults() - -Hoek.applyToDefaults({}, {}); -Hoek.applyToDefaults({}, true); -Hoek.applyToDefaults({}, false); -Hoek.applyToDefaults({}, null); -Hoek.applyToDefaults({ a: 1 } as Foo, { b: 'x' }); -Hoek.applyToDefaults({ a: 1 } as object, { b: 'x' }); -Hoek.applyToDefaults({ a: 1 } as Foo, { b: 'x' }, { shallow: ['b'] }); -Hoek.applyToDefaults({ a: 1 } as object, { b: 'x' }, { shallow: ['c'] }); - -expect.type(Hoek.applyToDefaults({}, {})); -expect.type(Hoek.applyToDefaults({ a: 1 } as Foo, { b: 'x' })); -expect.type(Hoek.applyToDefaults({}, {}, { shallow: [] })); - -expect.error(Hoek.applyToDefaults({} as Foo, 0)); -expect.error(Hoek.applyToDefaults({} as Foo, 0, { shallow: [] })); - - -// intersect() - -Hoek.intersect([1], [1, 2]); -Hoek.intersect(['a'], ['b', 'b']); -Hoek.intersect([1], [1, 2], { first: true }); -Hoek.intersect([1], [1, 2], { first: false }); -Hoek.intersect([1], null); -Hoek.intersect(null, [1, 2]); -Hoek.intersect([1], null, { first: true }); -Hoek.intersect(null, [1, 2], { first: true }); -Hoek.intersect(new Set([1]), new Set([1, 2])); -Hoek.intersect([1], new Set([1, 2])); -Hoek.intersect(new Set([1]), [1, 2]); - -expect.type(Hoek.intersect([1], [1, 2])); -expect.type(Hoek.intersect([1], [1, 2], { first: false })); -expect.type(Hoek.intersect([1], [1, 2], { first: true })); - -expect.error(Hoek.intersect()); -expect.error(Hoek.intersect(1, 2)); -expect.error(Hoek.intersect([1], 2)); -expect.error(Hoek.intersect([1], [2], 'x')); -expect.error(Hoek.intersect(new Map(), [2])); - - -// contain() - -Hoek.contain('abc', 'a'); -Hoek.contain('abc', ['a', 'x']); -Hoek.contain('abc', ['a', 'd'], { once: true, part: true, deep: true, symbols: true, only: true }); -Hoek.contain({ a: 1 }, 'a'); -Hoek.contain({ a: 1 }, { a: 1 }); -Hoek.contain({ a: 1, b: 2 }, ['a', 'x'], { part: true, deep: true, symbols: true, only: true }); -Hoek.contain([1], 1); -Hoek.contain([1], [1]); -Hoek.contain([1], [1], { once: true, part: true, deep: true, symbols: true, only: true }); - -expect.type(Hoek.contain('abc', 'a')); -expect.type(Hoek.contain({ a: 1 }, 'a')); -expect.type(Hoek.contain([1], 1)); - -expect.error(Hoek.contain('abc', 'a', { unknown: true })); -expect.error(Hoek.contain(['a'], 'a', { unknown: true })); -expect.error(Hoek.contain({ a: 1 }, 'a', { unknown: true })); -expect.error(Hoek.contain('abc', 1)); -expect.error(Hoek.contain('abc', [1])); -expect.error(Hoek.contain('abc', [{}])); -expect.error(Hoek.contain('abc', {})); -expect.error(Hoek.contain({}, 1)); -expect.error(Hoek.contain({ a: 1, b: 2 }, ['a', 'x'], { once: true })); - -// flatten() - -Hoek.flatten([1, [2, 3]]); -Hoek.flatten([1, [2, 3]], [4, 5]); - -expect.type(Hoek.flatten([1, [2, 3]])); -expect.type(Hoek.flatten([1, [2, 3]], [])); - -expect.error(Hoek.flatten()); -expect.error(Hoek.flatten(1)); -expect.error(Hoek.flatten([], 1)); - - -// reach() - -Hoek.reach(null, false); -Hoek.reach(null, '0'); -Hoek.reach(null, ['0']); -Hoek.reach(['abc'], false); -Hoek.reach(['abc'], null); -Hoek.reach(['abc'], undefined); -Hoek.reach(['abc'], [0]); -Hoek.reach(['abc'], ['0']); -Hoek.reach(['abc'], '0'); -Hoek.reach({ a: { b: { c: 3 } } }, 'a.b.c'); -Hoek.reach({ a: { b: { c: 3 } } }, ['a', 'b', 'c']); -Hoek.reach({ a: { b: { c: 3 } } }, 'a/b/c', { separator: '/', default: 4, strict: true, functions: true, iterables: true }); - -expect.type(Hoek.reach(['abc'], [0])); -expect.type(Hoek.reach({ a: { b: { c: 3 } } }, 'a.b.c')); - -expect.error(Hoek.reach()); -expect.error(Hoek.reach([])); -expect.error(Hoek.reach(1, '0')); -expect.error(Hoek.reach('abc', '0')); -expect.error(Hoek.reach([0], 0)); -expect.error(Hoek.reach(['abc'], '0', { unknown: false })); -expect.error(Hoek.reach(['abc'], '0', { separator: false })); - - -// reachTemplate() - -Hoek.reachTemplate(null, 'a{b}c'); -Hoek.reachTemplate([1, 2], 'a{1}c'); -Hoek.reachTemplate({ b: 2 }, 'a{b}c'); -Hoek.reachTemplate({ a: { b: { c: 3 } } }, '{a/b/c}', { separator: '/', default: 4, strict: true, functions: true }); - -expect.type(Hoek.reachTemplate([1, 2], 'a{1}c')); - -expect.error(Hoek.reachTemplate()); -expect.error(Hoek.reachTemplate([])); -expect.error(Hoek.reachTemplate(1, '0')); -expect.error(Hoek.reachTemplate('abc', '0')); -expect.error(Hoek.reachTemplate([0], 0)); -expect.error(Hoek.reachTemplate(['abc'], '{0}', { unknown: false })); -expect.error(Hoek.reachTemplate(['abc'], '{0}', { separator: false })); - - -// assert() - -Hoek.assert(true); -Hoek.assert(true, 'some', 'message', 10); -Hoek.assert(1, 'error'); -Hoek.assert(true, new Error('message')); - -expect.type(Hoek.assert(true)); - - -// Bench - -const bench = new Hoek.Bench(); -expect.type(bench.ts); -expect.type(bench.reset()); -expect.type(bench.elapsed()); -expect.type(Hoek.Bench.now()); - -expect.error(new Hoek.Bench({})); -expect.error(bench.reset(true)); -expect.error(bench.elapsed(true)); -expect.error(Hoek.Bench.now(true)); - - -// escapeRegex() - -Hoek.escapeRegex('something?'); - -expect.type(Hoek.escapeRegex('^?')); - -expect.error(Hoek.escapeRegex()); -expect.error(Hoek.escapeRegex(true)); -expect.error(Hoek.escapeRegex({})); - - -// escapeHeaderAttribute() - -Hoek.escapeHeaderAttribute('something?'); - -expect.type(Hoek.escapeHeaderAttribute('^?')); - -expect.error(Hoek.escapeHeaderAttribute()); -expect.error(Hoek.escapeHeaderAttribute(true)); -expect.error(Hoek.escapeHeaderAttribute({})); - - -// escapeHtml() - -Hoek.escapeHtml('something?'); - -expect.type(Hoek.escapeHtml('^?')); - -expect.error(Hoek.escapeHtml()); -expect.error(Hoek.escapeHtml(true)); -expect.error(Hoek.escapeHtml({})); - - -// escapeJson() - -Hoek.escapeJson('something?'); - -expect.type(Hoek.escapeJson('^?')); - -expect.error(Hoek.escapeJson()); -expect.error(Hoek.escapeJson(true)); -expect.error(Hoek.escapeJson({})); - - -// once() - -Hoek.once(() => 4); -Hoek.once(() => undefined); - -expect.type<() => void>(Hoek.once(() => 'x')); - -expect.error(Hoek.once()); -expect.error(Hoek.once('x')); -expect.error(Hoek.once({})); - - -// ignore() - -Hoek.ignore(); -Hoek.ignore(1, 2, 'x'); - -expect.type<() => void>(Hoek.ignore); -expect.type(Hoek.ignore()); - - -// stringify() - -Hoek.stringify(123); -Hoek.stringify({}, null, 4); - -expect.type(Hoek.stringify(123)); - -expect.error(Hoek.stringify()); - - -// wait() - -// $lab:types:off$ -await Hoek.wait(); -await Hoek.wait(123); - -expect.type>(Hoek.wait()); -expect.type(await Hoek.wait(100)); -expect.type>(Hoek.wait(100, 'ok')); - -expect.error(Hoek.wait({})); -// $lab:types:on$ - - -// block() - -// $lab:types:off$ -Hoek.wait(); - -expect.type>(Hoek.block()); -expect.type(await Hoek.block()); - -expect.error(Hoek.block(123)); -// $lab:types:on$ - - -// isPromise() - -Hoek.isPromise(1); -Hoek.isPromise({}); -Hoek.isPromise(null); - -expect.type(Hoek.isPromise(1)); - -expect.error(Hoek.isPromise()); -expect.error(Hoek.isPromise(1, 2)); - - -// ts - -interface X { a: number; }; -interface Y { b: number; }; - -function xor(input: Hoek.ts.XOR): number { - - return input.a || input.b || 10; -} - -xor({ a: 1 }); -xor({ b: 2 }); -expect.error(xor({ a: 1, b: 2 })); - -// AssertError - -expect.type(new Hoek.AssertError()); -expect.type(new Hoek.AssertError('fail')); -expect.type<'AssertError'>(new Hoek.AssertError().name); -expect.type(new Hoek.AssertError().message); - -expect.error(new Hoek.AssertError(new Error())); -expect.error(new Hoek.AssertError('fail', 'again')); +describe('merge()', () => { + it('deep copies source items', () => { + const sym1 = Symbol('1'); + const sym2 = Symbol('2'); + const sym3 = Symbol('3'); + + const target = { + b: 3, + d: [], + [sym1]: true, + [sym3]: true, + } as any; + + const source = { + c: { + d: 1, + }, + d: [{ e: 1 }], + [sym2]: true, + [sym3]: false, + }; + + Hoek.merge(target, source); + expect(target.c).not.toBe(source.c); + expect(target.c).toHoequal(source.c); + expect(target.d).not.toBe(source.d); + expect(target.d[0]).not.toBe(source.d[0]); + expect(target.d).toHoequal(source.d); + + expect(target[sym1]).toBe(true); + expect(target[sym2]).toBe(true); + expect(target[sym3]).toBe(false); + }); + + it('deep copies source items without symbols', () => { + const sym1 = Symbol('1'); + const sym2 = Symbol('2'); + const sym3 = Symbol('3'); + + const target = { + b: 3, + d: [], + [sym1]: true, + [sym3]: true, + } as any; + + const source = { + c: { + d: 1, + }, + d: [{ e: 1 }], + [sym2]: true, + [sym3]: false, + }; + + Hoek.merge(target, source, { symbols: false }); + expect(target.c).not.toBe(source.c); + expect(target.c).toHoequal(source.c); + expect(target.d).not.toBe(source.d); + expect(target.d[0]).not.toBe(source.d[0]); + expect(target.d).toHoequal(source.d); + + expect(target[sym1]).toBe(true); + expect(target[sym2]).toBeUndefined(); + expect(target[sym3]).toBe(true); + }); + + it('merges array over an object', () => { + const a = { + x: ['n', 'm'], + } as any; + + const b = { + x: { + n: '1', + m: '2', + }, + }; + + Hoek.merge(b, a); + expect(a.x[0]).toEqual('n'); + expect(a.x.n).toBeUndefined(); + }); + + it('merges object over an array', () => { + const a = { + x: ['n', 'm'], + } as any; + + const b = { + x: { + n: '1', + m: '2', + }, + }; + + Hoek.merge(a, b); + expect(a.x.n).toEqual('1'); + expect(a.x[0]).toBeUndefined(); + }); + + it('merges from null prototype objects', () => { + const a = {} as any; + + const b = Object.create(null); + b.x = true; + + Hoek.merge(a, b); + expect(a.x).toBe(true); + }); + + it('skips non-enumerable properties', () => { + const a = { x: 0 } as any; + + const b = {}; + Object.defineProperty(b, 'x', { + enumerable: false, + value: 1, + }); + + Hoek.merge(a, b); + expect(a.x).toEqual(0); + }); + + it('does not throw if source is null', () => { + const a = {} as any; + const b = null; + let c = null; + + expect(() => { + c = Hoek.merge(a, b); + }).not.toThrow(); + + expect(c).toHoequal(a); + }); + + it('does not throw if source is undefined', () => { + const a = {} as any; + const b = undefined; + let c = null; + + expect(() => { + c = Hoek.merge(a, b); + }).not.toThrow(); + + expect(c).toHoequal(a); + }); + + it('throws if source is not an object', () => { + expect(() => { + const a = {} as any; + const b = 0; + + // @ts-expect-error - intentionally invalid + Hoek.merge(a, b); + }).toThrow('Invalid source value: must be null, undefined, or an object'); + }); + + it('throws if target is not an object', () => { + expect(() => { + const a = 0; + const b = {}; + + // @ts-expect-error - intentionally invalid + Hoek.merge(a, b); + }).toThrow('Invalid target value: must be an object'); + }); + + it('throws if target is not an array and source is', () => { + expect(() => { + const a = {} as any; + const b = [1, 2]; + + Hoek.merge(a, b); + }).toThrow('Cannot merge array onto an object'); + }); + + it('returns the same object when merging arrays', () => { + const a = [] as any[]; + const b = [1, 2]; + + expect(Hoek.merge(a, b)).toHoequal(a); + }); + + it('combines an empty object with a non-empty object', () => { + const a = {} as any; + const b = nestedObj; + + const c = Hoek.merge(a, b); + expect(a).toHoequal(b); + expect(c).toHoequal(b); + }); + + it('overrides values in target', () => { + const a = { x: 1, y: 2, z: 3, v: 5, t: 'test', s: 1, m: 'abc' }; + const b = { x: null, z: 4, v: 0, t: { u: 6 }, s: undefined, m: '123' }; + + const c = Hoek.merge(a, b); + expect(c.x).toHoequal(null as never); + expect(c.y).toEqual(2); + expect(c.z).toEqual(4); + expect(c.v).toEqual(0); + expect(c.m).toEqual('123'); + expect(c.t).toHoequal({ u: 6 }); + expect(c.s).toBeUndefined(); + }); + + it('overrides values in target (flip)', () => { + const a = { x: 1, y: 2, z: 3, v: 5, t: 'test', s: 1, m: 'abc' }; + const b = { x: null, z: 4, v: 0, t: { u: 6 }, s: undefined, m: '123' }; + + const d = Hoek.merge(b, a); + expect(d.x).toEqual(1); + expect(d.y).toEqual(2); + expect(d.z).toEqual(3); + expect(d.v).toEqual(5); + expect(d.m).toEqual('abc'); + expect(d.t).toEqual('test'); + expect(d.s).toEqual(1); + }); + + it('retains Date properties', () => { + const a = { x: new Date(1378776452757) }; + + const b = Hoek.merge({}, a); + expect(a.x.getTime()).toHoequal(b.x.getTime()); + }); + + it('retains Date properties when merging keys', () => { + const a = { x: new Date(1378776452757) }; + + const b = Hoek.merge({ x: {} }, a); + expect(a.x.getTime()).toHoequal((b.x as Date).getTime()); + }); + + it('overrides Buffer', () => { + const a = { x: Buffer.from('abc') }; + + Hoek.merge({ x: {} }, a); + expect(a.x.toString()).toEqual('abc'); + }); + + it('overrides RegExp', () => { + const a = { x: /test/ }; + + Hoek.merge({ x: {} }, a); + expect(a.x).toHoequal(/test/); + }); + + it('overrides Symbol properties', () => { + const sym = Symbol(); + const a = { [sym]: 1 }; + + Hoek.merge({ [sym]: {} }, a); + expect(a[sym]).toEqual(1); + }); + + it('skips __proto__', () => { + const a = '{ "ok": "value", "__proto__": { "test": "value" } }'; + + const b = Hoek.merge({}, JSON.parse(a)); + expect(b).toHoequal({ ok: 'value' }); + expect(b.test).toBeUndefined(); + }); +}); + +describe('applyToDefaults()', () => { + it('throws when target is null', () => { + expect(() => { + Hoek.applyToDefaults(null as never, {}); + }).toThrow('Invalid defaults value: must be an object'); + }); + + it('throws when options are invalid', () => { + expect(() => { + Hoek.applyToDefaults({}, {}, false as never); + }).toThrow('Invalid options: must be an object'); + + expect(() => { + Hoek.applyToDefaults({}, {}, 123 as never); + }).toThrow('Invalid options: must be an object'); + }); + + it('returns null if source is false', () => { + const defaults = { + a: 1, + b: 2, + c: { + d: 3, + e: [5, 6], + }, + f: 6, + g: 'test', + }; + + const result = Hoek.applyToDefaults(defaults, false); + expect(result).toBeNull(); + }); + + it('returns null if source is null', () => { + const defaults = { + a: 1, + b: 2, + c: { + d: 3, + e: [5, 6], + }, + f: 6, + g: 'test', + }; + + const result = Hoek.applyToDefaults(defaults, null as never); + expect(result).toBeNull(); + }); + + it('returns null if source is undefined', () => { + const defaults = { + a: 1, + b: 2, + c: { + d: 3, + e: [5, 6], + }, + f: 6, + g: 'test', + }; + + const result = Hoek.applyToDefaults(defaults, undefined); + expect(result).toBeNull(); + }); + + it('returns a copy of defaults if source is true', () => { + const defaults = { + a: 1, + b: 2, + c: { + d: 3, + e: [5, 6], + }, + f: 6, + g: 'test', + }; + + const result = Hoek.applyToDefaults(defaults, true); + expect(result).toHoequal(defaults); + }); + + it('applies object to defaults', () => { + const defaults = { + a: 1, + b: 2, + c: { + d: 3, + e: [5, 6], + }, + f: 6, + g: 'test', + }; + + const obj = { + a: null, + c: { + e: [4], + }, + f: 0, + g: { + h: 5, + }, + }; + + type D = MergeTypes; + + const result = Hoek.applyToDefaults(defaults, obj)!; + expect(result.c!.e).toHoequal([4]); + expect(result.a).toEqual(1); + expect(result.b).toEqual(2); + expect(result.f).toEqual(0); + expect(result.g).toHoequal({ h: 5 }); + }); + + it('applies object to defaults with null', () => { + const defaults = { + a: 1, + b: 2, + c: { + d: 3, + e: [5, 6], + }, + f: 6, + g: 'test', + }; + + const obj = { + a: null, + c: { + e: [4], + }, + f: 0, + g: { + h: 5, + }, + }; + + type D = MergeTypes; + + const result = Hoek.applyToDefaults(defaults, obj, { nullOverride: true })!; + expect(result.c!.e).toHoequal([4]); + expect(result.a).toBeNull(); + expect(result.b).toEqual(2); + expect(result.f).toEqual(0); + expect(result.g).toHoequal({ h: 5 }); + }); + + it('shallow copies the listed keys from source without merging', () => { + const defaults = { + a: { + b: 5, + e: 3, + }, + c: { + d: 7, + g: 1, + }, + }; + + const source = { + a: { + b: 4, + }, + c: { + d: 6, + f: 7, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['a'] })!; + expect(merged).toHoequal({ a: { b: 4 }, c: { d: 6, g: 1, f: 7 } }); + expect(merged.a).toBe(source.a); + expect(merged.a).not.toHoequal(defaults.a); + expect(merged.c).not.toHoequal(source.c); + expect(merged.c).not.toHoequal(defaults.c); + }); + + it('shallow copies the nested keys (override)', () => { + const defaults = { + a: { + b: 5, + }, + c: { + d: 7, + g: 1, + }, + }; + + const source = { + a: { + b: 4, + }, + c: { + d: 6, + g: { + h: 8, + }, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g'] })!; + expect(merged).toHoequal({ a: { b: 4 }, c: { d: 6, g: { h: 8 } } }); + expect(merged.c!.g).toBe(source.c.g); + }); + + it('shallow copies the nested keys (missing)', () => { + const defaults = { + a: { + b: 5, + }, + }; + + const source = { + a: { + b: 4, + }, + c: { + g: { + h: 8, + }, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g'] })!; + expect(merged).toHoequal({ a: { b: 4 }, c: { g: { h: 8 } } }); + expect(merged.c!.g).toBe(source.c.g); + }); + + it('shallow copies the nested keys (override)', () => { + const defaults = { + a: { + b: 5, + }, + c: { + g: { + d: 7, + }, + }, + }; + + const source = { + a: { + b: 4, + }, + c: { + g: { + h: 8, + }, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g'] })!; + expect(merged).toHoequal({ a: { b: 4 }, c: { g: { h: 8 } } }); + expect(merged.c!.g).toBe(source.c.g); + }); + + it('shallow copies the nested keys (deeper)', () => { + const defaults = { + a: { + b: 5, + }, + }; + + const source = { + a: { + b: 4, + }, + c: { + g: { + r: { + h: 8, + }, + }, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['c.g.r'] })!; + expect(merged).toHoequal({ a: { b: 4 }, c: { g: { r: { h: 8 } } } }); + expect(merged.c!.g.r).toBe(source.c.g.r); + }); + + it('shallow copies the nested keys (not present)', () => { + const defaults = { + a: { + b: 5, + }, + }; + + const source = { + a: { + b: 4, + }, + c: { + g: { + r: { + h: 8, + }, + }, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['x.y'] }); + expect(merged).toHoequal({ a: { b: 4 }, c: { g: { r: { h: 8 } } } }); + }); + + it('shallow copies the nested keys (non-object)', () => { + const defaults = { + // All falsy values: + _undefined: { + a: 1, + }, + _null: { + a: 2, + }, + _false: { + a: 3, + }, + _emptyString: { + a: 4, + }, + _zero: { + a: 5, + }, + _NaN: { + a: 6, + }, + // Other non-object values: + _string: { + a: 7, + }, + _number: { + a: 8, + }, + _true: { + a: 9, + }, + _function: { + a: 10, + }, + }; + + const source = { + _undefined: undefined, + _null: null, + _false: false, + _emptyString: '', + _zero: 0, + _NaN: NaN, + _string: 'foo', + _number: 42, + _true: true, + _function: () => {}, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { + shallow: [ + '_undefined.a', + '_null.a', + '_false.a', + '_emptyString.a', + '_zero.a', + '_NaN.a', + '_string.a', + '_number.a', + '_true.a', + '_function.a', + ], + }); + expect(merged).toHoequal({ + _undefined: { a: 1 }, + _null: { a: 2 }, + _false: false, + _emptyString: '', + _zero: 0, + _NaN: NaN, + _string: 'foo', + _number: 42, + _true: true, + _function: source._function, + }); + }); + + it('shallow copies the listed keys in the defaults', () => { + const defaults = { + a: { + b: 1, + }, + }; + + const merged = Hoek.applyToDefaults(defaults, {}, { shallow: ['a'] })!; + expect(merged.a).toBe(defaults.a); + }); + + it('shallow copies the listed keys in the defaults (true)', () => { + const defaults = { + a: { + b: 1, + }, + }; + + const merged = Hoek.applyToDefaults(defaults, true, { shallow: ['a'] })!; + expect(merged.a).toBe(defaults.a); + }); + + it('returns null on false', () => { + const defaults = { + a: { + b: 1, + }, + }; + + const merged = Hoek.applyToDefaults(defaults, false, { shallow: ['a'] }); + expect(merged).toBeNull(); + }); + + it('handles missing shallow key in defaults', () => { + const defaults = { + a: { + b: 1, + }, + }; + + const options = { + a: { + b: 4, + }, + c: { + d: 2, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, options, { shallow: ['c'] })!; + expect(merged).toHoequal({ a: { b: 4 }, c: { d: 2 } }); + expect(merged.c).toBe(options.c); + + expect(Hoek.applyToDefaults(defaults, true, { shallow: ['c'] })).toHoequal({ a: { b: 1 } }); + }); + + it('throws on missing defaults', () => { + expect(() => Hoek.applyToDefaults(null as never, {}, { shallow: ['a'] })).toThrow( + 'Invalid defaults value: must be an object', + ); + }); + + it('throws on invalid defaults', () => { + expect(() => Hoek.applyToDefaults('abc', {}, { shallow: ['a'] })).toThrow( + 'Invalid defaults value: must be an object', + ); + }); + + it('throws on invalid source', () => { + expect(() => Hoek.applyToDefaults({}, 'abc', { shallow: ['a'] })).toThrow( + 'Invalid source value: must be true, falsy or an object', + ); + }); + + it('throws on missing keys', () => { + expect(() => Hoek.applyToDefaults({}, true, { shallow: 123 } as never)).toThrow('Invalid keys'); + }); + + it('handles array keys', () => { + const sym = Symbol(); + + const defaults = { + a: { + b: 5, + e: 3, + }, + c: { + d: 7, + [sym]: { + f: 9, + }, + }, + }; + + const options = { + a: { + b: 4, + }, + c: { + d: 6, + [sym]: { + g: 1, + }, + }, + }; + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, options, { shallow: [['c', sym]] })!; + expect(merged).toHoequal({ a: { b: 4, e: 3 }, c: { d: 6, [sym]: { g: 1 } } }); + expect(merged.c![sym]).toBe(options.c[sym]); + }); + + it('does not modify shallow entries in source', () => { + const defaults = { + a: { + b: 5, + }, + }; + + const source = {} as { a?: { b: number } }; + + Object.defineProperty(source, 'a', { value: { b: 4 } }); + + type D = MergeTypes; + + const merged = Hoek.applyToDefaults(defaults, source, { shallow: ['a'] })!; + expect(merged).toHoequal({ a: { b: 4 } }); + expect(merged.a).toBe(source.a); + expect(merged.a).not.toHoequal(defaults.a); + }); + + it('should respect nullOverride when shallow is used', () => { + const defaults = { host: 'localhost', port: 8000 }; + const source = { host: null, port: 8080 }; + + type D = MergeTypes; + + const result = Hoek.applyToDefaults(defaults, source, { nullOverride: true, shallow: [] })!; + expect(result.host).toBeNull(); + expect(result.port).toEqual(8080); + }); +}); + +describe('deepEqual()', () => { + it('compares identical references', () => { + const x = {}; + + expect(Hoek.deepEqual(x, x)).toBe(true); + }); + + it('compares simple values', () => { + expect(Hoek.deepEqual('x', 'x')).toBe(true); + expect(Hoek.deepEqual('x', 'y')).toBe(false); + expect(Hoek.deepEqual('x1', 'x')).toBe(false); + expect(Hoek.deepEqual(-0, +0)).toBe(false); + expect(Hoek.deepEqual(-0, -0)).toBe(true); + expect(Hoek.deepEqual(+0, +0)).toBe(true); + expect(Hoek.deepEqual(+0, -0)).toBe(false); + expect(Hoek.deepEqual(1, 1)).toBe(true); + expect(Hoek.deepEqual(0, 0)).toBe(true); + expect(Hoek.deepEqual(-1, 1)).toBe(false); + expect(Hoek.deepEqual(NaN, 0)).toBe(false); + expect(Hoek.deepEqual(NaN, NaN)).toBe(true); + }); + + it('compares different types', () => { + expect(Hoek.deepEqual([], 5, { prototype: false })).toBe(false); + expect(Hoek.deepEqual(5, [], { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, null, { prototype: false })).toBe(false); + expect(Hoek.deepEqual(null, {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual('abc', {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, 'abc', { prototype: false })).toBe(false); + }); + + it('compares empty structures', () => { + expect(Hoek.deepEqual([], [])).toBe(true); + expect(Hoek.deepEqual({}, {})).toBe(true); + expect(Hoek.deepEqual([], {})).toBe(false); + expect(Hoek.deepEqual([], {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, [], { prototype: false })).toBe(false); + }); + + it('compares empty arguments object', () => { + const compare = function () { + expect(Hoek.deepEqual([], arguments)).toBe(false); // eslint-disable-line prefer-rest-params + }; + + compare(); + }); + + it('compares empty arguments objects', () => { + const compare = function () { + const arg1 = arguments; // eslint-disable-line prefer-rest-params + + const inner = function () { + // callee is not supported in strict mode, was previously false becuse callee was different + expect(Hoek.deepEqual(arg1, arguments)).toBe(true); // eslint-disable-line prefer-rest-params + }; + + inner(); + }; + + compare(); + }); + + it('compares symbol object properties', () => { + const sym = Symbol(); + + const ne = {}; + Object.defineProperty(ne, sym, { value: true }); + + expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: { c: true } })).toBe(true); + expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: { c: false } })).toBe(false); + expect(Hoek.deepEqual({ [sym]: { c: true } }, { [sym]: true })).toBe(false); + expect(Hoek.deepEqual({ [sym]: undefined }, { [sym]: undefined })).toBe(true); + expect(Hoek.deepEqual({ [sym]: undefined }, {})).toBe(false); + expect(Hoek.deepEqual({}, { [sym]: undefined })).toBe(false); + + expect(Hoek.deepEqual({}, ne)).toBe(true); + expect(Hoek.deepEqual(ne, {})).toBe(true); + expect(Hoek.deepEqual({ [sym]: true }, ne)).toBe(false); + expect(Hoek.deepEqual(ne, { [sym]: true })).toBe(false); + expect(Hoek.deepEqual(ne, { [Symbol()]: undefined })).toBe(false); + + expect(Hoek.deepEqual({ [sym]: true }, { [sym]: true })).toBe(true); + expect(Hoek.deepEqual({ [sym]: true }, {})).toBe(false); + expect(Hoek.deepEqual({ [sym]: true }, {}, { symbols: false })).toBe(true); + }); + + it('compares dates', () => { + expect(Hoek.deepEqual(new Date(2015, 1, 1), new Date('2015/02/01'))).toBe(true); + expect(Hoek.deepEqual(new Date(100), new Date(101))).toBe(false); + expect(Hoek.deepEqual(new Date(), {})).toBe(false); + expect(Hoek.deepEqual(new Date(2015, 1, 1), new Date('2015/02/01'), { prototype: false })).toBe(true); + expect(Hoek.deepEqual(new Date(), {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, new Date(), { prototype: false })).toBe(false); + }); + + it('compares regular expressions', () => { + expect(Hoek.deepEqual(/\s/, new RegExp('\\\s'))).toBe(true); + expect(Hoek.deepEqual(/\s/g, /\s/g)).toBe(true); + expect(Hoek.deepEqual(/a/, {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual(/\s/g, /\s/i)).toBe(false); + expect(Hoek.deepEqual(/a/g, /b/g)).toBe(false); + }); + + it('compares errors', () => { + expect(Hoek.deepEqual(new Error('a'), new Error('a'))).toBe(true); + expect(Hoek.deepEqual(new Error('a'), new Error('b'))).toBe(false); + + expect(Hoek.deepEqual(new Error('a'), new TypeError('a'))).toBe(false); + expect(Hoek.deepEqual(new Error('a'), new TypeError('a'), { prototype: false })).toBe(false); + + expect(Hoek.deepEqual(new Error(), {})).toBe(false); + expect(Hoek.deepEqual(new Error(), {}, { prototype: false })).toBe(false); + + expect(Hoek.deepEqual({}, new Error())).toBe(false); + expect(Hoek.deepEqual({}, new Error(), { prototype: false })).toBe(false); + + const error = new Error('a'); + expect(Hoek.deepEqual(Hoek.clone(error), error)).toBe(true); + expect(Hoek.deepEqual(Hoek.clone(error), error, { prototype: false })).toBe(true); + }); + + it('compares arrays', () => { + expect(Hoek.deepEqual([[1]], [[1]])).toBe(true); + expect(Hoek.deepEqual([1, 2, 3], [1, 2, 3])).toBe(true); + expect(Hoek.deepEqual([1, 2, 3], [1, 3, 2])).toBe(false); + expect(Hoek.deepEqual([1, 2, 3], [1, 2])).toBe(false); + expect(Hoek.deepEqual([1], [1])).toBe(true); + const item1 = { key: 'value1' }; + const item2 = { key: 'value2' }; + expect(Hoek.deepEqual([item1, item1], [item1, item2])).toBe(false); + }); + + it('compares sets', () => { + expect(Hoek.deepEqual(new Set(), new Set())).toBe(true); + expect(Hoek.deepEqual(new Set([1]), new Set([1]))).toBe(true); + expect(Hoek.deepEqual(new Set([]), new Set([]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([1, 2, 3]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([3, 2, 1]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([1, 2, 4]))).toBe(false); + expect(Hoek.deepEqual(new Set([1, 2, 3]), new Set([1, 2]))).toBe(false); + expect(Hoek.deepEqual(new Set([1, 2, 1]), new Set([1, 2]))).toBe(true); + expect(Hoek.deepEqual(new Set([+0]), new Set([-0]))).toBe(true); + expect(Hoek.deepEqual(new Set([NaN]), new Set([NaN]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, {}]), new Set([1, {}]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, {}]), new Set([{}, 1]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, {}, {}]), new Set([{}, 1, {}]))).toBe(true); + expect(Hoek.deepEqual(new Set([1, { a: 1 }]), new Set([{ a: 0 }, 1]))).toBe(false); + expect(Hoek.deepEqual(new WeakSet(), new WeakSet())).toBe(true); + const obj = {}; + expect(Hoek.deepEqual(new WeakSet([obj]), new WeakSet())).toBe(true); + expect(Hoek.deepEqual(new WeakSet(), new Set(), { prototype: false })).toBe(false); + + const sets = [new Set(), new Set()].map((set) => { + (set as any).modified = true; + return set; + }); + expect(Hoek.deepEqual(sets[0], sets[1])).toBe(true); + expect(Hoek.deepEqual(sets[0], new Set())).toBe(false); + }); + + it('compares extended sets', () => { + class PrivateSet extends Set { + has(): boolean { + throw new Error('not allowed'); + } + } + + const entries = ['a', undefined]; + expect(Hoek.deepEqual(new PrivateSet(), new PrivateSet())).toBe(true); + expect(Hoek.deepEqual(new PrivateSet(entries), new PrivateSet(entries))).toBe(true); + expect(Hoek.deepEqual(new PrivateSet(entries), new Set(entries), { prototype: false })).toBe(true); + expect(Hoek.deepEqual(new PrivateSet(entries), new Set(entries), { prototype: true })).toBe(false); + expect(Hoek.deepEqual(new PrivateSet(), new PrivateSet(entries))).toBe(false); + expect(Hoek.deepEqual(new PrivateSet(entries), new PrivateSet())).toBe(false); + + class LockableSet extends Set { + locked: boolean; + + constructor(values?: any[], locked = true) { + super(values); + this.locked = locked; + } + + has(key: any) { + if (this.locked) { + throw new Error('not allowed'); + } + + return super.has(key); + } + } + + expect(Hoek.deepEqual(new LockableSet(), new LockableSet())).toBe(true); + expect(Hoek.deepEqual(new LockableSet(entries), new LockableSet(entries))).toBe(true); + expect(Hoek.deepEqual(new LockableSet(entries, false), new LockableSet(entries, false))).toBe(true); + expect(Hoek.deepEqual(new LockableSet(entries, true), new LockableSet(entries, false))).toBe(false); + expect(Hoek.deepEqual(new LockableSet(entries, false), new LockableSet(entries, true))).toBe(false); + expect(Hoek.deepEqual(new LockableSet(entries), new Set(entries), { prototype: false })).toBe(false); + expect(Hoek.deepEqual(new LockableSet(entries), new PrivateSet(entries), { prototype: false })).toBe(false); + }); + + it('compares maps', () => { + const item1 = { key: 'value1' }; + const item2 = { key: 'value2' }; + expect(Hoek.deepEqual(new Map(), new Map())).toBe(true); + expect(Hoek.deepEqual(new Map([[1, {}]]), new Map([[1, {}]]))).toBe(true); + expect(Hoek.deepEqual(new Map([[1, item1]]), new Map([[1, item1]]))).toBe(true); + expect(Hoek.deepEqual(new Map([[1, item1]]), new Map([[1, item2]]))).toBe(false); + expect(Hoek.deepEqual(new Map([[1, undefined]]), new Map([[1, undefined]]))).toBe(true); + expect(Hoek.deepEqual(new Map([[1, undefined]]), new Map([[2, undefined]]))).toBe(false); + expect( + Hoek.deepEqual( + new Map([[1, {}]]), + new Map([ + [1, {}], + [2, {}], + ]), + ), + ).toBe(false); + expect(Hoek.deepEqual(new Map([[item1, 1]]), new Map([[item1, 1]]))).toBe(true); + expect(Hoek.deepEqual(new Map([[{}, 1]]), new Map([[{}, 1]]))).toBe(false); + expect(Hoek.deepEqual(new WeakMap(), new WeakMap())).toBe(true); + expect(Hoek.deepEqual(new WeakMap([[item1, 1]]), new WeakMap())).toBe(true); + expect(Hoek.deepEqual(new WeakMap(), new Map(), { prototype: false })).toBe(false); + + const maps = [new Map(), new Map()].map((map) => { + (map as any).modified = true; + return map; + }); + expect(Hoek.deepEqual(maps[0], maps[1])).toBe(true); + expect(Hoek.deepEqual(maps[0], new Map())).toBe(false); + }); + + it('compares extended maps', () => { + class PrivateMap extends Map { + constructor(args?: any) { + // @ts-expect-error - Map typing is a MapConstructor, but for whatever reason, it thinks it doesn't accept arguments + super(args); + } + + get() { + throw new Error('not allowed'); + } + } + + const entries = [ + ['a', 1], + ['b', undefined], + ] as const; + + expect(Hoek.deepEqual(new PrivateMap(), new PrivateMap())).toBe(true); + expect(Hoek.deepEqual(new PrivateMap(entries), new PrivateMap(entries))).toBe(true); + expect(Hoek.deepEqual(new PrivateMap(entries), new Map(entries), { prototype: false })).toBe(true); + expect(Hoek.deepEqual(new PrivateMap(entries), new Map(entries), { prototype: true })).toBe(false); + expect(Hoek.deepEqual(new PrivateMap(), new PrivateMap(entries))).toBe(false); + expect(Hoek.deepEqual(new PrivateMap(entries), new PrivateMap())).toBe(false); + + class LockableMap extends Map { + locked: boolean; + + constructor(kvs?: any, locked = true) { + // https://stackoverflow.com/questions/70677360/in-typescript-when-i-use-mymap-to-extends-map-but-it-s-show-expected-0-argument + // https://stackoverflow.com/questions/67631458/no-overload-matches-this-call-while-constructing-map-from-array + // @ts-expect-error - See above + super(kvs); + + this.locked = locked; + } + + get() { + if (this.locked) { + throw new Error('not allowed'); + } + } + } + + expect(Hoek.deepEqual(new LockableMap(), new LockableMap())).toBe(true); + expect(Hoek.deepEqual(new LockableMap(entries), new LockableMap(entries))).toBe(true); + expect(Hoek.deepEqual(new LockableMap(entries, false), new LockableMap(entries, false))).toBe(true); + expect(Hoek.deepEqual(new LockableMap(entries, true), new LockableMap(entries, false))).toBe(false); + expect(Hoek.deepEqual(new LockableMap(entries, false), new LockableMap(entries, true))).toBe(false); + expect(Hoek.deepEqual(new LockableMap(entries), new Map(entries), { prototype: false })).toBe(false); + expect(Hoek.deepEqual(new LockableMap(entries), new PrivateMap(entries), { prototype: false })).toBe(false); + }); + + it('compares promises', () => { + const a = new Promise(() => {}); + + expect(Hoek.deepEqual(a, a)).toBe(true); + expect(Hoek.deepEqual(a, new Promise(() => {}))).toBe(false); + }); + + it('compares urls', () => { + const a = new URL('https://hapi.dev/'); + + expect(Hoek.deepEqual(a, a)).toBe(true); + expect(Hoek.deepEqual(a, new URL('https://hapi.dev/?new'))).toBe(false); + expect(Hoek.deepEqual(a, {}, { prototype: false })).toBe(false); + }); + + it('compares buffers', () => { + expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2, 3]))).toBe(true); + expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 3, 2]))).toBe(false); + expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2]))).toBe(false); + expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), {})).toBe(false); + expect(Hoek.deepEqual(Buffer.from([1, 2, 3]), [1, 2, 3])).toBe(false); + }); + + it('compares string objects', () => { + /* eslint-disable no-new-wrappers */ + expect(Hoek.deepEqual(new String('a'), new String('a'))).toBe(true); + expect(Hoek.deepEqual(new String('a'), new String('b'))).toBe(false); + expect(Hoek.deepEqual(new String(''), {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, new String(''), { prototype: false })).toBe(false); + expect(Hoek.deepEqual(new String('a'), 'a', { prototype: false })).toBe(false); + expect(Hoek.deepEqual('a', new String('a'), { prototype: false })).toBe(false); + /* eslint-enable no-new-wrappers */ + }); + + it('compares number objects', () => { + /* eslint-disable no-new-wrappers */ + expect(Hoek.deepEqual(new Number(1), new Number(1))).toBe(true); + expect(Hoek.deepEqual(new Number(1), new Number(2))).toBe(false); + expect(Hoek.deepEqual(new Number(+0), new Number(-0))).toBe(false); + expect(Hoek.deepEqual(new Number(NaN), new Number(NaN))).toBe(true); + expect(Hoek.deepEqual(new Number(0), {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, new Number(0), { prototype: false })).toBe(false); + expect(Hoek.deepEqual(new Number(1), 1, { prototype: false })).toBe(false); + expect(Hoek.deepEqual(1, new Number(1), { prototype: false })).toBe(false); + /* eslint-enable no-new-wrappers */ + }); + + it('compares boolean objects', () => { + /* eslint-disable no-new-wrappers */ + expect(Hoek.deepEqual(new Boolean(true), new Boolean(true))).toBe(true); + expect(Hoek.deepEqual(new Boolean(true), new Boolean(false))).toBe(false); + expect(Hoek.deepEqual(new Boolean(false), {}, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({}, new Boolean(false), { prototype: false })).toBe(false); + expect(Hoek.deepEqual(new Boolean(true), true, { prototype: false })).toBe(false); + expect(Hoek.deepEqual(true, new Boolean(true), { prototype: false })).toBe(false); + /* eslint-enable no-new-wrappers */ + }); + + it('compares objects', () => { + expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(true); + expect(Hoek.deepEqual({ foo: 'bar' }, { foo: 'baz' })).toBe(false); + expect(Hoek.deepEqual({ foo: { bar: 'foo' } }, { foo: { bar: 'baz' } })).toBe(false); + expect(Hoek.deepEqual({ foo: undefined }, {})).toBe(false); + expect(Hoek.deepEqual({}, { foo: undefined })).toBe(false); + expect(Hoek.deepEqual({ foo: undefined }, { bar: undefined })).toBe(false); + }); + + it('compares functions', () => { + const f1 = () => 1; + const f2 = () => 2; + const f2a = () => 2; + + expect(Hoek.deepEqual({ f1 }, { f1 })).toBe(true); + expect(Hoek.deepEqual({ f1 }, { f1: f2 })).toBe(false); + expect(Hoek.deepEqual({ f2 }, { f2: f2a })).toBe(false); + expect(Hoek.deepEqual({ f2 }, { f2: f2a }, { deepFunction: true })).toBe(true); + expect(Hoek.deepEqual({ f2 }, { f2: f1 }, { deepFunction: true })).toBe(false); + + const f3 = () => 3; + f3.x = 1; + + const f3a = () => 3; + f3a.x = 1; + + const f3b = () => 3; + f3b.x = 2; + + expect(Hoek.deepEqual({ f3 }, { f3: f3a }, { deepFunction: true })).toBe(true); + expect(Hoek.deepEqual({ f3 }, { f3: f3b }, { deepFunction: true })).toBe(false); + }); + + it('skips keys', () => { + expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 })).toBe(false); + expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 }, { skip: ['c'] })).toBe(true); + + expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false); + expect(Hoek.deepEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }, { skip: ['c'] })).toBe(true); + + const sym = Symbol('test'); + expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2, [sym]: 4 })).toBe(false); + expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2, [sym]: 4 }, { skip: [sym] })).toBe(true); + + expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2 })).toBe(false); + expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3 }, { a: 1, b: 2 }, { skip: [sym] })).toBe(true); + expect(Hoek.deepEqual({ a: 1, b: 2, [sym]: 3, [Symbol('other')]: true }, { a: 1, b: 2 }, { skip: [sym] })).toBe( + false, + ); + + expect(Hoek.deepEqual({ a: 1, b: 2 }, { a: 1 }, { skip: ['a'] })).toBe(false); + expect(Hoek.deepEqual({ a: 1 }, { a: 1, b: 2 }, { skip: ['a'] })).toBe(false); + }); + + it('handles circular dependency', () => { + const a = {} as any; + a.x = a; + + const b = Hoek.clone(a); + expect(Hoek.deepEqual(a, b)).toBe(true); + }); + + it('handles obj only circular dependency', () => { + const a = {} as any; + a.x = a; + + const b = { x: {} }; + expect(Hoek.deepEqual(a, b)).toBe(false); + expect(Hoek.deepEqual(b, a)).toBe(false); + }); + + it('handles irregular circular dependency', () => { + const a = {} as any; + a.x = a; + + const b = { x: {} } as any; + + b.x.x = b; + + const c = { x: { x: {} } } as any; + c.x.x.x = c; + + expect(Hoek.deepEqual(a, b)).toBe(true); + expect(Hoek.deepEqual(b, a)).toBe(true); + expect(Hoek.deepEqual(a, c)).toBe(true); + expect(Hoek.deepEqual(b, c)).toBe(true); + expect(Hoek.deepEqual(c, a)).toBe(true); + expect(Hoek.deepEqual(c, b)).toBe(true); + + b.x.y = 1; + + expect(Hoek.deepEqual(a, b)).toBe(false); + expect(Hoek.deepEqual(b, a)).toBe(false); + }); + + it('handles cross circular dependency', () => { + const a = {} as any; + const b = { x: {}, y: a } as any; + + b.x.x = b; + b.x.y = b.x; + a.x = b; + a.y = a; + + expect(Hoek.deepEqual(b, a)).toBe(true); + expect(Hoek.deepEqual(a, b)).toBe(true); + + b.x.y = 1; + + expect(Hoek.deepEqual(b, a)).toBe(false); + expect(Hoek.deepEqual(a, b)).toBe(false); + }); + + it('handles reuse of objects', () => { + const date1 = { year: 2018, month: 1, day: 1 }; + const date2 = { year: 2000, month: 1, day: 1 }; + + expect(Hoek.deepEqual({ start: date1, end: date1 }, { start: date1, end: date2 })).toBe(false); + }); + + it('handles valueOf() that throws', () => { + const throwing = class { + value: string; + + constructor(value: string) { + this.value = value; + } + + valueOf() { + throw new Error('failed'); + } + }; + + expect(Hoek.deepEqual(new throwing('a'), new throwing('a'))).toBe(true); + expect(Hoek.deepEqual(new throwing('a'), new throwing('b'))).toBe(false); + expect(Hoek.deepEqual(new throwing('a'), { value: 'a' }, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({ value: 'a' }, new throwing('a'), { prototype: false })).toBe(false); + }); + + it('handles valueOf() that returns similar value', () => { + const identity = class { + value: string; + + constructor(value: string) { + this.value = value; + } + + valueOf() { + return { value: this.value }; + } + }; + + expect(Hoek.deepEqual(new identity('a'), new identity('a'))).toBe(true); + expect(Hoek.deepEqual(new identity('a'), new identity('b'))).toBe(false); + expect(Hoek.deepEqual(new identity('a'), { value: 'a' }, { prototype: true })).toBe(false); + expect(Hoek.deepEqual(new identity('a'), { value: 'a' }, { prototype: false })).toBe(true); + expect(Hoek.deepEqual({ value: 'a' }, new identity('a'), { prototype: true })).toBe(false); + expect(Hoek.deepEqual({ value: 'a' }, new identity('a'), { prototype: false })).toBe(true); + }); + + interface BaseItf { + value: string; + surprice?: number; + } + + it('skips enumerable properties on prototype chain', () => { + const base: any = function (this: BaseItf, value: string, surprice?: number) { + this.value = value; + + if (surprice) { + this.surprice = surprice; + } + } as any; + + Object.defineProperty(base.prototype, 'enum', { + enumerable: true, + configurable: true, + value: true, + }); + + expect('enum' in new base('a')).toBe(true); + expect(Hoek.deepEqual(new base('a'), new base('a'))).toBe(true); + expect(Hoek.deepEqual(new base('a'), new base('b'))).toBe(false); + expect(Hoek.deepEqual(new base('a'), { value: 'a' }, { prototype: false })).toBe(true); + expect(Hoek.deepEqual({ value: 'a' }, new base('a'), { prototype: false })).toBe(true); + expect(Hoek.deepEqual(new base('a', 1), { value: 'a', enum: true }, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({ value: 'a', enum: true }, new base('a', 1), { prototype: false })).toBe(false); + }); + + it('skips non-enumerable properties', () => { + const base: any = function Base(this: BaseItf, value: string, surprice: number) { + this.value = value; + + if (surprice) { + this.surprice = surprice; + } + }; + + const createObj = (...args: any[]) => { + const obj = new base(...args); + + Object.defineProperty(obj, 'hidden', { + enumerable: false, + configurable: true, + value: true, + }); + + return obj; + }; + + expect(Hoek.deepEqual(createObj('a'), createObj('a'))).toBe(true); + expect(Hoek.deepEqual(createObj('a'), createObj('b'))).toBe(false); + expect(Hoek.deepEqual(createObj('a'), { value: 'a' }, { prototype: false })).toBe(true); + expect(Hoek.deepEqual({ value: 'a' }, createObj('a'), { prototype: false })).toBe(true); + expect(Hoek.deepEqual(createObj('a', 1), { value: 'a', hidden: true }, { prototype: false })).toBe(false); + expect(Hoek.deepEqual({ value: 'a', hidden: true }, createObj('a', 1), { prototype: false })).toBe(false); + }); + + it('compares an object with property getter while executing it', () => { + const obj = {} as any; + const value = 1; + let execCount = 0; + + Object.defineProperty(obj, 'test', { + enumerable: true, + configurable: true, + get: function () { + ++execCount; + return value; + }, + }); + + const copy = Hoek.clone(obj); + + expect(Hoek.deepEqual(obj, copy)).toBe(true); + expect(execCount).toEqual(2); + expect(copy.test).toEqual(1); + expect(execCount).toEqual(3); + }); + + it('compares objects with property getters', () => { + const obj = {}; + const ref = {}; + + Object.defineProperty(obj, 'test', { + enumerable: true, + configurable: true, + get: function () { + return 1; + }, + }); + + Object.defineProperty(ref, 'test', { + enumerable: true, + configurable: true, + get: function () { + return 2; + }, + }); + + expect(Hoek.deepEqual(obj, ref)).toBe(false); + }); + + it('compares object prototypes', () => { + interface Itf { + a: number; + } + + const Obj: any = function (this: Itf) { + this.a = 5; + }; + + Obj.prototype.b = function () { + return this.a; + }; + + const Ref: any = function (this: Itf) { + this.a = 5; + }; + + Ref.prototype.b = function () { + return this.a; + }; + + expect(Hoek.deepEqual(new Obj(), new Ref())).toBe(false); + expect(Hoek.deepEqual(new Obj(), new Obj())).toBe(true); + expect(Hoek.deepEqual(new Ref(), new Ref())).toBe(true); + }); + + it('compares plain objects', () => { + const a = Object.create(null); + const b = Object.create(null); + + a.b = 'c'; + b.b = 'c'; + + expect(Hoek.deepEqual(a, b)).toBe(true); + expect(Hoek.deepEqual(a, { b: 'c' })).toBe(false); + }); + + it('compares an object with an empty object', () => { + const a = { a: 1, b: 2 }; + + expect(Hoek.deepEqual({}, a)).toBe(false); + expect(Hoek.deepEqual(a, {})).toBe(false); + }); + + it('compares an object ignoring the prototype', () => { + const a = Object.create(null); + const b = {}; + + expect(Hoek.deepEqual(a, b, { prototype: false })).toBe(true); + }); + + it('compares an object ignoring the prototype recursively', () => { + const a = [Object.create(null)]; + const b = [{}]; + + expect(Hoek.deepEqual(a, b, { prototype: false })).toBe(true); + }); +}); + +describe('intersect()', () => { + it('returns the common objects of two arrays', () => { + const array1 = [1, 2, 3, 4, 4, 5, 5]; + const array2 = [5, 4, 5, 6, 7]; + const common = Hoek.intersect(array1, array2); + expect(common).toHoequal([5, 4]); + }); + + it('returns the common objects of array and set', () => { + const array1 = new Set([1, 2, 3, 4, 4, 5, 5]); + const array2 = [5, 4, 5, 6, 7]; + const common = Hoek.intersect(array1, array2); + expect(common).toHoequal([5, 4]); + }); + + it('returns the common objects of set and array', () => { + const array1 = [1, 2, 3, 4, 4, 5, 5]; + const array2 = new Set([5, 4, 5, 6, 7]); + const common = Hoek.intersect(array1, array2); + expect(common).toHoequal([5, 4]); + }); + + it('returns the common objects of two sets', () => { + const array1 = new Set([1, 2, 3, 4, 4, 5, 5]); + const array2 = new Set([5, 4, 5, 6, 7]); + const common = Hoek.intersect(array1, array2); + expect(common).toHoequal([5, 4]); + }); + + it('returns just the first common object of two arrays', () => { + const array1 = [1, 2, 3, 4, 4, 5, 5]; + const array2 = [5, 4, 5, 6, 7]; + const common = Hoek.intersect(array1, array2, { first: true }); + expect(common).toEqual(5); + }); + + it('returns null when no common and returning just the first common object of two arrays', () => { + const array1 = [1, 2, 3, 4, 4, 5, 5]; + const array2 = [6, 7]; + const common = Hoek.intersect(array1, array2, { first: true }); + expect(common).toBeNull(); + }); + + it('returns an empty array if either input is null', () => { + expect(Hoek.intersect([1], null)).toHaveLength(0); + expect(Hoek.intersect(null, [1])).toHaveLength(0); + expect(Hoek.intersect(null, [1], { first: true })).toBeNull(); + }); + + it('returns the common objects of object and array', () => { + const array1 = { 1: true, 2: true, 3: true, 4: true, 5: true }; + const array2 = [5, 4, 5, 6, 7]; + const common = Hoek.intersect(array1, array2) as number[]; + expect(common).toHaveLength(2); + }); +}); + +describe('contain()', () => { + it('tests strings', () => { + expect(Hoek.contain('abc', 'ab')).toBe(true); + expect(Hoek.contain('abc', 'abc', { only: true })).toBe(true); + expect(Hoek.contain('aaa', 'a', { only: true })).toBe(true); + expect(Hoek.contain('aaa', 'a', { only: true, once: true })).toBe(false); + expect(Hoek.contain('abc', 'b', { once: true })).toBe(true); + expect(Hoek.contain('abc', ['a', 'c'])).toBe(true); + expect(Hoek.contain('abc', ['a', 'd'], { part: true })).toBe(true); + expect(Hoek.contain('aaa', ['a', 'a'], { only: true, once: true })).toBe(false); + expect(Hoek.contain('aaa', ['a', 'a'], { only: true })).toBe(true); + expect(Hoek.contain('aaa', ['a', 'a', 'a'], { only: true, once: true })).toBe(true); + + expect(Hoek.contain('abc', 'ac')).toBe(false); + expect(Hoek.contain('abcd', 'abc', { only: true })).toBe(false); + expect(Hoek.contain('aab', 'a', { only: true })).toBe(false); + expect(Hoek.contain('abb', 'b', { once: true })).toBe(false); + expect(Hoek.contain('abc', ['a', 'd'])).toBe(false); + expect(Hoek.contain('abc', ['ab', 'bc'])).toBe(false); // Overlapping values not supported + + expect(Hoek.contain('', 'a')).toBe(false); + expect(Hoek.contain('', 'a', { only: true })).toBe(false); + + expect(Hoek.contain('', '')).toBe(true); + expect(Hoek.contain('', '', { only: true })).toBe(true); + expect(Hoek.contain('', '', { once: true })).toBe(true); + expect(Hoek.contain('', ['', ''])).toBe(true); + expect(Hoek.contain('', ['', ''], { only: true })).toBe(true); + expect(Hoek.contain('', ['', ''], { once: true })).toBe(false); + + expect(Hoek.contain('a', '')).toBe(true); + expect(Hoek.contain('a', '', { only: true })).toBe(false); + expect(Hoek.contain('a', '', { once: true })).toBe(false); + expect(Hoek.contain('a', ['', ''])).toBe(true); + expect(Hoek.contain('a', ['', ''], { only: true })).toBe(false); + expect(Hoek.contain('a', ['', ''], { once: true })).toBe(false); + + expect(Hoek.contain('ab', ['a', 'b', 'c'])).toBe(false); + expect(Hoek.contain('ab', ['a', 'b', 'c'], { only: true })).toBe(false); + expect(Hoek.contain('ab', ['a', 'b', 'c'], { only: true, once: true })).toBe(false); + + expect(Hoek.contain('ab', ['c'], { part: true })).toBe(false); + expect(Hoek.contain('ab', ['b'], { part: true })).toBe(true); + }); + + it('tests arrays', () => { + expect(Hoek.contain([1, 2, 3], 1)).toBe(true); + expect(Hoek.contain([{ a: 1 }], { a: 1 }, { deep: true })).toBe(true); + expect(Hoek.contain([1, 2, 3], [1, 2])).toBe(true); + expect(Hoek.contain([{ a: 1 }], [{ a: 1 }], { deep: true })).toBe(true); + expect(Hoek.contain([1, 1, 2], [1, 2], { only: true })).toBe(true); + expect(Hoek.contain([1, 2], [1, 2], { once: true })).toBe(true); + expect(Hoek.contain([1, 2, 3], [1, 4], { part: true })).toBe(true); + expect(Hoek.contain([null, 2, 3], [null, 4], { part: true })).toBe(true); + expect(Hoek.contain([null], null, { deep: true })).toBe(true); + expect(Hoek.contain([[1], [2]], [[1]], { deep: true })).toBe(true); + expect(Hoek.contain([[1], [2], 3], [[1]], { deep: true })).toBe(true); + expect(Hoek.contain([[1, 2]], [[1]], { deep: true, part: true })).toBe(true); + expect(Hoek.contain<(number[] | number)[]>([[1, 2]], [[1], 2], { deep: true, part: true })).toBe(true); + expect(Hoek.contain([1, 2, 1], [1, 1, 2], { only: true })).toBe(true); + expect(Hoek.contain([1, 2, 1], [1, 1, 2], { only: true, once: true })).toBe(true); + expect(Hoek.contain([1, 2, 1], [1, 2, 2], { only: true })).toBe(false); + expect(Hoek.contain([1, 2, 1], [1, 2, 2], { only: true, part: true })).toBe(true); + expect(Hoek.contain([1, 1, 1], [1, 1, 1, 1])).toBe(false); + expect(Hoek.contain([1, 1, 1], [1, 1, 1, 1], { part: true })).toBe(true); + + expect(Hoek.contain([1, 2, 3], 4)).toBe(false); + expect(Hoek.contain([{ a: 1 }], { a: 2 }, { deep: true })).toBe(false); + expect(Hoek.contain([{ a: 1 }, { a: 1 }], [{ a: 1 }, { a: 1 }], { deep: true, once: true, only: true })).toBe( + true, + ); + expect(Hoek.contain([{ a: 1 }, { a: 1 }], [{ a: 1 }, { a: 2 }], { deep: true, once: true, only: true })).toBe( + false, + ); + expect(Hoek.contain([{ a: 1 }], { a: 1 })).toBe(false); + expect(Hoek.contain([1, 2, 3], [4, 5])).toBe(false); + expect(Hoek.contain([[3], [2]], [[1]])).toBe(false); + expect(Hoek.contain([[1], [2]], [[1]])).toBe(false); + expect(Hoek.contain([[1, 2]], [[1]], { deep: true })).toBe(false); + expect(Hoek.contain([{ a: 1 }], [{ a: 2 }], { deep: true })).toBe(false); + expect(Hoek.contain([1, 3, 2], [1, 2], { only: true })).toBe(false); + expect(Hoek.contain([1, 2, 2], [1, 2], { once: true })).toBe(false); + expect(Hoek.contain([0, 2, 3], [1, 4], { part: true })).toBe(false); + expect(Hoek.contain([1, 2, 1], [1, 2, 2], { only: true, once: true })).toBe(false); + expect(Hoek.contain([1, 2, 1], [1, 2], { only: true, once: true })).toBe(false); + + expect(Hoek.contain([], 1)).toBe(false); + expect(Hoek.contain([], 1, { only: true })).toBe(false); + + expect(Hoek.contain(['a', 'b'], ['a', 'b', 'c'])).toBe(false); + expect(Hoek.contain(['a', 'b'], ['a', 'b', 'c'], { only: true })).toBe(false); + expect(Hoek.contain(['a', 'b'], ['a', 'b', 'c'], { only: true, once: true })).toBe(false); + + expect(Hoek.contain(['a', 'b'], ['c'], { part: true })).toBe(false); + expect(Hoek.contain(['a', 'b'], ['b'], { part: true })).toBe(true); + + expect(Hoek.contain([{ a: 1 }], [1], { deep: true })).toBe(false); + }); + + it('tests objects', () => { + type TTnum = Record; + type TTstr = Record; + type TTo = Record; + type TTn = Record; + type TTa = Record; + + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, 'a')).toBe(true); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, ['a', 'c'])).toBe(true); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, ['a', 'b', 'c'], { only: true })).toBe(true); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1 })).toBe(true); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, c: 3 })).toBe(true); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 4 }, { part: true })).toBe(true); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 }, { only: true })).toBe(true); + expect(Hoek.contain({ a: [1], b: [2], c: [3] }, { a: [1], c: [3] }, { deep: true })).toBe(true); + expect( + Hoek.contain({ a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, { a: [{ b: 1 }, { d: 3 }] }, { deep: true }), + ).toBe(false); + expect( + Hoek.contain( + { a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, + { a: [{ b: 1 }, { d: 3 }] }, + { deep: true, part: true }, + ), + ).toBe(true); + expect( + Hoek.contain( + { a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, + { a: [{ b: 1 }, { d: 3 }] }, + { deep: true, part: false }, + ), + ).toBe(false); + expect( + Hoek.contain( + { a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, + { a: [{ b: 1 }, { d: 3 }] }, + { deep: true, only: true }, + ), + ).toBe(false); + expect( + Hoek.contain( + { a: [{ b: 1 }, { c: 2 }, { d: 3, e: 4 }] }, + { a: [{ b: 1 }, { d: 3 }] }, + { deep: true, only: false }, + ), + ).toBe(true); + expect(Hoek.contain({ a: [1, 2, 3] }, { a: [2, 4, 6] }, { deep: true, part: true })).toBe(true); + + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, 'd')).toBe(false); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, ['a', 'd'])).toBe(false); + expect(Hoek.contain({ a: 1, b: 2, c: 3, d: 4 }, ['a', 'b', 'c'], { only: true })).toBe(false); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 2 })).toBe(false); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 2, b: 2 }, { part: true })).toBe(false); // part does not ignore bad value + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 3 })).toBe(false); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, d: 4 })).toBe(false); + expect(Hoek.contain({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }, { only: true })).toBe(false); + expect(Hoek.contain({ a: [1], b: [2], c: [3] }, { a: [1], c: [3] })).toBe(false); + expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } })).toBe(false); + expect(Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true })).toBe(false); + expect( + Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, only: true }), + ).toBe(false); + expect( + Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, only: false }), + ).toBe(true); + expect( + Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, part: true }), + ).toBe(true); + expect( + Hoek.contain({ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 1 } } }, { deep: true, part: false }), + ).toBe(false); + expect(Hoek.contain({ a: [1, 2, 3] }, { a: [4, 5, 6] }, { deep: true, part: true })).toBe(false); + + expect(Hoek.contain({}, 'a')).toBe(false); + expect(Hoek.contain({}, 'a', { only: true })).toBe(false); + + expect(Hoek.contain({ a: 'foo', b: 'bar' }, ['a', 'b', 'c'])).toBe(false); + expect(Hoek.contain({ a: 'foo', b: 'bar' }, ['a', 'b', 'c'], { only: true })).toBe(false); + expect(Hoek.contain({ a: 'foo', b: 'bar' }, { a: 'foo', b: 'bar', c: 'x' })).toBe(false); + expect(Hoek.contain({ a: 'foo', b: 'bar' }, { a: 'foo', b: 'bar', c: 'x' }, { only: true })).toBe(false); + + expect(Hoek.contain({ a: 1, b: 2 }, ['c'], { part: true })).toBe(false); + expect(Hoek.contain({ a: 1, b: 2 }, ['b'], { part: true })).toBe(true); + + // Getter check + + { + type FooType = { + bar?: string; + baz?: string; + }; + + interface FooItf extends FooType { + new (bar: string): FooType; + } + + type FooRecord = Record; + + const Foo = function (this: FooItf, bar: string) { + this.bar = bar; + } as unknown as FooItf; + + const getBar = function (this: FooItf) { + return this.bar; + }; + + const createFoo = (value: string) => { + const foo = new Foo(value); + + Object.defineProperty(foo, 'baz', { + enumerable: true, + get: getBar, + }); + + return foo; + }; + + expect(Hoek.contain({ a: createFoo('b') }, { a: createFoo('b') }, { deep: true })).toBe(true); + expect(Hoek.contain({ a: createFoo('b') }, { a: createFoo('b') }, { deep: true, part: true })).toBe(true); + expect(Hoek.contain({ a: createFoo('b') }, { a: { bar: 'b', baz: 'b' } }, { deep: true })).toBe( + true, + ); + expect( + Hoek.contain( + { a: createFoo('b') }, + { a: { bar: 'b', baz: 'b' } }, + { deep: true, only: true }, + ), + ).toBe(false); + expect( + Hoek.contain({ a: createFoo('b') }, { a: { baz: 'b' } }, { deep: true, part: false }), + ).toBe(false); + expect( + Hoek.contain({ a: createFoo('b') }, { a: { baz: 'b' } }, { deep: true, part: true }), + ).toBe(true); + expect(Hoek.contain({ a: createFoo('b') }, { a: createFoo('b') }, { deep: true })).toBe(true); + } + + // Properties on prototype not visible + + { + type FooType = { + a: number; + b: number; + c: number; + }; + + interface FooItf extends FooType { + new (): FooType; + } + + const Foo = function (this: FooItf) { + this.a = 1; + } as unknown as FooItf; + + Object.defineProperty(Foo.prototype, 'b', { + enumerable: true, + value: 2, + }); + + const Bar = function (this: FooItf) { + Foo.call(this); + this.c = 3; + } as unknown as FooItf; + + Util.inherits(Bar, Foo); + + expect(new Bar().a).toEqual(1); + expect(new Bar().b).toEqual(2); + expect(new Bar().c).toEqual(3); + expect(Hoek.contain(new Bar(), { a: 1, c: 3 }, { only: true })).toBe(true); + expect(Hoek.contain(new Bar(), 'b')).toBe(false); + } + + // Non-Enumerable properties + + { + const foo = { a: 1, b: 2 } as { + a: number; + b: number; + c?: number; + }; + + Object.defineProperty(foo, 'c', { + enumerable: false, + value: 3, + }); + + expect(Hoek.contain(foo, 'c')).toBe(true); + expect(Hoek.contain(foo, { c: 3 })).toBe(true); + expect(Hoek.contain(foo, { a: 1, b: 2, c: 3 }, { only: true })).toBe(true); + } + }); + + it('supports symbols', () => { + const sym = Symbol(); + + expect(Hoek.contain([sym], sym)).toBe(true); + expect(Hoek.contain({ [sym]: 1 }, sym)).toBe(true); + expect(Hoek.contain({ [sym]: 1, a: 2 }, { [sym]: 1 })).toBe(true); + + expect(Hoek.contain([sym], Symbol())).toBe(false); + expect(Hoek.contain({ [sym]: 1 }, Symbol())).toBe(false); + }); + + it('compares error keys', () => { + const error = new Error('test') as Error & { + x?: number; + y?: number; + }; + + expect(Hoek.contain(error, { x: 1 })).toBe(false); + expect(Hoek.contain(error, { x: 1 }, { part: true })).toBe(false); + + error.x = 1; + + expect(Hoek.contain(error, { x: 1 })).toBe(true); + expect(Hoek.contain(error, { x: 1 }, { part: true })).toBe(true); + + expect(Hoek.contain(error, { x: 1, y: 2 })).toBe(false); + expect(Hoek.contain(error, { x: 1, y: 2 }, { part: true })).toBe(true); + }); +}); + +describe('flatten()', () => { + it('returns a flat array', () => { + const result = Hoek.flatten([1, 2, [3, 4, [5, 6], [7], 8], [9], [10, [11, 12]], 13]); + expect(result).toHaveLength(13); + expect(result).toHoequal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + }); + + it('appends to a target array', () => { + const target = [4, [5]]; + const result = Hoek.flatten([1, [2, 3]], target); + + expect(result).toBe(target); // Target is modified in place + expect(result).toHoequal([4, [5], 1, 2, 3]); // Target items are not flattened + }); + + it('keeps array holes', () => { + // oxlint-disable-next-line no-sparse-arrays -- the hole is the point of the test + expect(Hoek.flatten([1, , 2])).toHoequal([1, undefined, 2]); + }); +}); + +describe('reach()', () => { + const sym = Symbol(); + const obj = { + a: { + b: { + c: { + d: 1, + e: 2, + }, + f: 'hello', + }, + g: { + h: 3, + }, + '-2': true, + [sym]: { + v: true, + }, + }, + i: function () {} as (() => any) & { x: number }, + j: null, + k: [4, 8, 9, 1], + }; + + obj.i.x = 5; + + it('returns object itself', () => { + expect(Hoek.reach(obj, null)).toHoequal(obj); + expect(Hoek.reach(obj, false)).toHoequal(obj); + expect(Hoek.reach(obj)).toHoequal(obj); + expect(Hoek.reach(obj, [])).toHoequal(obj); + }); + + it('returns values of array', () => { + expect(Hoek.reach(obj, 'k.0')).toEqual(4); + expect(Hoek.reach(obj, 'k.1')).toEqual(8); + }); + + it('returns last value of array using negative index', () => { + expect(Hoek.reach(obj, 'k.-1')).toEqual(1); + expect(Hoek.reach(obj, 'k.-2')).toEqual(9); + }); + + it('returns object property with negative index for non-array', () => { + expect(Hoek.reach(obj, 'a.-2')).toHoequal(true); + }); + + it('returns a valid member', () => { + expect(Hoek.reach(obj, 'a.b.c.d')).toEqual(1); + }); + + it('returns a valid member with separator override', () => { + expect(Hoek.reach(obj, 'a/b/c/d', '/')).toEqual(1); + }); + + it('returns undefined on null object', () => { + expect(Hoek.reach(null, 'a.b.c.d')).toBeUndefined(); + }); + + it('returns undefined on missing object member', () => { + expect(Hoek.reach(obj, 'a.b.c.d.x')).toBeUndefined(); + }); + + it('returns undefined on missing function member', () => { + expect(Hoek.reach(obj, 'i.y', { functions: true })).toBeUndefined(); + }); + + it('throws on missing member in strict mode', () => { + expect(() => { + Hoek.reach(obj, 'a.b.c.o.x', { strict: true }); + }).toThrow('Missing segment o in reach path a.b.c.o.x'); + }); + + it('returns undefined on invalid member', () => { + expect(Hoek.reach(obj, 'a.b.c.d-.x')).toBeUndefined(); + expect(Hoek.reach(obj, 'k.x')).toBeUndefined(); + expect(Hoek.reach(obj, 'k.1000')).toBeUndefined(); + expect(Hoek.reach(obj, 'k/0.5', '/')).toBeUndefined(); + }); + + it('returns function member', () => { + expect(typeof Hoek.reach(obj, 'i')).toEqual('function'); + }); + + it('returns function property', () => { + expect(Hoek.reach(obj, 'i.x')).toEqual(5); + }); + + it('returns null', () => { + expect(Hoek.reach(obj, 'j')).toBeNull(); + }); + + it('throws on function property when functions not allowed', () => { + expect(() => { + Hoek.reach(obj, 'i.x', { functions: false }); + }).toThrow('Invalid segment x in reach path i.x'); + }); + + it('will return a default value if property is not found', () => { + expect(Hoek.reach(obj, 'a.b.q', { default: 'defaultValue' })).toEqual('defaultValue'); + }); + + it('will return a default value if path is not found', () => { + expect(Hoek.reach(obj, 'q', { default: 'defaultValue' })).toEqual('defaultValue'); + }); + + it('allows a falsey value to be used as the default value', () => { + expect(Hoek.reach(obj, 'q', { default: '' })).toEqual(''); + }); + + it('allows array-based lookup', () => { + expect(Hoek.reach(obj, ['a', 'b', 'c', 'd'])).toEqual(1); + expect(Hoek.reach(obj, ['k', '1'])).toEqual(8); + expect(Hoek.reach(obj, ['k', 1])).toEqual(8); + expect(Hoek.reach(obj, ['k', '-2'])).toEqual(9); + expect(Hoek.reach(obj, ['k', -2])).toEqual(9); + }); + + it('allows array-based lookup with symbols', () => { + expect(Hoek.reach(obj, ['a', sym, 'v'])).toHoequal(true); + expect(Hoek.reach(obj, ['a', Symbol(), 'v'])).toBeUndefined(); + }); + + it('returns character in string', () => { + expect(Hoek.reach(['abc'], [0])).toEqual('abc'); + expect(Hoek.reach(['abc'], ['0'])).toEqual('abc'); + }); + + it('reaches sets and maps', () => { + const value = { + a: { + b: new Set([ + { x: 1 }, + { x: 2 }, + { + y: new Map([ + ['v', 4], + ['w', 5], + ]), + }, + ]), + }, + }; + + expect(Hoek.reach(value, 'a.b.2.y.w')).toBeUndefined(); + expect(Hoek.reach(value, 'a.b.2.y.w', { iterables: true })).toEqual(5); + }); + + it('reaches set and map properties when iterables is not enabled', () => { + const set = new Set([1, 2]) as Set & { custom: string }; + set.custom = 'x'; + + expect(Hoek.reach(set, 'size')).toEqual(2); + expect(Hoek.reach(set, 'custom')).toEqual('x'); + expect(Hoek.reach(new Map([['a', 1]]), 'size')).toEqual(1); + }); + + it('throws on a set or map segment when strict is enabled', () => { + const value = { a: new Map([['b', { c: 1 }]]) }; + + expect(() => Hoek.reach(value, 'a.b.c', { strict: true })).toThrow('Missing segment b in reach path a.b.c'); + expect(Hoek.reach(value, 'a.b.c')).toBeUndefined(); + expect(Hoek.reach(value, 'a.b.c', { iterables: true })).toEqual(1); + }); +}); + +describe('reachTemplate()', () => { + it('applies object to template', () => { + const obj = { + a: { + b: { + c: { + d: 1, + }, + }, + }, + j: null, + k: [4, 8, 9, 1], + }; + + const template = '{k.0}:{k.-2}:{a.b.c.d}:{x.y}:{j}'; + + expect(Hoek.reachTemplate(obj, template)).toEqual('4:9:1::'); + }); + + it('applies object to template (options)', () => { + const obj = { + a: { + b: { + c: { + d: 1, + }, + }, + }, + j: null, + k: [4, 8, 9, 1], + }; + + const template = '{k/0}:{k/-2}:{a/b/c/d}:{x/y}:{j}'; + + expect(Hoek.reachTemplate(obj, template, '/')).toEqual('4:9:1::'); + }); + + it("isn't prone to ReDoS given an adversarial template", () => { + const sizes = [0, 1, 2, 3, 4]; // Should be evenly-spaced + const times = []; + const diffs = []; + + for (const size of sizes) { + const start = Date.now(); + Hoek.reachTemplate({}, '{'.repeat(size * 10000)); + times.push(Date.now() - start); + } + + for (let i = 1; i < times.length; ++i) { + diffs.push(times[i]! - times[i - 1]!); + } + + // Under ReDoS, as the size of the input increases the timing accelerates upwards, + // i.e. each timing diff would be greater than the last. + + const diffsMonotonic = diffs[0]! < diffs[1]! && diffs[1]! < diffs[2]! && diffs[2]! < diffs[3]!; + + expect(diffsMonotonic, 'Timing diffs monotonic').toBe(false); + }); +}); + +describe('assert()', () => { + it('throws an Error when using assert in a test', () => { + expect(() => { + Hoek.assert(false, 'my error message'); + }).toThrow(new Hoek.AssertError('my error message')); + }); + + it('throws an Error when using assert in a test with no message', () => { + expect(() => { + Hoek.assert(false); + }).toThrow(new Hoek.AssertError('Unknown error')); + }); + + it('throws an Error when using assert in a test with multipart message', () => { + expect(() => { + Hoek.assert(false, 'This', 'is', 'my message'); + }).toThrow(new Hoek.AssertError('This is my message')); + }); + + it('throws an Error when using assert in a test with multipart message (empty)', () => { + expect(() => { + Hoek.assert(false, 'This', 'is', '', 'my message'); + }).toThrow(new Hoek.AssertError('This is my message')); + }); + + it('throws an Error when using assert in a test with object message', () => { + expect(() => { + Hoek.assert(false, 'This', 'is', { spinal: 'tap' } as never); + }).toThrow(new Hoek.AssertError('This is {"spinal":"tap"}')); + }); + + it('throws an Error when using assert in a test with multipart string and error messages', () => { + expect(() => { + Hoek.assert(false, new Error('This'), 'is', 'spinal', new Error('tap')); + }).toThrow(new Hoek.AssertError('This is spinal tap')); + }); + + it('throws an Error when using assert in a test with error object message', () => { + const err = new TypeError('This is spinal tap'); + let got; + expect(() => { + try { + Hoek.assert(false, err); + } catch (e) { + got = e; + throw e; + } + }).toThrow(new TypeError('This is spinal tap')); + expect(got).toBe(err); + }); + + it('throws the same Error that is passed to it if there is only one error passed', () => { + const error = new Error('ruh roh'); + const error2 = new Error('ruh roh'); + + const fn = function () { + Hoek.assert(false, error); + }; + + try { + fn(); + } catch (err) { + expect(err).toHoequal(error); // should be the same reference + expect(err).not.toBe(error2); // error with the same message should not match + } + }); +}); + +describe('AssertError', () => { + it('takes an optional message', () => { + expect(new Hoek.AssertError().message).toEqual('Unknown error'); + expect(new Hoek.AssertError(null).message).toEqual('Unknown error'); + expect(new Hoek.AssertError('msg').message).toEqual('msg'); + }); + + it('has AssertError name property', () => { + expect(new Hoek.AssertError().name).toEqual('AssertError'); + expect(new Hoek.AssertError('msg').name).toEqual('AssertError'); + }); + + it('uses ctor argument to hide stack', { skip: typeof Error.captureStackTrace !== 'function' }, () => { + const parentFn = () => { + throw new Hoek.AssertError('msg', parentFn); + }; + + let err; + expect(() => { + try { + parentFn(); + } catch (e) { + err = e; + throw e; + } + }).toThrow(Hoek.AssertError); + + expect(err!.stack).not.toContain('parentFn'); + }); +}); + +describe('Bench', () => { + it('returns time elapsed', async () => { + const timer = new Hoek.Bench(); + await Hoek.wait(12); + expect(timer.elapsed()).toBeGreaterThan(9); + }); +}); + +describe('escapeRegex()', () => { + it('escapes all special regular expression characters', () => { + const a = Hoek.escapeRegex('4^f$s.4*5+-_?%=#!:@|~\\/`"(>)[<]d{}s,'); + expect(a).toEqual('4\\^f\\$s\\.4\\*5\\+\\-_\\?%\\=#\\!\\:@\\|~\\\\\\/`"\\(>\\)\\[<\\]d\\{\\}s\\,'); + }); +}); + +describe('escapeHeaderAttribute()', () => { + it('should not alter ascii values', () => { + const a = Hoek.escapeHeaderAttribute('My Value'); + expect(a).toEqual('My Value'); + }); + + it('escapes all special HTTP header attribute characters', () => { + const a = Hoek.escapeHeaderAttribute('I said go!!!#"' + String.fromCharCode(92)); + expect(a).toEqual('I said go!!!#\\"\\\\'); + }); + + it('throws on large unicode characters', () => { + expect(() => { + Hoek.escapeHeaderAttribute('this is a test' + String.fromCharCode(500) + String.fromCharCode(300)); + }).toThrow(Error); + }); + + it('throws on CRLF to prevent response splitting', () => { + expect(() => { + Hoek.escapeHeaderAttribute('this is a test\r\n'); + }).toThrow(Error); + }); +}); + +describe('escapeHtml()', () => { + it('escapes all special HTML characters', () => { + const a = Hoek.escapeHtml('&<>"\'`'); + expect(a).toEqual('&<>"'`'); + }); + + it('returns empty string on falsy input', () => { + const a = Hoek.escapeHtml(''); + expect(a).toEqual(''); + }); + + it('returns unchanged string on no reserved input', () => { + const a = Hoek.escapeHtml('abc'); + expect(a).toEqual('abc'); + }); +}); + +describe('once()', () => { + it('allows function to only execute once', () => { + let gen = 0; + let add = function (x: number) { + gen += x; + }; + + add(5); + expect(gen).toEqual(5); + add = Hoek.once(add); + add(5); + expect(gen).toEqual(10); + add(5); + expect(gen).toEqual(10); + }); + + it('double once wraps one time', () => { + let method = function () {} as (() => void) & { x?: number }; + method = Hoek.once(method); + method.x = 1; + method = Hoek.once(method); + expect(method.x).toEqual(1); + }); +}); + +describe('ignore()', () => { + it('exists', () => { + expect(Hoek.ignore).toBeDefined(); + expect(typeof Hoek.ignore).toEqual('function'); + }); +}); + +describe('stringify()', () => { + it('converts object to string', () => { + const obj = { a: 1 }; + expect(Hoek.stringify(obj)).toEqual('{"a":1}'); + }); + + it('returns error in result string', () => { + const obj = { a: 1 } as any; + obj.b = obj; + expect(Hoek.stringify(obj)).toContain('Cannot display object'); + }); +}); + +describe('isPromise()', () => { + it('determines if an object is a promise', async () => { + expect(Hoek.isPromise({})).toBe(false); + expect(Hoek.isPromise(null)).toBe(false); + expect(Hoek.isPromise(false)).toBe(false); + expect(Hoek.isPromise(0)).toBe(false); + expect(Hoek.isPromise('')).toBe(false); + expect(Hoek.isPromise({ then: 1 })).toBe(false); + expect(Hoek.isPromise([])).toBe(false); + + const items = [Promise.resolve(), Promise.reject()]; + + expect(Hoek.isPromise(items[0])).toBe(true); + expect(Hoek.isPromise(items[1])).toBe(true); + expect(Hoek.isPromise(new Promise(Hoek.ignore))).toBe(true); + expect(Hoek.isPromise({ then: Hoek.ignore })).toBe(true); + + try { + await Promise.all(items); + } catch {} + }); +}); + +describe('wait()', () => { + it('delays for timeout ms', async () => { + const timeout = {} as { before?: boolean; after?: boolean }; + setTimeout(() => (timeout.before = true), 10); + const wait = Hoek.wait(10); + setTimeout(() => (timeout.after = true), 10); + + await wait; + + expect(timeout.before).toBe(true); + expect(timeout.after).toBeUndefined(); + }); + + it('delays for timeout ms as bigint', async () => { + const timeout = {} as { before?: boolean; after?: boolean }; + setTimeout(() => (timeout.before = true), 10); + const wait = Hoek.wait(10n); + setTimeout(() => (timeout.after = true), 10); + + await wait; + + expect(timeout.before).toBe(true); + expect(timeout.after).toBeUndefined(); + }); + + it('handles timeouts >= 2^31', async () => { + const flow = []; + let no = 0; + + const fakeTimeout = function (cb: Function, time: number) { + const timer = ++no; + + flow.push(`CALL(${timer}): ${time}`); + setImmediate(() => { + flow.push(`PRE(${timer})`); + cb(); + flow.push(`POST(${timer})`); + }); + }; + + await Hoek.wait(2 ** 31, null, { setTimeout: fakeTimeout }); + flow.push('DONE1'); + await Hoek.wait(2 ** 32 + 2 ** 30, null, { setTimeout: fakeTimeout }); + flow.push('DONE2'); + + expect(flow).toHoequal([ + 'CALL(1): 2147483647', + 'PRE(1)', + 'CALL(2): 1', + 'POST(1)', + 'PRE(2)', + 'POST(2)', + 'DONE1', + 'CALL(3): 2147483647', + 'PRE(3)', + 'CALL(4): 2147483647', + 'POST(3)', + 'PRE(4)', + 'CALL(5): 1073741826', + 'POST(4)', + 'PRE(5)', + 'POST(5)', + 'DONE2', + ]); + }); + + it('returns never resolving promise when timeout >= Number.MAX_SAFE_INTEGER', async () => { + let calls = 0; + const fakeTimeout = function (cb: Function) { + ++calls; + process.nextTick(cb); + }; + + await Hoek.wait(2 ** 31 - 1, null, { setTimeout: fakeTimeout }); + expect(calls).toEqual(1); + + const waited = Symbol('waited'); + + const result = await Promise.race([ + Hoek.wait(1, waited), + Hoek.wait(Number.MAX_SAFE_INTEGER, null, { setTimeout: fakeTimeout }), + Hoek.wait(Infinity, null, { setTimeout: fakeTimeout }), + ]); + + expect(result).toHoequal(waited); + expect(calls).toEqual(1); + }); + + it('handles a return value', async () => { + const uniqueValue = {}; + const timeout = {} as { before?: boolean; after?: boolean }; + setTimeout(() => (timeout.before = true), 10); + const wait = Hoek.wait(10, uniqueValue); + setTimeout(() => (timeout.after = true), 10); + + expect(await wait).toBe(uniqueValue); + expect(timeout.before).toBe(true); + expect(timeout.after).toBeUndefined(); + }); + + it('undefined timeout resolves immediately', async () => { + const waited = Symbol('waited'); + const result = await Promise.race([Hoek.wait(undefined, waited), Hoek.wait(0)]); + + expect(result).toHoequal(waited); + }); + + it('NaN timeout resolves immediately', async () => { + const waited = Symbol('waited'); + const result = await Promise.race([Hoek.wait(Number.NaN, waited), Hoek.wait(0)]); + + expect(result).toHoequal(waited); + }); + + it('rejects on weird timeout values', async () => { + await expect(() => Hoek.wait({} as never)).toThrow(); + await expect(() => Hoek.wait(Symbol('hi') as never)).toThrow(); + }); +}); + +describe('block()', () => { + it('returns a promise', () => { + expect(Hoek.block()).toBeInstanceOf(Promise); + }); + + it('does not immediately reject or resolve', async () => { + const promise = Hoek.block(); + const waited = Symbol('waited'); + + const result = await Promise.race([Hoek.wait(1, waited), promise]); + + expect(result).toHoequal(waited); + }); +}); diff --git a/test/typings.ts b/test/typings.ts new file mode 100755 index 00000000..fb9f22a4 --- /dev/null +++ b/test/typings.ts @@ -0,0 +1,515 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import * as Hoek from '../src/index.js'; + +interface Foo { + a?: number; + b?: string; +} + +interface Bar { + b?: string; + c?: boolean; +} + +describe('typings', () => { + it('exposes functions directly', () => { + expectTypeOf(Hoek.deepEqual).toBeFunction(); + expectTypeOf(Hoek.deepEqual(1, 2)).toBeBoolean(); + }); + + describe('deepEqual()', () => { + it('accepts valid calls', () => { + Hoek.deepEqual('some', 'some'); + Hoek.deepEqual('some', 3); + Hoek.deepEqual({}, {}); + Hoek.deepEqual({}, {}, { prototype: false, symbols: true, part: false, deepFunction: true }); + Hoek.deepEqual({}, {}, { skip: ['a', 'b', Symbol('test')] }); + + expectTypeOf(Hoek.deepEqual(1, 2)).toBeBoolean(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires two arguments + Hoek.deepEqual(); + // @ts-expect-error too many arguments + Hoek.deepEqual(1, 2, {}, 'x'); + // @ts-expect-error unknown option + Hoek.deepEqual({}, {}, { unknown: true }); + // @ts-expect-error skip must be string/symbol keys + Hoek.deepEqual({}, {}, { skip: [1] }); + } catch {} + }); + }); + + describe('clone()', () => { + it('accepts valid calls', () => { + Hoek.clone('string'); + Hoek.clone(123); + Hoek.clone({ a: 1 }); + Hoek.clone({ a: 1 }, { prototype: true, symbols: true }); + Hoek.clone({}, { shallow: [] }); + Hoek.clone(1, { shallow: [] }); + Hoek.clone(null, { shallow: [] }); + Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: true }); + Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: ['b'] }); + Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: [['b']] }); + Hoek.clone({ a: 1, b: { c: [2] } }, { shallow: ['b'], prototype: true, symbols: true }); + + expectTypeOf(Hoek.clone('string')).toBeString(); + expectTypeOf(Hoek.clone({})).toBeObject(); + expectTypeOf(Hoek.clone({} as Bar)).toEqualTypeOf(); + expectTypeOf(Hoek.clone({ a: 1 } as Foo, { shallow: ['b'] })).toEqualTypeOf(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error unknown option + Hoek.clone({}, { unknown: true }); + // @ts-expect-error shallow entries must be paths + Hoek.clone({}, { shallow: [null] }); + // @ts-expect-error shallow must be an array or boolean + Hoek.clone({}, { shallow: 1 }); + } catch {} + }); + }); + + describe('merge()', () => { + it('accepts valid calls', () => { + Hoek.merge({ a: 1 } as Foo, { b: 'x' } as Bar); + Hoek.merge({ a: 1 }, { a: null }); + Hoek.merge({ a: 1 }, { a: null }, { mergeArrays: true, nullOverride: true, symbols: false }); + + expectTypeOf(Hoek.merge({}, {})).toBeObject(); + expectTypeOf(Hoek.merge({ a: 1 } as Foo, { b: 'x' } as Bar)).toEqualTypeOf(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error cannot merge non-object + Hoek.merge(1, 2); + // @ts-expect-error options must be an object + Hoek.merge({ a: 1 }, { a: null }, true); + // @ts-expect-error unknown option + Hoek.merge({ a: 1 }, { a: null }, { unknown: true }); + } catch {} + }); + }); + + describe('applyToDefaults()', () => { + it('accepts valid calls', () => { + Hoek.applyToDefaults({}, {}); + Hoek.applyToDefaults({}, true); + Hoek.applyToDefaults({}, false); + Hoek.applyToDefaults({}, null); + Hoek.applyToDefaults({ a: 1 } as Foo, { b: 'x' }); + Hoek.applyToDefaults({ a: 1 } as object, { b: 'x' }); + Hoek.applyToDefaults({ a: 1 } as Foo, { b: 'x' }, { shallow: ['b'] }); + Hoek.applyToDefaults({ a: 1 } as object, { b: 'x' }, { shallow: ['c'] }); + + expectTypeOf(Hoek.applyToDefaults({}, {})!).toBeObject(); + expectTypeOf(Hoek.applyToDefaults({ a: 1 } as Foo, { b: 'x' })!).toBeObject(); + expectTypeOf(Hoek.applyToDefaults({}, {}, { shallow: [] })!).toBeObject(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error source must be an object, falsy, or true + Hoek.applyToDefaults({} as Foo, 'zzz'); + // @ts-expect-error unknown option + Hoek.applyToDefaults({}, {}, { unknown: true }); + } catch {} + }); + }); + + describe('intersect()', () => { + it('accepts valid calls', () => { + Hoek.intersect([1], [1, 2]); + Hoek.intersect(['a'], ['b', 'b']); + Hoek.intersect([1], [1, 2], { first: true }); + Hoek.intersect([1], [1, 2], { first: false }); + Hoek.intersect([1], null); + Hoek.intersect(null, [1, 2]); + Hoek.intersect([1], null, { first: true }); + Hoek.intersect(null, [1, 2], { first: true }); + Hoek.intersect(new Set([1]), new Set([1, 2])); + Hoek.intersect([1], new Set([1, 2])); + Hoek.intersect(new Set([1]), [1, 2]); + + expectTypeOf(Hoek.intersect([1], [1, 2])).toEqualTypeOf(); + expectTypeOf(Hoek.intersect([1], [1, 2], { first: false })).toEqualTypeOf(); + expectTypeOf(Hoek.intersect([1], [1, 2], { first: true })).toEqualTypeOf(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires two arguments + Hoek.intersect(); + // @ts-expect-error arguments must be arrays or sets + Hoek.intersect(1, 2); + // @ts-expect-error second argument must be an array or set + Hoek.intersect([1], 2); + // @ts-expect-error options must be an object + Hoek.intersect([1], [2], 'x'); + // @ts-expect-error maps are not supported + Hoek.intersect(new Map(), [2]); + } catch {} + }); + }); + + describe('contain()', () => { + it('accepts valid calls', () => { + Hoek.contain('abc', 'a'); + Hoek.contain('abc', ['a', 'x']); + Hoek.contain('abc', ['a', 'd'], { once: true, part: true, deep: true, symbols: true, only: true }); + Hoek.contain({ a: 1 }, 'a'); + Hoek.contain({ a: 1 }, { a: 1 }); + Hoek.contain({ a: 1, b: 2 }, ['a', 'b'], { part: true, deep: true, symbols: true, only: true }); + Hoek.contain([1], 1); + Hoek.contain([1], [1]); + Hoek.contain([1], [1], { once: true, part: true, deep: true, symbols: true, only: true }); + + expectTypeOf(Hoek.contain('abc', 'a')).toBeBoolean(); + expectTypeOf(Hoek.contain({ a: 1 }, 'a')).toBeBoolean(); + expectTypeOf(Hoek.contain([1], 1)).toBeBoolean(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error unknown option + Hoek.contain('abc', 'a', { unknown: true }); + // @ts-expect-error unknown option + Hoek.contain(['a'], 'a', { unknown: true }); + // @ts-expect-error unknown option + Hoek.contain({ a: 1 }, 'a', { unknown: true }); + // @ts-expect-error string haystack needs string values + Hoek.contain('abc', [{}]); + // @ts-expect-error string haystack needs string values + Hoek.contain('abc', {}); + // @ts-expect-error value must be a known key of the object + Hoek.contain({ a: 1, b: 2 }, ['a', 'x']); + } catch {} + }); + }); + + describe('flatten()', () => { + it('accepts valid calls', () => { + Hoek.flatten([1, [2, 3]]); + Hoek.flatten([1, [2, 3]], [4, [5]]); + + expectTypeOf(Hoek.flatten([1, [2, 3]])).toBeArray(); + expectTypeOf(Hoek.flatten([1, [2, [3, [4]]]])).toEqualTypeOf(); + expectTypeOf(Hoek.flatten([1, ['a']])).toEqualTypeOf<(number | string)[]>(); + expectTypeOf(Hoek.flatten([1, [2]], [3, [4]])).toEqualTypeOf<(number | number[])[]>(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires an array + Hoek.flatten(); + // @ts-expect-error argument must be an array + Hoek.flatten(1); + // @ts-expect-error target must be an array + Hoek.flatten([], 1); + } catch {} + }); + }); + + describe('reach()', () => { + it('accepts valid calls', () => { + Hoek.reach([]); + Hoek.reach(null, false); + Hoek.reach(null, '0'); + Hoek.reach(null, ['0']); + Hoek.reach(['abc'], false); + Hoek.reach(['abc'], null); + Hoek.reach(['abc'], undefined); + Hoek.reach(['abc'], [0]); + Hoek.reach(['abc'], ['0']); + Hoek.reach(['abc'], '0'); + Hoek.reach({ a: { b: { c: 3 } } }, 'a.b.c'); + Hoek.reach({ a: { b: { c: 3 } } }, ['a', 'b', 'c']); + Hoek.reach({ a: { b: { c: 3 } } }, 'a/b/c', { + separator: '/', + default: 4, + strict: true, + functions: true, + iterables: true, + }); + + expectTypeOf(Hoek.reach(['abc'], [0])).toBeUnknown(); + expectTypeOf(Hoek.reach({ a: { b: { c: 3 } } }, 'a.b.c')).toBeUnknown(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires an object or null + Hoek.reach(); + // @ts-expect-error obj must be an object or null + Hoek.reach(1, '0'); + // @ts-expect-error obj must be an object or null + Hoek.reach('abc', '0'); + // @ts-expect-error chain must be a string/array/null/false + Hoek.reach([0], 0); + // @ts-expect-error unknown option + Hoek.reach(['abc'], '0', { unknown: false }); + // @ts-expect-error separator must be a string + Hoek.reach(['abc'], '0', { separator: false }); + } catch {} + }); + }); + + describe('reachTemplate()', () => { + it('accepts valid calls', () => { + Hoek.reachTemplate(null, 'a{b}c'); + Hoek.reachTemplate([1, 2], 'a{1}c'); + Hoek.reachTemplate({ b: 2 }, 'a{b}c'); + Hoek.reachTemplate({ a: { b: { c: 3 } } }, '{a/b/c}', { + separator: '/', + default: 4, + strict: true, + functions: true, + }); + + expectTypeOf(Hoek.reachTemplate([1, 2], 'a{1}c')).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires a template + Hoek.reachTemplate(); + // @ts-expect-error requires a template + Hoek.reachTemplate([]); + // @ts-expect-error obj must be an object or null + Hoek.reachTemplate(1, '0'); + // @ts-expect-error obj must be an object or null + Hoek.reachTemplate('abc', '0'); + // @ts-expect-error template must be a string + Hoek.reachTemplate([0], 0); + // @ts-expect-error unknown option + Hoek.reachTemplate(['abc'], '{0}', { unknown: false }); + // @ts-expect-error separator must be a string + Hoek.reachTemplate(['abc'], '{0}', { separator: false }); + } catch {} + }); + }); + + describe('assert()', () => { + it('accepts valid calls', () => { + Hoek.assert(true); + Hoek.assert(true, 'some', 'message', 10); + Hoek.assert(1, 'error'); + Hoek.assert(true, new Error('message')); + + expectTypeOf(Hoek.assert(true)).toBeVoid(); + }); + }); + + describe('Bench', () => { + const bench = new Hoek.Bench(); + + it('exposes instance and static members', () => { + expectTypeOf(bench.ts).toBeNumber(); + expectTypeOf(bench.reset()).toBeVoid(); + expectTypeOf(bench.elapsed()).toBeNumber(); + expectTypeOf(Hoek.Bench.now()).toBeNumber(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error constructor takes no arguments + new Hoek.Bench({}); + // @ts-expect-error reset takes no arguments + bench.reset(true); + // @ts-expect-error elapsed takes no arguments + bench.elapsed(true); + // @ts-expect-error now takes no arguments + Hoek.Bench.now(true); + } catch {} + }); + }); + + describe('escapeRegex()', () => { + it('accepts valid calls', () => { + Hoek.escapeRegex('something?'); + + expectTypeOf(Hoek.escapeRegex('^?')).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires a string + Hoek.escapeRegex(); + // @ts-expect-error argument must be a string + Hoek.escapeRegex(true); + // @ts-expect-error argument must be a string + Hoek.escapeRegex({}); + } catch {} + }); + }); + + describe('escapeHeaderAttribute()', () => { + it('accepts valid calls', () => { + Hoek.escapeHeaderAttribute('something?'); + + expectTypeOf(Hoek.escapeHeaderAttribute('^?')).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires a string + Hoek.escapeHeaderAttribute(); + // @ts-expect-error argument must be a string + Hoek.escapeHeaderAttribute(true); + // @ts-expect-error argument must be a string + Hoek.escapeHeaderAttribute({}); + } catch {} + }); + }); + + describe('escapeHtml()', () => { + it('accepts valid calls', () => { + Hoek.escapeHtml('something?'); + + expectTypeOf(Hoek.escapeHtml('^?')).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error argument must be a string + Hoek.escapeHtml(true); + // @ts-expect-error argument must be a string + Hoek.escapeHtml({}); + } catch {} + }); + }); + + describe('escapeJson()', () => { + it('accepts valid calls', () => { + Hoek.escapeJson('something?'); + + expectTypeOf(Hoek.escapeJson('^?')).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error argument must be a string + Hoek.escapeJson(true); + // @ts-expect-error argument must be a string + Hoek.escapeJson({}); + } catch {} + }); + }); + + describe('once()', () => { + it('accepts valid calls', () => { + Hoek.once(() => 4); + Hoek.once(() => undefined); + + expectTypeOf(Hoek.once(() => 'x')).toEqualTypeOf<() => string>(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires a function + Hoek.once(); + // @ts-expect-error argument must be a function + Hoek.once('x'); + // @ts-expect-error argument must be a function + Hoek.once({}); + } catch {} + }); + }); + + describe('ignore()', () => { + it('accepts any arguments', () => { + Hoek.ignore(); + Hoek.ignore(1, 2, 'x'); + + expectTypeOf(Hoek.ignore).toEqualTypeOf<(...args: unknown[]) => void>(); + expectTypeOf(Hoek.ignore()).toBeVoid(); + }); + }); + + describe('stringify()', () => { + it('accepts valid calls', () => { + Hoek.stringify(123); + Hoek.stringify({}, null, 4); + + expectTypeOf(Hoek.stringify(123)).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires a value + Hoek.stringify(); + } catch {} + }); + }); + + describe('wait()', () => { + it('accepts valid calls', () => { + expectTypeOf(Hoek.wait(0)).toEqualTypeOf>(); + expectTypeOf(Hoek.wait(100, 'ok')).toEqualTypeOf>(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error timeout must be a number/bigint + Hoek.wait({}); + } catch {} + }); + }); + + describe('block()', () => { + it('accepts valid calls', () => { + expectTypeOf(Hoek.block()).toEqualTypeOf>(); + expectTypeOf>>().toBeVoid(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error takes no arguments + Hoek.block(123); + } catch {} + }); + }); + + describe('isPromise()', () => { + it('accepts valid calls', () => { + Hoek.isPromise(1); + Hoek.isPromise({}); + Hoek.isPromise(null); + + expectTypeOf(Hoek.isPromise(1)).toBeBoolean(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error requires an argument + Hoek.isPromise(); + // @ts-expect-error too many arguments + Hoek.isPromise(1, 2); + } catch {} + }); + }); + + describe('AssertError', () => { + it('exposes the expected shape', () => { + expectTypeOf(new Hoek.AssertError()).toEqualTypeOf(); + expectTypeOf(new Hoek.AssertError('fail')).toEqualTypeOf(); + expectTypeOf(new Hoek.AssertError().name).toEqualTypeOf<'AssertError'>(); + expectTypeOf(new Hoek.AssertError().message).toBeString(); + }); + + it('rejects invalid calls', () => { + try { + // @ts-expect-error message must be a string + new Hoek.AssertError(new Error()); + // @ts-expect-error takes a single argument + new Hoek.AssertError('fail', 'again'); + } catch {} + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 00000000..a833a6bc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "erasableSyntaxOnly": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "rewriteRelativeImportExtensions": true, + "sourceMap": true, + "strict": true, + "target": "ESNext", + "types": ["node", "./vitest.d.ts"] + }, + "include": ["src", "test"] +} diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 00000000..89a72e70 --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['./src/index.ts'], + outDir: './dist', + clean: true, + exports: true, + hash: false, + format: 'esm', + target: 'node22', +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..41181547 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.ts'], + setupFiles: ['vitest.setup.ts'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['src/**'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig; diff --git a/vitest.d.ts b/vitest.d.ts new file mode 100644 index 00000000..9863298d --- /dev/null +++ b/vitest.d.ts @@ -0,0 +1,9 @@ +import 'vitest'; + +import type { DeepEqualOptions } from './src/deepEqual.js'; + +declare module 'vitest' { + interface Assertion { + toHoequal(expected: T, options?: DeepEqualOptions): T; + } +} diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 00000000..4d451c20 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,19 @@ +import { expect } from 'vitest'; + +import { deepEqual, type DeepEqualOptions } from './src/deepEqual.js'; + +// Custom matcher backed by hoek's own deepEqual — the same engine @hapi/code.equal() +// wrapped, minus the dependency. Defaults mirror code.equal(): prototype comparison off, +// deep function comparison on. Tests opt into prototype/symbol checks via the options arg. +expect.extend({ + toHoequal(received: unknown, expected: unknown, options?: DeepEqualOptions) { + const pass = deepEqual(received, expected, { prototype: false, deepFunction: true, ...options }); + + return { + pass, + message: () => `Expected values ${pass ? 'not ' : ''}to be deeply equal`, + actual: received, + expected, + }; + }, +});