From 40b7ecd2fe2d03bc86a23fc9b57f37fe2a58be35 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 1 Feb 2025 12:20:16 +0100 Subject: [PATCH] Refactor tests (#541) --- .../__tests__/IdentityGenerator.test.ts | 6 +- .../base/BaseIdentityGenerator.ts | 2 +- .../IIdentityGenerator.ts | 2 +- .../generators/Password/PasswordGenerator.ts | 102 ++++++++++++++++++ .../__tests__/PasswordGenerator.test.ts | 91 ++++++++++++++++ 5 files changed, 198 insertions(+), 5 deletions(-) rename browser-extensions/chrome/src/generators/Identity/{implementations => }/__tests__/IdentityGenerator.test.ts (87%) rename browser-extensions/chrome/src/generators/Identity/{types => interfaces}/IIdentityGenerator.ts (65%) create mode 100644 browser-extensions/chrome/src/generators/Password/PasswordGenerator.ts create mode 100644 browser-extensions/chrome/src/generators/Password/__tests__/PasswordGenerator.test.ts diff --git a/browser-extensions/chrome/src/generators/Identity/implementations/__tests__/IdentityGenerator.test.ts b/browser-extensions/chrome/src/generators/Identity/__tests__/IdentityGenerator.test.ts similarity index 87% rename from browser-extensions/chrome/src/generators/Identity/implementations/__tests__/IdentityGenerator.test.ts rename to browser-extensions/chrome/src/generators/Identity/__tests__/IdentityGenerator.test.ts index 7e72b565a..febbf0ff7 100644 --- a/browser-extensions/chrome/src/generators/Identity/implementations/__tests__/IdentityGenerator.test.ts +++ b/browser-extensions/chrome/src/generators/Identity/__tests__/IdentityGenerator.test.ts @@ -1,7 +1,7 @@ -import { IdentityGeneratorEn } from '../IdentityGeneratorEn'; -import { IdentityGeneratorNl } from '../IdentityGeneratorNl'; +import { IdentityGeneratorEn } from '../implementations/IdentityGeneratorEn'; +import { IdentityGeneratorNl } from '../implementations/IdentityGeneratorNl'; import { describe, it, expect } from 'vitest'; -import { IIdentityGenerator } from '../../types/IIdentityGenerator'; +import { IIdentityGenerator } from '../interfaces/IIdentityGenerator'; // Test factory function to run tests for each language implementation const testIdentityGenerator = ( diff --git a/browser-extensions/chrome/src/generators/Identity/implementations/base/BaseIdentityGenerator.ts b/browser-extensions/chrome/src/generators/Identity/implementations/base/BaseIdentityGenerator.ts index de0859be0..4ed795ef3 100644 --- a/browser-extensions/chrome/src/generators/Identity/implementations/base/BaseIdentityGenerator.ts +++ b/browser-extensions/chrome/src/generators/Identity/implementations/base/BaseIdentityGenerator.ts @@ -1,6 +1,6 @@ import { UsernameEmailGenerator } from '../../UsernameEmailGenerator'; import { Gender } from '../../types/Gender'; -import { IIdentityGenerator } from '../../types/IIdentityGenerator'; +import { IIdentityGenerator } from '../../interfaces/IIdentityGenerator'; import { Identity } from '../../types/Identity'; import * as fs from 'fs'; diff --git a/browser-extensions/chrome/src/generators/Identity/types/IIdentityGenerator.ts b/browser-extensions/chrome/src/generators/Identity/interfaces/IIdentityGenerator.ts similarity index 65% rename from browser-extensions/chrome/src/generators/Identity/types/IIdentityGenerator.ts rename to browser-extensions/chrome/src/generators/Identity/interfaces/IIdentityGenerator.ts index 8d7d59044..437622b51 100644 --- a/browser-extensions/chrome/src/generators/Identity/types/IIdentityGenerator.ts +++ b/browser-extensions/chrome/src/generators/Identity/interfaces/IIdentityGenerator.ts @@ -1,4 +1,4 @@ -import { Identity } from "./Identity"; +import { Identity } from "../types/Identity"; export interface IIdentityGenerator { generateRandomIdentity(): Promise; diff --git a/browser-extensions/chrome/src/generators/Password/PasswordGenerator.ts b/browser-extensions/chrome/src/generators/Password/PasswordGenerator.ts new file mode 100644 index 000000000..6e416a471 --- /dev/null +++ b/browser-extensions/chrome/src/generators/Password/PasswordGenerator.ts @@ -0,0 +1,102 @@ +export class PasswordGenerator { + private readonly lowercaseChars = 'abcdefghijklmnopqrstuvwxyz'; + private readonly uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + private readonly numberChars = '0123456789'; + private readonly specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?'; + + private length: number = 18; + private useLowercase: boolean = true; + private useUppercase: boolean = true; + private useNumbers: boolean = true; + private useSpecial: boolean = true; + + public setLength(length: number): PasswordGenerator { + this.length = length; + return this; + } + + public useLowercaseLetters(use: boolean): PasswordGenerator { + this.useLowercase = use; + return this; + } + + public useUppercaseLetters(use: boolean): PasswordGenerator { + this.useUppercase = use; + return this; + } + + public useNumericCharacters(use: boolean): PasswordGenerator { + this.useNumbers = use; + return this; + } + + public useSpecialCharacters(use: boolean): PasswordGenerator { + this.useSpecial = use; + return this; + } + + private getUnbiasedRandomIndex(max: number): number { + // Calculate the largest multiple of max that fits within Uint32 + const limit = Math.floor((2 ** 32) / max) * max; + + while (true) { + const array = new Uint32Array(1); + crypto.getRandomValues(array); + const value = array[0]; + + // Reject values that would introduce bias + if (value < limit) { + return value % max; + } + } + } + + public generateRandomPassword(): string { + let chars = ''; + let password = ''; + + // Build character set based on options + if (this.useLowercase) chars += this.lowercaseChars; + if (this.useUppercase) chars += this.uppercaseChars; + if (this.useNumbers) chars += this.numberChars; + if (this.useSpecial) chars += this.specialChars; + + // Ensure at least one character set is selected + if (chars.length === 0) { + chars = this.lowercaseChars; + } + + // Generate password + for (let i = 0; i < this.length; i++) { + password += chars[this.getUnbiasedRandomIndex(chars.length)]; + } + + // Ensure password contains at least one character from each selected set + if (this.useLowercase && !password.match(/[a-z]/)) { + const pos = this.getUnbiasedRandomIndex(this.length); + password = password.substring(0, pos) + + this.lowercaseChars[this.getUnbiasedRandomIndex(this.lowercaseChars.length)] + + password.substring(pos + 1); + } + if (this.useUppercase && !password.match(/[A-Z]/)) { + const pos = this.getUnbiasedRandomIndex(this.length); + password = password.substring(0, pos) + + this.uppercaseChars[this.getUnbiasedRandomIndex(this.uppercaseChars.length)] + + password.substring(pos + 1); + } + if (this.useNumbers && !password.match(/[0-9]/)) { + const pos = this.getUnbiasedRandomIndex(this.length); + password = password.substring(0, pos) + + this.numberChars[this.getUnbiasedRandomIndex(this.numberChars.length)] + + password.substring(pos + 1); + } + if (this.useSpecial && !password.match(/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/)) { + const pos = this.getUnbiasedRandomIndex(this.length); + password = password.substring(0, pos) + + this.specialChars[this.getUnbiasedRandomIndex(this.specialChars.length)] + + password.substring(pos + 1); + } + + return password; + } +} diff --git a/browser-extensions/chrome/src/generators/Password/__tests__/PasswordGenerator.test.ts b/browser-extensions/chrome/src/generators/Password/__tests__/PasswordGenerator.test.ts new file mode 100644 index 000000000..9845a229a --- /dev/null +++ b/browser-extensions/chrome/src/generators/Password/__tests__/PasswordGenerator.test.ts @@ -0,0 +1,91 @@ +import { PasswordGenerator } from '../PasswordGenerator'; +import { describe, it, expect, beforeEach } from 'vitest'; + +describe('PasswordGenerator', () => { + let generator: PasswordGenerator; + + beforeEach(() => { + generator = new PasswordGenerator(); + }); + + it('generates password with default settings', () => { + const password = generator.generateRandomPassword(); + + // Default length is 18 + expect(password.length).toBe(18); + + // Should contain at least one of each character type by default + expect(password).toMatch(/[a-z]/); // lowercase + expect(password).toMatch(/[A-Z]/); // uppercase + expect(password).toMatch(/[0-9]/); // numbers + expect(password).toMatch(/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/); // special + }); + + it('respects custom length setting', () => { + const customLength = 24; + const password = generator.setLength(customLength).generateRandomPassword(); + expect(password.length).toBe(customLength); + }); + + it('respects lowercase setting', () => { + const password = generator.useLowercaseLetters(false).generateRandomPassword(); + expect(password).not.toMatch(/[a-z]/); + }); + + it('respects uppercase setting', () => { + const password = generator.useUppercaseLetters(false).generateRandomPassword(); + expect(password).not.toMatch(/[A-Z]/); + }); + + it('respects numbers setting', () => { + const password = generator.useNumericCharacters(false).generateRandomPassword(); + expect(password).not.toMatch(/[0-9]/); + }); + + it('respects special characters setting', () => { + const password = generator.useSpecialCharacters(false).generateRandomPassword(); + expect(password).not.toMatch(/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/); + }); + + it('generates different passwords on subsequent calls', () => { + const password1 = generator.generateRandomPassword(); + const password2 = generator.generateRandomPassword(); + expect(password1).not.toBe(password2); + }); + + it('handles minimum character requirements', () => { + // Generate multiple passwords to ensure consistency + for (let i = 0; i < 100; i++) { + const password = generator.generateRandomPassword(); + + // Each password should contain at least one character from each enabled set + expect(password).toMatch(/[a-z]/); + expect(password).toMatch(/[A-Z]/); + expect(password).toMatch(/[0-9]/); + expect(password).toMatch(/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/); + } + }); + + it('falls back to lowercase when all options disabled', () => { + const password = generator + .useLowercaseLetters(false) + .useUppercaseLetters(false) + .useNumericCharacters(false) + .useSpecialCharacters(false) + .generateRandomPassword(); + + // Should fall back to lowercase + expect(password).toMatch(/^[a-z]+$/); + }); + + it('maintains method chaining', () => { + const result = generator + .setLength(20) + .useLowercaseLetters(true) + .useUppercaseLetters(true) + .useNumericCharacters(true) + .useSpecialCharacters(true); + + expect(result).toBe(generator); + }); +}); \ No newline at end of file