WIP
This commit is contained in:
362
backend/tests/unit/authService.test.js
Normal file
362
backend/tests/unit/authService.test.js
Normal file
@ -0,0 +1,362 @@
|
||||
const AuthService = require('../../src/services/AuthService');
|
||||
const User = require('../../src/models/User');
|
||||
const emailService = require('../../src/services/EmailService');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../src/models/User');
|
||||
jest.mock('../../src/services/EmailService');
|
||||
jest.mock('jsonwebtoken');
|
||||
|
||||
describe('AuthService Unit Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('generateToken', () => {
|
||||
it('should generate a valid JWT token', () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
is_verified: true
|
||||
};
|
||||
|
||||
const mockToken = 'mock-jwt-token';
|
||||
jwt.sign.mockReturnValue(mockToken);
|
||||
|
||||
const token = AuthService.generateToken(mockUser);
|
||||
|
||||
expect(jwt.sign).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: 'user-123',
|
||||
email: 'test@example.com',
|
||||
isVerified: true
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
|
||||
issuer: 'bookmark-manager',
|
||||
audience: 'bookmark-manager-users'
|
||||
}
|
||||
);
|
||||
expect(token).toBe(mockToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyToken', () => {
|
||||
it('should verify a valid token', () => {
|
||||
const mockPayload = { userId: 'user-123', email: 'test@example.com' };
|
||||
jwt.verify.mockReturnValue(mockPayload);
|
||||
|
||||
const result = AuthService.verifyToken('valid-token');
|
||||
|
||||
expect(jwt.verify).toHaveBeenCalledWith(
|
||||
'valid-token',
|
||||
process.env.JWT_SECRET,
|
||||
{
|
||||
issuer: 'bookmark-manager',
|
||||
audience: 'bookmark-manager-users'
|
||||
}
|
||||
);
|
||||
expect(result).toEqual(mockPayload);
|
||||
});
|
||||
|
||||
it('should return null for invalid token', () => {
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('Invalid token');
|
||||
});
|
||||
|
||||
const result = AuthService.verifyToken('invalid-token');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should successfully register a new user', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
toSafeObject: () => ({ id: 'user-123', email: 'test@example.com' })
|
||||
};
|
||||
|
||||
User.create.mockResolvedValue(mockUser);
|
||||
emailService.sendVerificationEmail.mockResolvedValue({ message: 'Email sent' });
|
||||
|
||||
const result = await AuthService.register('test@example.com', 'password123');
|
||||
|
||||
expect(User.create).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
});
|
||||
expect(emailService.sendVerificationEmail).toHaveBeenCalled();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('User registered successfully. Please check your email for verification.');
|
||||
});
|
||||
|
||||
it('should handle registration failure', async () => {
|
||||
User.create.mockRejectedValue(new Error('Email already exists'));
|
||||
|
||||
const result = await AuthService.register('test@example.com', 'password123');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Email already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should successfully login a verified user', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
is_verified: true,
|
||||
toSafeObject: () => ({ id: 'user-123', email: 'test@example.com' })
|
||||
};
|
||||
|
||||
User.authenticate.mockResolvedValue(mockUser);
|
||||
jwt.sign.mockReturnValue('mock-token');
|
||||
|
||||
const result = await AuthService.login('test@example.com', 'password123');
|
||||
|
||||
expect(User.authenticate).toHaveBeenCalledWith('test@example.com', 'password123');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.token).toBe('mock-token');
|
||||
expect(result.user).toEqual({ id: 'user-123', email: 'test@example.com' });
|
||||
});
|
||||
|
||||
it('should fail login for invalid credentials', async () => {
|
||||
User.authenticate.mockResolvedValue(null);
|
||||
|
||||
const result = await AuthService.login('test@example.com', 'wrongpassword');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Invalid email or password');
|
||||
});
|
||||
|
||||
it('should fail login for unverified user', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
is_verified: false
|
||||
};
|
||||
|
||||
User.authenticate.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.login('test@example.com', 'password123');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Please verify your email before logging in');
|
||||
expect(result.requiresVerification).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyEmail', () => {
|
||||
it('should successfully verify email', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
is_verified: false,
|
||||
verifyEmail: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
User.findByVerificationToken.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.verifyEmail('valid-token');
|
||||
|
||||
expect(User.findByVerificationToken).toHaveBeenCalledWith('valid-token');
|
||||
expect(mockUser.verifyEmail).toHaveBeenCalled();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Email verified successfully');
|
||||
});
|
||||
|
||||
it('should handle invalid verification token', async () => {
|
||||
User.findByVerificationToken.mockResolvedValue(null);
|
||||
|
||||
const result = await AuthService.verifyEmail('invalid-token');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Invalid or expired verification token');
|
||||
});
|
||||
|
||||
it('should handle already verified email', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
is_verified: true
|
||||
};
|
||||
|
||||
User.findByVerificationToken.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.verifyEmail('valid-token');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Email already verified');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestPasswordReset', () => {
|
||||
it('should send reset email for existing user', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
setResetToken: jest.fn().mockResolvedValue('reset-token')
|
||||
};
|
||||
|
||||
User.findByEmail.mockResolvedValue(mockUser);
|
||||
emailService.sendPasswordResetEmail.mockResolvedValue({ message: 'Email sent' });
|
||||
|
||||
const result = await AuthService.requestPasswordReset('test@example.com');
|
||||
|
||||
expect(User.findByEmail).toHaveBeenCalledWith('test@example.com');
|
||||
expect(mockUser.setResetToken).toHaveBeenCalled();
|
||||
expect(emailService.sendPasswordResetEmail).toHaveBeenCalledWith(mockUser, 'reset-token');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should not reveal if email does not exist', async () => {
|
||||
User.findByEmail.mockResolvedValue(null);
|
||||
|
||||
const result = await AuthService.requestPasswordReset('nonexistent@example.com');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('If an account with that email exists, a password reset link has been sent.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should successfully reset password', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
updatePassword: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
User.findByResetToken.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.resetPassword('valid-token', 'newPassword123');
|
||||
|
||||
expect(User.findByResetToken).toHaveBeenCalledWith('valid-token');
|
||||
expect(mockUser.updatePassword).toHaveBeenCalledWith('newPassword123');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toBe('Password reset successfully');
|
||||
});
|
||||
|
||||
it('should handle invalid reset token', async () => {
|
||||
User.findByResetToken.mockResolvedValue(null);
|
||||
|
||||
const result = await AuthService.resetPassword('invalid-token', 'newPassword123');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Invalid or expired reset token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should successfully change password', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
password_hash: 'hashed-password',
|
||||
updatePassword: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
User.findById.mockResolvedValue(mockUser);
|
||||
User.verifyPassword.mockResolvedValue(true);
|
||||
|
||||
const result = await AuthService.changePassword('user-123', 'currentPassword', 'newPassword123');
|
||||
|
||||
expect(User.findById).toHaveBeenCalledWith('user-123');
|
||||
expect(User.verifyPassword).toHaveBeenCalledWith('currentPassword', 'hashed-password');
|
||||
expect(mockUser.updatePassword).toHaveBeenCalledWith('newPassword123');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail with incorrect current password', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
password_hash: 'hashed-password'
|
||||
};
|
||||
|
||||
User.findById.mockResolvedValue(mockUser);
|
||||
User.verifyPassword.mockResolvedValue(false);
|
||||
|
||||
const result = await AuthService.changePassword('user-123', 'wrongPassword', 'newPassword123');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Current password is incorrect');
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should successfully refresh token', async () => {
|
||||
const mockPayload = { userId: 'user-123' };
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
toSafeObject: () => ({ id: 'user-123', email: 'test@example.com' })
|
||||
};
|
||||
|
||||
jwt.verify.mockReturnValue(mockPayload);
|
||||
User.findById.mockResolvedValue(mockUser);
|
||||
jwt.sign.mockReturnValue('new-token');
|
||||
|
||||
const result = await AuthService.refreshToken('old-token');
|
||||
|
||||
expect(jwt.verify).toHaveBeenCalledWith('old-token', process.env.JWT_SECRET, {
|
||||
issuer: 'bookmark-manager',
|
||||
audience: 'bookmark-manager-users'
|
||||
});
|
||||
expect(User.findById).toHaveBeenCalledWith('user-123');
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.token).toBe('new-token');
|
||||
});
|
||||
|
||||
it('should fail with invalid token', async () => {
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('Invalid token');
|
||||
});
|
||||
|
||||
const result = await AuthService.refreshToken('invalid-token');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('Invalid token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAuthToken', () => {
|
||||
it('should validate token and return user', async () => {
|
||||
const mockPayload = { userId: 'user-123' };
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
is_verified: true
|
||||
};
|
||||
|
||||
jwt.verify.mockReturnValue(mockPayload);
|
||||
User.findById.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.validateAuthToken('valid-token');
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should return null for invalid token', async () => {
|
||||
jwt.verify.mockImplementation(() => {
|
||||
throw new Error('Invalid token');
|
||||
});
|
||||
|
||||
const result = await AuthService.validateAuthToken('invalid-token');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for unverified user', async () => {
|
||||
const mockPayload = { userId: 'user-123' };
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
is_verified: false
|
||||
};
|
||||
|
||||
jwt.verify.mockReturnValue(mockPayload);
|
||||
User.findById.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await AuthService.validateAuthToken('valid-token');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
570
backend/tests/unit/bookmark.test.js
Normal file
570
backend/tests/unit/bookmark.test.js
Normal file
@ -0,0 +1,570 @@
|
||||
const Bookmark = require('../../src/models/Bookmark');
|
||||
const dbConnection = require('../../src/database/connection');
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../src/database/connection');
|
||||
|
||||
describe('Bookmark Model Unit Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
describe('validateBookmark', () => {
|
||||
it('should validate correct bookmark data', () => {
|
||||
const validBookmark = {
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
folder: 'Test Folder',
|
||||
status: 'valid'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(validBookmark);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should reject bookmark without title', () => {
|
||||
const invalidBookmark = {
|
||||
url: 'https://example.com'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Title is required');
|
||||
});
|
||||
|
||||
it('should reject bookmark with empty title', () => {
|
||||
const invalidBookmark = {
|
||||
title: ' ',
|
||||
url: 'https://example.com'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Title is required');
|
||||
});
|
||||
|
||||
it('should reject bookmark with title too long', () => {
|
||||
const invalidBookmark = {
|
||||
title: 'a'.repeat(501),
|
||||
url: 'https://example.com'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Title must be 500 characters or less');
|
||||
});
|
||||
|
||||
it('should reject bookmark without URL', () => {
|
||||
const invalidBookmark = {
|
||||
title: 'Test Bookmark'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('URL is required');
|
||||
});
|
||||
|
||||
it('should reject bookmark with invalid URL', () => {
|
||||
const invalidBookmark = {
|
||||
title: 'Test Bookmark',
|
||||
url: 'not-a-valid-url'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Invalid URL format');
|
||||
});
|
||||
|
||||
it('should reject bookmark with folder name too long', () => {
|
||||
const invalidBookmark = {
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
folder: 'a'.repeat(256)
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Folder name must be 255 characters or less');
|
||||
});
|
||||
|
||||
it('should reject bookmark with invalid status', () => {
|
||||
const invalidBookmark = {
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
status: 'invalid-status'
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(invalidBookmark);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Invalid status value');
|
||||
});
|
||||
|
||||
it('should accept valid status values', () => {
|
||||
const validStatuses = ['unknown', 'valid', 'invalid', 'testing', 'duplicate'];
|
||||
|
||||
validStatuses.forEach(status => {
|
||||
const bookmark = {
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
status
|
||||
};
|
||||
|
||||
const result = Bookmark.validateBookmark(bookmark);
|
||||
expect(result.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database Operations', () => {
|
||||
describe('create', () => {
|
||||
it('should create a new bookmark successfully', async () => {
|
||||
const userId = 'user-123';
|
||||
const bookmarkData = {
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
folder: 'Test Folder',
|
||||
status: 'valid'
|
||||
};
|
||||
|
||||
const mockCreatedBookmark = {
|
||||
id: 'bookmark-123',
|
||||
user_id: userId,
|
||||
...bookmarkData,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockCreatedBookmark]
|
||||
});
|
||||
|
||||
const result = await Bookmark.create(userId, bookmarkData);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO bookmarks'),
|
||||
[
|
||||
userId,
|
||||
'Test Bookmark',
|
||||
'https://example.com',
|
||||
'Test Folder',
|
||||
expect.any(Date),
|
||||
undefined,
|
||||
undefined,
|
||||
'valid'
|
||||
]
|
||||
);
|
||||
expect(result).toBeInstanceOf(Bookmark);
|
||||
expect(result.title).toBe('Test Bookmark');
|
||||
expect(result.user_id).toBe(userId);
|
||||
});
|
||||
|
||||
it('should reject invalid bookmark data', async () => {
|
||||
const userId = 'user-123';
|
||||
const invalidBookmarkData = {
|
||||
title: '',
|
||||
url: 'invalid-url'
|
||||
};
|
||||
|
||||
await expect(Bookmark.create(userId, invalidBookmarkData))
|
||||
.rejects.toThrow('Bookmark validation failed');
|
||||
});
|
||||
|
||||
it('should trim whitespace from title, url, and folder', async () => {
|
||||
const userId = 'user-123';
|
||||
const bookmarkData = {
|
||||
title: ' Test Bookmark ',
|
||||
url: ' https://example.com ',
|
||||
folder: ' Test Folder '
|
||||
};
|
||||
|
||||
const mockCreatedBookmark = {
|
||||
id: 'bookmark-123',
|
||||
user_id: userId,
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
folder: 'Test Folder'
|
||||
};
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockCreatedBookmark]
|
||||
});
|
||||
|
||||
await Bookmark.create(userId, bookmarkData);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO bookmarks'),
|
||||
expect.arrayContaining([
|
||||
userId,
|
||||
'Test Bookmark',
|
||||
'https://example.com',
|
||||
'Test Folder'
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByUserId', () => {
|
||||
it('should find bookmarks by user ID with default options', async () => {
|
||||
const userId = 'user-123';
|
||||
const mockBookmarks = [
|
||||
{ id: 'bookmark-1', user_id: userId, title: 'Bookmark 1' },
|
||||
{ id: 'bookmark-2', user_id: userId, title: 'Bookmark 2' }
|
||||
];
|
||||
|
||||
dbConnection.query
|
||||
.mockResolvedValueOnce({ rows: [{ count: '2' }] }) // Count query
|
||||
.mockResolvedValueOnce({ rows: mockBookmarks }); // Data query
|
||||
|
||||
const result = await Bookmark.findByUserId(userId);
|
||||
|
||||
expect(result.bookmarks).toHaveLength(2);
|
||||
expect(result.pagination.totalCount).toBe(2);
|
||||
expect(result.pagination.page).toBe(1);
|
||||
expect(result.pagination.limit).toBe(50);
|
||||
});
|
||||
|
||||
it('should apply folder filter', async () => {
|
||||
const userId = 'user-123';
|
||||
const options = { folder: 'Work' };
|
||||
|
||||
dbConnection.query
|
||||
.mockResolvedValueOnce({ rows: [{ count: '1' }] })
|
||||
.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
await Bookmark.findByUserId(userId, options);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('folder = $2'),
|
||||
expect.arrayContaining([userId, 'Work'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply status filter', async () => {
|
||||
const userId = 'user-123';
|
||||
const options = { status: 'valid' };
|
||||
|
||||
dbConnection.query
|
||||
.mockResolvedValueOnce({ rows: [{ count: '1' }] })
|
||||
.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
await Bookmark.findByUserId(userId, options);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('status = $2'),
|
||||
expect.arrayContaining([userId, 'valid'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply search filter', async () => {
|
||||
const userId = 'user-123';
|
||||
const options = { search: 'test' };
|
||||
|
||||
dbConnection.query
|
||||
.mockResolvedValueOnce({ rows: [{ count: '1' }] })
|
||||
.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
await Bookmark.findByUserId(userId, options);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('title ILIKE $2 OR url ILIKE $2'),
|
||||
expect.arrayContaining([userId, '%test%'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle pagination correctly', async () => {
|
||||
const userId = 'user-123';
|
||||
const options = { page: 2, limit: 10 };
|
||||
|
||||
dbConnection.query
|
||||
.mockResolvedValueOnce({ rows: [{ count: '25' }] })
|
||||
.mockResolvedValueOnce({ rows: [] });
|
||||
|
||||
const result = await Bookmark.findByUserId(userId, options);
|
||||
|
||||
expect(result.pagination.page).toBe(2);
|
||||
expect(result.pagination.limit).toBe(10);
|
||||
expect(result.pagination.totalCount).toBe(25);
|
||||
expect(result.pagination.totalPages).toBe(3);
|
||||
expect(result.pagination.hasNext).toBe(true);
|
||||
expect(result.pagination.hasPrev).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByIdAndUserId', () => {
|
||||
it('should find bookmark by ID and user ID', async () => {
|
||||
const bookmarkId = 'bookmark-123';
|
||||
const userId = 'user-123';
|
||||
const mockBookmark = {
|
||||
id: bookmarkId,
|
||||
user_id: userId,
|
||||
title: 'Test Bookmark'
|
||||
};
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockBookmark]
|
||||
});
|
||||
|
||||
const result = await Bookmark.findByIdAndUserId(bookmarkId, userId);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM bookmarks WHERE id = $1 AND user_id = $2',
|
||||
[bookmarkId, userId]
|
||||
);
|
||||
expect(result).toBeInstanceOf(Bookmark);
|
||||
expect(result.id).toBe(bookmarkId);
|
||||
});
|
||||
|
||||
it('should return null if bookmark not found', async () => {
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: []
|
||||
});
|
||||
|
||||
const result = await Bookmark.findByIdAndUserId('nonexistent', 'user-123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreate', () => {
|
||||
it('should create multiple bookmarks', async () => {
|
||||
const userId = 'user-123';
|
||||
const bookmarksData = [
|
||||
{ title: 'Bookmark 1', url: 'https://example1.com' },
|
||||
{ title: 'Bookmark 2', url: 'https://example2.com' }
|
||||
];
|
||||
|
||||
const mockCreatedBookmarks = bookmarksData.map((data, index) => ({
|
||||
id: `bookmark-${index + 1}`,
|
||||
user_id: userId,
|
||||
...data
|
||||
}));
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: mockCreatedBookmarks
|
||||
});
|
||||
|
||||
const result = await Bookmark.bulkCreate(userId, bookmarksData);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBeInstanceOf(Bookmark);
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO bookmarks'),
|
||||
expect.any(Array)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty array for empty input', async () => {
|
||||
const result = await Bookmark.bulkCreate('user-123', []);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(dbConnection.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate all bookmarks before creation', async () => {
|
||||
const userId = 'user-123';
|
||||
const bookmarksData = [
|
||||
{ title: 'Valid Bookmark', url: 'https://example.com' },
|
||||
{ title: '', url: 'invalid-url' } // Invalid bookmark
|
||||
];
|
||||
|
||||
await expect(Bookmark.bulkCreate(userId, bookmarksData))
|
||||
.rejects.toThrow('Bookmark validation failed');
|
||||
|
||||
expect(dbConnection.query).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Instance Methods', () => {
|
||||
describe('update', () => {
|
||||
it('should update bookmark successfully', async () => {
|
||||
const bookmark = new Bookmark({
|
||||
id: 'bookmark-123',
|
||||
user_id: 'user-123',
|
||||
title: 'Old Title',
|
||||
url: 'https://old-url.com'
|
||||
});
|
||||
|
||||
const updates = {
|
||||
title: 'New Title',
|
||||
url: 'https://new-url.com'
|
||||
};
|
||||
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 1 });
|
||||
|
||||
const result = await bookmark.update(updates);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE bookmarks SET'),
|
||||
expect.arrayContaining(['New Title', 'https://new-url.com', 'bookmark-123', 'user-123'])
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
expect(bookmark.title).toBe('New Title');
|
||||
expect(bookmark.url).toBe('https://new-url.com');
|
||||
});
|
||||
|
||||
it('should validate updates before applying', async () => {
|
||||
const bookmark = new Bookmark({
|
||||
id: 'bookmark-123',
|
||||
user_id: 'user-123',
|
||||
title: 'Valid Title',
|
||||
url: 'https://valid-url.com'
|
||||
});
|
||||
|
||||
const invalidUpdates = {
|
||||
title: '', // Invalid title
|
||||
url: 'invalid-url' // Invalid URL
|
||||
};
|
||||
|
||||
await expect(bookmark.update(invalidUpdates))
|
||||
.rejects.toThrow('Bookmark validation failed');
|
||||
|
||||
expect(dbConnection.query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false if no valid fields to update', async () => {
|
||||
const bookmark = new Bookmark({
|
||||
id: 'bookmark-123',
|
||||
user_id: 'user-123'
|
||||
});
|
||||
|
||||
const result = await bookmark.update({});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(dbConnection.query).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete bookmark successfully', async () => {
|
||||
const bookmark = new Bookmark({
|
||||
id: 'bookmark-123',
|
||||
user_id: 'user-123'
|
||||
});
|
||||
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 1 });
|
||||
|
||||
const result = await bookmark.delete();
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
'DELETE FROM bookmarks WHERE id = $1 AND user_id = $2',
|
||||
['bookmark-123', 'user-123']
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if bookmark not found', async () => {
|
||||
const bookmark = new Bookmark({
|
||||
id: 'bookmark-123',
|
||||
user_id: 'user-123'
|
||||
});
|
||||
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 0 });
|
||||
|
||||
const result = await bookmark.delete();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSafeObject', () => {
|
||||
it('should return safe bookmark object without user_id', () => {
|
||||
const bookmark = new Bookmark({
|
||||
id: 'bookmark-123',
|
||||
user_id: 'user-123',
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com',
|
||||
folder: 'Test Folder',
|
||||
status: 'valid',
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
const safeObject = bookmark.toSafeObject();
|
||||
|
||||
expect(safeObject).toHaveProperty('id');
|
||||
expect(safeObject).toHaveProperty('title');
|
||||
expect(safeObject).toHaveProperty('url');
|
||||
expect(safeObject).toHaveProperty('folder');
|
||||
expect(safeObject).toHaveProperty('status');
|
||||
expect(safeObject).toHaveProperty('created_at');
|
||||
expect(safeObject).toHaveProperty('updated_at');
|
||||
|
||||
expect(safeObject).not.toHaveProperty('user_id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Static Utility Methods', () => {
|
||||
describe('getFoldersByUserId', () => {
|
||||
it('should get folders with counts', async () => {
|
||||
const userId = 'user-123';
|
||||
const mockFolders = [
|
||||
{ folder: 'Work', count: '5' },
|
||||
{ folder: 'Personal', count: '3' }
|
||||
];
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: mockFolders
|
||||
});
|
||||
|
||||
const result = await Bookmark.getFoldersByUserId(userId);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('GROUP BY folder'),
|
||||
[userId]
|
||||
);
|
||||
expect(result).toEqual(mockFolders);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatsByUserId', () => {
|
||||
it('should get bookmark statistics', async () => {
|
||||
const userId = 'user-123';
|
||||
const mockStats = {
|
||||
total_bookmarks: '10',
|
||||
total_folders: '3',
|
||||
valid_bookmarks: '7',
|
||||
invalid_bookmarks: '2',
|
||||
duplicate_bookmarks: '1',
|
||||
unknown_bookmarks: '0'
|
||||
};
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockStats]
|
||||
});
|
||||
|
||||
const result = await Bookmark.getStatsByUserId(userId);
|
||||
|
||||
expect(result).toEqual(mockStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAllByUserId', () => {
|
||||
it('should delete all bookmarks for user', async () => {
|
||||
const userId = 'user-123';
|
||||
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 5 });
|
||||
|
||||
const result = await Bookmark.deleteAllByUserId(userId);
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
'DELETE FROM bookmarks WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
420
backend/tests/unit/user.test.js
Normal file
420
backend/tests/unit/user.test.js
Normal file
@ -0,0 +1,420 @@
|
||||
const User = require('../../src/models/User');
|
||||
const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
const dbConnection = require('../../src/database/connection');
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('bcrypt');
|
||||
jest.mock('crypto');
|
||||
jest.mock('../../src/database/connection');
|
||||
|
||||
describe('User Model Unit Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Password Hashing', () => {
|
||||
describe('hashPassword', () => {
|
||||
it('should hash password with bcrypt', async () => {
|
||||
const password = 'testPassword123';
|
||||
const hashedPassword = 'hashed-password';
|
||||
|
||||
bcrypt.hash.mockResolvedValue(hashedPassword);
|
||||
|
||||
const result = await User.hashPassword(password);
|
||||
|
||||
expect(bcrypt.hash).toHaveBeenCalledWith(password, 12);
|
||||
expect(result).toBe(hashedPassword);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyPassword', () => {
|
||||
it('should verify password correctly', async () => {
|
||||
const password = 'testPassword123';
|
||||
const hash = 'hashed-password';
|
||||
|
||||
bcrypt.compare.mockResolvedValue(true);
|
||||
|
||||
const result = await User.verifyPassword(password, hash);
|
||||
|
||||
expect(bcrypt.compare).toHaveBeenCalledWith(password, hash);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for incorrect password', async () => {
|
||||
const password = 'wrongPassword';
|
||||
const hash = 'hashed-password';
|
||||
|
||||
bcrypt.compare.mockResolvedValue(false);
|
||||
|
||||
const result = await User.verifyPassword(password, hash);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
describe('validateEmail', () => {
|
||||
it('should validate correct email formats', () => {
|
||||
const validEmails = [
|
||||
'test@example.com',
|
||||
'user.name@domain.co.uk',
|
||||
'user+tag@example.org',
|
||||
'user123@test-domain.com'
|
||||
];
|
||||
|
||||
validEmails.forEach(email => {
|
||||
expect(User.validateEmail(email)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject invalid email formats', () => {
|
||||
const invalidEmails = [
|
||||
'invalid-email',
|
||||
'@example.com',
|
||||
'user@',
|
||||
'user@.com',
|
||||
'user..name@example.com',
|
||||
'user name@example.com'
|
||||
];
|
||||
|
||||
invalidEmails.forEach(email => {
|
||||
expect(User.validateEmail(email)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePassword', () => {
|
||||
it('should validate strong passwords', () => {
|
||||
const strongPasswords = [
|
||||
'Password123!',
|
||||
'MyStr0ng@Pass',
|
||||
'C0mplex#Password',
|
||||
'Secure123$'
|
||||
];
|
||||
|
||||
strongPasswords.forEach(password => {
|
||||
const result = User.validatePassword(password);
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject weak passwords', () => {
|
||||
const weakPasswords = [
|
||||
{ password: 'short', expectedErrors: ['Password must be at least 8 characters long', 'Password must contain at least one uppercase letter', 'Password must contain at least one number', 'Password must contain at least one special character'] },
|
||||
{ password: 'nouppercase123!', expectedErrors: ['Password must contain at least one uppercase letter'] },
|
||||
{ password: 'NOLOWERCASE123!', expectedErrors: ['Password must contain at least one lowercase letter'] },
|
||||
{ password: 'NoNumbers!', expectedErrors: ['Password must contain at least one number'] },
|
||||
{ password: 'NoSpecialChars123', expectedErrors: ['Password must contain at least one special character'] }
|
||||
];
|
||||
|
||||
weakPasswords.forEach(({ password, expectedErrors }) => {
|
||||
const result = User.validatePassword(password);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toEqual(expect.arrayContaining(expectedErrors));
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null or undefined password', () => {
|
||||
const result = User.validatePassword(null);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toContain('Password must be at least 8 characters long');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token Generation', () => {
|
||||
describe('generateToken', () => {
|
||||
it('should generate a random token', () => {
|
||||
const mockToken = 'random-hex-token';
|
||||
const mockBuffer = Buffer.from('random-bytes');
|
||||
|
||||
crypto.randomBytes.mockReturnValue(mockBuffer);
|
||||
mockBuffer.toString = jest.fn().mockReturnValue(mockToken);
|
||||
|
||||
const result = User.generateToken();
|
||||
|
||||
expect(crypto.randomBytes).toHaveBeenCalledWith(32);
|
||||
expect(mockBuffer.toString).toHaveBeenCalledWith('hex');
|
||||
expect(result).toBe(mockToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database Operations', () => {
|
||||
describe('create', () => {
|
||||
it('should create a new user successfully', async () => {
|
||||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
const mockHashedPassword = 'hashed-password';
|
||||
const mockToken = 'verification-token';
|
||||
const mockUserData = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
password_hash: mockHashedPassword,
|
||||
verification_token: mockToken
|
||||
};
|
||||
|
||||
User.findByEmail = jest.fn().mockResolvedValue(null);
|
||||
bcrypt.hash.mockResolvedValue(mockHashedPassword);
|
||||
crypto.randomBytes.mockReturnValue(Buffer.from('random'));
|
||||
Buffer.prototype.toString = jest.fn().mockReturnValue(mockToken);
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockUserData]
|
||||
});
|
||||
|
||||
const result = await User.create(userData);
|
||||
|
||||
expect(User.findByEmail).toHaveBeenCalledWith('test@example.com');
|
||||
expect(bcrypt.hash).toHaveBeenCalledWith('Password123!', 12);
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('INSERT INTO users'),
|
||||
['test@example.com', mockHashedPassword, mockToken]
|
||||
);
|
||||
expect(result).toBeInstanceOf(User);
|
||||
expect(result.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should reject invalid email', async () => {
|
||||
const userData = {
|
||||
email: 'invalid-email',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow('Invalid email format');
|
||||
});
|
||||
|
||||
it('should reject weak password', async () => {
|
||||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password: 'weak'
|
||||
};
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow('Password validation failed');
|
||||
});
|
||||
|
||||
it('should reject duplicate email', async () => {
|
||||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password: 'Password123!'
|
||||
};
|
||||
|
||||
const existingUser = new User({ id: 'existing-user', email: 'test@example.com' });
|
||||
User.findByEmail = jest.fn().mockResolvedValue(existingUser);
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow('User with this email already exists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findByEmail', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the mock implementation for each test
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should find user by email', async () => {
|
||||
const mockUserData = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com'
|
||||
};
|
||||
|
||||
// Mock the dbErrorHandler wrapper
|
||||
const { dbErrorHandler } = require('../../src/middleware/errorHandler');
|
||||
jest.mock('../../src/middleware/errorHandler', () => ({
|
||||
dbErrorHandler: jest.fn((fn) => fn())
|
||||
}));
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockUserData]
|
||||
});
|
||||
|
||||
const result = await User.findByEmail('test@example.com');
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM users WHERE email = $1',
|
||||
['test@example.com']
|
||||
);
|
||||
expect(result).toBeInstanceOf(User);
|
||||
expect(result.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should return null if user not found', async () => {
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: []
|
||||
});
|
||||
|
||||
const result = await User.findByEmail('nonexistent@example.com');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should find user by ID', async () => {
|
||||
const mockUserData = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com'
|
||||
};
|
||||
|
||||
dbConnection.query.mockResolvedValue({
|
||||
rows: [mockUserData]
|
||||
});
|
||||
|
||||
const result = await User.findById('user-123');
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
'SELECT * FROM users WHERE id = $1',
|
||||
['user-123']
|
||||
);
|
||||
expect(result).toBeInstanceOf(User);
|
||||
expect(result.id).toBe('user-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('should authenticate user with correct credentials', async () => {
|
||||
const mockUser = new User({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
password_hash: 'hashed-password'
|
||||
});
|
||||
|
||||
User.findByEmail = jest.fn().mockResolvedValue(mockUser);
|
||||
bcrypt.compare.mockResolvedValue(true);
|
||||
mockUser.updateLastLogin = jest.fn().mockResolvedValue(true);
|
||||
|
||||
const result = await User.authenticate('test@example.com', 'password123');
|
||||
|
||||
expect(User.findByEmail).toHaveBeenCalledWith('test@example.com');
|
||||
expect(bcrypt.compare).toHaveBeenCalledWith('password123', 'hashed-password');
|
||||
expect(mockUser.updateLastLogin).toHaveBeenCalled();
|
||||
expect(result).toBe(mockUser);
|
||||
});
|
||||
|
||||
it('should return null for non-existent user', async () => {
|
||||
User.findByEmail = jest.fn().mockResolvedValue(null);
|
||||
|
||||
const result = await User.authenticate('nonexistent@example.com', 'password123');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for incorrect password', async () => {
|
||||
const mockUser = new User({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
password_hash: 'hashed-password'
|
||||
});
|
||||
|
||||
User.findByEmail = jest.fn().mockResolvedValue(mockUser);
|
||||
bcrypt.compare.mockResolvedValue(false);
|
||||
|
||||
const result = await User.authenticate('test@example.com', 'wrongpassword');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Instance Methods', () => {
|
||||
describe('verifyEmail', () => {
|
||||
it('should verify user email', async () => {
|
||||
const user = new User({ id: 'user-123', is_verified: false });
|
||||
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 1 });
|
||||
|
||||
const result = await user.verifyEmail();
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE users SET is_verified = true'),
|
||||
['user-123']
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
expect(user.is_verified).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePassword', () => {
|
||||
it('should update user password', async () => {
|
||||
const user = new User({ id: 'user-123' });
|
||||
const newPassword = 'NewPassword123!';
|
||||
const hashedPassword = 'new-hashed-password';
|
||||
|
||||
bcrypt.hash.mockResolvedValue(hashedPassword);
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 1 });
|
||||
|
||||
const result = await user.updatePassword(newPassword);
|
||||
|
||||
expect(bcrypt.hash).toHaveBeenCalledWith(newPassword, 12);
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE users SET password_hash'),
|
||||
[hashedPassword, 'user-123']
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
expect(user.password_hash).toBe(hashedPassword);
|
||||
});
|
||||
|
||||
it('should reject weak password', async () => {
|
||||
const user = new User({ id: 'user-123' });
|
||||
|
||||
await expect(user.updatePassword('weak')).rejects.toThrow('Password validation failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setResetToken', () => {
|
||||
it('should set password reset token', async () => {
|
||||
const user = new User({ id: 'user-123' });
|
||||
const mockToken = 'reset-token';
|
||||
|
||||
crypto.randomBytes.mockReturnValue(Buffer.from('random'));
|
||||
Buffer.prototype.toString = jest.fn().mockReturnValue(mockToken);
|
||||
dbConnection.query.mockResolvedValue({ rowCount: 1 });
|
||||
|
||||
const result = await user.setResetToken();
|
||||
|
||||
expect(dbConnection.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE users SET reset_token'),
|
||||
[mockToken, expect.any(Date), 'user-123']
|
||||
);
|
||||
expect(result).toBe(mockToken);
|
||||
expect(user.reset_token).toBe(mockToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSafeObject', () => {
|
||||
it('should return safe user object without sensitive data', () => {
|
||||
const user = new User({
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
password_hash: 'sensitive-hash',
|
||||
verification_token: 'sensitive-token',
|
||||
reset_token: 'sensitive-reset-token',
|
||||
is_verified: true,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
last_login: new Date()
|
||||
});
|
||||
|
||||
const safeObject = user.toSafeObject();
|
||||
|
||||
expect(safeObject).toHaveProperty('id');
|
||||
expect(safeObject).toHaveProperty('email');
|
||||
expect(safeObject).toHaveProperty('is_verified');
|
||||
expect(safeObject).toHaveProperty('created_at');
|
||||
expect(safeObject).toHaveProperty('updated_at');
|
||||
expect(safeObject).toHaveProperty('last_login');
|
||||
|
||||
expect(safeObject).not.toHaveProperty('password_hash');
|
||||
expect(safeObject).not.toHaveProperty('verification_token');
|
||||
expect(safeObject).not.toHaveProperty('reset_token');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user