feat: password generator to test translation

This commit is contained in:
Ibrahima G. Coulibaly
2025-07-14 18:33:59 +01:00
parent 3b5f852287
commit 4441f987d2
8 changed files with 407 additions and 129 deletions

View File

@@ -17,6 +17,7 @@ import { tool as stringTruncate } from './truncate/meta';
import { tool as stringBase64 } from './base64/meta';
import { tool as stringStatistic } from './statistic/meta';
import { tool as stringCensor } from './censor/meta';
import { tool as stringPasswordGenerator } from './password-generator/meta';
export const stringTools = [
stringSplit,
@@ -37,5 +38,6 @@ export const stringTools = [
stringRot13,
stringBase64,
stringStatistic,
stringCensor
stringCensor,
stringPasswordGenerator
];

View File

@@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { Box, Checkbox, FormControlLabel, FormGroup } from '@mui/material';
import { generatePassword } from './service';
import { initialValues, InitialValuesType } from './initialValues';
import ToolContent from '@components/ToolContent';
import ToolTextResult from '@components/result/ToolTextResult';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { ToolComponentProps } from '@tools/defineTool';
import { GetGroupsType } from '@components/options/ToolOptions';
import { CardExampleType } from '@components/examples/ToolExamples';
import { useTranslation } from 'react-i18next';
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Strong Password (12 characters)',
description:
'Generate a secure password with all character types including symbols.',
sampleText: '',
sampleResult: 'A7#mK9$pL2@x',
sampleOptions: {
length: '12',
includeLowercase: true,
includeUppercase: true,
includeNumbers: true,
includeSymbols: true,
avoidAmbiguous: false
}
},
{
title: 'Simple Password (8 characters)',
description: 'Generate a basic password with letters and numbers only.',
sampleText: '',
sampleResult: 'Ab3mK9pL',
sampleOptions: {
length: '8',
includeLowercase: true,
includeUppercase: true,
includeNumbers: true,
includeSymbols: false,
avoidAmbiguous: false
}
},
{
title: 'Clear Password (No ambiguous)',
description:
'Generate a password without ambiguous characters (i, I, l, 0, O).',
sampleText: '',
sampleResult: 'A7#mK9$pL2@x',
sampleOptions: {
length: '12',
includeLowercase: true,
includeUppercase: true,
includeNumbers: true,
includeSymbols: true,
avoidAmbiguous: true
}
}
];
export default function PasswordGenerator({ title }: ToolComponentProps) {
const { t } = useTranslation('string');
const [result, setResult] = useState<string>('');
function compute(values: InitialValuesType) {
setResult(generatePassword(values));
}
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: t('passwordGenerator.optionsTitle'),
component: (
<Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
<TextFieldWithDesc
description={t('passwordGenerator.lengthDesc')}
placeholder={t('passwordGenerator.lengthPlaceholder')}
value={values.length}
onOwnChange={(val) => updateField('length', val)}
type="number"
/>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={values.includeLowercase}
onChange={(e) =>
updateField('includeLowercase', e.target.checked)
}
/>
}
label={t('passwordGenerator.includeLowercase')}
/>
<FormControlLabel
control={
<Checkbox
checked={values.includeUppercase}
onChange={(e) =>
updateField('includeUppercase', e.target.checked)
}
/>
}
label={t('passwordGenerator.includeUppercase')}
/>
<FormControlLabel
control={
<Checkbox
checked={values.includeNumbers}
onChange={(e) =>
updateField('includeNumbers', e.target.checked)
}
/>
}
label={t('passwordGenerator.includeNumbers')}
/>
<FormControlLabel
control={
<Checkbox
checked={values.includeSymbols}
onChange={(e) =>
updateField('includeSymbols', e.target.checked)
}
/>
}
label={t('passwordGenerator.includeSymbols')}
/>
<FormControlLabel
control={
<Checkbox
checked={values.avoidAmbiguous}
onChange={(e) =>
updateField('avoidAmbiguous', e.target.checked)
}
/>
}
label={t('passwordGenerator.avoidAmbiguous')}
/>
</FormGroup>
</Box>
)
}
];
return (
<ToolContent
title={title}
initialValues={initialValues}
getGroups={getGroups}
compute={compute}
resultComponent={
<ToolTextResult
title={t('passwordGenerator.resultTitle')}
value={result}
/>
}
toolInfo={{
title: t('passwordGenerator.toolInfo.title'),
description: t('passwordGenerator.toolInfo.description')
}}
exampleCards={exampleCards}
/>
);
}

View File

@@ -0,0 +1,17 @@
export type InitialValuesType = {
length: string; // user enters a number here
includeLowercase: boolean;
includeUppercase: boolean;
includeNumbers: boolean;
includeSymbols: boolean;
avoidAmbiguous: boolean;
};
export const initialValues: InitialValuesType = {
length: '12',
includeLowercase: true,
includeUppercase: true,
includeNumbers: true,
includeSymbols: true,
avoidAmbiguous: false
};

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('string', {
path: 'password-generator',
icon: 'material-symbols:key',
keywords: ['password', 'generator', 'random', 'secure'],
component: lazy(() => import('./index')),
i18n: {
name: 'string:passwordGenerator.title',
description: 'string:passwordGenerator.description',
shortDescription: 'string:passwordGenerator.shortDescription'
}
});

View File

@@ -0,0 +1,143 @@
import { describe, expect, it } from 'vitest';
import { generatePassword } from './service';
import { initialValues } from './initialValues';
describe('generatePassword', () => {
it('should generate a password with the specified length', () => {
const options = { ...initialValues, length: '10' };
const result = generatePassword(options);
expect(result).toHaveLength(10);
});
it('should return empty string for invalid length', () => {
const options = { ...initialValues, length: '0' };
const result = generatePassword(options);
expect(result).toBe('');
});
it('should return empty string for non-numeric length', () => {
const options = { ...initialValues, length: 'abc' };
const result = generatePassword(options);
expect(result).toBe('');
});
it('should return empty string when no character types are selected', () => {
const options = {
...initialValues,
includeLowercase: false,
includeUppercase: false,
includeNumbers: false,
includeSymbols: false
};
const result = generatePassword(options);
expect(result).toBe('');
});
it('should only include lowercase letters when only lowercase is selected', () => {
const options = {
...initialValues,
length: '20',
includeLowercase: true,
includeUppercase: false,
includeNumbers: false,
includeSymbols: false
};
const result = generatePassword(options);
expect(result).toMatch(/^[a-z]+$/);
expect(result).toHaveLength(20);
});
it('should only include uppercase letters when only uppercase is selected', () => {
const options = {
...initialValues,
length: '15',
includeLowercase: false,
includeUppercase: true,
includeNumbers: false,
includeSymbols: false
};
const result = generatePassword(options);
expect(result).toMatch(/^[A-Z]+$/);
expect(result).toHaveLength(15);
});
it('should only include numbers when only numbers is selected', () => {
const options = {
...initialValues,
length: '8',
includeLowercase: false,
includeUppercase: false,
includeNumbers: true,
includeSymbols: false
};
const result = generatePassword(options);
expect(result).toMatch(/^[0-9]+$/);
expect(result).toHaveLength(8);
});
it('should include mixed character types when multiple are selected', () => {
const options = {
...initialValues,
length: '100', // larger sample for better testing
includeLowercase: true,
includeUppercase: true,
includeNumbers: true,
includeSymbols: false
};
const result = generatePassword(options);
expect(result).toMatch(/^[a-zA-Z0-9]+$/);
expect(result).toHaveLength(100);
});
it('should exclude ambiguous characters when avoidAmbiguous is true', () => {
const options = {
...initialValues,
length: '50',
avoidAmbiguous: true
};
const result = generatePassword(options);
expect(result).not.toMatch(/[iIl0O]/);
expect(result).toHaveLength(50);
});
it('should include symbols when includeSymbols is true', () => {
const options = {
...initialValues,
length: '30',
includeLowercase: false,
includeUppercase: false,
includeNumbers: false,
includeSymbols: true
};
const result = generatePassword(options);
expect(result).toMatch(/^[!@#$%^&*()_+~`|}{[\]:;?><,./-=]+$/);
expect(result).toHaveLength(30);
});
it('should exclude ambiguous characters from symbols too', () => {
const options = {
...initialValues,
length: '50',
includeLowercase: false,
includeUppercase: false,
includeNumbers: true,
includeSymbols: true,
avoidAmbiguous: true
};
const result = generatePassword(options);
expect(result).not.toMatch(/[iIl0O]/);
expect(result).toHaveLength(50);
});
it('should handle edge case with very short length', () => {
const options = { ...initialValues, length: '1' };
const result = generatePassword(options);
expect(result).toHaveLength(1);
});
it('should handle negative length', () => {
const options = { ...initialValues, length: '-5' };
const result = generatePassword(options);
expect(result).toBe('');
});
});

View File

@@ -0,0 +1,38 @@
import type { InitialValuesType } from './initialValues';
export function generatePassword(options: InitialValuesType): string {
const length = parseInt(options.length || '', 10);
if (isNaN(length) || length <= 0) {
return '';
}
let charset = '';
const lower = 'abcdefghijklmnopqrstuvwxyz';
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
const symbols = '!@#$%^&*()_+~`|}{[]:;?><,./-=';
if (options.includeLowercase) charset += lower;
if (options.includeUppercase) charset += upper;
if (options.includeNumbers) charset += numbers;
if (options.includeSymbols) charset += symbols;
if (options.avoidAmbiguous) {
// ambiguous set = i, I, l, 0, O
const ambig = new Set(['i', 'I', 'l', '0', 'O']);
charset = Array.from(charset)
.filter((c) => !ambig.has(c))
.join('');
}
if (!charset) {
return ''; // nothing to pick from
}
let pwd = '';
for (let i = 0; i < length; i++) {
const idx = Math.floor(Math.random() * charset.length);
pwd += charset[idx];
}
return pwd;
}