Files
doormilxpress_astryx/tests/utils/password.test.js
2026-08-27 13:40:48 +05:30

161 lines
5.7 KiB
JavaScript

import { isNumber, isLowercaseChar, isUppercaseChar, isSpecialChar, minLength } from '@/utils/password-validation';
import { strengthColor, strengthIndicator } from '@/utils/password-strength';
/**
* Password rules for the login and change-password forms.
*
* These gate what the console will accept as a credential, so the boundaries
* matter more than the happy path: a rule that is one character loose is a
* weaker password policy than the UI claims to enforce.
*/
describe('password-validation', () => {
describe('character-class rules', () => {
it.each([
['isNumber', isNumber, 'abc1', 'abcd'],
['isLowercaseChar', isLowercaseChar, 'ABCd', 'ABCD'],
['isUppercaseChar', isUppercaseChar, 'abcD', 'abcd'],
['isSpecialChar', isSpecialChar, 'abc!', 'abcd']
])('%s should accept a qualifying value and reject one without it', (_name, rule, pass, fail) => {
expect(rule(pass)).toBe(true);
expect(rule(fail)).toBe(false);
});
it('should accept a digit anywhere in the value, not only at the end', () => {
expect(isNumber('1abc')).toBe(true);
expect(isNumber('ab1c')).toBe(true);
});
it.each(['-', '+', '_', '!', '@', '#', '$', '%', '^', '&', '*', '.', ',', '?'])(
'should treat %s as a special character',
(char) => {
expect(isSpecialChar(`abc${char}`)).toBe(true);
}
);
it.each(['(', ')', '=', '/', '\\', '~', '|', '<', '>', '[', ']', '{', '}', ';', ':', "'", '"'])(
'should NOT treat %s as a special character under the current rule set',
(char) => {
// Documents the accepted set precisely: a user typing a symbol outside
// it is told their password has no special character.
expect(isSpecialChar(`abc${char}`)).toBe(false);
}
);
it.each([isNumber, isLowercaseChar, isUppercaseChar, isSpecialChar])(
'should reject an empty value',
(rule) => {
expect(rule('')).toBe(false);
}
);
});
describe('minLength', () => {
it('should reject a value of exactly eight characters minus one', () => {
expect(minLength('1234567')).toBe(false);
});
it('should accept a value of eight characters', () => {
// The boundary the UI advertises as "at least 8 characters".
expect(minLength('12345678')).toBe(true);
});
it('should accept anything longer', () => {
expect(minLength('123456789')).toBe(true);
});
it('should reject an empty value', () => {
expect(minLength('')).toBe(false);
});
});
});
describe('password-strength', () => {
describe('strengthIndicator', () => {
it('should score an empty password at zero', () => {
expect(strengthIndicator('')).toBe(0);
});
it('should score a short lowercase-only password at zero', () => {
expect(strengthIndicator('abcde')).toBe(0);
});
it('should award a point once the password passes five characters', () => {
expect(strengthIndicator('abcdef')).toBe(1);
});
it('should award a second point once it passes seven characters', () => {
expect(strengthIndicator('abcdefgh')).toBe(2);
});
it('should award a point for a digit', () => {
expect(strengthIndicator('abcde1')).toBe(2);
});
it('should award a point for a special character', () => {
expect(strengthIndicator('abcde!')).toBe(2);
});
it('should award a point only when BOTH cases are present', () => {
expect(strengthIndicator('abcdeF')).toBe(2);
expect(strengthIndicator('ABCDEF')).toBe(1);
});
it('should award every point for a long mixed password', () => {
expect(strengthIndicator('Abcdef1!')).toBe(5);
});
it('should never exceed five, which is the maximum the rules can award', () => {
// Worth pinning: strengthColor branches on `count < 6`, which is only
// reachable if this invariant is ever broken.
const samples = ['', 'a', 'Abcdef1!', 'A1!aaaaaaaaaaaaaaaaaaaaaaaaaa', '!!!!!!!!!!AAAAaaaa1111'];
samples.forEach((password) => {
expect(strengthIndicator(password)).toBeLessThanOrEqual(5);
expect(strengthIndicator(password)).toBeGreaterThanOrEqual(0);
});
});
});
describe('strengthColor', () => {
it.each([
[0, 'Poor'],
[1, 'Poor'],
[2, 'Weak'],
[3, 'Normal'],
[4, 'Good'],
[5, 'Strong']
])('should label a score of %d as %s', (count, label) => {
expect(strengthColor(count).label).toBe(label);
});
it('should return a colour token alongside every label', () => {
[0, 1, 2, 3, 4, 5].forEach((count) => {
expect(strengthColor(count).color).toEqual(expect.any(String));
expect(strengthColor(count).color).not.toBe('');
});
});
it('should escalate the label monotonically as the score rises', () => {
const ranked = ['Poor', 'Weak', 'Normal', 'Good', 'Strong'];
const labels = [0, 1, 2, 3, 4, 5].map((count) => strengthColor(count).label);
const indices = labels.map((label) => ranked.indexOf(label));
indices.slice(1).forEach((index, i) => expect(index).toBeGreaterThanOrEqual(indices[i]));
});
});
describe('the two modules together', () => {
it('should rate a password that satisfies every validation rule as Strong', () => {
const password = 'Abcdef1!';
expect(minLength(password)).toBe(true);
expect(isNumber(password)).toBe(true);
expect(isLowercaseChar(password)).toBe(true);
expect(isUppercaseChar(password)).toBe(true);
expect(isSpecialChar(password)).toBe(true);
expect(strengthColor(strengthIndicator(password)).label).toBe('Strong');
});
it('should rate a password that satisfies none of them as Poor', () => {
expect(strengthColor(strengthIndicator('abc')).label).toBe('Poor');
});
});
});