Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
doc/*
!doc/signup_dataflow.md
!doc/password_reset_dataflow.md
2 changes: 2 additions & 0 deletions LocalMind-Backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@eslint/js": "^9.36.0",
"@types/argon2": "^0.15.4",
"@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/node": "^24.7.2",
"@types/nodemailer": "^7.0.3",
Expand Down Expand Up @@ -60,6 +61,7 @@
"chalk": "^5.6.2",
"cloudflared-tunnel": "^1.0.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"d3-dsv": "^2.0.0",
"dotenv": "^17.2.3",
"express": "^5.1.0",
Expand Down
693 changes: 693 additions & 0 deletions LocalMind-Backend/pnpm-lock.yaml

Large diffs are not rendered by default.

295 changes: 291 additions & 4 deletions LocalMind-Backend/src/api/v1/user/__test__/user.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { describe, it, expect, beforeAll } from '@jest/globals'
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
import axios from 'axios'
import mongoose from 'mongoose'
import crypto from 'crypto'
import { env } from '../../../../constant/env.constant'
import UserUtils from '../user.utils'
import mongoose from 'mongoose'
import User from '../user.model'

const API_URL = env.BACKEND_URL

describe('User Registration Tests', () => {
let userExists = false
Expand Down Expand Up @@ -38,7 +42,7 @@ describe('User Registration Tests', () => {
}

try {
const res = await axios.post(`${env.BACKEND_URL}/auth/signup`, {
const res = await axios.post(`${API_URL}/auth/signup`, {
firstName: 'Test User',
birthPlace: 'Test City',
location: 'Test Country',
Expand Down Expand Up @@ -72,7 +76,7 @@ describe('User Registration Tests', () => {
}

try {
const res = await axios.post(`${env.BACKEND_URL}/auth/signup`, {
const res = await axios.post(`${API_URL}/auth/signup`, {
firstName: 'Duplicate User',
birthPlace: 'Duplicate City',
location: 'Duplicate Country',
Expand All @@ -90,3 +94,286 @@ describe('User Registration Tests', () => {
}
}, 10000)
})

describe('Password Validation Tests', () => {
const validUserData = {
firstName: 'Password Test',
birthPlace: 'Test City',
location: 'Test Country',
email: `pwtest_${Date.now()}@example.com`,
}

it('should reject password without uppercase letter', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...validUserData,
email: `test_noupper_${Date.now()}@example.com`,
password: 'test@1234', // No uppercase
})
throw new Error('Should have rejected password without uppercase')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject password without lowercase letter', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...validUserData,
email: `test_nolower_${Date.now()}@example.com`,
password: 'TEST@1234', // No lowercase
})
throw new Error('Should have rejected password without lowercase')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject password without number', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...validUserData,
email: `test_nonum_${Date.now()}@example.com`,
password: 'Test@test', // No number
})
throw new Error('Should have rejected password without number')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject password without special character', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...validUserData,
email: `test_nospecial_${Date.now()}@example.com`,
password: 'Test12345', // No special char
})
throw new Error('Should have rejected password without special character')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject password shorter than 8 characters', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...validUserData,
email: `test_short_${Date.now()}@example.com`,
password: 'Te@1', // Too short
})
throw new Error('Should have rejected short password')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)
})

describe('Input Validation Edge Cases', () => {
const baseUserData = {
firstName: 'Edge Case Test',
birthPlace: 'Test City',
location: 'Test Country',
password: 'ValidPass@123',
}

it('should reject empty firstName', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...baseUserData,
firstName: '',
email: `test_nofname_${Date.now()}@example.com`,
})
throw new Error('Should have rejected empty firstName')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject invalid email format', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...baseUserData,
email: 'invalid-email-format',
})
throw new Error('Should have rejected invalid email')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject empty birthPlace', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...baseUserData,
birthPlace: '',
email: `test_nobirth_${Date.now()}@example.com`,
})
throw new Error('Should have rejected empty birthPlace')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should reject empty location', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...baseUserData,
location: '',
email: `test_noloc_${Date.now()}@example.com`,
})
throw new Error('Should have rejected empty location')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)

it('should accept valid portfolioUrl', async () => {
const uniqueEmail = `test_portfolio_${Date.now()}@example.com`
try {
const res = await axios.post(`${API_URL}/auth/signup`, {
...baseUserData,
email: uniqueEmail,
portfolioUrl: 'https://portfolio.example.com',
})
expect(res.status).toBe(201)
} catch (error: any) {
if (error.response?.status !== 409) {
throw error
}
}
}, 10000)

it('should reject bio longer than 50 characters', async () => {
try {
await axios.post(`${API_URL}/auth/signup`, {
...baseUserData,
email: `test_longbio_${Date.now()}@example.com`,
bio: 'A'.repeat(51), // 51 characters
})
throw new Error('Should have rejected long bio')
} catch (error: any) {
expect(error.response).toBeDefined()
expect(error.response.status).toBe(400)
}
}, 10000)
})

describe('Login Endpoint Tests', () => {
const loginTestEmail = env.YOUR_EMAIL || 'test@example.com'
const validPassword = 'Test@1234'

it('should successfully login with valid credentials', async () => {
try {
const res = await axios.post(`${API_URL}/user/login`, {
email: loginTestEmail,
password: validPassword,
})

expect(res.status).toBe(200)
expect(res.data).toBeDefined()
expect(res.data.message).toBeDefined()
} catch (error: any) {
if (error.response?.status === 401 || error.response?.status === 404) {
console.log('Test user not found, skipping login success test')
expect(true).toBe(true)
} else {
throw error
}
}
}, 10000)
Comment on lines +275 to +293
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test case for a successful login is not robust because it depends on a user existing from a previous test suite. If the user doesn't exist, it catches the error and considers the test passed, which can hide real issues. It's better to make tests independent by creating the necessary user in a beforeAll or beforeEach hook for this test suite to ensure a consistent and reliable test environment.


it('should reject login with wrong password', async () => {
try {
await axios.post(`${API_URL}/user/login`, {
email: loginTestEmail,
password: 'WrongPassword@123',
})
throw new Error('Should have rejected wrong password')
} catch (error: any) {
expect(error.response).toBeDefined()
expect([401, 404]).toContain(error.response.status)
}
}, 10000)

it('should reject login with non-existent email', async () => {
try {
await axios.post(`${API_URL}/user/login`, {
email: 'nonexistent_user_12345@example.com',
password: 'SomePassword@123',
})
throw new Error('Should have rejected non-existent email')
} catch (error: any) {
expect(error.response).toBeDefined()
expect([401, 404]).toContain(error.response.status)
}
}, 10000)
})

describe('Password Reset Tests', () => {
const testEmail = env.YOUR_EMAIL || 'test@example.com'
// Generate a token we can control
const rawToken = 'test-reset-token-123'
const hashedToken = crypto.createHash('sha256').update(rawToken).digest('hex')

it('should send reset email (forgot password)', async () => {
const res = await axios.post(`${API_URL}/auth/forgot-password`, {
email: testEmail,
})

expect(res.status).toBe(200)
expect(res.data.message).toMatch(/reset link has been sent/i)
})

it('should successfully reset password with valid token', async () => {
// 1. Setup: Manually inject token into DB for the test user
const user = await User.findOne({ email: testEmail })
if (!user) throw new Error('Test user not found')

user.resetPasswordToken = hashedToken
user.resetPasswordExpire = new Date(Date.now() + 10 * 60 * 1000) // 10 mins from now
await user.save()

// 2. Call Reset Password Endpoint with RAW token
const newPassword = 'NewSecurePassword123!'
const res = await axios.post(`${API_URL}/auth/reset-password/${rawToken}`, {
password: newPassword,
})

expect(res.status).toBe(200)
expect(res.data.message).toMatch(/password reset successfully/i)

// 3. Verify Login with New Password works
const loginRes = await axios.post(`${API_URL}/user/login`, {
email: testEmail,
password: newPassword,
})
expect(loginRes.status).toBe(200)
})

it('should fail reset with invalid token', async () => {
try {
await axios.post(`${API_URL}/auth/reset-password/invalid-token`, {
password: 'NewPassword123!',
})
throw new Error('Should have failed')
} catch (error: any) {
expect(error.response.status).toBe(500) // or 400 depending on implementation
// Checking for "Invalid token" message we just added
expect(error.response.data.message).toMatch(/Invalid token/i)
}
})
})

afterAll(async () => {
await mongoose.connection.close()
})
5 changes: 1 addition & 4 deletions LocalMind-Backend/src/api/v1/user/user.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ enum UserConstant {
FORBIDDEN = 'Forbidden access',

// ✅ USER & INPUT VALIDATION
INVALID_ROLE = 'Invalid role',
INVALID_URL = 'Invalid portfolio URL',
BIO_MAX_LENGTH = 'Bio must be at most 300 characters',
USER_NOT_FOUND = 'User not found',
EMAIL_ALREADY_EXISTS = 'Email already exists',
INVALID_INPUT = 'User is not available in request',
Expand Down Expand Up @@ -95,7 +92,7 @@ export const AllowedUserRoles = ['user', 'admin', 'creator'] as const

export const PasswordConfig = {
minLength: 8,
maxLength: 20,
maxLength: 128,
saltRounds: 10,
}

Expand Down
Loading