Files
rentall-app/backend/tests/unit/routes/auth.test.js
jackiettran e408880cae fixed tests
2026-01-03 21:19:23 -05:00

1185 lines
36 KiB
JavaScript

const request = require('supertest');
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { OAuth2Client } = require('google-auth-library');
// Mock dependencies
jest.mock('jsonwebtoken');
jest.mock('google-auth-library');
jest.mock('sequelize', () => ({
Op: {
or: 'or'
}
}));
jest.mock('../../../models', () => ({
User: {
findOne: jest.fn(),
create: jest.fn(),
findByPk: jest.fn()
},
AlphaInvitation: {
findOne: jest.fn()
}
}));
// Mock middleware
jest.mock('../../../middleware/validation', () => ({
sanitizeInput: (req, res, next) => next(),
validateRegistration: (req, res, next) => next(),
validateLogin: (req, res, next) => next(),
validateGoogleAuth: (req, res, next) => next(),
validateForgotPassword: (req, res, next) => next(),
validateResetPassword: (req, res, next) => next(),
validateVerifyResetToken: (req, res, next) => next(),
}));
jest.mock('../../../middleware/csrf', () => ({
csrfProtection: (req, res, next) => next(),
getCSRFToken: (req, res) => res.json({ csrfToken: 'test-csrf-token' })
}));
jest.mock('../../../middleware/rateLimiter', () => ({
loginLimiter: (req, res, next) => next(),
registerLimiter: (req, res, next) => next(),
passwordResetLimiter: (req, res, next) => next(),
emailVerificationLimiter: (req, res, next) => next(),
}));
jest.mock('../../../middleware/auth', () => ({
optionalAuth: (req, res, next) => next(),
authenticateToken: (req, res, next) => {
req.user = { id: 'user-123' };
next();
},
}));
jest.mock('../../../services/email', () => ({
auth: {
sendVerificationEmail: jest.fn().mockResolvedValue(),
sendPasswordResetEmail: jest.fn().mockResolvedValue(),
sendPasswordChangedEmail: jest.fn().mockResolvedValue(),
}
}));
jest.mock('../../../utils/logger', () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
withRequestId: jest.fn(() => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
})),
}));
const { User, AlphaInvitation } = require('../../../models');
const emailService = require('../../../services/email');
// Set up OAuth2Client mock before requiring authRoutes
const mockGoogleClient = {
verifyIdToken: jest.fn(),
getToken: jest.fn()
};
OAuth2Client.mockImplementation(() => mockGoogleClient);
const authRoutes = require('../../../routes/auth');
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use('/auth', authRoutes);
// Add error handler
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
describe('Auth Routes', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset environment
process.env.JWT_ACCESS_SECRET = 'test-access-secret';
process.env.JWT_REFRESH_SECRET = 'test-refresh-secret';
process.env.GOOGLE_CLIENT_ID = 'test-google-client-id';
process.env.NODE_ENV = 'test';
delete process.env.ALPHA_TESTING_ENABLED;
// Reset JWT mock to return different tokens for each call
let tokenCallCount = 0;
jwt.sign.mockImplementation(() => {
tokenCallCount++;
return tokenCallCount % 2 === 1 ? 'access-token' : 'refresh-token';
});
});
describe('GET /auth/csrf-token', () => {
it('should return CSRF token', async () => {
const response = await request(app)
.get('/auth/csrf-token');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('csrfToken');
expect(response.body.csrfToken).toBe('test-csrf-token');
});
});
describe('POST /auth/register', () => {
it('should register a new user successfully', async () => {
User.findOne.mockResolvedValue(null);
const newUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: false,
jwtVersion: 1,
role: 'user',
verificationToken: 'test-verification-token',
generateVerificationToken: jest.fn().mockResolvedValue()
};
User.create.mockResolvedValue(newUser);
emailService.auth.sendVerificationEmail.mockResolvedValue();
const response = await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'StrongPass123!',
firstName: 'Test',
lastName: 'User',
phone: '1234567890'
});
expect(response.status).toBe(201);
expect(response.body.user).toMatchObject({
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: false
});
expect(response.body.verificationEmailSent).toBe(true);
expect(newUser.generateVerificationToken).toHaveBeenCalled();
expect(emailService.auth.sendVerificationEmail).toHaveBeenCalledWith(newUser, newUser.verificationToken);
// Check that cookies are set
expect(response.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('accessToken'),
expect.stringContaining('refreshToken')
])
);
});
it('should reject registration with existing email', async () => {
User.findOne.mockResolvedValue({ id: 1, email: 'test@example.com' });
const response = await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'StrongPass123!',
firstName: 'Test',
lastName: 'User'
});
expect(response.status).toBe(400);
expect(response.body.error).toBe('Registration failed');
expect(response.body.details[0].message).toBe('An account with this email already exists');
});
it('should handle registration errors', async () => {
User.findOne.mockResolvedValue(null);
User.create.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'StrongPass123!',
firstName: 'Test',
lastName: 'User'
});
expect(response.status).toBe(500);
expect(response.body.error).toBe('Registration failed. Please try again.');
});
it('should continue registration even if verification email fails', async () => {
User.findOne.mockResolvedValue(null);
const newUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: false,
jwtVersion: 1,
role: 'user',
verificationToken: 'test-verification-token',
generateVerificationToken: jest.fn().mockResolvedValue()
};
User.create.mockResolvedValue(newUser);
emailService.auth.sendVerificationEmail.mockRejectedValue(new Error('Email service down'));
const response = await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'StrongPass123!',
firstName: 'Test',
lastName: 'User'
});
expect(response.status).toBe(201);
expect(response.body.user.id).toBe(1);
expect(response.body.verificationEmailSent).toBe(false);
});
});
describe('POST /auth/login', () => {
it('should login user with valid credentials', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: true,
jwtVersion: 1,
role: 'user',
isLocked: jest.fn().mockReturnValue(false),
comparePassword: jest.fn().mockResolvedValue(true),
resetLoginAttempts: jest.fn().mockResolvedValue()
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.body.user).toMatchObject({
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User'
});
expect(mockUser.resetLoginAttempts).toHaveBeenCalled();
expect(response.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('accessToken'),
expect.stringContaining('refreshToken')
])
);
});
it('should reject login with invalid email', async () => {
User.findOne.mockResolvedValue(null);
const response = await request(app)
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123'
});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Please check your email and password, or create an account.');
});
it('should reject login with invalid password', async () => {
const mockUser = {
id: 1,
isLocked: jest.fn().mockReturnValue(false),
comparePassword: jest.fn().mockResolvedValue(false),
incLoginAttempts: jest.fn().mockResolvedValue()
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword'
});
expect(response.status).toBe(401);
expect(response.body.error).toBe('Please check your email and password, or create an account.');
expect(mockUser.incLoginAttempts).toHaveBeenCalled();
});
it('should reject login for locked account', async () => {
const mockUser = {
id: 1,
isLocked: jest.fn().mockReturnValue(true)
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(423);
expect(response.body.error).toContain('Account is temporarily locked');
});
it('should handle login errors', async () => {
User.findOne.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(500);
expect(response.body.error).toBe('Login failed. Please try again.');
});
});
describe('POST /auth/google', () => {
const mockGooglePayload = {
sub: 'google123',
email: 'test@gmail.com',
given_name: 'Test',
family_name: 'User',
picture: 'https://example.com/profile.jpg'
};
beforeEach(() => {
mockGoogleClient.getToken.mockResolvedValue({
tokens: { id_token: 'mock-id-token' }
});
mockGoogleClient.verifyIdToken.mockResolvedValue({
getPayload: () => mockGooglePayload
});
});
it('should handle Google OAuth login for new user', async () => {
User.findOne
.mockResolvedValueOnce(null) // No existing Google user
.mockResolvedValueOnce(null); // No existing email user
const newUser = {
id: 1,
email: 'test@gmail.com',
firstName: 'Test',
lastName: 'User',
imageFilename: 'https://example.com/profile.jpg',
isVerified: true,
jwtVersion: 1,
role: 'user'
};
User.create.mockResolvedValue(newUser);
const response = await request(app)
.post('/auth/google')
.send({ code: 'valid-auth-code' });
expect(response.status).toBe(200);
expect(response.body.user).toMatchObject({
id: 1,
email: 'test@gmail.com',
firstName: 'Test',
lastName: 'User'
});
expect(User.create).toHaveBeenCalledWith(expect.objectContaining({
email: 'test@gmail.com',
firstName: 'Test',
lastName: 'User',
authProvider: 'google',
providerId: 'google123',
isVerified: true
}));
});
it('should handle Google OAuth login for existing user', async () => {
const existingUser = {
id: 1,
email: 'test@gmail.com',
firstName: 'Test',
lastName: 'User',
isVerified: true,
jwtVersion: 1,
role: 'user'
};
User.findOne.mockResolvedValue(existingUser);
const response = await request(app)
.post('/auth/google')
.send({ code: 'valid-auth-code' });
expect(response.status).toBe(200);
expect(response.body.user).toMatchObject({
id: 1,
email: 'test@gmail.com'
});
});
it('should reject when email exists with different auth provider', async () => {
User.findOne
.mockResolvedValueOnce(null) // No Google user
.mockResolvedValueOnce({ id: 1, email: 'test@gmail.com', authProvider: 'local' }); // Existing email user
const response = await request(app)
.post('/auth/google')
.send({ code: 'valid-auth-code' });
expect(response.status).toBe(409);
expect(response.body.error).toContain('An account with this email already exists');
});
it('should reject missing authorization code', async () => {
const response = await request(app)
.post('/auth/google')
.send({});
expect(response.status).toBe(400);
expect(response.body.error).toBe('Authorization code is required');
});
it('should handle invalid authorization code', async () => {
mockGoogleClient.getToken.mockRejectedValue(new Error('invalid_grant'));
const response = await request(app)
.post('/auth/google')
.send({ code: 'invalid-code' });
expect(response.status).toBe(401);
expect(response.body.error).toContain('Invalid or expired authorization code');
});
it('should handle redirect URI mismatch', async () => {
mockGoogleClient.getToken.mockRejectedValue(new Error('redirect_uri_mismatch'));
const response = await request(app)
.post('/auth/google')
.send({ code: 'some-code' });
expect(response.status).toBe(400);
expect(response.body.error).toContain('Redirect URI mismatch');
});
it('should handle missing email permission', async () => {
mockGoogleClient.verifyIdToken.mockResolvedValue({
getPayload: () => ({ sub: 'google123' }) // No email
});
const response = await request(app)
.post('/auth/google')
.send({ code: 'valid-auth-code' });
expect(response.status).toBe(400);
expect(response.body.error).toContain('Email permission is required');
});
it('should generate fallback name from email when not provided', async () => {
mockGoogleClient.verifyIdToken.mockResolvedValue({
getPayload: () => ({
sub: 'google123',
email: 'john.doe@gmail.com'
// No given_name or family_name
})
});
User.findOne
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null);
const newUser = {
id: 1,
email: 'john.doe@gmail.com',
firstName: 'John',
lastName: 'Doe',
isVerified: true,
jwtVersion: 1,
role: 'user'
};
User.create.mockResolvedValue(newUser);
const response = await request(app)
.post('/auth/google')
.send({ code: 'valid-auth-code' });
expect(response.status).toBe(200);
expect(User.create).toHaveBeenCalledWith(expect.objectContaining({
firstName: 'John',
lastName: 'Doe'
}));
});
it('should handle Google auth errors gracefully', async () => {
mockGoogleClient.getToken.mockRejectedValue(new Error('Unknown error'));
const response = await request(app)
.post('/auth/google')
.send({ code: 'some-code' });
expect(response.status).toBe(500);
expect(response.body.error).toBe('Google authentication failed. Please try again.');
});
});
describe('POST /auth/verify-email', () => {
it('should verify email with valid 6-digit code', async () => {
const mockUser = {
id: 'user-123',
email: 'test@example.com',
isVerified: false,
verificationToken: '123456',
verificationTokenExpiry: new Date(Date.now() + 3600000), // 1 hour from now
verificationAttempts: 0,
isVerificationLocked: jest.fn().mockReturnValue(false),
isVerificationTokenValid: jest.fn().mockReturnValue(true),
verifyEmail: jest.fn().mockResolvedValue()
};
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(200);
expect(response.body.message).toBe('Email verified successfully');
expect(response.body.user).toMatchObject({
id: 'user-123',
email: 'test@example.com',
isVerified: true
});
expect(mockUser.verifyEmail).toHaveBeenCalled();
});
it('should reject missing code', async () => {
const response = await request(app)
.post('/auth/verify-email')
.send({});
expect(response.status).toBe(400);
expect(response.body.error).toBe('Verification code required');
expect(response.body.code).toBe('CODE_REQUIRED');
});
it('should reject invalid code format (not 6 digits)', async () => {
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '12345' }); // Only 5 digits
expect(response.status).toBe(400);
expect(response.body.error).toBe('Verification code must be 6 digits');
expect(response.body.code).toBe('INVALID_CODE_FORMAT');
});
it('should reject when user not found', async () => {
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(404);
expect(response.body.error).toBe('User not found');
expect(response.body.code).toBe('USER_NOT_FOUND');
});
it('should reject already verified user', async () => {
const mockUser = {
id: 'user-123',
isVerified: true
};
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(400);
expect(response.body.error).toBe('Email already verified');
expect(response.body.code).toBe('ALREADY_VERIFIED');
});
it('should reject when too many verification attempts', async () => {
const mockUser = {
id: 'user-123',
isVerified: false,
isVerificationLocked: jest.fn().mockReturnValue(true)
};
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(429);
expect(response.body.error).toContain('Too many verification attempts');
expect(response.body.code).toBe('TOO_MANY_ATTEMPTS');
});
it('should reject when no verification code exists', async () => {
const mockUser = {
id: 'user-123',
isVerified: false,
verificationToken: null,
isVerificationLocked: jest.fn().mockReturnValue(false)
};
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(400);
expect(response.body.error).toContain('No verification code found');
expect(response.body.code).toBe('NO_CODE');
});
it('should reject expired verification code', async () => {
const mockUser = {
id: 'user-123',
isVerified: false,
verificationToken: '123456',
verificationTokenExpiry: new Date(Date.now() - 3600000), // 1 hour ago (expired)
isVerificationLocked: jest.fn().mockReturnValue(false)
};
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(400);
expect(response.body.error).toContain('expired');
expect(response.body.code).toBe('VERIFICATION_EXPIRED');
});
it('should handle verification errors', async () => {
User.findByPk.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/auth/verify-email')
.send({ code: '123456' });
expect(response.status).toBe(500);
expect(response.body.error).toBe('Email verification failed. Please try again.');
});
});
describe('POST /auth/resend-verification', () => {
it('should resend verification email for authenticated unverified user', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
isVerified: false,
verificationToken: 'new-token',
generateVerificationToken: jest.fn().mockResolvedValue()
};
jwt.verify.mockReturnValue({ id: 1 });
User.findByPk.mockResolvedValue(mockUser);
emailService.auth.sendVerificationEmail.mockResolvedValue();
const response = await request(app)
.post('/auth/resend-verification')
.set('Cookie', ['accessToken=valid-token']);
expect(response.status).toBe(200);
expect(response.body.message).toBe('Verification email sent successfully');
expect(mockUser.generateVerificationToken).toHaveBeenCalled();
expect(emailService.auth.sendVerificationEmail).toHaveBeenCalledWith(mockUser, mockUser.verificationToken);
});
it('should reject when no access token provided', async () => {
const response = await request(app)
.post('/auth/resend-verification');
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required');
expect(response.body.code).toBe('NO_TOKEN');
});
it('should reject expired access token', async () => {
const error = new Error('jwt expired');
error.name = 'TokenExpiredError';
jwt.verify.mockImplementation(() => { throw error; });
const response = await request(app)
.post('/auth/resend-verification')
.set('Cookie', ['accessToken=expired-token']);
expect(response.status).toBe(401);
expect(response.body.error).toContain('Session expired');
expect(response.body.code).toBe('TOKEN_EXPIRED');
});
it('should reject when user not found', async () => {
jwt.verify.mockReturnValue({ id: 999 });
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/auth/resend-verification')
.set('Cookie', ['accessToken=valid-token']);
expect(response.status).toBe(404);
expect(response.body.error).toBe('User not found');
});
it('should reject when user already verified', async () => {
const mockUser = {
id: 1,
isVerified: true
};
jwt.verify.mockReturnValue({ id: 1 });
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/resend-verification')
.set('Cookie', ['accessToken=valid-token']);
expect(response.status).toBe(400);
expect(response.body.error).toBe('Email already verified');
});
it('should handle email service failure', async () => {
const mockUser = {
id: 1,
isVerified: false,
generateVerificationToken: jest.fn().mockResolvedValue()
};
jwt.verify.mockReturnValue({ id: 1 });
User.findByPk.mockResolvedValue(mockUser);
emailService.auth.sendVerificationEmail.mockRejectedValue(new Error('Email failed'));
const response = await request(app)
.post('/auth/resend-verification')
.set('Cookie', ['accessToken=valid-token']);
expect(response.status).toBe(500);
expect(response.body.error).toContain('Failed to send verification email');
});
});
describe('POST /auth/refresh', () => {
it('should refresh access token with valid refresh token', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: true,
jwtVersion: 1,
role: 'user'
};
jwt.verify.mockReturnValue({ id: 1, type: 'refresh', jwtVersion: 1 });
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/refresh')
.set('Cookie', ['refreshToken=valid-refresh-token']);
expect(response.status).toBe(200);
expect(response.body.user).toMatchObject({
id: 1,
email: 'test@example.com'
});
expect(response.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('accessToken')
])
);
});
it('should reject missing refresh token', async () => {
const response = await request(app)
.post('/auth/refresh');
expect(response.status).toBe(401);
expect(response.body.error).toBe('Refresh token required');
});
it('should reject invalid refresh token', async () => {
jwt.verify.mockImplementation(() => { throw new Error('Invalid token'); });
const response = await request(app)
.post('/auth/refresh')
.set('Cookie', ['refreshToken=invalid-token']);
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid or expired refresh token');
});
it('should reject non-refresh token type', async () => {
jwt.verify.mockReturnValue({ id: 1 }); // Missing type: 'refresh'
const response = await request(app)
.post('/auth/refresh')
.set('Cookie', ['refreshToken=access-token']);
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid refresh token');
});
it('should reject refresh token for non-existent user', async () => {
jwt.verify.mockReturnValue({ id: 999, type: 'refresh' });
User.findByPk.mockResolvedValue(null);
const response = await request(app)
.post('/auth/refresh')
.set('Cookie', ['refreshToken=valid-token']);
expect(response.status).toBe(401);
expect(response.body.error).toBe('User not found');
});
it('should reject token with mismatched jwtVersion', async () => {
const mockUser = {
id: 1,
jwtVersion: 2 // Different from token's jwtVersion
};
jwt.verify.mockReturnValue({ id: 1, type: 'refresh', jwtVersion: 1 });
User.findByPk.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/refresh')
.set('Cookie', ['refreshToken=old-token']);
expect(response.status).toBe(401);
expect(response.body.error).toContain('password change');
expect(response.body.code).toBe('JWT_VERSION_MISMATCH');
});
});
describe('POST /auth/logout', () => {
it('should logout user and clear cookies', async () => {
const response = await request(app)
.post('/auth/logout');
expect(response.status).toBe(200);
expect(response.body.message).toBe('Logged out successfully');
expect(response.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('accessToken=;'),
expect.stringContaining('refreshToken=;')
])
);
});
});
describe('GET /auth/status', () => {
it('should return authenticated true when user is logged in', async () => {
// The optionalAuth middleware sets req.user if authenticated
// We need to modify the mock for this specific test
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: true
};
// Create a custom app for this test with user set
const statusApp = express();
statusApp.use(express.json());
statusApp.use((req, res, next) => {
req.user = mockUser;
next();
});
statusApp.use('/auth', authRoutes);
const response = await request(statusApp)
.get('/auth/status');
expect(response.status).toBe(200);
expect(response.body.authenticated).toBe(true);
expect(response.body.user).toMatchObject({
id: 1,
email: 'test@example.com'
});
});
it('should return authenticated false when user is not logged in', async () => {
const response = await request(app)
.get('/auth/status');
expect(response.status).toBe(200);
expect(response.body.authenticated).toBe(false);
expect(response.body.user).toBeUndefined();
});
});
describe('POST /auth/forgot-password', () => {
it('should send password reset email for existing user', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
authProvider: 'local',
generatePasswordResetToken: jest.fn().mockResolvedValue('reset-token')
};
User.findOne.mockResolvedValue(mockUser);
emailService.auth.sendPasswordResetEmail.mockResolvedValue();
const response = await request(app)
.post('/auth/forgot-password')
.send({ email: 'test@example.com' });
expect(response.status).toBe(200);
expect(response.body.message).toContain('If an account exists');
expect(mockUser.generatePasswordResetToken).toHaveBeenCalled();
expect(emailService.auth.sendPasswordResetEmail).toHaveBeenCalledWith(mockUser, 'reset-token');
});
it('should return success even for non-existent email (security)', async () => {
User.findOne.mockResolvedValue(null);
const response = await request(app)
.post('/auth/forgot-password')
.send({ email: 'nonexistent@example.com' });
expect(response.status).toBe(200);
expect(response.body.message).toContain('If an account exists');
});
it('should return success for OAuth user (security)', async () => {
User.findOne.mockResolvedValue(null); // Query for local provider returns null
const response = await request(app)
.post('/auth/forgot-password')
.send({ email: 'google@example.com' });
expect(response.status).toBe(200);
expect(response.body.message).toContain('If an account exists');
});
it('should handle errors gracefully', async () => {
User.findOne.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/auth/forgot-password')
.send({ email: 'test@example.com' });
expect(response.status).toBe(500);
expect(response.body.error).toContain('Failed to process password reset');
});
});
describe('POST /auth/verify-reset-token', () => {
it('should verify valid reset token', async () => {
const mockUser = {
id: 1,
passwordResetToken: crypto.createHash('sha256').update('valid-token').digest('hex'),
isPasswordResetTokenValid: jest.fn().mockReturnValue(true)
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-reset-token')
.send({ token: 'valid-token' });
expect(response.status).toBe(200);
expect(response.body.valid).toBe(true);
});
it('should reject invalid reset token', async () => {
User.findOne.mockResolvedValue(null);
const response = await request(app)
.post('/auth/verify-reset-token')
.send({ token: 'invalid-token' });
expect(response.status).toBe(400);
expect(response.body.valid).toBe(false);
expect(response.body.code).toBe('TOKEN_INVALID');
});
it('should reject expired reset token', async () => {
const mockUser = {
id: 1,
isPasswordResetTokenValid: jest.fn().mockReturnValue(false)
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/verify-reset-token')
.send({ token: 'expired-token' });
expect(response.status).toBe(400);
expect(response.body.valid).toBe(false);
expect(response.body.code).toBe('TOKEN_EXPIRED');
});
});
describe('POST /auth/reset-password', () => {
it('should reset password with valid token', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
passwordResetToken: crypto.createHash('sha256').update('valid-token').digest('hex'),
isPasswordResetTokenValid: jest.fn().mockReturnValue(true),
resetPassword: jest.fn().mockResolvedValue()
};
User.findOne.mockResolvedValue(mockUser);
emailService.auth.sendPasswordChangedEmail.mockResolvedValue();
const response = await request(app)
.post('/auth/reset-password')
.send({
token: 'valid-token',
newPassword: 'NewStrongPass123!'
});
expect(response.status).toBe(200);
expect(response.body.message).toContain('Password has been reset successfully');
expect(mockUser.resetPassword).toHaveBeenCalledWith('NewStrongPass123!');
expect(emailService.auth.sendPasswordChangedEmail).toHaveBeenCalledWith(mockUser);
});
it('should reject invalid reset token', async () => {
User.findOne.mockResolvedValue(null);
const response = await request(app)
.post('/auth/reset-password')
.send({
token: 'invalid-token',
newPassword: 'NewStrongPass123!'
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('Invalid or expired reset token');
expect(response.body.code).toBe('TOKEN_INVALID');
});
it('should reject expired reset token', async () => {
const mockUser = {
id: 1,
isPasswordResetTokenValid: jest.fn().mockReturnValue(false)
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/reset-password')
.send({
token: 'expired-token',
newPassword: 'NewStrongPass123!'
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('expired');
expect(response.body.code).toBe('TOKEN_EXPIRED');
});
it('should handle reset password errors', async () => {
User.findOne.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.post('/auth/reset-password')
.send({
token: 'some-token',
newPassword: 'NewStrongPass123!'
});
expect(response.status).toBe(500);
expect(response.body.error).toContain('Failed to reset password');
});
});
describe('Cookie settings', () => {
it('should set secure cookies in production', async () => {
process.env.NODE_ENV = 'prod';
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: true,
jwtVersion: 1,
role: 'user',
isLocked: jest.fn().mockReturnValue(false),
comparePassword: jest.fn().mockResolvedValue(true),
resetLoginAttempts: jest.fn().mockResolvedValue()
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringMatching(/accessToken=.*Secure/i),
expect.stringMatching(/refreshToken=.*Secure/i)
])
);
});
it('should set httpOnly cookies', async () => {
const mockUser = {
id: 1,
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
isVerified: true,
jwtVersion: 1,
role: 'user',
isLocked: jest.fn().mockReturnValue(false),
comparePassword: jest.fn().mockResolvedValue(true),
resetLoginAttempts: jest.fn().mockResolvedValue()
};
User.findOne.mockResolvedValue(mockUser);
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
});
expect(response.status).toBe(200);
expect(response.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('HttpOnly'),
expect.stringContaining('HttpOnly')
])
);
});
});
});