UNPKG

@ibgib/helper-gib

Version:

common helper/utils/etc used in ibgib libs. Node v19+ needed for heavily-used isomorphic webcrypto hashing consumed in both node and browsers.

283 lines (233 loc) 12.1 kB
import { getTimestamp, getSaferSubstring, getTimestampInTicks, pickRandom, pickRandom_Letters, replaceCharAt, hash, HashAlgorithm, getUUID, clone, extractErrorMsg, } from "./utils-helper.mjs"; // #region test data const SOME_STRING = "This is some stringy stuff..."; const SOME_STRING_HASH = "5DC14EA1027B956AD6BA51F11372DF823FCF3429B5F2063F1DDA358E0F4F2992"; const SOME_STRING_HASH_512 = "C48C780DA7C43EE7047CDAB0D7A2FDF5DC050DF3291D3CAE973FB0F51BE8535668592EB25A730C95A23D3B4D57924893E7E87C86DC0F52C6D5D2E34EE41203E5"; const SOME_OTHER_STRING = "This is quite a different string of stuff."; const TEST_HASHES: { [key: string]: string } = { [SOME_STRING + HashAlgorithm.sha_256]: SOME_STRING_HASH, [SOME_STRING + HashAlgorithm.sha_512]: SOME_STRING_HASH_512, [SOME_OTHER_STRING + HashAlgorithm.sha_256]: 'AE4C18B37B2329770E05EA3F946C6EB6DE56D2DC568E1F5CBB395E2A1556F58A', [SOME_OTHER_STRING + HashAlgorithm.sha_512]: '2F1A72B21C914ED459319DF4B5D70E81CEE67C4B48BC74CD6BA8F41DCEC20DCD100C914913F370B2782985268D05C590B46F3FE9005A7477BA952935D2454E53', } // #endregion test data describe('utils', () => { describe(`hash`, () => { it(`should digest simple string consistently`, async () => { let h = await hash({ s: '42' }); expect(h).withContext('42').toBe('73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049'); }); it(`should digest simple stringified ibgib`, async () => { let ibgib: any = { ib: 'ib', gib: 'gib' }; let h = await hash({ s: JSON.stringify(ibgib) }); expect(h).withContext('ib^gib').toBe('cbad0694a257358c044611ea1fa88ace71a01a9b8409d2354d0387d8043f7671'); }); }); describe(`clone`, () => { it(`should copy deep objects`, async () => { const objSimple = { a: SOME_STRING }; const objADeep = { levelOne: { levelTwo: { buckle: "your shoe", three: "four", objSimple: objSimple, } } }; const cloneADeep = clone(objADeep); expect(cloneADeep?.levelOne?.levelTwo?.buckle).toBe("your shoe"); expect(cloneADeep?.levelOne?.levelTwo?.three).toBe("four"); expect(cloneADeep?.levelOne?.levelTwo?.objSimple).toEqual(objSimple); cloneADeep.levelOne.levelTwo.objSimple.a = SOME_OTHER_STRING; // original should **still** be the first value expect(objSimple.a).toBe(SOME_STRING); // clone should be changed. expect(cloneADeep.levelOne.levelTwo.objSimple.a).toBe(SOME_OTHER_STRING); }); }); describe(`getTimestamp`, () => { it(`should get the current date as UTCString`, async () => { // implementation detail hmm.... const timestamp = getTimestamp(); const date = new Date(timestamp); const dateAsUTCString = date.toUTCString(); expect(timestamp).toBe(dateAsUTCString); }); }); describe(`hash`, () => { const TEST_ALGORITHMS: HashAlgorithm[] = Object.values(HashAlgorithm); // implicit is just SHA-256 it(`should hash consistently with implicit SHA-256`, async () => { const hashed = await hash({ s: SOME_STRING }) || ""; expect(hashed.toUpperCase()).toBe(TEST_HASHES[SOME_STRING + 'SHA-256']); }); for (let algorithm of TEST_ALGORITHMS) { it(`should hash consistently with explicit ${algorithm}`, async () => { // const hash = await hash({s: SOME_STRING, algorithm: "SHA-256"}) || ""; let hashed = await hash({ s: SOME_STRING, algorithm }) || ""; expect(hashed.toUpperCase()).toBe(TEST_HASHES[SOME_STRING + algorithm]); hashed = await hash({ s: SOME_OTHER_STRING, algorithm }) || ""; expect(hashed.toUpperCase()).toBe(TEST_HASHES[SOME_OTHER_STRING + algorithm]); }); it(`should hash without collisions, 1000 times, ${algorithm}`, async () => { const hashes: string[] = []; const salt = await getUUID(1024); // console.log(`salt: ${salt}`); for (let i = 0; i < 1000; i++) { const hashed = await hash({ s: salt + i.toString(), algorithm }) || ""; // console.log(hash); expect(hashes).not.toContain(hashed); hashes.push(hashed); } }); }; }); describe(`generating UUIDs`, () => { it(`shouldn't duplicate UUIDs`, async () => { const ids: string[] = []; for (let i = 0; i < 100; i++) { const id = await getUUID(); expect(ids).not.toContain(id); ids.push(id); } }); }); describe(`extractErrorMsg`, () => { it(`should return canned msg when error is falsy`, () => { const defaultMsg = '[error is falsy]'; // duplicate of code in helper.mjs [null, undefined, ''].forEach(error => { expect(extractErrorMsg(error)).withContext(JSON.stringify(error)).toEqual(defaultMsg); }); }); it(`should return incoming error if it is a string`, () => { ['string here', 'undefined', '42', 'ibgib'].forEach(stringError => { expect(extractErrorMsg(stringError)).withContext(JSON.stringify(stringError)).toEqual(stringError); }); }); it(`should return error.message if it's a thrown error`, () => { ['string here', 'undefined', 'something went wrong', 'danger. out of memory (E: ce86ffd7a0174c1d8ce5e56c807dd4b1)'] .map(x => new Error(x)) .forEach(error => { expect(extractErrorMsg(error)).withContext(error.message).toEqual(error.message); }); }); it(`should return incoming error stringified if it is a number`, () => { [1234, 0, 1_000, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, -5, 1 / 23].forEach(numberError => { expect(extractErrorMsg(numberError)).withContext(JSON.stringify(numberError)).toEqual(JSON.stringify(numberError)); }); }); it(`should return canned response with type if incoming error is none of the above`, () => { let error = { x: 1, y: 2 }; let msg = extractErrorMsg(error); expect(msg).withContext(JSON.stringify(error)).toBeTruthy(); }); }); describe('getTimestampInTicks', () => { it('result in ticks should be an integer string', () => { for (let i = 0; i < 1000; i++) { const ticks = getTimestampInTicks(); const x: number = Number.parseInt(ticks); expect(Number.isInteger(x)).toBeTrue(); } }); it('timestamp arg should provide known ticks value', () => { const timestamp = "Thu Oct 27 2022 11:54:10 GMT-0500 (Central Daylight Time)"; const knownTicks = 1666889650000; const ticksAsString = getTimestampInTicks(timestamp); const ticksAsInt = Number.parseInt(ticksAsString); expect(ticksAsInt).toEqual(knownTicks); }); it('real use case of timestamp to ticks to timestamp', () => { // from timestamp const timestamp = getTimestamp(); // UTC String const dateFromTimestamp = new Date(timestamp); // get ticks from that timestamp const ticks = getTimestampInTicks(timestamp); // get a completely new date object using the ticks const dateFromTicks = new Date(); dateFromTicks.setTime(Number.parseInt(ticks)); // both date objects should output the same UTC string expect(timestamp).toEqual(dateFromTicks.toUTCString()); expect(dateFromTimestamp.toUTCString()).toEqual(dateFromTicks.toUTCString()); }); }); describe('getSaferSubstring', () => { const textsWithQuestionMarks = ['????yo?', '?start', 'end?', 'i?got?questions',]; const textsWithOnlyNonAlphanumerics = ['(*^*%$%#%^#^%#??//', ':";\' "']; const textsWithCharacters = [...textsWithQuestionMarks, ...textsWithOnlyNonAlphanumerics, 'i have spaces', 'i-have-hyphens', 'i/got/slashes', 'got\\back\\slashes']; describe('with keepliterals empty', () => { it('should remove non alphanumerics', () => { for (let i = 0; i < textsWithCharacters.length; i++) { const text = textsWithCharacters[i]; const saferText = getSaferSubstring({ text, keepLiterals: [] }); expect(saferText.match(/^\w+$/)).toBeTruthy(`nope: ${text}`); } }); }); }); describe('pickRandom', () => { it('should pick a random letter from an array of letters', () => { const letters = ['a', 'b', 'c', 'd', 'E']; const letter = pickRandom({ x: letters })!; expect(letters.includes(letter)).toBeTruthy(); }); it('should pick a random number from an array of numbers', () => { const numbers = [0, 1, 2, 3, 4, 5, 6, 42]; const n = pickRandom({ x: numbers })!; expect(numbers.includes(n)).toBeTruthy(); }); it('should ultimately pick each of the items over many iterations (m=10, i=1000)', () => { const numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 42]; const numbersPicked: Set<number> = new Set<number>(); for (let i = 0; i < 1000; i++) { const n = pickRandom({ x: numbers })!; expect(n).not.toBeUndefined(); numbersPicked.add(n); } expect(numbersPicked.size).toEqual(numbers.length); }); }); describe('pickRandom_Letters', () => { it('should pick some random letters the size of count', () => { const counts = [1, 4, 15, 30, 100]; for (let i = 0; i < counts.length; i++) { const count = counts[i]; const letters = pickRandom_Letters({ count })!; expect(letters).toBeTruthy(); expect(letters.length).toEqual(count); expect(letters.match(/^\w+$/)).toBeTruthy(); } }); it('should NOT pick the same letters in tight loop (counts=10,15 i=100)', () => { const counts = [10, 15]; const iterations = 100; const alreadyPicked: Set<string> = new Set<string>(); for (let i = 0; i < counts.length; i++) { const count = counts[i]; for (let j = 0; j < iterations; j++) { const letters = pickRandom_Letters({ count }); expect(alreadyPicked.has(letters)).toBeFalse(); alreadyPicked.add(letters); } } }); }); describe('replaceCharAt', () => { it('should replace chars in strings', () => { let test = 'this is a string (1) here woohoo!\nAnd this is (2) the second line.'; let newChar = '_'; let index1 = test.indexOf('1'); let manuallyReplaced1 = `this is a string (${newChar}) here woohoo!\nAnd this is (2) the second line.`; let result1 = replaceCharAt({ s: test, pos: index1, newChar: '_' }); expect(result1).toEqual(manuallyReplaced1); let index2 = test.indexOf('2'); let manuallyReplaced2 = `this is a string (1) here woohoo!\nAnd this is (${newChar}) the second line.`; let result2 = replaceCharAt({ s: test, pos: index2, newChar: '_' }); expect(result2).toEqual(manuallyReplaced2); }); }); });