Refactor tests (#541)

This commit is contained in:
Leendert de Borst
2025-02-01 12:20:16 +01:00
parent f6c66a9964
commit 40b7ecd2fe
5 changed files with 198 additions and 5 deletions

View File

@@ -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 = (

View File

@@ -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';

View File

@@ -1,4 +1,4 @@
import { Identity } from "./Identity";
import { Identity } from "../types/Identity";
export interface IIdentityGenerator {
generateRandomIdentity(): Promise<Identity>;

View File

@@ -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;
}
}

View File

@@ -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);
});
});