|
1 | | -import { loggerMock } from '@sim/testing' |
2 | | -import { describe, expect, it, vi } from 'vitest' |
3 | | -import { quickValidateEmail, validateEmail } from '@/lib/messaging/email/validation' |
4 | | - |
5 | | -vi.mock('@sim/logger', () => loggerMock) |
6 | | - |
7 | | -vi.mock('dns', () => ({ |
8 | | - resolveMx: ( |
9 | | - _domain: string, |
10 | | - callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void |
11 | | - ) => { |
12 | | - callback(null, [{ exchange: 'mail.example.com', priority: 10 }]) |
13 | | - }, |
14 | | -})) |
15 | | - |
16 | | -describe('Email Validation', () => { |
17 | | - describe('validateEmail', () => { |
18 | | - it.concurrent('should validate a correct email', async () => { |
19 | | - const result = await validateEmail('user@example.com') |
20 | | - expect(result.isValid).toBe(true) |
21 | | - expect(result.checks.syntax).toBe(true) |
22 | | - expect(result.checks.disposable).toBe(true) |
23 | | - }) |
| 1 | +import { describe, expect, it } from 'vitest' |
| 2 | +import { quickValidateEmail } from '@/lib/messaging/email/validation' |
| 3 | + |
| 4 | +describe('quickValidateEmail', () => { |
| 5 | + it.concurrent('should validate a correct email', () => { |
| 6 | + const result = quickValidateEmail('user@example.com') |
| 7 | + expect(result.isValid).toBe(true) |
| 8 | + expect(result.checks.syntax).toBe(true) |
| 9 | + expect(result.checks.disposable).toBe(true) |
| 10 | + expect(result.checks.mxRecord).toBe(true) |
| 11 | + expect(result.confidence).toBe('medium') |
| 12 | + }) |
24 | 13 |
|
25 | | - it.concurrent('should reject invalid syntax', async () => { |
26 | | - const result = await validateEmail('invalid-email') |
27 | | - expect(result.isValid).toBe(false) |
28 | | - expect(result.reason).toBe('Invalid email format') |
29 | | - expect(result.checks.syntax).toBe(false) |
30 | | - }) |
| 14 | + it.concurrent('should reject invalid syntax', () => { |
| 15 | + const result = quickValidateEmail('invalid-email') |
| 16 | + expect(result.isValid).toBe(false) |
| 17 | + expect(result.reason).toBe('Invalid email format') |
| 18 | + }) |
31 | 19 |
|
32 | | - it.concurrent('should reject disposable email addresses', async () => { |
33 | | - const result = await validateEmail('test@10minutemail.com') |
| 20 | + it.concurrent('should reject disposable email addresses', () => { |
| 21 | + const disposableDomains = [ |
| 22 | + 'mailinator.com', |
| 23 | + 'yopmail.com', |
| 24 | + 'guerrillamail.com', |
| 25 | + 'temp-mail.org', |
| 26 | + 'throwaway.email', |
| 27 | + 'getnada.com', |
| 28 | + 'sharklasers.com', |
| 29 | + 'spam4.me', |
| 30 | + 'sharebot.net', |
| 31 | + 'oakon.com', |
| 32 | + 'catchmail.io', |
| 33 | + 'salt.email', |
| 34 | + 'mail.gw', |
| 35 | + 'tempmail.org', |
| 36 | + ] |
| 37 | + |
| 38 | + for (const domain of disposableDomains) { |
| 39 | + const result = quickValidateEmail(`test@${domain}`) |
34 | 40 | expect(result.isValid).toBe(false) |
35 | 41 | expect(result.reason).toBe('Disposable email addresses are not allowed') |
36 | 42 | expect(result.checks.disposable).toBe(false) |
37 | | - }) |
| 43 | + } |
| 44 | + }) |
38 | 45 |
|
39 | | - it.concurrent('should reject consecutive dots (RFC violation)', async () => { |
40 | | - const result = await validateEmail('user..name@example.com') |
41 | | - expect(result.isValid).toBe(false) |
42 | | - expect(result.reason).toBe('Email contains suspicious patterns') |
43 | | - }) |
| 46 | + it.concurrent('should reject consecutive dots (RFC violation)', () => { |
| 47 | + const result = quickValidateEmail('user..name@example.com') |
| 48 | + expect(result.isValid).toBe(false) |
| 49 | + expect(result.reason).toBe('Email contains suspicious patterns') |
| 50 | + expect(result.confidence).toBe('medium') |
| 51 | + }) |
44 | 52 |
|
45 | | - it.concurrent('should reject very long local parts (RFC violation)', async () => { |
46 | | - const longLocalPart = 'a'.repeat(65) |
47 | | - const result = await validateEmail(`${longLocalPart}@example.com`) |
48 | | - expect(result.isValid).toBe(false) |
49 | | - expect(result.reason).toBe('Email contains suspicious patterns') |
50 | | - }) |
| 53 | + it.concurrent('should reject very long local parts (RFC violation)', () => { |
| 54 | + const longLocalPart = 'a'.repeat(65) |
| 55 | + const result = quickValidateEmail(`${longLocalPart}@example.com`) |
| 56 | + expect(result.isValid).toBe(false) |
| 57 | + expect(result.reason).toBe('Email contains suspicious patterns') |
| 58 | + }) |
51 | 59 |
|
52 | | - it.concurrent('should reject email with missing domain', async () => { |
53 | | - const result = await validateEmail('user@') |
54 | | - expect(result.isValid).toBe(false) |
55 | | - expect(result.reason).toBe('Invalid email format') |
56 | | - }) |
| 60 | + it.concurrent('should reject email with missing domain', () => { |
| 61 | + const result = quickValidateEmail('user@') |
| 62 | + expect(result.isValid).toBe(false) |
| 63 | + expect(result.reason).toBe('Invalid email format') |
| 64 | + }) |
57 | 65 |
|
58 | | - it.concurrent('should reject email with domain starting with dot', async () => { |
59 | | - const result = await validateEmail('user@.example.com') |
60 | | - expect(result.isValid).toBe(false) |
61 | | - expect(result.reason).toBe('Invalid email format') |
62 | | - }) |
| 66 | + it.concurrent('should reject email with domain starting with dot', () => { |
| 67 | + const result = quickValidateEmail('user@.example.com') |
| 68 | + expect(result.isValid).toBe(false) |
| 69 | + expect(result.reason).toBe('Invalid email format') |
| 70 | + }) |
63 | 71 |
|
64 | | - it.concurrent('should reject email with domain ending with dot', async () => { |
65 | | - const result = await validateEmail('user@example.') |
66 | | - expect(result.isValid).toBe(false) |
67 | | - expect(result.reason).toBe('Invalid email format') |
68 | | - }) |
| 72 | + it.concurrent('should reject email with domain ending with dot', () => { |
| 73 | + const result = quickValidateEmail('user@example.') |
| 74 | + expect(result.isValid).toBe(false) |
| 75 | + expect(result.reason).toBe('Invalid email format') |
| 76 | + }) |
69 | 77 |
|
70 | | - it.concurrent('should reject email with domain missing TLD', async () => { |
71 | | - const result = await validateEmail('user@localhost') |
72 | | - expect(result.isValid).toBe(false) |
73 | | - expect(result.reason).toBe('Invalid domain format') |
74 | | - }) |
| 78 | + it.concurrent('should reject email with domain missing TLD', () => { |
| 79 | + const result = quickValidateEmail('user@localhost') |
| 80 | + expect(result.isValid).toBe(false) |
| 81 | + expect(result.reason).toBe('Invalid domain format') |
| 82 | + }) |
75 | 83 |
|
76 | | - it.concurrent('should reject email longer than 254 characters', async () => { |
77 | | - const longLocal = 'a'.repeat(64) |
78 | | - const longDomain = `${'b'.repeat(180)}.com` |
79 | | - const result = await validateEmail(`${longLocal}@${longDomain}`) |
80 | | - expect(result.isValid).toBe(false) |
81 | | - }) |
82 | | - |
83 | | - it.concurrent('should validate various known disposable email domains', async () => { |
84 | | - const disposableDomains = [ |
85 | | - 'mailinator.com', |
86 | | - 'yopmail.com', |
87 | | - 'guerrillamail.com', |
88 | | - 'temp-mail.org', |
89 | | - 'throwaway.email', |
90 | | - 'getnada.com', |
91 | | - 'sharklasers.com', |
92 | | - 'spam4.me', |
93 | | - 'sharebot.net', |
94 | | - 'oakon.com', |
95 | | - 'catchmail.io', |
96 | | - 'salt.email', |
97 | | - 'mail.gw', |
98 | | - ] |
99 | | - |
100 | | - for (const domain of disposableDomains) { |
101 | | - const result = await validateEmail(`test@${domain}`) |
102 | | - expect(result.isValid).toBe(false) |
103 | | - expect(result.reason).toBe('Disposable email addresses are not allowed') |
104 | | - expect(result.checks.disposable).toBe(false) |
105 | | - } |
106 | | - }) |
107 | | - |
108 | | - it.concurrent('should accept valid email formats', async () => { |
109 | | - const validEmails = [ |
110 | | - 'simple@example.com', |
111 | | - 'very.common@example.com', |
112 | | - 'disposable.style.email.with+symbol@example.com', |
113 | | - 'other.email-with-hyphen@example.com', |
114 | | - 'fully-qualified-domain@example.com', |
115 | | - 'user.name+tag+sorting@example.com', |
116 | | - 'x@example.com', |
117 | | - 'example-indeed@strange-example.com', |
118 | | - 'example@s.example', |
119 | | - ] |
120 | | - |
121 | | - for (const email of validEmails) { |
122 | | - const result = await validateEmail(email) |
123 | | - expect(result.checks.syntax).toBe(true) |
124 | | - expect(result.checks.disposable).toBe(true) |
125 | | - } |
126 | | - }) |
127 | | - |
128 | | - it.concurrent('should return high confidence for syntax failures', async () => { |
129 | | - const result = await validateEmail('not-an-email') |
130 | | - expect(result.confidence).toBe('high') |
131 | | - }) |
132 | | - |
133 | | - it.concurrent('should handle email with special characters in local part', async () => { |
134 | | - const result = await validateEmail("user!#$%&'*+/=?^_`{|}~@example.com") |
135 | | - expect(result.checks.syntax).toBe(true) |
136 | | - }) |
| 84 | + it.concurrent('should reject email longer than 254 characters', () => { |
| 85 | + const longLocal = 'a'.repeat(64) |
| 86 | + const longDomain = `${'b'.repeat(180)}.com` |
| 87 | + const result = quickValidateEmail(`${longLocal}@${longDomain}`) |
| 88 | + expect(result.isValid).toBe(false) |
137 | 89 | }) |
138 | 90 |
|
139 | | - describe('quickValidateEmail', () => { |
140 | | - it.concurrent('should validate quickly without MX check', () => { |
141 | | - const result = quickValidateEmail('user@example.com') |
| 91 | + it.concurrent('should accept valid email formats', () => { |
| 92 | + const validEmails = [ |
| 93 | + 'simple@example.com', |
| 94 | + 'very.common@example.com', |
| 95 | + 'disposable.style.email.with+symbol@example.com', |
| 96 | + 'other.email-with-hyphen@example.com', |
| 97 | + 'user.name+tag+sorting@example.com', |
| 98 | + 'x@example.com', |
| 99 | + 'example-indeed@strange-example.com', |
| 100 | + 'example@s.example', |
| 101 | + ] |
| 102 | + |
| 103 | + for (const email of validEmails) { |
| 104 | + const result = quickValidateEmail(email) |
142 | 105 | expect(result.isValid).toBe(true) |
143 | | - expect(result.checks.mxRecord).toBe(true) |
144 | | - expect(result.confidence).toBe('medium') |
145 | | - }) |
146 | | - |
147 | | - it.concurrent('should reject invalid emails quickly', () => { |
148 | | - const result = quickValidateEmail('invalid-email') |
149 | | - expect(result.isValid).toBe(false) |
150 | | - expect(result.reason).toBe('Invalid email format') |
151 | | - }) |
152 | | - |
153 | | - it.concurrent('should reject disposable emails quickly', () => { |
154 | | - const result = quickValidateEmail('test@tempmail.org') |
155 | | - expect(result.isValid).toBe(false) |
156 | | - expect(result.reason).toBe('Disposable email addresses are not allowed') |
157 | | - }) |
158 | | - |
159 | | - it.concurrent('should reject email with missing domain', () => { |
160 | | - const result = quickValidateEmail('user@') |
161 | | - expect(result.isValid).toBe(false) |
162 | | - expect(result.reason).toBe('Invalid email format') |
163 | | - }) |
164 | | - |
165 | | - it.concurrent('should reject email with invalid domain format', () => { |
166 | | - const result = quickValidateEmail('user@.invalid') |
167 | | - expect(result.isValid).toBe(false) |
168 | | - expect(result.reason).toBe('Invalid email format') |
169 | | - }) |
170 | | - |
171 | | - it.concurrent('should return medium confidence for suspicious patterns', () => { |
172 | | - const result = quickValidateEmail('user..double@example.com') |
173 | | - expect(result.isValid).toBe(false) |
174 | | - expect(result.reason).toBe('Email contains suspicious patterns') |
175 | | - expect(result.confidence).toBe('medium') |
176 | | - }) |
| 106 | + expect(result.checks.syntax).toBe(true) |
| 107 | + expect(result.checks.disposable).toBe(true) |
| 108 | + } |
| 109 | + }) |
177 | 110 |
|
178 | | - it.concurrent('should return high confidence for syntax errors', () => { |
179 | | - const result = quickValidateEmail('not-valid-email') |
180 | | - expect(result.confidence).toBe('high') |
181 | | - }) |
| 111 | + it.concurrent('should return high confidence for syntax errors', () => { |
| 112 | + const result = quickValidateEmail('not-valid-email') |
| 113 | + expect(result.confidence).toBe('high') |
| 114 | + }) |
182 | 115 |
|
183 | | - it.concurrent('should handle empty string', () => { |
184 | | - const result = quickValidateEmail('') |
185 | | - expect(result.isValid).toBe(false) |
186 | | - expect(result.reason).toBe('Invalid email format') |
187 | | - }) |
| 116 | + it.concurrent('should handle special characters in local part', () => { |
| 117 | + const result = quickValidateEmail("user!#$%&'*+/=?^_`{|}~@example.com") |
| 118 | + expect(result.checks.syntax).toBe(true) |
| 119 | + }) |
188 | 120 |
|
189 | | - it.concurrent('should handle email with only @ symbol', () => { |
190 | | - const result = quickValidateEmail('@') |
191 | | - expect(result.isValid).toBe(false) |
192 | | - expect(result.reason).toBe('Invalid email format') |
193 | | - }) |
| 121 | + it.concurrent('should handle empty string', () => { |
| 122 | + const result = quickValidateEmail('') |
| 123 | + expect(result.isValid).toBe(false) |
| 124 | + expect(result.reason).toBe('Invalid email format') |
| 125 | + }) |
194 | 126 |
|
195 | | - it.concurrent('should handle email with spaces', () => { |
196 | | - const result = quickValidateEmail('user name@example.com') |
197 | | - expect(result.isValid).toBe(false) |
198 | | - expect(result.reason).toBe('Invalid email format') |
199 | | - }) |
| 127 | + it.concurrent('should handle email with only @ symbol', () => { |
| 128 | + const result = quickValidateEmail('@') |
| 129 | + expect(result.isValid).toBe(false) |
| 130 | + }) |
200 | 131 |
|
201 | | - it.concurrent('should handle email with multiple @ symbols', () => { |
202 | | - const result = quickValidateEmail('user@domain@example.com') |
203 | | - expect(result.isValid).toBe(false) |
204 | | - expect(result.reason).toBe('Invalid email format') |
205 | | - }) |
| 132 | + it.concurrent('should handle email with spaces', () => { |
| 133 | + const result = quickValidateEmail('user name@example.com') |
| 134 | + expect(result.isValid).toBe(false) |
| 135 | + }) |
206 | 136 |
|
207 | | - it.concurrent('should validate complex but valid local parts', () => { |
208 | | - const result = quickValidateEmail('user+tag@example.com') |
209 | | - expect(result.isValid).toBe(true) |
210 | | - expect(result.checks.syntax).toBe(true) |
211 | | - }) |
| 137 | + it.concurrent('should handle email with multiple @ symbols', () => { |
| 138 | + const result = quickValidateEmail('user@domain@example.com') |
| 139 | + expect(result.isValid).toBe(false) |
| 140 | + }) |
212 | 141 |
|
213 | | - it.concurrent('should validate subdomains', () => { |
214 | | - const result = quickValidateEmail('user@mail.subdomain.example.com') |
215 | | - expect(result.isValid).toBe(true) |
216 | | - expect(result.checks.domain).toBe(true) |
217 | | - }) |
218 | | - |
219 | | - it.concurrent('should reject spam domains from the inline blocklist', () => { |
220 | | - const spamDomains = ['sharebot.net', 'oakon.com', 'catchmail.io', 'salt.email', 'mail.gw'] |
221 | | - for (const domain of spamDomains) { |
222 | | - const result = quickValidateEmail(`user@${domain}`) |
223 | | - expect(result.isValid).toBe(false) |
224 | | - expect(result.reason).toBe('Disposable email addresses are not allowed') |
225 | | - } |
226 | | - }) |
| 142 | + it.concurrent('should validate subdomains', () => { |
| 143 | + const result = quickValidateEmail('user@mail.subdomain.example.com') |
| 144 | + expect(result.isValid).toBe(true) |
| 145 | + expect(result.checks.domain).toBe(true) |
227 | 146 | }) |
228 | 147 | }) |
0 commit comments