diff --git a/backend/controller/user.controller.js b/backend/controller/user.controller.js index 141e174..ff23c3c 100644 --- a/backend/controller/user.controller.js +++ b/backend/controller/user.controller.js @@ -44,30 +44,49 @@ const updatelanguage = expressAsyncHandler(async (req, res) => { const registeruser = expressAsyncHandler (async (req,res) => { const {name,email,password,pic} = req.body + // Validate required fields if (!name || !email || !password) { return res.status(400).json({ success: false, message: "Please provide all fields" }); } - if (name.length < 2 || name.length > 50) { + // Validate name length + const trimmedName = name.trim(); + if (trimmedName.length < 2 || trimmedName.length > 50) { return res.status(400).json({ success: false, message: "Name must be between 2 and 50 characters" }); } + // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { + const trimmedEmail = email.trim().toLowerCase(); + if (!emailRegex.test(trimmedEmail)) { return res.status(400).json({ success: false, message: "Please provide a valid email address" }); } + // Validate password strength if (password.length < 6) { return res.status(400).json({ success: false, message: "Password must be at least 6 characters long" }); } + + // Additional password validation: check for at least one letter and one number + if (!/(?=.*[a-zA-Z])(?=.*\d)/.test(password)) { + return res.status(400).json({ success: false, message: "Password must contain at least one letter and one number" }); + } - const userExists = await User.findOne({email}) + // Check if user already exists + const userExists = await User.findOne({ email: trimmedEmail }) if(userExists) { return res.status(409).json({ success: false, message: "User already exists" }); } - const user = await User.create({name, email, password,pic}) + + // Create new user with trimmed and normalized data + const user = await User.create({ + name: trimmedName, + email: trimmedEmail, + password, + pic: pic || undefined + }) if(user){ res.status(201).json({ diff --git a/frontend/src/components/Chat/__tests__/ChatHeader.test.jsx b/frontend/src/components/Chat/__tests__/ChatHeader.test.jsx new file mode 100644 index 0000000..3ccd27f --- /dev/null +++ b/frontend/src/components/Chat/__tests__/ChatHeader.test.jsx @@ -0,0 +1,48 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import ChatHeader from '../ChatHeader'; + +// Mock dependencies +vi.mock('../../../stores', () => ({ + useAuthStore: () => ({ user: { _id: 'user1', name: 'Test User' } }), + useChatStore: () => ({ + selectedChat: { _id: 'chat1', chatName: 'Test Chat' }, + setSelectedChat: vi.fn(), + }), +})); + +const MockedChatHeader = (props) => ( + + + +); + +describe('ChatHeader Component', () => { + it('renders chat name', () => { + render(); + expect(document.body).toBeTruthy(); + }); + + it('renders user info for one-on-one chats', () => { + const mockChat = { + _id: 'chat1', + isGroupChat: false, + users: [{ _id: 'user2', name: 'Other User', pic: 'pic.jpg' }], + }; + render(); + expect(document.body).toBeTruthy(); + }); + + it('renders group chat info', () => { + const mockChat = { + _id: 'chat1', + isGroupChat: true, + chatName: 'Group Chat', + users: [{ _id: 'user2' }, { _id: 'user3' }], + }; + render(); + expect(document.body).toBeTruthy(); + }); +}); + diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 02c408d..c6893cf 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -5,14 +5,72 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +/** + * Get user profile pictures from a chat + * @param chat - The chat object containing users + * @param currentUserId - The ID of the current user to exclude + * @returns Array of user profile picture URLs + */ export function getUserPics(chat, currentUserId) { - if (!chat?.users) return []; + if (!chat?.users || !Array.isArray(chat.users)) return []; - const otherUsers = chat.users.filter(user => user?._id?.toString() !== currentUserId?.toString()); + const otherUsers = chat.users.filter(user => { + if (!user?._id) return false; + return user._id.toString() !== currentUserId?.toString(); + }); if (chat.isGroupChat) { - return otherUsers.slice(0, 3).map(user => user?.pic).filter(Boolean); + // For group chats, return up to 3 user pics + return otherUsers + .slice(0, 3) + .map(user => user?.pic) + .filter(Boolean); } else { - return otherUsers.length > 0 && otherUsers[0]?.pic ? [otherUsers[0].pic] : []; + // For one-on-one chats, return the other user's pic + return otherUsers.length > 0 && otherUsers[0]?.pic + ? [otherUsers[0].pic] + : []; } } + +/** + * Validate email format + * @param email - Email string to validate + * @returns Boolean indicating if email is valid + */ +export function isValidEmail(email) { + if (!email || typeof email !== 'string') return false; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email.trim()); +} + +/** + * Format file size to human-readable format + * @param bytes - File size in bytes + * @returns Formatted string (e.g., "1.5 MB") + */ +export function formatFileSize(bytes) { + if (!bytes || bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; +} + +/** + * Debounce function to limit function calls + * @param func - Function to debounce + * @param wait - Wait time in milliseconds + * @returns Debounced function + */ +export function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} diff --git a/frontend/src/utils/__tests__/validationUtils.test.js b/frontend/src/utils/__tests__/validationUtils.test.js new file mode 100644 index 0000000..29cbf5e --- /dev/null +++ b/frontend/src/utils/__tests__/validationUtils.test.js @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { isValidEmail, formatFileSize, debounce } from '../../lib/utils'; + +describe('Validation Utils', () => { + describe('isValidEmail', () => { + it('validates correct email formats', () => { + expect(isValidEmail('test@example.com')).toBe(true); + expect(isValidEmail('user.name@domain.co.uk')).toBe(true); + expect(isValidEmail('user+tag@example.com')).toBe(true); + }); + + it('rejects invalid email formats', () => { + expect(isValidEmail('invalid-email')).toBe(false); + expect(isValidEmail('@example.com')).toBe(false); + expect(isValidEmail('user@')).toBe(false); + expect(isValidEmail('')).toBe(false); + expect(isValidEmail(null)).toBe(false); + expect(isValidEmail(undefined)).toBe(false); + }); + + it('handles whitespace', () => { + expect(isValidEmail(' test@example.com ')).toBe(true); + }); + }); + + describe('formatFileSize', () => { + it('formats bytes correctly', () => { + expect(formatFileSize(0)).toBe('0 Bytes'); + expect(formatFileSize(1024)).toBe('1 KB'); + expect(formatFileSize(1048576)).toBe('1 MB'); + expect(formatFileSize(1073741824)).toBe('1 GB'); + }); + + it('handles edge cases', () => { + expect(formatFileSize(null)).toBe('0 Bytes'); + expect(formatFileSize(undefined)).toBe('0 Bytes'); + }); + }); + + describe('debounce', () => { + it('delays function execution', (done) => { + let callCount = 0; + const func = () => { callCount++; }; + const debouncedFunc = debounce(func, 100); + + debouncedFunc(); + debouncedFunc(); + debouncedFunc(); + + expect(callCount).toBe(0); + + setTimeout(() => { + expect(callCount).toBe(1); + done(); + }, 150); + }); + }); +}); +