/** * 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. */ 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(), })); // 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' }); }, })); 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); 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 () => { // Clean up users before each test await User.destroy({ where: {}, truncate: true, cascade: true }); await AlphaInvitation.destroy({ where: {}, truncate: true, cascade: 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('Invalid credentials'); }); 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('Invalid credentials'); }); 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 5 failed login attempts for (let i = 0; i < 5; i++) { await request(app) .post('/auth/login') .send({ email: 'login@example.com', password: 'WrongPassword!', }); } // 6th 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 verificationToken; beforeEach(async () => { testUser = await createTestUser({ email: 'unverified@example.com', isVerified: false, }); await testUser.generateVerificationToken(); await testUser.reload(); verificationToken = testUser.verificationToken; }); it('should verify email with valid token', async () => { const response = await request(app) .post('/auth/verify-email') .send({ token: verificationToken }) .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 token', async () => { const response = await request(app) .post('/auth/verify-email') .send({ token: 'invalid-token' }) .expect(400); expect(response.body.code).toBe('VERIFICATION_TOKEN_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') .send({ token: verificationToken }) .expect(400); expect(response.body.code).toBe('VERIFICATION_TOKEN_INVALID'); }); }); 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'); }); }); });