174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import {
|
|
passwordMatchValidator,
|
|
PasswordMatchGroupLike,
|
|
} from '../../frontend/src/app/features/setup/setup-create-admin.validators';
|
|
import {
|
|
usernameError,
|
|
passwordError,
|
|
confirmError,
|
|
} from '../../frontend/src/app/features/setup/setup-create-admin.field-errors';
|
|
|
|
function makeGroup(password: string | null | undefined, confirm: string | null | undefined): PasswordMatchGroupLike {
|
|
return {
|
|
get: (name) => {
|
|
if (name === 'password') return { value: password };
|
|
if (name === 'passwordConfirm') return { value: confirm };
|
|
return null;
|
|
},
|
|
};
|
|
}
|
|
|
|
const USERNAME_PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
const USERNAME_MIN = 3;
|
|
const USERNAME_MAX = 32;
|
|
|
|
function usernameErrors(value: string): string | null {
|
|
if (!value) return 'required';
|
|
if (value.length < USERNAME_MIN) return 'minlength';
|
|
if (value.length > USERNAME_MAX) return 'maxlength';
|
|
if (!USERNAME_PATTERN.test(value)) return 'pattern';
|
|
return null;
|
|
}
|
|
|
|
function isFormReadyForSubmit(username: string, password: string, confirm: string): boolean {
|
|
if (usernameErrors(username) !== null) return false;
|
|
if (!password) return false;
|
|
if (!confirm) return false;
|
|
const groupLike: PasswordMatchGroupLike = {
|
|
get: (name) => (name === 'password' ? { value: password } : { value: confirm }),
|
|
};
|
|
if (passwordMatchValidator(groupLike)?.passwordMismatch) return false;
|
|
return true;
|
|
}
|
|
|
|
describe('passwordMatchValidator', () => {
|
|
it('returns null when either field is empty (let other validators handle required)', () => {
|
|
expect(passwordMatchValidator(makeGroup('', 'x'))).toBeNull();
|
|
expect(passwordMatchValidator(makeGroup('x', ''))).toBeNull();
|
|
expect(passwordMatchValidator(makeGroup(undefined, undefined))).toBeNull();
|
|
});
|
|
|
|
it('returns null when passwords match', () => {
|
|
expect(passwordMatchValidator(makeGroup('Sup3r!', 'Sup3r!'))).toBeNull();
|
|
});
|
|
|
|
it('returns { passwordMismatch: true } when passwords differ', () => {
|
|
expect(passwordMatchValidator(makeGroup('Sup3r!', 'Sup4r!'))).toEqual({
|
|
passwordMismatch: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Username field rules (mirroring component validators)', () => {
|
|
const PATTERN = /^[a-zA-Z0-9_.-]+$/;
|
|
const MIN = 3;
|
|
const MAX = 32;
|
|
|
|
function isValidUsername(value: string): boolean {
|
|
return value.length >= MIN && value.length <= MAX && PATTERN.test(value);
|
|
}
|
|
|
|
it('accepts a 3-32 char username with allowed characters', () => {
|
|
expect(isValidUsername('first.admin_01')).toBe(true);
|
|
});
|
|
|
|
it('rejects usernames shorter than 3', () => {
|
|
expect(isValidUsername('a')).toBe(false);
|
|
});
|
|
|
|
it('rejects usernames with disallowed characters', () => {
|
|
expect(isValidUsername('bad name')).toBe(false);
|
|
expect(isValidUsername('user!')).toBe(false);
|
|
expect(isValidUsername('user@')).toBe(false);
|
|
});
|
|
|
|
it('rejects usernames longer than 32', () => {
|
|
expect(isValidUsername('a'.repeat(33))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Composed submit gate (mirrors SetupCreateAdminComponent.submit)', () => {
|
|
it('rejects an all-empty form (no required field passes)', () => {
|
|
expect(isFormReadyForSubmit('', '', '')).toBe(false);
|
|
});
|
|
|
|
it('rejects a partial form (username only)', () => {
|
|
expect(isFormReadyForSubmit('first.admin', '', '')).toBe(false);
|
|
});
|
|
|
|
it('rejects when password and confirm are empty even with a valid username', () => {
|
|
expect(isFormReadyForSubmit('first.admin', '', '')).toBe(false);
|
|
});
|
|
|
|
it('rejects when passwordConfirm is empty', () => {
|
|
expect(isFormReadyForSubmit('first.admin', 'Sup3rSecret!Pass', '')).toBe(false);
|
|
});
|
|
|
|
it('rejects when password and confirm differ', () => {
|
|
expect(isFormReadyForSubmit('first.admin', 'Sup3rSecret!Pass', 'Different!Pass1')).toBe(false);
|
|
});
|
|
|
|
it('rejects when username violates the pattern', () => {
|
|
expect(isFormReadyForSubmit('bad name!', 'Sup3rSecret!Pass', 'Sup3rSecret!Pass')).toBe(false);
|
|
});
|
|
|
|
it('accepts a fully valid, matching form', () => {
|
|
expect(isFormReadyForSubmit('first.admin', 'Sup3rSecret!Pass', 'Sup3rSecret!Pass')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Field-error helpers (mirror the template render)', () => {
|
|
it('usernameError returns "Username is required" when touched + required', () => {
|
|
expect(
|
|
usernameError({ value: '', touched: true, errors: { required: true } }),
|
|
).toBe('Username is required');
|
|
});
|
|
|
|
it('usernameError returns the minlength message when too short', () => {
|
|
expect(
|
|
usernameError({ value: 'ab', touched: true, errors: { minlength: true } }),
|
|
).toBe('Username must be at least 3 characters');
|
|
});
|
|
|
|
it('usernameError returns null when not touched even if errors are present', () => {
|
|
expect(
|
|
usernameError({ value: '', touched: false, errors: { required: true } }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('passwordError returns "Password is required" when touched + required', () => {
|
|
expect(
|
|
passwordError({ value: '', touched: true, errors: { required: true } }),
|
|
).toBe('Password is required');
|
|
});
|
|
|
|
it('passwordError returns null when touched with no errors (policy is server-side)', () => {
|
|
expect(
|
|
passwordError({ value: 'x', touched: true, errors: null }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('confirmError returns "Please confirm your password" when touched + required', () => {
|
|
expect(
|
|
confirmError({ value: '', touched: true, errors: { required: true } }),
|
|
).toBe('Please confirm your password');
|
|
});
|
|
|
|
it('confirmError returns "Passwords do not match" when group mismatch is set', () => {
|
|
expect(
|
|
confirmError({
|
|
value: 'a',
|
|
touched: true,
|
|
errors: null,
|
|
groupMismatch: true,
|
|
}),
|
|
).toBe('Passwords do not match');
|
|
});
|
|
|
|
it('confirmError returns null when not touched and no group mismatch', () => {
|
|
expect(
|
|
confirmError({ value: 'a', touched: false, errors: null }),
|
|
).toBeNull();
|
|
});
|
|
});
|