@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
text/typescript
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);
});
});
});