LibreChat/api/server/routes/auth.reset-password-ratelimit.test.js
Danny Avila b15d40e3e4
🪣 refactor: Rate-Limit Token Routes and Cap Remote File Downloads (#13978)
* harden token and remote file handling

* sort s3 storage imports

* split token submission rate limits
2026-06-26 12:19:03 -04:00

121 lines
4.6 KiB
JavaScript

const express = require('express');
const request = require('supertest');
const mockCheckBan = jest.fn((req, res, next) => next());
const mockResetPasswordSubmissionLimiter = jest.fn((req, res, next) => next());
const mockValidatePasswordReset = jest.fn((req, res, next) => next());
const mockResetPasswordController = jest.fn((req, res) => res.status(204).end());
jest.mock('@librechat/api', () => ({
createSetBalanceConfig: jest.fn(() => (req, res, next) => next()),
forceRefreshCloudFrontAuthCookies: jest.fn(),
}));
jest.mock('~/server/controllers/AuthController', () => ({
refreshController: jest.fn((req, res) => res.status(204).end()),
registrationController: jest.fn((req, res) => res.status(204).end()),
resetPasswordController: (...args) => mockResetPasswordController(...args),
resetPasswordRequestController: jest.fn((req, res) => res.status(204).end()),
graphTokenController: jest.fn((req, res) => res.status(204).end()),
}));
jest.mock('~/server/controllers/TwoFactorController', () => ({
enable2FA: jest.fn((req, res) => res.status(204).end()),
verify2FA: jest.fn((req, res) => res.status(204).end()),
confirm2FA: jest.fn((req, res) => res.status(204).end()),
disable2FA: jest.fn((req, res) => res.status(204).end()),
regenerateBackupCodes: jest.fn((req, res) => res.status(204).end()),
}));
jest.mock('~/server/controllers/auth/TwoFactorAuthController', () => ({
verify2FAWithTempToken: jest.fn((req, res) => res.status(204).end()),
}));
jest.mock('~/server/controllers/auth/LogoutController', () => ({
logoutController: jest.fn((req, res) => res.status(204).end()),
}));
jest.mock('~/server/controllers/auth/LoginController', () => ({
loginController: jest.fn((req, res) => res.status(204).end()),
}));
jest.mock('~/models', () => ({
findBalanceByUser: jest.fn(),
upsertBalanceFields: jest.fn(),
}));
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn(),
}));
jest.mock('~/server/middleware', () => {
const pass = (req, res, next) => next();
return {
logHeaders: pass,
loginLimiter: pass,
setTwoFactorTempUser: pass,
twoFactorTempLimiter: pass,
checkBan: (...args) => mockCheckBan(...args),
requireLocalAuth: pass,
requireLdapAuth: pass,
registerLimiter: pass,
checkInviteUser: pass,
validateRegistration: pass,
resetPasswordLimiter: pass,
resetPasswordSubmissionLimiter: (...args) => mockResetPasswordSubmissionLimiter(...args),
validatePasswordReset: (...args) => mockValidatePasswordReset(...args),
requireJwtAuth: pass,
};
});
const authRouter = require('./auth');
describe('POST /api/auth/resetPassword rate limiting', () => {
let app;
beforeEach(() => {
jest.clearAllMocks();
mockCheckBan.mockImplementation((req, res, next) => next());
mockResetPasswordSubmissionLimiter.mockImplementation((req, res, next) => next());
mockValidatePasswordReset.mockImplementation((req, res, next) => next());
mockResetPasswordController.mockImplementation((req, res) => res.status(204).end());
app = express();
app.use(express.json());
app.use('/api/auth', authRouter);
});
it('limits before ban checks, validation, and password reset work', async () => {
await request(app).post('/api/auth/resetPassword').send({ token: 'token' }).expect(204);
expect(mockResetPasswordSubmissionLimiter).toHaveBeenCalledTimes(1);
expect(mockCheckBan).toHaveBeenCalledTimes(1);
expect(mockValidatePasswordReset).toHaveBeenCalledTimes(1);
expect(mockResetPasswordController).toHaveBeenCalledTimes(1);
expect(mockResetPasswordSubmissionLimiter.mock.invocationCallOrder[0]).toBeLessThan(
mockCheckBan.mock.invocationCallOrder[0],
);
expect(mockCheckBan.mock.invocationCallOrder[0]).toBeLessThan(
mockValidatePasswordReset.mock.invocationCallOrder[0],
);
expect(mockValidatePasswordReset.mock.invocationCallOrder[0]).toBeLessThan(
mockResetPasswordController.mock.invocationCallOrder[0],
);
});
it('does not validate or reset passwords after the limiter rejects the request', async () => {
mockResetPasswordSubmissionLimiter.mockImplementation((req, res) =>
res.status(429).json({ message: 'Too many password reset attempts' }),
);
const response = await request(app)
.post('/api/auth/resetPassword')
.send({ token: 'token' })
.expect(429);
expect(response.body).toEqual({ message: 'Too many password reset attempts' });
expect(mockCheckBan).not.toHaveBeenCalled();
expect(mockValidatePasswordReset).not.toHaveBeenCalled();
expect(mockResetPasswordController).not.toHaveBeenCalled();
});
});