/** * Authentication Integration Tests * * These tests use a real database connection to verify the complete * authentication flow including user registration, login, token management, * and password reset functionality. */ // Mock email services before importing routes jest.mock('../../services/email', () => ({ auth: { sendVerificationEmail: jest.fn().mockResolvedValue({ success: true }), sendPasswordResetEmail: jest.fn().mockResolvedValue({ success: true }), sendPasswordChangedEmail: jest.fn().mockResolvedValue({ success: true }), }, initialize: jest.fn().mockResolvedValue(), initialized: true, })); const request = require('supertest'); const express = require('express'); const cookieParser = require('cookie-parser'); const jwt = require('jsonwebtoken'); // Mock rate limiters before importing routes jest.mock('../../middleware/rateLimiter', () => ({ registerLimiter: (req, res, next) => next(), loginLimiter: (req, res, next) => next(), refreshLimiter: (req, res, next) => next(), passwordResetLimiter: (req, res, next) => next(), passwordResetRequestLimiter: (req, res, next) => next(), verifyEmailLimiter: (req, res, next) => next(), resendVerificationLimiter: (req, res, next) => next(), emailVerificationLimiter: (req, res, next) => next(), })); // Mock CSRF protection for tests jest.mock('../../middleware/csrf', () => ({ csrfProtection: (req, res, next) => next(), getCSRFToken: (req, res) => { res.set('x-csrf-token', 'test-csrf-token'); res.json({ csrfToken: 'test-csrf-token' }); }, })); // Mock sanitizeInput to avoid req.query setter issue in supertest // Keep the actual validation rules but skip DOMPurify sanitization jest.mock('../../middleware/validation', () => { const { body, validationResult } = require('express-validator'); // Validation error handler const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ error: 'Validation failed', details: errors.array().map((err) => ({ field: err.path, message: err.msg, })), }); } next(); }; // Password strength validation const passwordStrengthRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z])(?=.*[-@$!%*?&#^]).{8,}$/; return { sanitizeInput: (req, res, next) => next(), // Skip sanitization in tests validateRegistration: [ body('email').isEmail().normalizeEmail().withMessage('Please provide a valid email address'), body('password').isLength({ min: 8, max: 128 }).matches(passwordStrengthRegex).withMessage('Password does not meet requirements'), body('firstName').trim().isLength({ min: 1, max: 50 }).withMessage('First name is required'), body('lastName').trim().isLength({ min: 1, max: 50 }).withMessage('Last name is required'), handleValidationErrors, ], validateLogin: [ body('email').isEmail().normalizeEmail().withMessage('Please provide a valid email address'), body('password').notEmpty().withMessage('Password is required'), handleValidationErrors, ], validateGoogleAuth: [ body('code').notEmpty().withMessage('Authorization code is required'), handleValidationErrors, ], validateForgotPassword: [ body('email').isEmail().normalizeEmail().withMessage('Please provide a valid email address'), handleValidationErrors, ], validateResetPassword: [ body('token').notEmpty().withMessage('Token is required').isLength({ min: 64, max: 64 }).withMessage('Invalid token format'), body('newPassword').isLength({ min: 8, max: 128 }).matches(passwordStrengthRegex).withMessage('Password does not meet requirements'), handleValidationErrors, ], validateVerifyResetToken: [ body('token').notEmpty().withMessage('Token is required'), handleValidationErrors, ], }; }); const { sequelize, User, AlphaInvitation } = require('../../models'); const authRoutes = require('../../routes/auth'); // Test app setup const createTestApp = () => { const app = express(); app.use(express.json()); app.use(cookieParser()); // Add request ID middleware app.use((req, res, next) => { req.id = 'test-request-id'; next(); }); app.use('/auth', authRoutes); // Error handler for tests app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message || 'Internal Server Error', }); }); return app; }; // Test data factory const createTestUser = async (overrides = {}) => { const defaultData = { email: `test-${Date.now()}@example.com`, password: 'TestPassword123!', firstName: 'Test', lastName: 'User', isVerified: false, authProvider: 'local', }; return User.create({ ...defaultData, ...overrides }); }; const createAlphaInvitation = async (overrides = {}) => { // Generate a valid code matching pattern /^ALPHA-[A-Z0-9]{8}$/i const randomCode = Math.random().toString(36).substring(2, 10).toUpperCase().padEnd(8, 'X'); const defaultData = { code: `ALPHA-${randomCode.substring(0, 8)}`, email: `alpha-${Date.now()}@example.com`, // Email is required status: 'pending', // Valid values: pending, active, revoked }; return AlphaInvitation.create({ ...defaultData, ...overrides }); }; describe('Auth Integration Tests', () => { let app; beforeAll(async () => { // Set test environment variables process.env.NODE_ENV = 'test'; process.env.JWT_ACCESS_SECRET = 'test-access-secret'; process.env.JWT_REFRESH_SECRET = 'test-refresh-secret'; process.env.ALPHA_TESTING_ENABLED = 'false'; // Sync database await sequelize.sync({ force: true }); app = createTestApp(); }); afterAll(async () => { await sequelize.close(); }); beforeEach(async () => { // Use destroy without truncate for safer cleanup with foreign keys await User.destroy({ where: {}, force: true }); await AlphaInvitation.destroy({ where: {}, force: true }); }); describe('POST /auth/register', () => { it('should register a new user successfully', async () => { const userData = { email: 'newuser@example.com', password: 'SecurePassword123!', firstName: 'New', lastName: 'User', }; const response = await request(app) .post('/auth/register') .send(userData) .expect(201); expect(response.body.user).toBeDefined(); expect(response.body.user.email).toBe(userData.email); expect(response.body.user.firstName).toBe(userData.firstName); expect(response.body.user.isVerified).toBe(false); // Verify user was created in database const user = await User.findOne({ where: { email: userData.email } }); expect(user).not.toBeNull(); expect(user.firstName).toBe(userData.firstName); // Verify password was hashed expect(user.password).not.toBe(userData.password); // Verify cookies were set expect(response.headers['set-cookie']).toBeDefined(); const cookies = response.headers['set-cookie']; expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true); expect(cookies.some(c => c.startsWith('refreshToken='))).toBe(true); }); it('should reject registration with existing email', async () => { await createTestUser({ email: 'existing@example.com' }); const response = await request(app) .post('/auth/register') .send({ email: 'existing@example.com', password: 'SecurePassword123!', firstName: 'Another', lastName: 'User', }) .expect(400); expect(response.body.error).toBe('Registration failed'); expect(response.body.details[0].field).toBe('email'); }); it('should reject registration with invalid email format', async () => { const response = await request(app) .post('/auth/register') .send({ email: 'not-an-email', password: 'SecurePassword123!', firstName: 'Test', lastName: 'User', }) .expect(400); // Response should contain errors or error message expect(response.body.errors || response.body.error).toBeDefined(); }); it('should generate verification token on registration', async () => { const userData = { email: 'verify@example.com', password: 'SecurePassword123!', firstName: 'Verify', lastName: 'User', }; await request(app) .post('/auth/register') .send(userData) .expect(201); const user = await User.findOne({ where: { email: userData.email } }); expect(user.verificationToken).toBeDefined(); expect(user.verificationTokenExpiry).toBeDefined(); }); }); describe('POST /auth/login', () => { let testUser; beforeEach(async () => { testUser = await createTestUser({ email: 'login@example.com', password: 'TestPassword123!', isVerified: true, }); }); it('should login with valid credentials', async () => { const response = await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'TestPassword123!', }) .expect(200); expect(response.body.user).toBeDefined(); expect(response.body.user.email).toBe('login@example.com'); // Verify cookies were set const cookies = response.headers['set-cookie']; expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true); expect(cookies.some(c => c.startsWith('refreshToken='))).toBe(true); }); it('should reject login with wrong password', async () => { const response = await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'WrongPassword!', }) .expect(401); expect(response.body.error).toBe('Please check your email and password, or create an account.'); }); it('should reject login with non-existent email', async () => { const response = await request(app) .post('/auth/login') .send({ email: 'nonexistent@example.com', password: 'SomePassword123!', }) .expect(401); expect(response.body.error).toBe('Please check your email and password, or create an account.'); }); it('should increment login attempts on failed login', async () => { await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'WrongPassword!', }) .expect(401); const user = await User.findOne({ where: { email: 'login@example.com' } }); expect(user.loginAttempts).toBe(1); }); it('should lock account after too many failed attempts', async () => { // Make 10 failed login attempts (MAX_LOGIN_ATTEMPTS is 10) for (let i = 0; i < 10; i++) { await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'WrongPassword!', }); } // 11th attempt should return locked error const response = await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'TestPassword123!', // Correct password }) .expect(423); expect(response.body.error).toContain('Account is temporarily locked'); }); it('should reset login attempts on successful login', async () => { // First fail a login await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'WrongPassword!', }); // Verify attempts incremented let user = await User.findOne({ where: { email: 'login@example.com' } }); expect(user.loginAttempts).toBe(1); // Now login successfully await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'TestPassword123!', }) .expect(200); // Verify attempts reset user = await User.findOne({ where: { email: 'login@example.com' } }); expect(user.loginAttempts).toBe(0); }); }); describe('POST /auth/logout', () => { it('should clear cookies on logout', async () => { const response = await request(app) .post('/auth/logout') .expect(200); expect(response.body.message).toBe('Logged out successfully'); // Verify cookies are cleared const cookies = response.headers['set-cookie']; expect(cookies.some(c => c.includes('accessToken=;'))).toBe(true); expect(cookies.some(c => c.includes('refreshToken=;'))).toBe(true); }); }); describe('POST /auth/refresh', () => { let testUser; beforeEach(async () => { testUser = await createTestUser({ email: 'refresh@example.com', isVerified: true, }); }); it('should refresh access token with valid refresh token', async () => { // Create a valid refresh token const refreshToken = jwt.sign( { id: testUser.id, jwtVersion: testUser.jwtVersion, type: 'refresh' }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' } ); const response = await request(app) .post('/auth/refresh') .set('Cookie', [`refreshToken=${refreshToken}`]) .expect(200); expect(response.body.user).toBeDefined(); expect(response.body.user.email).toBe('refresh@example.com'); // Verify new access token cookie was set const cookies = response.headers['set-cookie']; expect(cookies.some(c => c.startsWith('accessToken='))).toBe(true); }); it('should reject refresh without token', async () => { const response = await request(app) .post('/auth/refresh') .expect(401); expect(response.body.error).toBe('Refresh token required'); }); it('should reject refresh with invalid token', async () => { const response = await request(app) .post('/auth/refresh') .set('Cookie', ['refreshToken=invalid-token']) .expect(401); expect(response.body.error).toBe('Invalid or expired refresh token'); }); it('should reject refresh with outdated JWT version', async () => { // Create refresh token with old JWT version const refreshToken = jwt.sign( { id: testUser.id, jwtVersion: testUser.jwtVersion - 1, type: 'refresh' }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' } ); const response = await request(app) .post('/auth/refresh') .set('Cookie', [`refreshToken=${refreshToken}`]) .expect(401); expect(response.body.code).toBe('JWT_VERSION_MISMATCH'); }); }); describe('GET /auth/status', () => { let testUser; beforeEach(async () => { testUser = await createTestUser({ email: 'status@example.com', isVerified: true, }); }); it('should return authenticated status with valid token', async () => { const accessToken = jwt.sign( { id: testUser.id, jwtVersion: testUser.jwtVersion }, process.env.JWT_ACCESS_SECRET, { expiresIn: '15m' } ); const response = await request(app) .get('/auth/status') .set('Cookie', [`accessToken=${accessToken}`]) .expect(200); expect(response.body.authenticated).toBe(true); expect(response.body.user.email).toBe('status@example.com'); }); it('should return unauthenticated status without token', async () => { const response = await request(app) .get('/auth/status') .expect(200); expect(response.body.authenticated).toBe(false); }); }); describe('POST /auth/verify-email', () => { let testUser; let verificationCode; let accessToken; beforeEach(async () => { testUser = await createTestUser({ email: 'unverified@example.com', isVerified: false, }); await testUser.generateVerificationToken(); await testUser.reload(); verificationCode = testUser.verificationToken; // Now a 6-digit code // Generate access token for authentication accessToken = jwt.sign( { id: testUser.id, email: testUser.email, jwtVersion: testUser.jwtVersion || 0 }, process.env.JWT_ACCESS_SECRET || 'test-access-secret', { expiresIn: '15m' } ); }); it('should verify email with valid code', async () => { const response = await request(app) .post('/auth/verify-email') .set('Cookie', `accessToken=${accessToken}`) .send({ code: verificationCode }) .expect(200); expect(response.body.message).toBe('Email verified successfully'); expect(response.body.user.isVerified).toBe(true); // Verify in database await testUser.reload(); expect(testUser.isVerified).toBe(true); expect(testUser.verificationToken).toBeNull(); }); it('should reject verification with invalid code', async () => { const response = await request(app) .post('/auth/verify-email') .set('Cookie', `accessToken=${accessToken}`) .send({ code: '000000' }) .expect(400); expect(response.body.code).toBe('VERIFICATION_INVALID'); }); it('should reject verification for already verified user', async () => { // First verify the user await testUser.verifyEmail(); const response = await request(app) .post('/auth/verify-email') .set('Cookie', `accessToken=${accessToken}`) .send({ code: verificationCode }) .expect(400); expect(response.body.code).toBe('ALREADY_VERIFIED'); }); }); describe('Password Reset Flow', () => { let testUser; beforeEach(async () => { testUser = await createTestUser({ email: 'reset@example.com', isVerified: true, authProvider: 'local', }); }); describe('POST /auth/forgot-password', () => { it('should accept valid email and generate reset token', async () => { const response = await request(app) .post('/auth/forgot-password') .send({ email: 'reset@example.com' }) .expect(200); expect(response.body.message).toContain('If an account exists'); // Verify token was generated in database await testUser.reload(); expect(testUser.passwordResetToken).toBeDefined(); expect(testUser.passwordResetTokenExpiry).toBeDefined(); }); it('should return success even for non-existent email (security)', async () => { const response = await request(app) .post('/auth/forgot-password') .send({ email: 'nonexistent@example.com' }) .expect(200); expect(response.body.message).toContain('If an account exists'); }); }); describe('POST /auth/reset-password', () => { let resetToken; beforeEach(async () => { resetToken = await testUser.generatePasswordResetToken(); }); it('should reset password with valid token', async () => { const newPassword = 'NewSecurePassword123!'; const response = await request(app) .post('/auth/reset-password') .send({ token: resetToken, newPassword }) .expect(200); expect(response.body.message).toContain('Password has been reset'); // Verify password was changed await testUser.reload(); const isValid = await testUser.comparePassword(newPassword); expect(isValid).toBe(true); // Verify token was cleared expect(testUser.passwordResetToken).toBeNull(); }); it('should reject reset with invalid token', async () => { const response = await request(app) .post('/auth/reset-password') .send({ token: 'invalid-token', newPassword: 'NewPassword123!' }) .expect(400); // Response should contain error (format may vary based on validation) expect(response.body.error || response.body.errors).toBeDefined(); }); it('should increment JWT version after password reset', async () => { const oldJwtVersion = testUser.jwtVersion; await request(app) .post('/auth/reset-password') .send({ token: resetToken, newPassword: 'NewPassword123!' }) .expect(200); await testUser.reload(); expect(testUser.jwtVersion).toBe(oldJwtVersion + 1); }); }); }); describe('CSRF Token', () => { it('should return CSRF token', async () => { const response = await request(app) .get('/auth/csrf-token') .expect(200); expect(response.headers['x-csrf-token']).toBeDefined(); }); }); describe('Alpha Testing Mode', () => { beforeEach(() => { process.env.ALPHA_TESTING_ENABLED = 'true'; }); afterEach(() => { process.env.ALPHA_TESTING_ENABLED = 'false'; }); it('should reject registration without alpha code when enabled', async () => { const response = await request(app) .post('/auth/register') .send({ email: 'alpha@example.com', password: 'SecurePassword123!', firstName: 'Alpha', lastName: 'User', }) .expect(403); expect(response.body.error).toContain('Alpha access required'); }); it('should allow registration with valid alpha code', async () => { const validCode = 'ALPHA-TEST1234'; const invitation = await createAlphaInvitation({ code: validCode, email: 'invited@example.com', // Required field }); // Cookie-parser parses JSON cookies that start with 'j:' const cookieValue = `j:${JSON.stringify({ code: validCode })}`; const response = await request(app) .post('/auth/register') .set('Cookie', [`alphaAccessCode=${cookieValue}`]) .send({ email: 'alphauser@example.com', password: 'SecurePassword123!', firstName: 'Alpha', lastName: 'User', }) .expect(201); expect(response.body.user.email).toBe('alphauser@example.com'); // Verify invitation was linked await invitation.reload(); expect(invitation.usedBy).toBeDefined(); expect(invitation.status).toBe('active'); }); }); });