import * as utils from "../../src/utils"; import { alphabetPad, averageBetweenStrings, baseToString, deepSortedObjectEntries, DEFAULT_ALPHABET, lexicographicCompare, nextString, prevString, simpleRetryOperation, stringToBase, } from "../../src/utils"; import { logger } from "../../src/logger"; // TODO: Fix types throughout describe("utils", function() { describe("encodeParams", function() { it("should url encode and concat with &s", function() { const params = { foo: "bar", baz: "beer@", }; expect(utils.encodeParams(params)).toEqual( "foo=bar&baz=beer%40", ); }); }); describe("encodeUri", function() { it("should replace based on object keys and url encode", function() { const path = "foo/bar/%something/%here"; const vals = { "%something": "baz", "%here": "beer@", }; expect(utils.encodeUri(path, vals)).toEqual( "foo/bar/baz/beer%40", ); }); }); describe("removeElement", function() { it("should remove only 1 element if there is a match", function() { const matchFn = function() { return true; }; const arr = [55, 66, 77]; utils.removeElement(arr, matchFn); expect(arr).toEqual([66, 77]); }); it("should be able to remove in reverse order", function() { const matchFn = function() { return true; }; const arr = [55, 66, 77]; utils.removeElement(arr, matchFn, true); expect(arr).toEqual([55, 66]); }); it("should remove nothing if the function never returns true", function() { const matchFn = function() { return false; }; const arr = [55, 66, 77]; utils.removeElement(arr, matchFn); expect(arr).toEqual(arr); }); }); describe("isFunction", function() { it("should return true for functions", function() { expect(utils.isFunction([])).toBe(false); expect(utils.isFunction([5, 3, 7])).toBe(false); expect(utils.isFunction(undefined)).toBe(false); expect(utils.isFunction(null)).toBe(false); expect(utils.isFunction({})).toBe(false); expect(utils.isFunction("foo")).toBe(false); expect(utils.isFunction(555)).toBe(false); expect(utils.isFunction(function() {})).toBe(true); const s = { foo: function() {} }; expect(utils.isFunction(s.foo)).toBe(true); }); }); describe("checkObjectHasKeys", function() { it("should throw for missing keys", function() { expect(function() { utils.checkObjectHasKeys({}, ["foo"]); }).toThrow(); expect(function() { utils.checkObjectHasKeys({ foo: "bar", }, ["foo"]); }).not.toThrow(); }); }); describe("checkObjectHasNoAdditionalKeys", function() { it("should throw for extra keys", function() { expect(function() { utils.checkObjectHasNoAdditionalKeys({ foo: "bar", baz: 4 }, ["foo"]); }).toThrow(); expect(function() { utils.checkObjectHasNoAdditionalKeys({ foo: "bar" }, ["foo"]); }).not.toThrow(); }); }); describe("deepCompare", function() { const assert = { isTrue: function(x) { expect(x).toBe(true); }, isFalse: function(x) { expect(x).toBe(false); }, }; it("should handle primitives", function() { assert.isTrue(utils.deepCompare(null, null)); assert.isFalse(utils.deepCompare(null, undefined)); assert.isTrue(utils.deepCompare("hi", "hi")); assert.isTrue(utils.deepCompare(5, 5)); assert.isFalse(utils.deepCompare(5, 10)); }); it("should handle regexps", function() { assert.isTrue(utils.deepCompare(/abc/, /abc/)); assert.isFalse(utils.deepCompare(/abc/, /123/)); const r = /abc/; assert.isTrue(utils.deepCompare(r, r)); }); it("should handle dates", function() { assert.isTrue(utils.deepCompare(new Date("2011-03-31"), new Date("2011-03-31"))); assert.isFalse(utils.deepCompare(new Date("2011-03-31"), new Date("1970-01-01"))); }); it("should handle arrays", function() { assert.isTrue(utils.deepCompare([], [])); assert.isTrue(utils.deepCompare([1, 2], [1, 2])); assert.isFalse(utils.deepCompare([1, 2], [2, 1])); assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3])); }); it("should handle simple objects", function() { assert.isTrue(utils.deepCompare({}, {})); assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 })); assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 })); assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 })); assert.isTrue(utils.deepCompare({ 1: { name: "mhc", age: 28 }, 2: { name: "arb", age: 26 }, }, { 1: { name: "mhc", age: 28 }, 2: { name: "arb", age: 26 }, })); assert.isFalse(utils.deepCompare({ 1: { name: "mhc", age: 28 }, 2: { name: "arb", age: 26 }, }, { 1: { name: "mhc", age: 28 }, 2: { name: "arb", age: 27 }, })); assert.isFalse(utils.deepCompare({}, null)); assert.isFalse(utils.deepCompare({}, undefined)); }); it("should handle functions", function() { // no two different function is equal really, they capture their // context variables so even if they have same toString(), they // won't have same functionality const func = function(x) { return true; }; const func2 = function(x) { return true; }; assert.isTrue(utils.deepCompare(func, func)); assert.isFalse(utils.deepCompare(func, func2)); assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } })); assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } })); }); }); describe("extend", function() { const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" }; it("should extend", function() { const target = { "prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo", }; const merged = { "prop1": 5, "prop2": 1, "string1": "baz", "string2": "x", "newprop": "new", }; const sourceOrig = JSON.stringify(SOURCE); utils.extend(target, SOURCE); expect(JSON.stringify(target)).toEqual(JSON.stringify(merged)); // check the originial wasn't modified expect(JSON.stringify(SOURCE)).toEqual(sourceOrig); }); it("should ignore null", function() { const target = { "prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo", }; const merged = { "prop1": 5, "prop2": 1, "string1": "baz", "string2": "x", "newprop": "new", }; const sourceOrig = JSON.stringify(SOURCE); utils.extend(target, null, SOURCE); expect(JSON.stringify(target)).toEqual(JSON.stringify(merged)); // check the originial wasn't modified expect(JSON.stringify(SOURCE)).toEqual(sourceOrig); }); it("should handle properties created with defineProperties", function() { const source = Object.defineProperties({}, { "enumerableProp": { get: function() { return true; }, enumerable: true, }, "nonenumerableProp": { get: function() { return true; }, }, }); // TODO: Fix type const target: any = {}; utils.extend(target, source); expect(target.enumerableProp).toBe(true); expect(target.nonenumerableProp).toBe(undefined); }); }); describe("chunkPromises", function() { it("should execute promises in chunks", async function() { let promiseCount = 0; async function fn1() { await utils.sleep(1); expect(promiseCount).toEqual(0); ++promiseCount; } async function fn2() { expect(promiseCount).toEqual(1); ++promiseCount; } await utils.chunkPromises([fn1, fn2], 1); expect(promiseCount).toEqual(2); }); }); describe('simpleRetryOperation', () => { it('should retry', async () => { let count = 0; const val = {}; const fn = (attempt) => { count++; // If this expectation fails then it can appear as a Jest Timeout due to // the retry running beyond the test limit. expect(attempt).toEqual(count); if (count > 1) { return Promise.resolve(val); } else { return Promise.reject(new Error("Iterative failure")); } }; const ret = await simpleRetryOperation(fn); expect(ret).toBe(val); expect(count).toEqual(2); }); // We don't test much else of the function because then we're just testing that the // underlying library behaves, which should be tested on its own. Our API surface is // all that concerns us. }); describe('DEFAULT_ALPHABET', () => { it('should be usefully printable ASCII in order', () => { expect(DEFAULT_ALPHABET).toEqual( " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", ); }); }); describe('alphabetPad', () => { it('should pad to the alphabet length', () => { const len = 12; expect(alphabetPad("a", len)).toEqual("a" + ("".padEnd(len - 1, DEFAULT_ALPHABET[0]))); expect(alphabetPad("a", len, "123")).toEqual("a" + ("".padEnd(len - 1, '1'))); }); }); describe('baseToString', () => { it('should calculate the appropriate string from numbers', () => { // Verify the whole alphabet for (let i = BigInt(1); i <= DEFAULT_ALPHABET.length; i++) { logger.log({ i }); // for debugging expect(baseToString(i)).toEqual(DEFAULT_ALPHABET[Number(i) - 1]); } // Just quickly double check that repeated characters aren't treated as padding, particularly // at the beginning of the alphabet where they are most vulnerable to this behaviour. expect(baseToString(BigInt(1))).toEqual(DEFAULT_ALPHABET[0].repeat(1)); expect(baseToString(BigInt(96))).toEqual(DEFAULT_ALPHABET[0].repeat(2)); expect(baseToString(BigInt(9121))).toEqual(DEFAULT_ALPHABET[0].repeat(3)); expect(baseToString(BigInt(866496))).toEqual(DEFAULT_ALPHABET[0].repeat(4)); expect(baseToString(BigInt(82317121))).toEqual(DEFAULT_ALPHABET[0].repeat(5)); expect(baseToString(BigInt(7820126496))).toEqual(DEFAULT_ALPHABET[0].repeat(6)); expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[9]); expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual('j'); expect(baseToString(BigInt(6337))).toEqual("ab"); expect(baseToString(BigInt(80), "abcdefghijklmnopqrstuvwxyz")).toEqual('cb'); }); }); describe('stringToBase', () => { it('should calculate the appropriate number for a string', () => { expect(stringToBase(DEFAULT_ALPHABET[0].repeat(1))).toEqual(BigInt(1)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(2))).toEqual(BigInt(96)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(3))).toEqual(BigInt(9121)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(4))).toEqual(BigInt(866496)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(5))).toEqual(BigInt(82317121)); expect(stringToBase(DEFAULT_ALPHABET[0].repeat(6))).toEqual(BigInt(7820126496)); expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(1)); expect(stringToBase("a")).toEqual(BigInt(66)); expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(3)); expect(stringToBase("ab")).toEqual(BigInt(6337)); expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(80)); }); }); describe('averageBetweenStrings', () => { it('should average appropriately', () => { expect(averageBetweenStrings(" ", "!!")).toEqual(" P"); expect(averageBetweenStrings(" ", "!")).toEqual(" "); expect(averageBetweenStrings('A', 'B')).toEqual('A '); expect(averageBetweenStrings('AA', 'BB')).toEqual('Aq'); expect(averageBetweenStrings('A', 'z')).toEqual(']'); expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m'); expect(averageBetweenStrings('AA', 'zz')).toEqual('^.'); expect(averageBetweenStrings('aa', 'zz', "abcdefghijklmnopqrstuvwxyz")).toEqual('mz'); expect(averageBetweenStrings('cat', 'doggo')).toEqual("d9>Cw"); expect(averageBetweenStrings('cat', 'doggo', "abcdefghijklmnopqrstuvwxyz")).toEqual("cumqh"); }); }); describe('nextString', () => { it('should find the next string appropriately', () => { expect(nextString('A')).toEqual('B'); expect(nextString('b', 'abcdefghijklmnopqrstuvwxyz')).toEqual('c'); expect(nextString('cat')).toEqual('cau'); expect(nextString('cat', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cau'); }); }); describe('prevString', () => { it('should find the next string appropriately', () => { expect(prevString('B')).toEqual('A'); expect(prevString('c', 'abcdefghijklmnopqrstuvwxyz')).toEqual('b'); expect(prevString('cau')).toEqual('cat'); expect(prevString('cau', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cat'); }); }); // Let's just ensure the ordering is sensible for lexicographic ordering describe('string averaging unified', () => { it('should be truly previous and next', () => { let midpoint = "cat"; // We run this test 100 times to ensure we end up with a sane sequence. for (let i = 0; i < 100; i++) { const next = nextString(midpoint); const prev = prevString(midpoint); logger.log({ i, midpoint, next, prev }); // for test debugging expect(lexicographicCompare(midpoint, next) < 0).toBe(true); expect(lexicographicCompare(midpoint, prev) > 0).toBe(true); expect(averageBetweenStrings(prev, next)).toBe(midpoint); midpoint = next; } }); it('should roll over', () => { const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1]; const firstAlpha = DEFAULT_ALPHABET[0]; const highRoll = firstAlpha + firstAlpha; const lowRoll = lastAlpha; expect(nextString(lowRoll)).toEqual(highRoll); expect(prevString(highRoll)).toEqual(lowRoll); }); it('should be reversible on small strings', () => { // Large scale reversibility is tested for max space order value const input = "cats"; expect(prevString(nextString(input))).toEqual(input); }); // We want to explicitly make sure that Space order values are supported and roll appropriately it('should properly handle rolling over at 50 characters', () => { // Note: we also test reversibility of large strings here. const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50); const fiftyFirstChar = DEFAULT_ALPHABET[0].repeat(51); expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar); expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue); // We're testing that the rollover happened, which means that the next string come before // the maximum space order value lexicographically. expect(lexicographicCompare(maxSpaceValue, fiftyFirstChar) > 0).toBe(true); }); }); describe('lexicographicCompare', () => { it('should work', () => { // Simple tests expect(lexicographicCompare('a', 'b') < 0).toBe(true); expect(lexicographicCompare('ab', 'b') < 0).toBe(true); expect(lexicographicCompare('cat', 'dog') < 0).toBe(true); // Simple tests (reversed) expect(lexicographicCompare('b', 'a') > 0).toBe(true); expect(lexicographicCompare('b', 'ab') > 0).toBe(true); expect(lexicographicCompare('dog', 'cat') > 0).toBe(true); // Simple equality tests expect(lexicographicCompare('a', 'a') === 0).toBe(true); expect(lexicographicCompare('A', 'A') === 0).toBe(true); // ASCII rule testing expect(lexicographicCompare('A', 'a') < 0).toBe(true); expect(lexicographicCompare('a', 'A') > 0).toBe(true); }); }); describe('deepSortedObjectEntries', () => { it('should auto-return non-objects', () => { expect(deepSortedObjectEntries(42)).toEqual(42); expect(deepSortedObjectEntries("not object")).toEqual("not object"); expect(deepSortedObjectEntries(true)).toEqual(true); expect(deepSortedObjectEntries([42])).toEqual([42]); expect(deepSortedObjectEntries(null)).toEqual(null); expect(deepSortedObjectEntries(undefined)).toEqual(undefined); }); it('should sort objects appropriately', () => { const input = { a: 42, b: { d: {}, a: "test", b: "alpha", }, [72]: "test", }; const output = [ ["72", "test"], ["a", 42], ["b", [ ["a", "test"], ["b", "alpha"], ["d", []], ]], ]; expect(deepSortedObjectEntries(input)).toMatchObject(output); }); }); describe("recursivelyAssign", () => { it("doesn't override with null/undefined", () => { const result = utils.recursivelyAssign( { string: "Hello world", object: {}, float: 0.1, }, { string: null, object: undefined, }, true, ); expect(result).toStrictEqual({ string: "Hello world", object: {}, float: 0.1, }); }); it("assigns recursively", () => { const result = utils.recursivelyAssign( { number: 42, object: { message: "Hello world", day: "Monday", langs: { compiled: ["c++"], }, }, thing: "string", }, { number: 2, object: { message: "How are you", day: "Friday", langs: { compiled: ["c++", "c"], }, }, thing: { aSubThing: "something", }, }, ); expect(result).toStrictEqual({ number: 2, object: { message: "How are you", day: "Friday", langs: { compiled: ["c++", "c"], }, }, thing: { aSubThing: "something", }, }); }); }); });