random-browser
Version:
The random module is used for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets.
431 lines (384 loc) • 12 kB
JavaScript
import { webcrypto } from 'crypto'
global.crypto = {
getRandomValues: function (buffer) { return webcrypto.getRandomValues(buffer) }
}
let r
if (process.env.NODE_TEST_MINIFIED) {
r = await import('./random.min.js')
} else {
r = await import('./random.js')
}
const maxInt = Number.MAX_SAFE_INTEGER
const minInt = Number.MIN_SAFE_INTEGER
test('choice(arrPets)', () => {
const randomChoices = []
const arrPets = ['Cat', 'Dog', 'Fish']
for (let i = 0; i < 100; i++) {
const value = r.choice(arrPets)
expect(arrPets).toContain(value)
randomChoices.push(value)
}
expect(randomChoices).toContain('Cat')
expect(randomChoices).toContain('Dog')
expect(randomChoices).toContain('Fish')
})
test('choice("ABC")', () => {
const randomChoices = []
for (let i = 0; i < 100; i++) {
const value = r.choice('ABC')
expect('ABC').toContain(value)
randomChoices.push(value)
}
expect(randomChoices).toContain('A')
expect(randomChoices).toContain('B')
expect(randomChoices).toContain('C')
})
test('randomBits(2)', () => {
const randomInts = []
for (let i = 0; i < 100; i++) {
const value = r.randomBits(2)
expect(value).toBeGreaterThanOrEqual(0)
expect(value).toBeLessThan(4)
randomInts.push(value)
}
expect(randomInts).toContain(0)
expect(randomInts).toContain(1)
expect(randomInts).toContain(2)
expect(randomInts).toContain(3)
})
test('randomBits(0) -> randomBits(48)', () => {
for (let i = 0; i <= 48; i++) {
const value = r.randomBits(i)
expect(value).toBeGreaterThanOrEqual(0)
expect(value).toBeLessThan(2 ** i)
}
})
test('randomBytes(0)', () => {
const value = r.randomBytes(0)
expect(value.length).toBe(0)
})
test('randomBytes(8)', () => {
const value = r.randomBytes(8)
expect(value.length).toBe(8)
})
test('randomBytes(65536)', () => {
const value = r.randomBytes(65536)
expect(value.length).toBe(65536)
})
test('randomInt(3)', () => {
const randomInts = []
for (let i = 0; i < 100; i++) {
const value = r.randomInt(3)
expect(value).toBeGreaterThanOrEqual(0)
expect(value).toBeLessThan(3)
randomInts.push(value)
}
expect(randomInts).toContain(0)
expect(randomInts).toContain(1)
expect(randomInts).toContain(2)
})
test('randomInt(1, 3)', () => {
const randomInts = []
for (let i = 0; i < 100; i++) {
const value = r.randomInt(1, 3)
expect(value).toBeGreaterThanOrEqual(1)
expect(value).toBeLessThan(3)
randomInts.push(value)
}
expect(randomInts).toContain(1)
expect(randomInts).toContain(2)
})
test('randomInt(-10, -8)', () => {
const randomInts = []
for (let i = 0; i < 100; i++) {
const value = r.randomInt(-10, -8)
expect(value).toBeGreaterThanOrEqual(-10)
expect(value).toBeLessThan(-8)
randomInts.push(value)
}
expect(randomInts).toContain(-10)
expect(randomInts).toContain(-9)
})
test('randomInt(0xFFFF_FFFF_FFFF)', () => {
const value = r.randomInt(0xFFFF_FFFF_FFFF)
expect(value).toBeGreaterThanOrEqual(0)
expect(value).toBeLessThan(0xFFFF_FFFF_FFFF)
})
test('randomInt(minInt, minInt + 5)', () => {
const value = r.randomInt(minInt, minInt + 5)
expect(value).toBeGreaterThanOrEqual(minInt)
expect(value).toBeLessThan(minInt + 5)
})
test('randomInt(maxInt - 5, maxInt)', () => {
const value = r.randomInt(maxInt - 5, maxInt)
expect(value).toBeGreaterThanOrEqual(maxInt - 5)
expect(value).toBeLessThan(maxInt)
})
test('randomInt(1) -> randomInt(1000)', () => {
for (let i = 1; i <= 1000; i++) {
const value = r.randomInt(i)
expect(value).toBeGreaterThanOrEqual(0)
expect(value).toBeLessThan(i)
}
})
test('shuffle([])', () => {
const arr = []
r.shuffle(arr)
expect(arr).toEqual([])
})
test('shuffle([1])', () => {
const arr = [1]
r.shuffle(arr)
expect(arr).toEqual([1])
})
test('shuffle([1, 2])', () => {
const firstElements = []
for (let i = 0; i < 100; i++) {
const arr = [1, 2]
r.shuffle(arr)
expect(arr).toContain(1)
expect(arr).toContain(2)
expect(arr.length).toBe(2)
firstElements.push(arr[0])
}
expect(firstElements).toContain(1)
expect(firstElements).toContain(2)
})
test('shuffle() includes all elements', () => {
const origList = []
const shufList = []
for (let i = 0; i < 1000; i++) {
origList.push(i)
shufList.push(i)
}
r.shuffle(shufList)
expect(shufList.length).toBe(origList.length)
expect(shufList).not.toEqual(origList)
expect(new Set(shufList)).toEqual(new Set(origList))
r.shuffle(origList)
expect(shufList).not.toEqual(origList)
expect(new Set(shufList)).toEqual(new Set(origList))
})
test('tokenHex() collision', () => {
let temp = ''
for (let i = 0; i < 100; i++) {
const value = r.tokenHex()
expect(value.length).toBe(64)
expect(value).toMatch(/^[a-f0-9]+$/)
expect(value).not.toBe(temp)
temp = value
}
})
test('tokenHex() length', () => {
for (let i = 0; i < 100; i++) {
expect(r.tokenHex(i).length).toBe(i * 2)
}
expect(r.tokenHex(65536).length).toBe(131072)
})
test('tokenUrlsafe() collision', () => {
let temp = ''
for (let i = 0; i < 100; i++) {
const value = r.tokenUrlsafe()
expect(value.length).toBe(43)
expect(value).toMatch(/^[A-Za-z0-9\-_]+$/)
expect(value).not.toBe(temp)
temp = value
}
})
test('tokenUrlsafe() length', () => {
for (let i = 0; i < 100; i++) {
expect(r.tokenUrlsafe(i).length).toBe(Math.ceil(i * 4 / 3))
}
expect(r.tokenUrlsafe(65536).length).toBe(87382)
})
test('uuidv7() format and order', () => {
let temp = ''
for (let i = 0; i < 100; i++) {
const value = r.uuidv7()
expect(value).toMatch(/^[a-f0-9]{8}-[a-f0-9]{4}-7[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/)
expect(value > temp).toBeTruthy()
temp = value
}
})
describe('errors', () => {
test.each([[], ''])('choice(%p)', (i) => {
expect(() => r.choice(i)).toThrow(RangeError)
})
test.each([2.5, 10, false, true, NaN, null, {}, undefined])('choice(%p)', (i) => {
expect(() => r.choice(i)).toThrow(TypeError)
})
test.each([-1, 49])('randomBits(%i)', (i) => {
expect(() => r.randomBits(i)).toThrow(RangeError)
})
test.each([2.5, '10', false, true, NaN, null, {}, [], undefined])('randomBits(%p)', (i) => {
expect(() => r.randomBits(i)).toThrow(new TypeError('"k" must be an integer.'))
})
test('randomBytes(-1)', () => {
expect(() => r.randomBytes(-1)).toThrow(RangeError)
})
test.each([2.5, '10', false, true, NaN, null, {}, [], undefined])('randomBytes(%p)', (i) => {
expect(() => r.randomBytes(i)).toThrow(new TypeError('The argument must be an integer.'))
})
test.each([
[0, 0], [1, 1], [3, 2], [-5, -5], [11, -10], [-1, 0xFFFF_FFFF_FFFF]
])('randomInt(%i, %i)', (min, max) => {
expect(() => r.randomInt(min, max)).toThrow(RangeError)
})
test.each([
[maxInt, maxInt + 1, '"max" is not a safe integer.'],
[minInt - 1, minInt, '"min" is not a safe integer.']
])('randomInt(%i, %i) -> %p', (min, max, err) => {
expect(() => r.randomInt(min, max)).toThrow(new TypeError(err))
})
test.each([2.5, '10', false, true, NaN, null, {}, []])('randomInt(%p)', (i) => {
expect(() => r.randomInt(i, 100)).toThrow(new TypeError('"min" is not a safe integer.'))
expect(() => r.randomInt(i)).toThrow(new TypeError('"max" is not a safe integer.'))
expect(() => r.randomInt(0, i)).toThrow(new TypeError('"max" is not a safe integer.'))
})
test.each([2.5, 10, '10', false, true, NaN, null, {}])('shuffle(%p)', (i) => {
expect(() => r.shuffle(i)).toThrow(new TypeError('The argument must be an array.'))
})
test.each([2.5, '10', false, true, NaN, null, {}, []])('tokenHex(%p)', (i) => {
expect(() => r.tokenHex(i)).toThrow(new TypeError('The argument must be an integer.'))
})
test.each([2.5, '10', false, true, NaN, null, {}, []])('tokenUrlsafe(%p)', (i) => {
expect(() => r.tokenUrlsafe(i)).toThrow(new TypeError('The argument must be an integer.'))
})
})
// Doesn't guarantee correctness, but at least the outputs are appearing in the full range.
describe('distribution', () => {
test.each([32, 256, 65536])('randomBytes(%i)', (m) => {
const dict = {}
for (let i = 0; i < 1_000_000 / m; i++) {
const bytes = r.randomBytes(m)
for (let j = 0; j < bytes.length; j++) {
if (bytes[j] in dict) {
dict[bytes[j]]++
} else {
dict[bytes[j]] = 1
}
}
}
expect(Object.keys(dict).length).toBe(256)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.8)
})
test.each([2, 14, 15, 16, 17, 254, 255, 256, 257])('randomInt(%i)', (m) => {
const dict = {}
for (let i = 0; i < 300 * m; i++) {
const n = r.randomInt(m)
if (n in dict) {
dict[n]++
} else {
dict[n] = 1
}
}
expect(Object.keys(dict).length).toBe(m)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.5)
})
test.each([1, 2, 3, 4, 5, 6, 7, 8, 9])('randomBits(%i)', (m) => {
const dict = {}
for (let i = 0; i < 300 * 2 ** m; i++) {
const n = r.randomBits(m)
if (n in dict) {
dict[n]++
} else {
dict[n] = 1
}
}
expect(Object.keys(dict).length).toBe(2 ** m)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.5)
})
test.each([[32, 1], [48, 1]])('randomBits(%i) average', (k, numDigits) => {
const expected = (2 ** k - 1) / 2
const rounds = 10_000
let avg = 0
for (let i = 0; i < rounds; i++) {
avg += r.randomBits(k)
}
avg /= rounds
expect(avg / expected).toBeCloseTo(1.0, numDigits)
})
test('shuffle([1, 2, 4, 8])', () => {
const dict = {}
for (let i = 0; i < 10_000; i++) {
const arr = [1, 2, 4, 8]
r.shuffle(arr)
for (let j = 0; j < arr.length; j++) {
const idx = arr[j] << (j * arr.length)
if (idx in dict) {
dict[idx]++
} else {
dict[idx] = 1
}
}
}
expect(Object.keys(dict).length).toBe(16)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.9)
})
test('tokenHex()', () => {
const dict = {}
for (let i = 0; i < 10_000; i++) {
const token = r.tokenHex()
for (let j = 0; j < token.length; j++) {
if (token[j] in dict) {
dict[token[j]]++
} else {
dict[token[j]] = 1
}
}
}
expect(Object.keys(dict).length).toBe(16)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.9)
})
test('tokenUrlsafe()', () => {
const dict = {}
for (let i = 0; i < 10_000; i++) {
const token = r.tokenUrlsafe()
for (let j = 0; j < token.length; j++) {
if (token[j] in dict) {
dict[token[j]]++
} else {
dict[token[j]] = 1
}
}
}
expect(Object.keys(dict).length).toBe(64)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.8)
})
test('uuidv7() last random bytes', () => {
const dict = {}
for (let i = 0; i < 10_000; i++) {
const token = r.uuidv7()
for (let j = 20; j < token.length; j++) {
if (token[j] in dict) {
dict[token[j]]++
} else if (token[j] !== '-') {
dict[token[j]] = 1
}
}
}
expect(Object.keys(dict).length).toBe(16)
const values = Object.values(dict)
const max = Math.max(...values)
const min = Math.min(...values)
expect(min / max).toBeGreaterThan(0.9)
})
})