Files
MixedAssets/server/services/passwordPolicy.test.js

81 lines
3.9 KiB
JavaScript

/*
* Tests for the password-policy validation and lockout helpers. These exercise the pure logic
* (complexity checks, identifier detection, lockout math) against an explicit policy object so
* they do not depend on the database-backed settings store.
*/
const { checkPassword, describe: describePolicy, DEFAULT_POLICY, isLocked, minAgeRemainingMinutes, passwordExpired } = require('./passwordPolicy');
const strictPolicy = {
...DEFAULT_POLICY,
min_length: 12,
require_upper: true,
require_lower: true,
require_number: true,
require_symbol: true,
disallow_identifiers: true,
history_count: 0 // disable DB-backed history for these pure tests
};
describe('password policy', () => {
test('a compliant password passes every rule', () => {
expect(checkPassword('Str0ng!Passphrase', null, strictPolicy)).toEqual([]);
});
test('each missing character class is reported', () => {
expect(checkPassword('short', null, strictPolicy).length).toBeGreaterThan(0);
expect(checkPassword('alllowercase1!', null, strictPolicy)).toContain('Add an uppercase letter');
expect(checkPassword('ALLUPPERCASE1!', null, strictPolicy)).toContain('Add a lowercase letter');
expect(checkPassword('NoNumbersHere!', null, strictPolicy)).toContain('Add a number');
expect(checkPassword('NoSymbolsHere1', null, strictPolicy)).toContain('Add a symbol');
});
test('minimum length is enforced', () => {
expect(checkPassword('Ab1!', null, strictPolicy)).toContain('Use at least 12 characters');
});
test('the user name or email cannot appear in the password', () => {
const user = { name: 'Jordan Banks', email: 'jbanks@example.com' };
expect(checkPassword('Jordan!Secret99', user, strictPolicy)).toContain('Must not contain your name or email');
expect(checkPassword('jbanks!Secret99', user, strictPolicy)).toContain('Must not contain your name or email');
// A password without the identifiers is fine.
expect(checkPassword('Unrelated!Secret99', user, strictPolicy)).toEqual([]);
});
test('identifier check is skipped when the policy disables it', () => {
const policy = { ...strictPolicy, disallow_identifiers: false };
const user = { name: 'Jordan Banks', email: 'jbanks@example.com' };
expect(checkPassword('Jordan!Secret99', user, policy)).toEqual([]);
});
test('describe lists the active rules', () => {
const rules = describePolicy(strictPolicy);
expect(rules.some((r) => /12 characters/.test(r))).toBe(true);
expect(rules.some((r) => /uppercase/.test(r))).toBe(true);
});
test('isLocked reflects the locked_until timestamp', () => {
expect(isLocked({ locked_until: new Date(Date.now() + 60000).toISOString() })).toBe(true);
expect(isLocked({ locked_until: new Date(Date.now() - 60000).toISOString() })).toBe(false);
expect(isLocked({ locked_until: null })).toBe(false);
});
test('passwordExpired compares age against expiry_days', () => {
const policy = { ...DEFAULT_POLICY, expiry_days: 30 };
const old = new Date(Date.now() - 40 * 86400000).toISOString();
const recent = new Date(Date.now() - 5 * 86400000).toISOString();
expect(passwordExpired({ password_changed_at: old }, policy)).toBe(true);
expect(passwordExpired({ password_changed_at: recent }, policy)).toBe(false);
// expiry_days 0 means passwords never expire.
expect(passwordExpired({ password_changed_at: old }, { ...policy, expiry_days: 0 })).toBe(false);
});
test('minAgeRemainingMinutes blocks changes that are too soon', () => {
const policy = { ...DEFAULT_POLICY, min_age_hours: 24 };
const justChanged = new Date(Date.now() - 60000).toISOString();
const longAgo = new Date(Date.now() - 48 * 3600000).toISOString();
expect(minAgeRemainingMinutes({ password_changed_at: justChanged }, policy)).toBeGreaterThan(0);
expect(minAgeRemainingMinutes({ password_changed_at: longAgo }, policy)).toBe(0);
});
});