Skip to content

Commit a0d626a

Browse files
feat: implement password validation
1 parent 203569c commit a0d626a

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { DatetimeValidator } from './validators/datetimes'
77
import type { EnumValidator } from './validators/enums'
88
import type { NumberValidator } from './validators/numbers'
99
import type { ObjectValidator } from './validators/objects'
10+
import type { PasswordValidator } from './validators/password'
1011
import type { StringValidator } from './validators/strings'
1112
import type { TimestampValidator } from './validators/timestamps'
1213
import type { UnixValidator } from './validators/unix'
@@ -259,4 +260,5 @@ export interface ValidationInstance {
259260
custom: <T>(validationFn: (value: T) => boolean, message: string) => CustomValidator<T>
260261
timestamp: () => TimestampValidator
261262
unix: () => UnixValidator
263+
password: () => PasswordValidator
262264
}

src/validation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { datetime } from './validators/datetimes'
77
import { enum_ } from './validators/enums'
88
import { number } from './validators/numbers'
99
import { object } from './validators/objects'
10+
import { password } from './validators/password'
1011
import { string } from './validators/strings'
1112
import { timestamp } from './validators/timestamps'
1213
import { unix } from './validators/unix'
@@ -23,4 +24,5 @@ export const v: ValidationInstance = {
2324
custom,
2425
timestamp,
2526
unix,
27+
password,
2628
}

src/validators/password.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import isAlphanumeric from '../lib/isAlphanumeric'
2+
import { BaseValidator } from './base'
3+
4+
export class PasswordValidator extends BaseValidator<string> {
5+
constructor() {
6+
super()
7+
this.addRule({
8+
name: 'string',
9+
test: (value: unknown): value is string => typeof value === 'string',
10+
message: 'Must be a string',
11+
})
12+
}
13+
14+
matches(confirmPassword: string): this {
15+
return this.addRule({
16+
name: 'matches',
17+
test: (value: string) => value === confirmPassword,
18+
message: 'Passwords must match',
19+
})
20+
}
21+
22+
minLength(length: number = 8): this {
23+
return this.addRule({
24+
name: 'minLength',
25+
test: (value: string) => value.length >= length,
26+
message: 'Password must be at least {length} characters long',
27+
params: { length },
28+
})
29+
}
30+
31+
maxLength(length: number = 128): this {
32+
return this.addRule({
33+
name: 'maxLength',
34+
test: (value: string) => value.length <= length,
35+
message: 'Password must be at most {length} characters long',
36+
params: { length },
37+
})
38+
}
39+
40+
hasUppercase(): this {
41+
return this.addRule({
42+
name: 'hasUppercase',
43+
test: (value: string) => /[A-Z]/.test(value),
44+
message: 'Password must contain at least one uppercase letter',
45+
})
46+
}
47+
48+
hasLowercase(): this {
49+
return this.addRule({
50+
name: 'hasLowercase',
51+
test: (value: string) => /[a-z]/.test(value),
52+
message: 'Password must contain at least one lowercase letter',
53+
})
54+
}
55+
56+
hasNumbers(): this {
57+
return this.addRule({
58+
name: 'hasNumbers',
59+
test: (value: string) => /[0-9]/.test(value),
60+
message: 'Password must contain at least one number',
61+
})
62+
}
63+
64+
hasSpecialCharacters(): this {
65+
return this.addRule({
66+
name: 'hasSpecialCharacters',
67+
test: (value: string) => /[!@#$%^&*(),.?":{}|<>]/.test(value),
68+
message: 'Password must contain at least one special character',
69+
})
70+
}
71+
72+
alphanumeric(): this {
73+
return this.addRule({
74+
name: 'alphanumeric',
75+
test: (value: string) => isAlphanumeric(value),
76+
message: 'Password must only contain letters and numbers',
77+
})
78+
}
79+
}
80+
81+
export function password(): PasswordValidator {
82+
return new PasswordValidator()
83+
}

test/validation.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,84 @@ describe('Validation Library', () => {
5454
})
5555
})
5656

57+
describe('Password Validator', () => {
58+
test('basic password validation', () => {
59+
const validator = v.password()
60+
expect(validator.test('password123')).toBe(true)
61+
expect(validator.test('')).toBe(true)
62+
expect(validator.test(123 as any)).toBe(false)
63+
})
64+
65+
test('password matching validation', () => {
66+
const originalPassword = 'MySecureP@ss123'
67+
const validator = v.password().matches(originalPassword)
68+
expect(validator.test(originalPassword)).toBe(true)
69+
expect(validator.test('DifferentP@ss123')).toBe(false)
70+
})
71+
72+
test('password length validation', () => {
73+
const validator = v.password().minLength(8).maxLength(20)
74+
expect(validator.test('Secure123')).toBe(true)
75+
expect(validator.test('Short1')).toBe(false)
76+
expect(validator.test('ThisPasswordIsWayTooLongToBeValid123')).toBe(false)
77+
})
78+
79+
test('password character requirements', () => {
80+
const validator = v.password()
81+
.hasUppercase()
82+
.hasLowercase()
83+
.hasNumbers()
84+
.hasSpecialCharacters()
85+
86+
// Valid password with all requirements
87+
expect(validator.test('MySecureP@ss123')).toBe(true)
88+
89+
// Missing uppercase
90+
expect(validator.test('mysecurep@ss123')).toBe(false)
91+
92+
// Missing lowercase
93+
expect(validator.test('MYSECUREP@SS123')).toBe(false)
94+
95+
// Missing numbers
96+
expect(validator.test('MySecureP@ssword')).toBe(false)
97+
98+
// Missing special characters
99+
expect(validator.test('MySecurePass123')).toBe(false)
100+
})
101+
102+
test('comprehensive password validation with multiple rules', () => {
103+
const validator = v.password()
104+
.minLength(8)
105+
.maxLength(20)
106+
.hasUppercase()
107+
.hasLowercase()
108+
.hasNumbers()
109+
.hasSpecialCharacters()
110+
111+
const result = validator.validate('weak')
112+
expect(result.valid).toBe(false)
113+
expect(result.errors.length).toBeGreaterThan(0)
114+
115+
// Check specific error messages
116+
const errorMessages = result.errors.map(e => e.message)
117+
expect(errorMessages).toContain('Password must be at least 8 characters long')
118+
expect(errorMessages).toContain('Password must contain at least one uppercase letter')
119+
expect(errorMessages).toContain('Password must contain at least one number')
120+
expect(errorMessages).toContain('Password must contain at least one special character')
121+
122+
// Test a valid password
123+
const validResult = validator.validate('MySecureP@ss123')
124+
expect(validResult.valid).toBe(true)
125+
expect(validResult.errors).toHaveLength(0)
126+
})
127+
128+
test('alphanumeric password validation', () => {
129+
const validator = v.password().alphanumeric()
130+
expect(validator.test('Password123')).toBe(true)
131+
expect(validator.test('Password@123')).toBe(false)
132+
})
133+
})
134+
57135
describe('Number Validator', () => {
58136
test('basic number validation', () => {
59137
const validator = v.number()

0 commit comments

Comments
 (0)