diff --git a/backend/tests/integration-setup.js b/backend/tests/integration-setup.js index c7e041f..191a5de 100644 --- a/backend/tests/integration-setup.js +++ b/backend/tests/integration-setup.js @@ -1,13 +1,30 @@ // Integration test setup -// Integration tests use a real database, so we don't mock DATABASE_URL -process.env.NODE_ENV = 'test'; +const path = require("path"); +require("dotenv").config({ path: path.join(__dirname, "..", ".env.test") }); -// Ensure JWT secrets are set for integration tests -process.env.JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || 'test-access-secret'; -process.env.JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'test-refresh-secret'; -process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret'; +process.env.NODE_ENV = "test"; -// Set other required env vars if not already set -process.env.GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || 'test-key'; -process.env.STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || 'sk_test_key'; +// Required environment variables - fail fast if missing +const requiredEnvVars = [ + "JWT_ACCESS_SECRET", + "JWT_REFRESH_SECRET", + "CSRF_SECRET", + "TOTP_ENCRYPTION_KEY", +]; + +const missingVars = requiredEnvVars.filter((v) => !process.env[v]); +if (missingVars.length > 0) { + throw new Error( + `Missing required environment variables for integration tests: ${missingVars.join(", ")}\n` + + `Please ensure these are set in your .env.test file.`, + ); +} + +// Optional variables with safe defaults +process.env.JWT_SECRET = + process.env.JWT_SECRET || process.env.JWT_ACCESS_SECRET; +process.env.EMAIL_ENABLED = "false"; +process.env.FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000"; +process.env.GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "test-key"; +process.env.STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || "sk_test_key"; diff --git a/backend/tests/integration/auth.integration.test.js b/backend/tests/integration/auth.integration.test.js index 4a774e8..436dc68 100644 --- a/backend/tests/integration/auth.integration.test.js +++ b/backend/tests/integration/auth.integration.test.js @@ -6,6 +6,17 @@ * 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'); @@ -32,6 +43,63 @@ jest.mock('../../middleware/csrf', () => ({ }, })); +// 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'); @@ -48,6 +116,14 @@ const createTestApp = () => { }); 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; }; @@ -98,9 +174,9 @@ describe('Auth Integration Tests', () => { }); beforeEach(async () => { - // Clean up users before each test - await User.destroy({ where: {}, truncate: true, cascade: true }); - await AlphaInvitation.destroy({ where: {}, truncate: true, cascade: true }); + // 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', () => {