more backend unit test coverage
This commit is contained in:
793
backend/tests/unit/routes/twoFactor.test.js
Normal file
793
backend/tests/unit/routes/twoFactor.test.js
Normal file
@@ -0,0 +1,793 @@
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
|
||||
// Mock dependencies before requiring routes
|
||||
jest.mock('../../../models', () => ({
|
||||
User: {
|
||||
findByPk: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/TwoFactorService', () => ({
|
||||
generateTotpSecret: jest.fn(),
|
||||
generateRecoveryCodes: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../services/email', () => ({
|
||||
auth: {
|
||||
sendTwoFactorEnabledEmail: jest.fn(),
|
||||
sendTwoFactorOtpEmail: jest.fn(),
|
||||
sendRecoveryCodeUsedEmail: jest.fn(),
|
||||
sendTwoFactorDisabledEmail: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/auth', () => ({
|
||||
authenticateToken: (req, res, next) => {
|
||||
req.user = { id: 'user-123' };
|
||||
next();
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/stepUpAuth', () => ({
|
||||
requireStepUpAuth: () => (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/csrf', () => ({
|
||||
csrfProtection: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/validation', () => ({
|
||||
sanitizeInput: (req, res, next) => next(),
|
||||
validateTotpCode: (req, res, next) => next(),
|
||||
validateEmailOtp: (req, res, next) => next(),
|
||||
validateRecoveryCode: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../middleware/rateLimiter', () => ({
|
||||
twoFactorVerificationLimiter: (req, res, next) => next(),
|
||||
twoFactorSetupLimiter: (req, res, next) => next(),
|
||||
recoveryCodeLimiter: (req, res, next) => next(),
|
||||
emailOtpSendLimiter: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/logger', () => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
}));
|
||||
|
||||
const { User } = require('../../../models');
|
||||
const TwoFactorService = require('../../../services/TwoFactorService');
|
||||
const emailServices = require('../../../services/email');
|
||||
const twoFactorRoutes = require('../../../routes/twoFactor');
|
||||
|
||||
describe('Two Factor Routes', () => {
|
||||
let app;
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use('/2fa', twoFactorRoutes);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// SETUP ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
describe('POST /2fa/setup/totp/init', () => {
|
||||
it('should initialize TOTP setup and return QR code', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
twoFactorEnabled: false,
|
||||
storePendingTotpSecret: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
TwoFactorService.generateTotpSecret.mockResolvedValue({
|
||||
qrCodeDataUrl: 'data:image/png;base64,test',
|
||||
encryptedSecret: 'encrypted-secret',
|
||||
encryptedSecretIv: 'iv-123',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/init');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.qrCodeDataUrl).toBe('data:image/png;base64,test');
|
||||
expect(response.body.message).toContain('Scan the QR code');
|
||||
expect(mockUser.storePendingTotpSecret).toHaveBeenCalledWith('encrypted-secret', 'iv-123');
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/init');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('User not found');
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA already enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/init');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain('already enabled');
|
||||
});
|
||||
|
||||
it('should handle errors during setup', async () => {
|
||||
User.findByPk.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/init');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toContain('Failed to initialize');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/setup/totp/verify', () => {
|
||||
it('should verify TOTP code and enable 2FA', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
twoFactorSetupPendingSecret: 'pending-secret',
|
||||
verifyPendingTotpCode: jest.fn().mockReturnValue(true),
|
||||
enableTotp: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
TwoFactorService.generateRecoveryCodes.mockResolvedValue({
|
||||
codes: ['XXXX-YYYY', 'AAAA-BBBB'],
|
||||
});
|
||||
emailServices.auth.sendTwoFactorEnabledEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toContain('enabled successfully');
|
||||
expect(response.body.recoveryCodes).toHaveLength(2);
|
||||
expect(response.body.warning).toContain('Save these recovery codes');
|
||||
expect(mockUser.enableTotp).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA already enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 when no pending secret', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
twoFactorSetupPendingSecret: null,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain('No pending TOTP setup');
|
||||
});
|
||||
|
||||
it('should return 400 for invalid code', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
twoFactorSetupPendingSecret: 'pending-secret',
|
||||
verifyPendingTotpCode: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain('Invalid verification code');
|
||||
});
|
||||
|
||||
it('should continue even if email fails', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
twoFactorSetupPendingSecret: 'pending-secret',
|
||||
verifyPendingTotpCode: jest.fn().mockReturnValue(true),
|
||||
enableTotp: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
TwoFactorService.generateRecoveryCodes.mockResolvedValue({ codes: ['XXXX-YYYY'] });
|
||||
emailServices.auth.sendTwoFactorEnabledEmail.mockRejectedValue(new Error('Email failed'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/totp/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/setup/email/init', () => {
|
||||
it('should send email OTP for setup', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
generateEmailOtp: jest.fn().mockResolvedValue('123456'),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendTwoFactorOtpEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/init');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toContain('Verification code sent');
|
||||
expect(emailServices.auth.sendTwoFactorOtpEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/init');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA already enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/init');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 500 when email fails', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
generateEmailOtp: jest.fn().mockResolvedValue('123456'),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendTwoFactorOtpEmail.mockRejectedValue(new Error('Email failed'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/init');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.error).toContain('Failed to send verification email');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/setup/email/verify', () => {
|
||||
it('should verify email OTP and enable email 2FA', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
isEmailOtpLocked: jest.fn().mockReturnValue(false),
|
||||
verifyEmailOtp: jest.fn().mockReturnValue(true),
|
||||
enableEmailTwoFactor: jest.fn().mockResolvedValue(),
|
||||
clearEmailOtp: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
TwoFactorService.generateRecoveryCodes.mockResolvedValue({ codes: ['XXXX-YYYY'] });
|
||||
emailServices.auth.sendTwoFactorEnabledEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.recoveryCodes).toBeDefined();
|
||||
expect(mockUser.enableEmailTwoFactor).toHaveBeenCalled();
|
||||
expect(mockUser.clearEmailOtp).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 429 when OTP locked', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
isEmailOtpLocked: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(429);
|
||||
expect(response.body.error).toContain('Too many failed attempts');
|
||||
});
|
||||
|
||||
it('should return 400 for invalid OTP', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
isEmailOtpLocked: jest.fn().mockReturnValue(false),
|
||||
verifyEmailOtp: jest.fn().mockReturnValue(false),
|
||||
incrementEmailOtpAttempts: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/setup/email/verify')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(mockUser.incrementEmailOtpAttempts).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// VERIFICATION ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
describe('POST /2fa/verify/totp', () => {
|
||||
it('should verify TOTP code for step-up auth', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'totp',
|
||||
verifyTotpCode: jest.fn().mockReturnValue(true),
|
||||
markTotpCodeUsed: jest.fn().mockResolvedValue(),
|
||||
updateStepUpSession: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/totp')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.verified).toBe(true);
|
||||
expect(mockUser.markTotpCodeUsed).toHaveBeenCalled();
|
||||
expect(mockUser.updateStepUpSession).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 when TOTP not enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/totp')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 when wrong 2FA method', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'email',
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/totp')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid code', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'totp',
|
||||
verifyTotpCode: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/totp')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/verify/email/send', () => {
|
||||
it('should send email OTP for step-up auth', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
generateEmailOtp: jest.fn().mockResolvedValue('123456'),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendTwoFactorOtpEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email/send');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toContain('Verification code sent');
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA not enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email/send');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 500 when email fails', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
generateEmailOtp: jest.fn().mockResolvedValue('123456'),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendTwoFactorOtpEmail.mockRejectedValue(new Error('Email failed'));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email/send');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/verify/email', () => {
|
||||
it('should verify email OTP for step-up auth', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
isEmailOtpLocked: jest.fn().mockReturnValue(false),
|
||||
verifyEmailOtp: jest.fn().mockReturnValue(true),
|
||||
updateStepUpSession: jest.fn().mockResolvedValue(),
|
||||
clearEmailOtp: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.verified).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA not enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 429 when locked', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
isEmailOtpLocked: jest.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(429);
|
||||
});
|
||||
|
||||
it('should return 400 and increment attempts for invalid OTP', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
isEmailOtpLocked: jest.fn().mockReturnValue(false),
|
||||
verifyEmailOtp: jest.fn().mockReturnValue(false),
|
||||
incrementEmailOtpAttempts: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/email')
|
||||
.send({ code: '123456' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(mockUser.incrementEmailOtpAttempts).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/verify/recovery', () => {
|
||||
it('should verify recovery code', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
useRecoveryCode: jest.fn().mockResolvedValue({ valid: true, remainingCodes: 5 }),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendRecoveryCodeUsedEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/recovery')
|
||||
.send({ code: 'XXXX-YYYY' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.verified).toBe(true);
|
||||
expect(response.body.remainingCodes).toBe(5);
|
||||
});
|
||||
|
||||
it('should warn when recovery codes are low', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
useRecoveryCode: jest.fn().mockResolvedValue({ valid: true, remainingCodes: 2 }),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendRecoveryCodeUsedEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/recovery')
|
||||
.send({ code: 'XXXX-YYYY' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.warning).toContain('running low');
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA not enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/recovery')
|
||||
.send({ code: 'XXXX-YYYY' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid recovery code', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
useRecoveryCode: jest.fn().mockResolvedValue({ valid: false, remainingCodes: 0 }),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/verify/recovery')
|
||||
.send({ code: 'XXXX-YYYY' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// MANAGEMENT ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
describe('GET /2fa/status', () => {
|
||||
it('should return 2FA status', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'totp',
|
||||
getRemainingRecoveryCodes: jest.fn().mockReturnValue(5),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/2fa/status');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.enabled).toBe(true);
|
||||
expect(response.body.method).toBe('totp');
|
||||
expect(response.body.hasRecoveryCodes).toBe(true);
|
||||
expect(response.body.lowRecoveryCodes).toBe(false);
|
||||
});
|
||||
|
||||
it('should return low recovery codes warning', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'totp',
|
||||
getRemainingRecoveryCodes: jest.fn().mockReturnValue(1),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/2fa/status');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.lowRecoveryCodes).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/2fa/status');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/disable', () => {
|
||||
it('should disable 2FA', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
disableTwoFactor: jest.fn().mockResolvedValue(),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
emailServices.auth.sendTwoFactorDisabledEmail.mockResolvedValue();
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/disable');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toContain('disabled');
|
||||
expect(mockUser.disableTwoFactor).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA not enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/disable');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/disable');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /2fa/recovery/regenerate', () => {
|
||||
it('should regenerate recovery codes', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: true,
|
||||
regenerateRecoveryCodes: jest.fn().mockResolvedValue(['NEW1-CODE', 'NEW2-CODE']),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/recovery/regenerate');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.recoveryCodes).toHaveLength(2);
|
||||
expect(response.body.warning).toContain('previous codes are now invalid');
|
||||
});
|
||||
|
||||
it('should return 400 when 2FA not enabled', async () => {
|
||||
User.findByPk.mockResolvedValue({
|
||||
id: 'user-123',
|
||||
twoFactorEnabled: false,
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/recovery/regenerate');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/2fa/recovery/regenerate');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /2fa/recovery/remaining', () => {
|
||||
it('should return recovery codes status', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
getRemainingRecoveryCodes: jest.fn().mockReturnValue(8),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/2fa/recovery/remaining');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.hasRecoveryCodes).toBe(true);
|
||||
expect(response.body.lowRecoveryCodes).toBe(false);
|
||||
});
|
||||
|
||||
it('should indicate when low on recovery codes', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-123',
|
||||
getRemainingRecoveryCodes: jest.fn().mockReturnValue(1),
|
||||
};
|
||||
|
||||
User.findByPk.mockResolvedValue(mockUser);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/2fa/recovery/remaining');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.lowRecoveryCodes).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 404 when user not found', async () => {
|
||||
User.findByPk.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/2fa/recovery/remaining');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user