backend unit tests

This commit is contained in:
jackiettran
2025-09-19 19:46:41 -04:00
parent cf6dd9be90
commit 649289bf90
28 changed files with 17266 additions and 57 deletions

View File

@@ -0,0 +1,682 @@
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(),
}));
const { User } = require('../../../models');
// Set up OAuth2Client mock before requiring authRoutes
const mockGoogleClient = {
verifyIdToken: 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'
};
User.create.mockResolvedValue(newUser);
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'
});
// 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' };
User.create.mockResolvedValue(newUser);
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);
User.create.mockResolvedValue({ id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User' });
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);
User.create.mockResolvedValue({ id: 1, username: 'test', email: 'test@example.com', firstName: 'Test', lastName: 'User' });
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');
});
});
});