Skip to content

Commit df8697a

Browse files
Merge branch 'master' into frgt_pwd_feat
2 parents e828d01 + 35d3ac0 commit df8697a

38 files changed

Lines changed: 4534 additions & 371 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
doc/*
2+
!doc/signup_dataflow.md
3+
!doc/password_reset_dataflow.md

LocalMind-Backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@eslint/js": "^9.36.0",
3131
"@types/argon2": "^0.15.4",
3232
"@types/bcrypt": "^6.0.0",
33+
"@types/cors": "^2.8.19",
3334
"@types/express": "^5.0.3",
3435
"@types/node": "^24.10.7",
3536
"@types/nodemailer": "^7.0.5",
@@ -63,6 +64,7 @@
6364
"chalk": "^5.6.2",
6465
"cloudflared-tunnel": "^1.0.3",
6566
"cookie-parser": "^1.4.7",
67+
"cors": "^2.8.5",
6668
"d3-dsv": "^2.0.0",
6769
"dotenv": "^17.2.3",
6870
"express": "^5.1.0",

LocalMind-Backend/pnpm-lock.yaml

Lines changed: 693 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LocalMind-Backend/src/api/v1/user/__test__/user.test.ts

Lines changed: 291 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { describe, it, expect, beforeAll } from '@jest/globals'
1+
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
22
import axios from 'axios'
3+
import mongoose from 'mongoose'
4+
import crypto from 'crypto'
35
import { env } from '../../../../constant/env.constant'
46
import UserUtils from '../user.utils'
5-
import mongoose from 'mongoose'
7+
import User from '../user.model'
8+
9+
const API_URL = env.BACKEND_URL
610

711
describe('User Registration Tests', () => {
812
let userExists = false
@@ -38,7 +42,7 @@ describe('User Registration Tests', () => {
3842
}
3943

4044
try {
41-
const res = await axios.post(`${env.BACKEND_URL}/auth/signup`, {
45+
const res = await axios.post(`${API_URL}/auth/signup`, {
4246
firstName: 'Test User',
4347
birthPlace: 'Test City',
4448
location: 'Test Country',
@@ -72,7 +76,7 @@ describe('User Registration Tests', () => {
7276
}
7377

7478
try {
75-
const res = await axios.post(`${env.BACKEND_URL}/auth/signup`, {
79+
const res = await axios.post(`${API_URL}/auth/signup`, {
7680
firstName: 'Duplicate User',
7781
birthPlace: 'Duplicate City',
7882
location: 'Duplicate Country',
@@ -90,3 +94,286 @@ describe('User Registration Tests', () => {
9094
}
9195
}, 10000)
9296
})
97+
98+
describe('Password Validation Tests', () => {
99+
const validUserData = {
100+
firstName: 'Password Test',
101+
birthPlace: 'Test City',
102+
location: 'Test Country',
103+
email: `pwtest_${Date.now()}@example.com`,
104+
}
105+
106+
it('should reject password without uppercase letter', async () => {
107+
try {
108+
await axios.post(`${API_URL}/auth/signup`, {
109+
...validUserData,
110+
email: `test_noupper_${Date.now()}@example.com`,
111+
password: 'test@1234', // No uppercase
112+
})
113+
throw new Error('Should have rejected password without uppercase')
114+
} catch (error: any) {
115+
expect(error.response).toBeDefined()
116+
expect(error.response.status).toBe(400)
117+
}
118+
}, 10000)
119+
120+
it('should reject password without lowercase letter', async () => {
121+
try {
122+
await axios.post(`${API_URL}/auth/signup`, {
123+
...validUserData,
124+
email: `test_nolower_${Date.now()}@example.com`,
125+
password: 'TEST@1234', // No lowercase
126+
})
127+
throw new Error('Should have rejected password without lowercase')
128+
} catch (error: any) {
129+
expect(error.response).toBeDefined()
130+
expect(error.response.status).toBe(400)
131+
}
132+
}, 10000)
133+
134+
it('should reject password without number', async () => {
135+
try {
136+
await axios.post(`${API_URL}/auth/signup`, {
137+
...validUserData,
138+
email: `test_nonum_${Date.now()}@example.com`,
139+
password: 'Test@test', // No number
140+
})
141+
throw new Error('Should have rejected password without number')
142+
} catch (error: any) {
143+
expect(error.response).toBeDefined()
144+
expect(error.response.status).toBe(400)
145+
}
146+
}, 10000)
147+
148+
it('should reject password without special character', async () => {
149+
try {
150+
await axios.post(`${API_URL}/auth/signup`, {
151+
...validUserData,
152+
email: `test_nospecial_${Date.now()}@example.com`,
153+
password: 'Test12345', // No special char
154+
})
155+
throw new Error('Should have rejected password without special character')
156+
} catch (error: any) {
157+
expect(error.response).toBeDefined()
158+
expect(error.response.status).toBe(400)
159+
}
160+
}, 10000)
161+
162+
it('should reject password shorter than 8 characters', async () => {
163+
try {
164+
await axios.post(`${API_URL}/auth/signup`, {
165+
...validUserData,
166+
email: `test_short_${Date.now()}@example.com`,
167+
password: 'Te@1', // Too short
168+
})
169+
throw new Error('Should have rejected short password')
170+
} catch (error: any) {
171+
expect(error.response).toBeDefined()
172+
expect(error.response.status).toBe(400)
173+
}
174+
}, 10000)
175+
})
176+
177+
describe('Input Validation Edge Cases', () => {
178+
const baseUserData = {
179+
firstName: 'Edge Case Test',
180+
birthPlace: 'Test City',
181+
location: 'Test Country',
182+
password: 'ValidPass@123',
183+
}
184+
185+
it('should reject empty firstName', async () => {
186+
try {
187+
await axios.post(`${API_URL}/auth/signup`, {
188+
...baseUserData,
189+
firstName: '',
190+
email: `test_nofname_${Date.now()}@example.com`,
191+
})
192+
throw new Error('Should have rejected empty firstName')
193+
} catch (error: any) {
194+
expect(error.response).toBeDefined()
195+
expect(error.response.status).toBe(400)
196+
}
197+
}, 10000)
198+
199+
it('should reject invalid email format', async () => {
200+
try {
201+
await axios.post(`${API_URL}/auth/signup`, {
202+
...baseUserData,
203+
email: 'invalid-email-format',
204+
})
205+
throw new Error('Should have rejected invalid email')
206+
} catch (error: any) {
207+
expect(error.response).toBeDefined()
208+
expect(error.response.status).toBe(400)
209+
}
210+
}, 10000)
211+
212+
it('should reject empty birthPlace', async () => {
213+
try {
214+
await axios.post(`${API_URL}/auth/signup`, {
215+
...baseUserData,
216+
birthPlace: '',
217+
email: `test_nobirth_${Date.now()}@example.com`,
218+
})
219+
throw new Error('Should have rejected empty birthPlace')
220+
} catch (error: any) {
221+
expect(error.response).toBeDefined()
222+
expect(error.response.status).toBe(400)
223+
}
224+
}, 10000)
225+
226+
it('should reject empty location', async () => {
227+
try {
228+
await axios.post(`${API_URL}/auth/signup`, {
229+
...baseUserData,
230+
location: '',
231+
email: `test_noloc_${Date.now()}@example.com`,
232+
})
233+
throw new Error('Should have rejected empty location')
234+
} catch (error: any) {
235+
expect(error.response).toBeDefined()
236+
expect(error.response.status).toBe(400)
237+
}
238+
}, 10000)
239+
240+
it('should accept valid portfolioUrl', async () => {
241+
const uniqueEmail = `test_portfolio_${Date.now()}@example.com`
242+
try {
243+
const res = await axios.post(`${API_URL}/auth/signup`, {
244+
...baseUserData,
245+
email: uniqueEmail,
246+
portfolioUrl: 'https://portfolio.example.com',
247+
})
248+
expect(res.status).toBe(201)
249+
} catch (error: any) {
250+
if (error.response?.status !== 409) {
251+
throw error
252+
}
253+
}
254+
}, 10000)
255+
256+
it('should reject bio longer than 50 characters', async () => {
257+
try {
258+
await axios.post(`${API_URL}/auth/signup`, {
259+
...baseUserData,
260+
email: `test_longbio_${Date.now()}@example.com`,
261+
bio: 'A'.repeat(51), // 51 characters
262+
})
263+
throw new Error('Should have rejected long bio')
264+
} catch (error: any) {
265+
expect(error.response).toBeDefined()
266+
expect(error.response.status).toBe(400)
267+
}
268+
}, 10000)
269+
})
270+
271+
describe('Login Endpoint Tests', () => {
272+
const loginTestEmail = env.YOUR_EMAIL || 'test@example.com'
273+
const validPassword = 'Test@1234'
274+
275+
it('should successfully login with valid credentials', async () => {
276+
try {
277+
const res = await axios.post(`${API_URL}/user/login`, {
278+
email: loginTestEmail,
279+
password: validPassword,
280+
})
281+
282+
expect(res.status).toBe(200)
283+
expect(res.data).toBeDefined()
284+
expect(res.data.message).toBeDefined()
285+
} catch (error: any) {
286+
if (error.response?.status === 401 || error.response?.status === 404) {
287+
console.log('Test user not found, skipping login success test')
288+
expect(true).toBe(true)
289+
} else {
290+
throw error
291+
}
292+
}
293+
}, 10000)
294+
295+
it('should reject login with wrong password', async () => {
296+
try {
297+
await axios.post(`${API_URL}/user/login`, {
298+
email: loginTestEmail,
299+
password: 'WrongPassword@123',
300+
})
301+
throw new Error('Should have rejected wrong password')
302+
} catch (error: any) {
303+
expect(error.response).toBeDefined()
304+
expect([401, 404]).toContain(error.response.status)
305+
}
306+
}, 10000)
307+
308+
it('should reject login with non-existent email', async () => {
309+
try {
310+
await axios.post(`${API_URL}/user/login`, {
311+
email: 'nonexistent_user_12345@example.com',
312+
password: 'SomePassword@123',
313+
})
314+
throw new Error('Should have rejected non-existent email')
315+
} catch (error: any) {
316+
expect(error.response).toBeDefined()
317+
expect([401, 404]).toContain(error.response.status)
318+
}
319+
}, 10000)
320+
})
321+
322+
describe('Password Reset Tests', () => {
323+
const testEmail = env.YOUR_EMAIL || 'test@example.com'
324+
// Generate a token we can control
325+
const rawToken = 'test-reset-token-123'
326+
const hashedToken = crypto.createHash('sha256').update(rawToken).digest('hex')
327+
328+
it('should send reset email (forgot password)', async () => {
329+
const res = await axios.post(`${API_URL}/auth/forgot-password`, {
330+
email: testEmail,
331+
})
332+
333+
expect(res.status).toBe(200)
334+
expect(res.data.message).toMatch(/reset link has been sent/i)
335+
})
336+
337+
it('should successfully reset password with valid token', async () => {
338+
// 1. Setup: Manually inject token into DB for the test user
339+
const user = await User.findOne({ email: testEmail })
340+
if (!user) throw new Error('Test user not found')
341+
342+
user.resetPasswordToken = hashedToken
343+
user.resetPasswordExpire = new Date(Date.now() + 10 * 60 * 1000) // 10 mins from now
344+
await user.save()
345+
346+
// 2. Call Reset Password Endpoint with RAW token
347+
const newPassword = 'NewSecurePassword123!'
348+
const res = await axios.post(`${API_URL}/auth/reset-password/${rawToken}`, {
349+
password: newPassword,
350+
})
351+
352+
expect(res.status).toBe(200)
353+
expect(res.data.message).toMatch(/password reset successfully/i)
354+
355+
// 3. Verify Login with New Password works
356+
const loginRes = await axios.post(`${API_URL}/user/login`, {
357+
email: testEmail,
358+
password: newPassword,
359+
})
360+
expect(loginRes.status).toBe(200)
361+
})
362+
363+
it('should fail reset with invalid token', async () => {
364+
try {
365+
await axios.post(`${API_URL}/auth/reset-password/invalid-token`, {
366+
password: 'NewPassword123!',
367+
})
368+
throw new Error('Should have failed')
369+
} catch (error: any) {
370+
expect(error.response.status).toBe(500) // or 400 depending on implementation
371+
// Checking for "Invalid token" message we just added
372+
expect(error.response.data.message).toMatch(/Invalid token/i)
373+
}
374+
})
375+
})
376+
377+
afterAll(async () => {
378+
await mongoose.connection.close()
379+
})

LocalMind-Backend/src/api/v1/user/user.constant.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,6 @@ enum UserConstant {
6060
FORBIDDEN = 'Forbidden access',
6161

6262
// ✅ USER & INPUT VALIDATION
63-
INVALID_ROLE = 'Invalid role',
64-
INVALID_URL = 'Invalid portfolio URL',
65-
BIO_MAX_LENGTH = 'Bio must be at most 300 characters',
6663
USER_NOT_FOUND = 'User not found',
6764
EMAIL_ALREADY_EXISTS = 'Email already exists',
6865
INVALID_INPUT = 'User is not available in request',
@@ -102,7 +99,7 @@ export const AllowedUserRoles = ['user', 'admin', 'creator'] as const
10299

103100
export const PasswordConfig = {
104101
minLength: 8,
105-
maxLength: 20,
102+
maxLength: 128,
106103
saltRounds: 10,
107104
}
108105

0 commit comments

Comments
 (0)