diff --git a/app/assets/javascripts/actioncable_link.js b/app/assets/javascripts/actioncable_link.js index cf53d9e21b4a7592e30fbbdfbd1ec4c18b2ef7e4..c6d1662590738df4737681c1ad58587aa483f5a8 100644 --- a/app/assets/javascripts/actioncable_link.js +++ b/app/assets/javascripts/actioncable_link.js @@ -1,7 +1,7 @@ import { ApolloLink, Observable } from '@apollo/client/core'; import { print } from 'graphql'; import cable from '~/actioncable_consumer'; -import { uuids } from '~/lib/utils/uuids'; +import { random } from '~/lib/utils/uuids/v4'; export default class ActionCableLink extends ApolloLink { // eslint-disable-next-line class-methods-use-this @@ -13,7 +13,7 @@ export default class ActionCableLink extends ApolloLink { query: operation.query ? print(operation.query) : null, variables: operation.variables, operationName: operation.operationName, - nonce: uuids()[0], + nonce: random(), }, { received(data) { diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js index bcd9fa012786beb90bd1a0bcd0c47276c49a6743..2cff906d23d9490542e3b49b5753a3759a994e4d 100644 --- a/app/assets/javascripts/diffs/utils/diff_file.js +++ b/app/assets/javascripts/diffs/utils/diff_file.js @@ -1,7 +1,7 @@ import { diffViewerModes as viewerModes } from '~/ide/constants'; import { changeInPercent, numberToHumanSize } from '~/lib/utils/number_utils'; import { truncateSha } from '~/lib/utils/text_utility'; -import { uuids } from '~/lib/utils/uuids'; +import { seeded } from '~/lib/utils/uuids/v4'; import { DIFF_FILE_SYMLINK_MODE, @@ -43,9 +43,9 @@ function identifier(file) { endpoint: file.load_collapsed_diff_url, }); - return uuids({ + return seeded({ seeds: [userOrGroup, project, id, file.file_identifier_hash, file.blob?.id], - })[0]; + }); } export const isNotDiffable = (file) => file?.viewer?.name === viewerModes.not_diffable; diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js index d585dc009e6a73437c5eb662af6219cfe5ef7aab..67c950b66c310914e2bf5ad379d1a6bff34600c8 100644 --- a/app/assets/javascripts/editor/source_editor.js +++ b/app/assets/javascripts/editor/source_editor.js @@ -4,7 +4,7 @@ import { defaultEditorOptions } from '~/ide/lib/editor_options'; import languages from '~/ide/lib/languages'; import { registerLanguages } from '~/ide/utils'; import { joinPaths } from '~/lib/utils/url_utility'; -import { uuids } from '~/lib/utils/uuids'; +import { random } from '~/lib/utils/uuids/v4'; import { SOURCE_EDITOR_INSTANCE_ERROR_NO_EL, URI_PREFIX, @@ -113,7 +113,7 @@ export default class SourceEditor { blobPath = '', blobContent = '', blobOriginalContent = '', - blobGlobalId = uuids()[0], + blobGlobalId = random(), isDiff = false, ...instanceOptions } = {}) { diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js index 2969121bf06b828a38ed94fd1059b347b0076a45..a9cfca29c7b58e91d1b8c8662a765f64f214540f 100644 --- a/app/assets/javascripts/google_tag_manager/index.js +++ b/app/assets/javascripts/google_tag_manager/index.js @@ -1,4 +1,4 @@ -import { v4 as uuidv4 } from 'uuid'; +import { random as uuidv4 } from '~/lib/utils/uuids/v4'; import { logError } from '~/lib/logger'; const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff'; diff --git a/app/assets/javascripts/lib/utils/intersection_observer.js b/app/assets/javascripts/lib/utils/intersection_observer.js index 0959df9a186f0c8123c2ca138e60e4461c8b4189..1c7df1761f55ff0e152fa7484306937d62b2bdf6 100644 --- a/app/assets/javascripts/lib/utils/intersection_observer.js +++ b/app/assets/javascripts/lib/utils/intersection_observer.js @@ -1,9 +1,9 @@ import { memoize } from 'lodash'; -import { uuids } from './uuids'; +import { random } from './uuids/v4'; export const create = memoize((options = {}) => { - const id = uuids()[0]; + const id = random(); return { id, diff --git a/app/assets/javascripts/lib/utils/recurrence.js b/app/assets/javascripts/lib/utils/recurrence.js index 8fd26f3e393abc64d71c1657b65db9bdb1c9708a..6d8d56b222b4c42b7cc67c839756d4d497be2c53 100644 --- a/app/assets/javascripts/lib/utils/recurrence.js +++ b/app/assets/javascripts/lib/utils/recurrence.js @@ -1,4 +1,4 @@ -import { uuids } from './uuids'; +import { random } from './uuids/v4'; /** * @module recurrence @@ -11,7 +11,7 @@ const instances = {}; * @returns {module:recurrence.RecurInstance} The newly created {@link module:recurrence~RecurInstance|RecurInstance} */ export function create() { - const id = uuids()[0]; + const id = random(); let handlers = {}; let count = 0; diff --git a/app/assets/javascripts/lib/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids/v4.js similarity index 55% rename from app/assets/javascripts/lib/utils/uuids.js rename to app/assets/javascripts/lib/utils/uuids/v4.js index 98fe4bf96646c6986ece952fd640fe77aaeb609d..1334199406d61bf63fb56fbc5a79d294294de169 100644 --- a/app/assets/javascripts/lib/utils/uuids.js +++ b/app/assets/javascripts/lib/utils/uuids/v4.js @@ -1,5 +1,5 @@ /** - * @module uuids + * @module uuidsV4 */ /** @@ -16,6 +16,14 @@ import { isString } from 'lodash'; import stringHash from 'string-hash'; import { v4 } from 'uuid'; +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; @@ -31,19 +39,22 @@ function getSeed(seeds) { } function getPseudoRandomNumberGenerator(...seeds) { + const usefulSeeds = seeds.filter((seed) => seed || seed === 0); let seedNumber; - if (seeds.length) { - seedNumber = getSeed(seeds); + if (usefulSeeds.length) { + seedNumber = getSeed(usefulSeeds); } else { - seedNumber = Math.floor(Math.random() * 10 ** 15); + throw new Error( + 'You must provide String and/or Number seed values' /* eslint-disable-line @gitlab/require-i18n-strings */, + ); } return new MersenneTwister(seedNumber); } -function randomValuesForUuid(prng) { - const randomValues = []; +function valuesForUuid(prng) { + const values = []; for (let i = 0; i <= 3; i += 1) { const buffer = new ArrayBuffer(4); @@ -51,26 +62,25 @@ function randomValuesForUuid(prng) { view.setUint32(0, prng.randomNumber()); - randomValues.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); + values.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); } - return randomValues; + return values; } /** - * Get an array of UUIDv4s + * Get an array of UUIDv4s, seeding the PRNG first * @param {Object} [options={}] * @param {Seed[]} [options.seeds=[]] - A list of mixed strings or numbers to seed the UUIDv4 generator * @param {Number} [options.count=1] - A total number of UUIDv4s to generate - * @returns {UUIDv4[]} An array of UUIDv4s + * @returns {UUIDv4|UUIDv4[]} A single UUIDv4, or an array of >=2 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) })) - ); +export function seeded({ seeds = [], count = 1 } = {}) { + const prng = getPseudoRandomNumberGenerator(...seeds); + + return count === 1 + ? v4({ random: valuesForUuid(prng) }) + : arrayOf(count).using(() => v4({ random: valuesForUuid(prng) })); } + +export { v4 as random }; diff --git a/spec/frontend/actioncable_link_spec.js b/spec/frontend/actioncable_link_spec.js index b15b93cb11d910e890f6c7485aff27b622bf8d59..d08002e45ed76da5e07fb9cdd33f9247f372073c 100644 --- a/spec/frontend/actioncable_link_spec.js +++ b/spec/frontend/actioncable_link_spec.js @@ -4,8 +4,8 @@ import cable from '~/actioncable_consumer'; import ActionCableLink from '~/actioncable_link'; // Mock uuids module for determinism -jest.mock('~/lib/utils/uuids', () => ({ - uuids: () => ['testuuid'], +jest.mock('~/lib/utils/uuids/v4', () => ({ + random: () => 'testuuid', })); const TEST_OPERATION = { diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js index 50811f43fc355698c49591e276dd79ed210ca445..f9b7a4eb99f7222ccf9de265cac0c56973ef405a 100644 --- a/spec/frontend/google_tag_manager/index_spec.js +++ b/spec/frontend/google_tag_manager/index_spec.js @@ -1,5 +1,4 @@ import { merge } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; import { trackCombinedGroupProjectForm, trackFreeTrialAccountSubmissions, @@ -19,7 +18,9 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { logError } from '~/lib/logger'; jest.mock('~/lib/logger'); -jest.mock('uuid'); +jest.mock('~/lib/utils/uuids/v4', () => ({ + random: () => '123', +})); describe('~/google_tag_manager/index', () => { let spy; @@ -372,7 +373,6 @@ describe('~/google_tag_manager/index', () => { const { selectedPlan, revenue, name, id } = planObject; expect(spy).not.toHaveBeenCalled(); - uuidv4.mockImplementationOnce(() => '123'); const transactionDetails = { paymentOption: 'visa', diff --git a/spec/frontend/lib/utils/uuids_spec.js b/spec/frontend/lib/utils/uuids/v4_spec.js similarity index 73% rename from spec/frontend/lib/utils/uuids_spec.js rename to spec/frontend/lib/utils/uuids/v4_spec.js index a7770d37566f4608a81493a349b687653e7b8f0d..fb68edf724569a0a30eaea6aadce2a375ff85f48 100644 --- a/spec/frontend/lib/utils/uuids_spec.js +++ b/spec/frontend/lib/utils/uuids/v4_spec.js @@ -1,4 +1,4 @@ -import { uuids } from '~/lib/utils/uuids'; +import { random, seeded } from '~/lib/utils/uuids/v4'; const HEX = /[a-f0-9]/i; const HEX_RE = HEX.source; @@ -7,8 +7,43 @@ const UUIDV4 = new RegExp( 'i', ); +function arrayOf(length) { + return { + using(uuidGenerator) { + return Array(length).fill(0).map(uuidGenerator); + }, + }; +} + describe('UUIDs Util', () => { - describe('uuids', () => { + describe('random', () => { + it('returns a version 4 UUID', () => { + expect(random()).toMatch(UUIDV4); + }); + + describe('unseeded UUID randomness', () => { + const nonRandom = arrayOf(6).using((_, i) => seeded({ seeds: [i] })); + const randomSet = arrayOf(6).using(() => random()); + const additionalRandomSet = arrayOf(6).using(() => random()); + + it('is different from a seeded result', () => { + randomSet.forEach((id, i) => { + expect(id).not.toEqual(nonRandom[i]); + }); + }); + + it('is different from other random results', () => { + randomSet.forEach((id, i) => { + expect(id).not.toEqual(additionalRandomSet[i]); + }); + }); + + it('never produces any duplicates', () => { + expect(new Set(randomSet).size).toEqual(randomSet.length); + }); + }); + }); + describe('seeded', () => { const SEQUENCE_FOR_GITLAB_SEED = [ 'a1826a44-316c-480e-a93d-8cdfeb36617c', 'e049db1f-a4cf-4cba-aa60-6d95e3b547dc', @@ -24,17 +59,6 @@ describe('UUIDs Util', () => { 'bfb85869-5fb9-4c5b-a750-5af727ac5576', ]; - it('returns version 4 UUIDs', () => { - expect(uuids()[0]).toMatch(UUIDV4); - }); - - it('outputs an array of UUIDs', () => { - const ids = uuids({ count: 11 }); - - expect(ids.length).toEqual(11); - expect(ids.every((id) => UUIDV4.test(id))).toEqual(true); - }); - it.each` seeds | uuid ${['some', 'special', 'seed']} | ${'6fa53e51-0f70-4072-9c84-1c1eee1b9934'} @@ -50,34 +74,10 @@ describe('UUIDs Util', () => { `( 'should always output the UUID $uuid when the options.seeds argument is $seeds', ({ uuid, seeds }) => { - expect(uuids({ seeds })[0]).toEqual(uuid); + expect(seeded({ seeds })).toEqual(uuid); }, ); - 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 }); - - it('is different from a seeded result', () => { - random.forEach((id, i) => { - expect(id).not.toEqual(nonRandom[i]); - }); - }); - - it('is different from other random results', () => { - random.forEach((id, i) => { - expect(id).not.toEqual(moreRandom[i]); - }); - }); - - it('never produces any duplicates', () => { - expect(new Set(random).size).toEqual(random.length); - }); - }); - it.each` seed | sequence ${'GitLab'} | ${SEQUENCE_FOR_GITLAB_SEED} @@ -85,7 +85,7 @@ describe('UUIDs Util', () => { `( 'should output the same sequence of UUIDs for the given seed "$seed"', ({ seed, sequence }) => { - expect(uuids({ seeds: [seed], count: 5 })).toEqual(sequence); + expect(seeded({ seeds: [seed], count: 5 })).toEqual(sequence); }, ); }); diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js index e89477ed5993547f287f055088afef3411894803..6237a322df535fc3264c4b449364170a70f19251 100644 --- a/spec/frontend/work_items/pages/create_work_item_spec.js +++ b/spec/frontend/work_items/pages/create_work_item_spec.js @@ -15,7 +15,7 @@ import { createWorkItemFromTaskMutationResponse, } from '../mock_data'; -jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] })); +jest.mock('~/lib/utils/uuids/v4', () => ({ random: () => 'testuuid' })); Vue.use(VueApollo);