From 30bc4f553d5efa2b2cf4be8314fe42e1f1cc983c Mon Sep 17 00:00:00 2001 From: Hubert Lin Date: Sun, 24 Apr 2022 23:28:43 -0600 Subject: [PATCH 1/2] test: nanoid --- tests/nanoid.test.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/nanoid.test.ts diff --git a/tests/nanoid.test.ts b/tests/nanoid.test.ts new file mode 100644 index 0000000..eb32459 --- /dev/null +++ b/tests/nanoid.test.ts @@ -0,0 +1,72 @@ +import { + assert, + assertEquals, + assertMatch, + assertThrows, +} from "https://deno.land/std@0.136.0/testing/asserts.ts"; +import { nanoid } from "../nanoid.ts"; +import { urlAlphabet } from "../urlAlphabet.ts"; + +Deno.test("nanoid / generates URL safe IDs", () => { + const id = nanoid(); + assert(typeof (id) === "string"); + assertMatch(id, /^[a-zA-Z0-9_-]*$/); +}); + +Deno.test("nanoid / default length of 21", () => { + const id = nanoid(); + assertEquals(id.length, 21); +}); + +Deno.test("nanoid / handles size of 0", () => { + const id = nanoid(0); + assertEquals(id.length, 0); +}); + +Deno.test("nanoid / throws when size is negative", () => { + assertThrows(() => nanoid(-1), Error, "Invalid typed array length"); +}); + +Deno.test("nanoid / throws when size too big", () => { + assertThrows( + () => nanoid(65537), + Error, + "The ArrayBufferView's byte length (65537) exceeds the number of bytes of entropy available via this API (65536)", + ); +}); + +Deno.test("nanoid / has no collisions", () => { + const used = new Map(); + for (let i = 0; i < 50 * 1000; i++) { + const id = nanoid(); + assertEquals(used.has(id), false); + used.set(id, true); + } +}); + +Deno.test("nanoid / has flat distribution", () => { + const COUNT = 50 * 1000; + const LENGTH = nanoid().length; + + const used = new Map(); + for (let i = 0; i < COUNT; i++) { + const id = nanoid(); + for (const char of id) { + const timesUsed = used.get(char) ?? 0; + used.set(char, timesUsed + 1); + } + } + + assertEquals(used.size, urlAlphabet.length); + + let max = 0; + let min = Number.MAX_SAFE_INTEGER; + for (const k in used.keys()) { + const occurences = used.get(k) ?? 0; + const distribution = (occurences * urlAlphabet.length) / (COUNT * LENGTH); + if (distribution > max) max = distribution; + if (distribution < min) min = distribution; + } + + assert((max - min) <= 0.05, "Max and min difference too big"); +}); From 1a80e3d3a7a7d64abe5081918ab3aab8c1b1ed02 Mon Sep 17 00:00:00 2001 From: Hubert Lin Date: Mon, 25 Apr 2022 00:45:20 -0600 Subject: [PATCH 2/2] test: customRandom --- customRandom.ts | 4 ++ tests/customRandom.test.ts | 83 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/customRandom.test.ts diff --git a/customRandom.ts b/customRandom.ts index 791ce96..3c430b1 100644 --- a/customRandom.ts +++ b/customRandom.ts @@ -1,6 +1,10 @@ export type CustomRandomGenerator = (size: number) => Uint8Array | Uint16Array | Uint32Array; export const customRandom = (random: CustomRandomGenerator, alphabet: string, size: number) => { + if (size === 0) return () => ""; + if (size < 0) throw Error("Size must be positive"); + if (alphabet.length > 256) throw Error("Alphabet must contain 256 symbols or less") + const mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1; const step = -~(1.6 * mask * size / alphabet.length); diff --git a/tests/customRandom.test.ts b/tests/customRandom.test.ts new file mode 100644 index 0000000..4153987 --- /dev/null +++ b/tests/customRandom.test.ts @@ -0,0 +1,83 @@ +import { + assert, + assertEquals, + assertMatch, + assertThrows, +} from "https://deno.land/std@0.136.0/testing/asserts.ts"; +import { customRandom } from "../customRandom.ts"; +import { random } from "../random.ts"; +import { urlAlphabet } from "../urlAlphabet.ts"; + +Deno.test("customRandom / generates URL safe IDs", () => { + const id = customRandom(random, urlAlphabet, 21)(); + assert(typeof (id) === "string"); + + // https://www.ietf.org/rfc/rfc3986.html#section-2.3 + assertMatch(id, /^[a-zA-Z0-9-._~]*$/); +}); + +Deno.test("customRandom / throws when alphabet length is greater than 255", () => { + const US_ALPHABET = + "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + const JAPANESE_ALPHABET = + "ーぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶ゛゜"; + const alphabet = US_ALPHABET + JAPANESE_ALPHABET; + assert(alphabet.length > 255); + assertThrows(() => customRandom(random, alphabet, 21)(), Error); +}); + +Deno.test("customRandom / handles size of 0", () => { + const id = customRandom(random, urlAlphabet, 0)(); + assertEquals(id.length, 0); +}); + +Deno.test("customRandom / throws when size is negative", () => { + assertThrows( + () => customRandom(random, urlAlphabet, -1)(), + Error, + ); +}); + +Deno.test("customRandom / throws when size too big", () => { + assertThrows( + () => customRandom(random, urlAlphabet, 65537)(), + Error, + "The ArrayBufferView's byte length (103221) exceeds the number of bytes of entropy available via this API (65536)", + ); +}); + +Deno.test("customRandom / has no collisions", () => { + const used = new Map(); + for (let i = 0; i < 50 * 1000; i++) { + const id = customRandom(random, urlAlphabet, 21)(); + assertEquals(used.has(id), false); + used.set(id, true); + } +}); + +Deno.test("customRandom / has flat distribution", () => { + const COUNT = 50 * 1000; + const LENGTH = customRandom(random, urlAlphabet, 21)().length; + + const used = new Map(); + for (let i = 0; i < COUNT; i++) { + const id = customRandom(random, urlAlphabet, 21)(); + for (const char of id) { + const timesUsed = used.get(char) ?? 0; + used.set(char, timesUsed + 1); + } + } + + assertEquals(used.size, urlAlphabet.length); + + let max = 0; + let min = Number.MAX_SAFE_INTEGER; + for (const k in used.keys()) { + const occurences = used.get(k) ?? 0; + const distribution = (occurences * urlAlphabet.length) / (COUNT * LENGTH); + if (distribution > max) max = distribution; + if (distribution < min) min = distribution; + } + + assert((max - min) <= 0.05, "Max and min difference too big"); +});