mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
6dda05f672
Eight files in services/ hold no state and touch no database: avatarUrl, conflictResult, demo, passwordPolicy, timezoneService, cookie, and the two crypto helpers. They are imported from 65 places, 62 of them already inside src/nest, so having them live in the legacy layer was pure inertia. They stay plain functions rather than becoming providers. Three importers sit outside the container and would have no way to reach an injectable one: db/migrations.ts runs before the app is built, and middleware/auth.ts and middleware/mfaPolicy.ts run beside it. Wrapping these in DI would buy ceremony and nothing else. mfaCrypto and apiKeyCrypto move together into common/crypto — they read the same ENCRYPTION_KEY, and the directory says so. services/ drops from 44 files to 36, 9,870 lines to 8,493. One trap worth recording: src/app-config and src/nest/app-config both exist. cookie.ts imported '../app-config' from services/, which resolved to the former; after the move the same string silently pointed at the latter, which has no readEnv. It is now explicit about which one it wants. Nothing else changes — no signatures, no behaviour, no tests rewritten beyond their import paths.
102 lines
4.5 KiB
TypeScript
102 lines
4.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { validatePassword } from '../../../src/nest/common/passwordPolicy';
|
|
|
|
describe('validatePassword', () => {
|
|
// AUTH-006 — Registration with weak password
|
|
describe('length requirement', () => {
|
|
it('AUTH-006: rejects passwords shorter than 8 characters', () => {
|
|
expect(validatePassword('Ab1!')).toEqual({ ok: false, reason: expect.stringContaining('8 characters') });
|
|
expect(validatePassword('Ab1!456')).toEqual({ ok: false, reason: expect.stringContaining('8 characters') });
|
|
});
|
|
|
|
it('accepts passwords of exactly 8 characters that meet all requirements', () => {
|
|
expect(validatePassword('Ab1!abcd')).toEqual({ ok: true });
|
|
});
|
|
});
|
|
|
|
describe('complexity requirements', () => {
|
|
it('AUTH-006: rejects password missing uppercase letter', () => {
|
|
const result = validatePassword('abcd1234!');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toContain('uppercase');
|
|
});
|
|
|
|
it('AUTH-006: rejects password missing lowercase letter', () => {
|
|
const result = validatePassword('ABCD1234!');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toContain('lowercase');
|
|
});
|
|
|
|
it('AUTH-006: rejects password missing a number', () => {
|
|
const result = validatePassword('Abcdefg!');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toContain('number');
|
|
});
|
|
|
|
it('AUTH-006: rejects password missing a special character', () => {
|
|
// 'TrekApp1' — has upper, lower, number, NO special char, NOT in blocklist
|
|
const result = validatePassword('TrekApp1');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toContain('special character');
|
|
});
|
|
});
|
|
|
|
// AUTH-007 — Registration with common password
|
|
describe('common password blocklist', () => {
|
|
it('AUTH-007: rejects password matching exact blocklist entry (case-insensitive)', () => {
|
|
// 'password1' is in the blocklist. A capitalised+special variant still matches
|
|
// because the check is COMMON_PASSWORDS.has(password.toLowerCase()).
|
|
// However, 'Password1!' lowercased is 'password1!' which is NOT in the set.
|
|
// We must use a password whose lowercase is exactly in the set:
|
|
// 'Iloveyou1!' — lowercased: 'iloveyou1!' — NOT in set.
|
|
// Use a password whose *lowercase* IS in set: 'changeme' → 'Changeme' is 8 chars
|
|
// but lacks uppercase/number/special — test blocklist with full complex variants:
|
|
// 'ILoveyou1!' lowercased = 'iloveyou1!' — not in set.
|
|
// Just test exact matches that satisfy complexity: use blocklist entry itself.
|
|
// 'Iloveyou' is 8 chars, no number/special → fails complexity, not blocklist.
|
|
// Better: pick a blocklist entry that, when capitalised + special added, still matches.
|
|
// The check is: COMMON_PASSWORDS.has(password.toLowerCase())
|
|
// So 'FOOTBALL!' lowercased = 'football!' — not in set ('football' is in set).
|
|
// We need password.toLowerCase() to equal a set entry exactly:
|
|
// 'football' → add uppercase → 'Football' is still 8 chars, no number, no special → fails complexity first
|
|
// The blocklist check happens BEFORE complexity checks, after length + repetitive checks.
|
|
// So any 8+ char string whose lowercase is in the blocklist gets caught first.
|
|
// 'Password1' lowercased = 'password1' → in blocklist! ✓ (length ok, not repetitive)
|
|
expect(validatePassword('Password1')).toEqual({
|
|
ok: false,
|
|
reason: expect.stringContaining('common'),
|
|
});
|
|
});
|
|
|
|
it('AUTH-007: rejects "Changeme" whose lowercase is in the blocklist', () => {
|
|
// 'changeme' is in the set; 'Changeme'.toLowerCase() === 'changeme' ✓
|
|
expect(validatePassword('Changeme')).toEqual({
|
|
ok: false,
|
|
reason: expect.stringContaining('common'),
|
|
});
|
|
});
|
|
|
|
it('accepts a strong password that is not in the blocklist', () => {
|
|
expect(validatePassword('MyUniq!1Trek')).toEqual({ ok: true });
|
|
});
|
|
});
|
|
|
|
describe('repetitive password', () => {
|
|
it('rejects passwords made of a single repeated character', () => {
|
|
const result = validatePassword('AAAAAAAA');
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toContain('repetitive');
|
|
});
|
|
});
|
|
|
|
describe('valid passwords', () => {
|
|
it('accepts a strong unique password', () => {
|
|
expect(validatePassword('Tr3k!SecurePass')).toEqual({ ok: true });
|
|
});
|
|
|
|
it('accepts a strong password with special characters', () => {
|
|
expect(validatePassword('MyP@ss#2024')).toEqual({ ok: true });
|
|
});
|
|
});
|
|
});
|