1024 lines
31 KiB
JavaScript
1024 lines
31 KiB
JavaScript
const request = require('supertest');
|
|
const express = require('express');
|
|
const cookieParser = require('cookie-parser');
|
|
const jwt = require('jsonwebtoken');
|
|
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()
|
|
}
|
|
}));
|
|
|
|
// 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(),
|
|
}));
|
|
|
|
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(),
|
|
}));
|
|
|
|
jest.mock('../../../services/emailService', () => ({
|
|
sendVerificationEmail: jest.fn()
|
|
}));
|
|
|
|
const { User } = require('../../../models');
|
|
const emailService = require('../../../services/emailService');
|
|
|
|
// 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);
|
|
|
|
describe('Auth Routes', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Reset environment
|
|
process.env.JWT_SECRET = 'test-secret';
|
|
process.env.GOOGLE_CLIENT_ID = 'test-google-client-id';
|
|
process.env.NODE_ENV = 'test';
|
|
|
|
// Reset JWT mock to return different tokens for each call
|
|
let tokenCallCount = 0;
|
|
jwt.sign.mockImplementation(() => {
|
|
tokenCallCount++;
|
|
return tokenCallCount === 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); // No existing user
|
|
|
|
const newUser = {
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isVerified: false,
|
|
generateVerificationToken: jest.fn().mockResolvedValue()
|
|
};
|
|
|
|
User.create.mockResolvedValue(newUser);
|
|
emailService.sendVerificationEmail.mockResolvedValue();
|
|
|
|
const response = await request(app)
|
|
.post('/auth/register')
|
|
.send({
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
password: 'StrongPass123!',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
phone: '1234567890'
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.user).toEqual({
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isVerified: false
|
|
});
|
|
expect(response.body.verificationEmailSent).toBe(true);
|
|
expect(newUser.generateVerificationToken).toHaveBeenCalled();
|
|
expect(emailService.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({
|
|
username: 'testuser',
|
|
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 reject registration with existing username', async () => {
|
|
User.findOne.mockResolvedValue({ id: 1, username: 'testuser' });
|
|
|
|
const response = await request(app)
|
|
.post('/auth/register')
|
|
.send({
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
password: 'StrongPass123!',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Registration failed');
|
|
});
|
|
|
|
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({
|
|
username: 'testuser',
|
|
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.');
|
|
});
|
|
});
|
|
|
|
describe('POST /auth/login', () => {
|
|
it('should login user with valid credentials', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isLocked: jest.fn().mockReturnValue(false),
|
|
comparePassword: jest.fn().mockResolvedValue(true),
|
|
resetLoginAttempts: jest.fn().mockResolvedValue()
|
|
};
|
|
|
|
User.findOne.mockResolvedValue(mockUser);
|
|
jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token');
|
|
|
|
const response = await request(app)
|
|
.post('/auth/login')
|
|
.send({
|
|
email: 'test@example.com',
|
|
password: 'password123'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.user).toEqual({
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
});
|
|
expect(mockUser.resetLoginAttempts).toHaveBeenCalled();
|
|
});
|
|
|
|
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('Invalid credentials');
|
|
});
|
|
|
|
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('Invalid credentials');
|
|
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', () => {
|
|
it('should handle Google OAuth login for new user', async () => {
|
|
const mockPayload = {
|
|
sub: 'google123',
|
|
email: 'test@gmail.com',
|
|
given_name: 'Test',
|
|
family_name: 'User',
|
|
picture: 'profile.jpg'
|
|
};
|
|
|
|
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
|
getPayload: () => mockPayload
|
|
});
|
|
|
|
User.findOne
|
|
.mockResolvedValueOnce(null) // No existing Google user
|
|
.mockResolvedValueOnce(null); // No existing email user
|
|
|
|
const newUser = {
|
|
id: 1,
|
|
username: 'test_gle123',
|
|
email: 'test@gmail.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
profileImage: 'profile.jpg'
|
|
};
|
|
|
|
User.create.mockResolvedValue(newUser);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'valid-google-token'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.user).toEqual(newUser);
|
|
expect(User.create).toHaveBeenCalledWith({
|
|
email: 'test@gmail.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
authProvider: 'google',
|
|
providerId: 'google123',
|
|
profileImage: 'profile.jpg',
|
|
username: 'test_gle123'
|
|
});
|
|
});
|
|
|
|
it('should handle Google OAuth login for existing user', async () => {
|
|
const mockPayload = {
|
|
sub: 'google123',
|
|
email: 'test@gmail.com',
|
|
given_name: 'Test',
|
|
family_name: 'User'
|
|
};
|
|
|
|
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
|
getPayload: () => mockPayload
|
|
});
|
|
|
|
const existingUser = {
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@gmail.com',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
};
|
|
|
|
User.findOne.mockResolvedValue(existingUser);
|
|
jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token');
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'valid-google-token'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.user).toEqual(existingUser);
|
|
});
|
|
|
|
it('should reject when email exists with different auth provider', async () => {
|
|
const mockPayload = {
|
|
sub: 'google123',
|
|
email: 'test@example.com',
|
|
given_name: 'Test',
|
|
family_name: 'User'
|
|
};
|
|
|
|
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
|
getPayload: () => mockPayload
|
|
});
|
|
|
|
User.findOne
|
|
.mockResolvedValueOnce(null) // No Google user
|
|
.mockResolvedValueOnce({ id: 1, email: 'test@example.com' }); // Existing email user
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'valid-google-token'
|
|
});
|
|
|
|
expect(response.status).toBe(409);
|
|
expect(response.body.error).toContain('An account with this email already exists');
|
|
});
|
|
|
|
it('should reject missing ID token', async () => {
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('ID token is required');
|
|
});
|
|
|
|
it('should handle expired Google token', async () => {
|
|
const error = new Error('Token used too late');
|
|
mockGoogleClient.verifyIdToken.mockRejectedValue(error);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'expired-token'
|
|
});
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.body.error).toBe('Google token has expired. Please try again.');
|
|
});
|
|
|
|
it('should handle invalid Google token', async () => {
|
|
const error = new Error('Invalid token signature');
|
|
mockGoogleClient.verifyIdToken.mockRejectedValue(error);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'invalid-token'
|
|
});
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(response.body.error).toBe('Invalid Google token. Please try again.');
|
|
});
|
|
|
|
it('should handle malformed Google token', async () => {
|
|
const error = new Error('Wrong number of segments in token');
|
|
mockGoogleClient.verifyIdToken.mockRejectedValue(error);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'malformed.token'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Malformed Google token. Please try again.');
|
|
});
|
|
|
|
it('should handle missing required user information', async () => {
|
|
const mockPayload = {
|
|
sub: 'google123',
|
|
email: 'test@gmail.com',
|
|
// Missing given_name and family_name
|
|
};
|
|
|
|
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
|
getPayload: () => mockPayload
|
|
});
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'valid-token'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Required user information not provided by Google');
|
|
});
|
|
|
|
it('should handle unexpected Google auth errors', async () => {
|
|
const unexpectedError = new Error('Unexpected Google error');
|
|
mockGoogleClient.verifyIdToken.mockRejectedValue(unexpectedError);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
idToken: 'error-token'
|
|
});
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.error).toBe('Google authentication failed. Please try again.');
|
|
});
|
|
});
|
|
|
|
describe('POST /auth/refresh', () => {
|
|
it('should refresh access token with valid refresh token', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
};
|
|
|
|
jwt.verify.mockReturnValue({ id: 1, type: 'refresh' });
|
|
User.findByPk.mockResolvedValue(mockUser);
|
|
jwt.sign.mockReturnValue('new-access-token');
|
|
|
|
const response = await request(app)
|
|
.post('/auth/refresh')
|
|
.set('Cookie', ['refreshToken=valid-refresh-token']);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.user).toEqual(mockUser);
|
|
expect(response.headers['set-cookie']).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining('accessToken=new-access-token')
|
|
])
|
|
);
|
|
});
|
|
|
|
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, type: 'access' });
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
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');
|
|
|
|
// Check that cookies are cleared
|
|
expect(response.headers['set-cookie']).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining('accessToken=;'),
|
|
expect.stringContaining('refreshToken=;')
|
|
])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Security features', () => {
|
|
it('should set secure cookies in production', async () => {
|
|
process.env.NODE_ENV = 'prod';
|
|
|
|
User.findOne.mockResolvedValue(null);
|
|
const newUser = {
|
|
id: 1,
|
|
username: 'test',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isVerified: false,
|
|
generateVerificationToken: jest.fn().mockResolvedValue()
|
|
};
|
|
User.create.mockResolvedValue(newUser);
|
|
emailService.sendVerificationEmail.mockResolvedValue();
|
|
jwt.sign.mockReturnValueOnce('access').mockReturnValueOnce('refresh');
|
|
|
|
const response = await request(app)
|
|
.post('/auth/register')
|
|
.send({
|
|
username: 'test',
|
|
email: 'test@example.com',
|
|
password: 'Password123!',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
// In production, cookies should have secure flag
|
|
expect(response.headers['set-cookie'][0]).toContain('Secure');
|
|
});
|
|
|
|
it('should generate unique username for Google users', async () => {
|
|
const mockPayload = {
|
|
sub: 'google123456',
|
|
email: 'test@gmail.com',
|
|
given_name: 'Test',
|
|
family_name: 'User'
|
|
};
|
|
|
|
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
|
getPayload: () => mockPayload
|
|
});
|
|
|
|
User.findOne
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce(null);
|
|
|
|
User.create.mockResolvedValue({
|
|
id: 1,
|
|
username: 'test_123456',
|
|
email: 'test@gmail.com'
|
|
});
|
|
|
|
jwt.sign.mockReturnValueOnce('token').mockReturnValueOnce('refresh');
|
|
|
|
await request(app)
|
|
.post('/auth/google')
|
|
.send({ idToken: 'valid-token' });
|
|
|
|
expect(User.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
username: 'test_123456' // email prefix + last 6 chars of Google ID
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Token management', () => {
|
|
it('should generate both access and refresh tokens on registration', async () => {
|
|
User.findOne.mockResolvedValue(null);
|
|
const newUser = {
|
|
id: 1,
|
|
username: 'test',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isVerified: false,
|
|
generateVerificationToken: jest.fn().mockResolvedValue()
|
|
};
|
|
User.create.mockResolvedValue(newUser);
|
|
emailService.sendVerificationEmail.mockResolvedValue();
|
|
|
|
jwt.sign
|
|
.mockReturnValueOnce('access-token')
|
|
.mockReturnValueOnce('refresh-token');
|
|
|
|
await request(app)
|
|
.post('/auth/register')
|
|
.send({
|
|
username: 'test',
|
|
email: 'test@example.com',
|
|
password: 'Password123!',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
});
|
|
|
|
expect(jwt.sign).toHaveBeenCalledWith(
|
|
{ id: 1 },
|
|
'test-secret',
|
|
{ expiresIn: '15m' }
|
|
);
|
|
expect(jwt.sign).toHaveBeenCalledWith(
|
|
{ id: 1, type: 'refresh' },
|
|
'test-secret',
|
|
{ expiresIn: '7d' }
|
|
);
|
|
});
|
|
|
|
it('should set correct cookie options', async () => {
|
|
User.findOne.mockResolvedValue(null);
|
|
const newUser = {
|
|
id: 1,
|
|
username: 'test',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isVerified: false,
|
|
generateVerificationToken: jest.fn().mockResolvedValue()
|
|
};
|
|
User.create.mockResolvedValue(newUser);
|
|
emailService.sendVerificationEmail.mockResolvedValue();
|
|
jwt.sign.mockReturnValueOnce('access').mockReturnValueOnce('refresh');
|
|
|
|
const response = await request(app)
|
|
.post('/auth/register')
|
|
.send({
|
|
username: 'test',
|
|
email: 'test@example.com',
|
|
password: 'Password123!',
|
|
firstName: 'Test',
|
|
lastName: 'User'
|
|
});
|
|
|
|
const cookies = response.headers['set-cookie'];
|
|
expect(cookies[0]).toContain('HttpOnly');
|
|
expect(cookies[0]).toContain('SameSite=Strict');
|
|
expect(cookies[1]).toContain('HttpOnly');
|
|
expect(cookies[1]).toContain('SameSite=Strict');
|
|
});
|
|
});
|
|
|
|
describe('Email Verification', () => {
|
|
describe('Registration with verification', () => {
|
|
it('should continue registration even if verification email fails', async () => {
|
|
User.findOne.mockResolvedValue(null);
|
|
|
|
const newUser = {
|
|
id: 1,
|
|
username: 'testuser',
|
|
email: 'test@example.com',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
isVerified: false,
|
|
generateVerificationToken: jest.fn().mockResolvedValue()
|
|
};
|
|
|
|
User.create.mockResolvedValue(newUser);
|
|
emailService.sendVerificationEmail.mockRejectedValue(new Error('Email service down'));
|
|
|
|
const response = await request(app)
|
|
.post('/auth/register')
|
|
.send({
|
|
username: 'testuser',
|
|
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);
|
|
expect(newUser.generateVerificationToken).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Google OAuth auto-verification', () => {
|
|
it('should auto-verify Google OAuth users', async () => {
|
|
const mockPayload = {
|
|
sub: 'google456',
|
|
email: 'oauth@gmail.com',
|
|
given_name: 'OAuth',
|
|
family_name: 'User',
|
|
picture: 'pic.jpg'
|
|
};
|
|
|
|
mockGoogleClient.getToken.mockResolvedValue({
|
|
tokens: { id_token: 'google-id-token' }
|
|
});
|
|
|
|
mockGoogleClient.verifyIdToken.mockResolvedValue({
|
|
getPayload: () => mockPayload
|
|
});
|
|
|
|
User.findOne
|
|
.mockResolvedValueOnce(null) // No Google user
|
|
.mockResolvedValueOnce(null); // No email user
|
|
|
|
const createdUser = {
|
|
id: 1,
|
|
username: 'oauth_gle456',
|
|
email: 'oauth@gmail.com',
|
|
firstName: 'OAuth',
|
|
lastName: 'User',
|
|
profileImage: 'pic.jpg',
|
|
isVerified: true
|
|
};
|
|
|
|
User.create.mockResolvedValue(createdUser);
|
|
jwt.sign.mockReturnValueOnce('access-token').mockReturnValueOnce('refresh-token');
|
|
|
|
const response = await request(app)
|
|
.post('/auth/google')
|
|
.send({
|
|
code: 'google-auth-code'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(User.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
isVerified: true,
|
|
verifiedAt: expect.any(Date)
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('POST /auth/verify-email', () => {
|
|
it('should verify email with valid token', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
email: 'test@example.com',
|
|
verificationToken: 'valid-token-123',
|
|
verificationTokenExpiry: new Date(Date.now() + 60 * 60 * 1000),
|
|
isVerified: false,
|
|
isVerificationTokenValid: function(token) {
|
|
return this.verificationToken === token &&
|
|
new Date() < new Date(this.verificationTokenExpiry);
|
|
},
|
|
verifyEmail: jest.fn().mockResolvedValue({
|
|
id: 1,
|
|
email: 'test@example.com',
|
|
isVerified: true
|
|
})
|
|
};
|
|
|
|
User.findOne.mockResolvedValue(mockUser);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/verify-email')
|
|
.send({ token: 'valid-token-123' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.message).toBe('Email verified successfully');
|
|
expect(response.body.user).toEqual({
|
|
id: 1,
|
|
email: 'test@example.com',
|
|
isVerified: true
|
|
});
|
|
expect(mockUser.verifyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject missing token', async () => {
|
|
const response = await request(app)
|
|
.post('/auth/verify-email')
|
|
.send({});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Verification token required');
|
|
expect(response.body.code).toBe('TOKEN_REQUIRED');
|
|
});
|
|
|
|
it('should reject invalid token', async () => {
|
|
User.findOne.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/verify-email')
|
|
.send({ token: 'invalid-token' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Invalid verification token');
|
|
expect(response.body.code).toBe('VERIFICATION_TOKEN_INVALID');
|
|
});
|
|
|
|
it('should reject already verified user', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
email: 'verified@example.com',
|
|
isVerified: true
|
|
};
|
|
|
|
User.findOne.mockResolvedValue(mockUser);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/verify-email')
|
|
.send({ token: 'some-token' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Email already verified');
|
|
expect(response.body.code).toBe('ALREADY_VERIFIED');
|
|
});
|
|
|
|
it('should reject expired token', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
email: 'test@example.com',
|
|
verificationToken: 'expired-token',
|
|
verificationTokenExpiry: new Date(Date.now() - 60 * 60 * 1000),
|
|
isVerified: false,
|
|
isVerificationTokenValid: jest.fn().mockReturnValue(false)
|
|
};
|
|
|
|
User.findOne.mockResolvedValue(mockUser);
|
|
|
|
const response = await request(app)
|
|
.post('/auth/verify-email')
|
|
.send({ token: 'expired-token' });
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Verification token has expired. Please request a new one.');
|
|
expect(response.body.code).toBe('VERIFICATION_TOKEN_EXPIRED');
|
|
});
|
|
|
|
it('should handle verification errors', async () => {
|
|
User.findOne.mockRejectedValue(new Error('Database error'));
|
|
|
|
const response = await request(app)
|
|
.post('/auth/verify-email')
|
|
.send({ token: 'valid-token' });
|
|
|
|
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',
|
|
firstName: 'Test',
|
|
isVerified: false,
|
|
verificationToken: 'new-token',
|
|
generateVerificationToken: jest.fn().mockResolvedValue()
|
|
};
|
|
|
|
jwt.verify.mockReturnValue({ id: 1 });
|
|
User.findByPk.mockResolvedValue(mockUser);
|
|
emailService.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.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).toBe('Session expired. Please log in again.');
|
|
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');
|
|
expect(response.body.code).toBe('USER_NOT_FOUND');
|
|
});
|
|
|
|
it('should reject when user already verified', async () => {
|
|
const mockUser = {
|
|
id: 1,
|
|
email: 'verified@example.com',
|
|
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');
|
|
expect(response.body.code).toBe('ALREADY_VERIFIED');
|
|
});
|
|
|
|
it('should handle email service failure', 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.sendVerificationEmail.mockRejectedValue(new Error('Email service down'));
|
|
|
|
const response = await request(app)
|
|
.post('/auth/resend-verification')
|
|
.set('Cookie', ['accessToken=valid-token']);
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.error).toBe('Failed to send verification email. Please try again.');
|
|
expect(mockUser.generateVerificationToken).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
}); |