diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js index 98fe4bf96646c6986ece952fd640fe77aaeb609d..5ee29c4b0e0c97d908395c13e9fcb47311ec8504 100644 --- a/app/assets/javascripts/lib/utils/uuids.js +++ b/app/assets/javascripts/lib/utils/uuids.js @@ -16,6 +16,16 @@ import { isString } from 'lodash'; import stringHash from 'string-hash'; import { v4 } from 'uuid'; +const getRandomUUIDFunction = () => window.crypto.randomUUID.bind(window.crypto); + +function arrayOf(length) { + return { + using(uuidGenerator) { + return Array(length).fill(0).map(uuidGenerator); + }, + }; +} + function getSeed(seeds) { return seeds.reduce((seedling, seed, i) => { let thisSeed = 0; @@ -36,7 +46,9 @@ function getPseudoRandomNumberGenerator(...seeds) { if (seeds.length) { seedNumber = getSeed(seeds); } else { - seedNumber = Math.floor(Math.random() * 10 ** 15); + throw new Error( + 'Seeding the random number generator requires initial seed values' /* eslint-disable-line @gitlab/require-i18n-strings */, + ); } return new MersenneTwister(seedNumber); @@ -65,12 +77,15 @@ function randomValuesForUuid(prng) { * @returns {UUIDv4[]} An array of UUIDv4s */ export function uuids({ seeds = [], count = 1 } = {}) { - const rng = getPseudoRandomNumberGenerator(...seeds); - return ( - // Create an array the same size as the number of UUIDs requested - Array(count) - .fill(0) - // Replace each slot in the array with a UUID which needs 16 (pseudo)random values to generate - .map(() => v4({ random: randomValuesForUuid(rng) })) - ); + const versionFour = { + random: () => arrayOf(count).using(getRandomUUIDFunction()), + seeded: ({ seeds: seedValues }) => { + const rng = getPseudoRandomNumberGenerator(...seedValues); + + return arrayOf(count).using(() => v4({ random: randomValuesForUuid(rng) })); + }, + }; + const type = seeds.length ? 'seeded' : 'random'; + + return versionFour[type]({ seeds }); } diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js index 011e1142c76801caa02203df59d79021afcab321..a8f8881c832305d39c48f9d02947117b153d6a28 100644 --- a/spec/frontend/__helpers__/shared_test_setup.js +++ b/spec/frontend/__helpers__/shared_test_setup.js @@ -1,4 +1,5 @@ /* Common setup for both unit and integration test environments */ +import crypto from 'crypto'; import { config as testUtilsConfig } from '@vue/test-utils'; import * as jqueryMatchers from 'custom-jquery-matchers'; import Vue from 'vue'; @@ -69,6 +70,9 @@ Object.assign(global, { cancelIdleCallback(id) { clearTimeout(id); }, + crypto: { + randomUUID: crypto.webcrypto.randomUUID, + }, }); beforeEach(() => { diff --git a/spec/frontend/lib/utils/uuids_spec.js b/spec/frontend/lib/utils/uuids_spec.js index a7770d37566f4608a81493a349b687653e7b8f0d..4a8fa8f30c475e11232377a83c4d7c9aba86f683 100644 --- a/spec/frontend/lib/utils/uuids_spec.js +++ b/spec/frontend/lib/utils/uuids_spec.js @@ -55,11 +55,17 @@ describe('UUIDs Util', () => { ); describe('unseeded UUID randomness', () => { - const nonRandom = Array(6) - .fill(0) - .map((_, i) => uuids({ seeds: [i] })[0]); - const random = uuids({ count: 6 }); - const moreRandom = uuids({ count: 6 }); + let nonRandom; + let random; + let moreRandom; + + beforeAll(() => { + nonRandom = Array(6) + .fill(0) + .map((_, i) => uuids({ seeds: [i] })[0]); + random = uuids({ count: 6 }); + moreRandom = uuids({ count: 6 }); + }); it('is different from a seeded result', () => { random.forEach((id, i) => {