794 lines
24 KiB
JavaScript
794 lines
24 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|