You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Jest测试纯Node.js应用中的限流功能?

Fixing Your Rate Limiter Tests & Implementation Issues

Hey John, let's work through your rate limiter testing problems step by step—first we'll fix the empty results array, patch up the rate limiter logic gaps, then write solid tests for both your limit rules.

1. Fix the Empty Test Results Array

The root cause here is broken loop syntax: you forgot to initialize i to 0, and i + 1 doesn't actually increment the variable (use i++ instead). Here's the corrected test loop:

test('Single device exceeds request limit', async () => {
  const results = [];
  // Fixed loop initialization and increment logic
  for (let i = 0; i < uniqueRequests + 5; i++) {
    results.push(
      request(server)
        .post('/api/login')
        .send({ email: 'email@email.com', password: '555555' })
    );
  }
  // Wait for all requests to resolve and capture responses
  const responses = await Promise.all(results);
  // Check if at least one request was blocked
  const hasBlockedRequest = responses.some(res => res.statusCode === 500);
  expect(hasBlockedRequest).toBe(true);
});

2. Fix Critical Rate Limiter Logic Bugs

Before writing reliable tests, we need to fix a few flaws in your rateLimiter.js that cause incorrect behavior:

  • Window reset timing: You were incrementing counters before checking if the window expired, leading to stale counts bleeding into new windows.
  • Undefined device request check: deviceRequests was undefined on first access, so your limit check wouldn't trigger until the second request.
  • Threshold mismatch: Using > instead of >= meant limits only triggered after one extra request (e.g., 21 requests for a 20-request cap).

Here's the corrected rate limiter:

const { plainTextResponse } = require('./utils');
const users = new Map();
let frameStart = Date.now();
let requestsCounter = 0;

const parseIp = req => 
  (typeof req.headers['x-forwarded-for'] === 'string' && req.headers['x-forwarded-for'].split(',').shift()) 
  || req.connection?.remoteAddress 
  || req.socket?.remoteAddress 
  || req.connection?.socket?.remoteAddress;

async function rateLimiter(req, res, next, window, requests, uniqueRequests) {
  // First, reset window if it's expired
  if (Date.now() - frameStart > window) {
    users.clear();
    frameStart = Date.now();
    requestsCounter = 0;
  }

  const ip = parseIp(req);
  const user = req.headers['user-agent'];
  const device = `${ip}:${user}`;
  const deviceRequests = users.get(device) || 0; // Default to 0 if device is new

  // Check limits BEFORE incrementing counters
  if (requestsCounter >= requests || deviceRequests >= uniqueRequests) {
    plainTextResponse(res, 500, 'Too much requests!');
    return; // Exit early to avoid running the next handler
  }

  // Increment counters only for allowed requests
  requestsCounter += 1;
  users.set(device, deviceRequests + 1);

  // Proceed to the request handler
  next(req, res);
}

module.exports = { rateLimiter };

3. Write Comprehensive Test Cases

Now we can test both your rate limiting rules effectively:

Test 1: Single Device Exceeds Unique Request Limit

This verifies that a single IP/User-Agent combo gets blocked after hitting uniqueRequests:

test('Single device exceeds unique request limit', async () => {
  const testLimit = uniqueRequests;
  const responses = [];

  // Send testLimit + 1 requests from the same device
  for (let i = 0; i < testLimit + 1; i++) {
    responses.push(
      request(server)
        .post('/api/login')
        .send({ email: 'test@example.com', password: 'password' })
    );
  }

  await Promise.all(responses);

  // First testLimit requests should succeed (adjust status code to match your postHandler)
  const successfulRequests = responses.filter(res => res.statusCode === 200);
  expect(successfulRequests.length).toBe(testLimit);

  // The last request should be blocked
  const blockedRequest = responses.find(res => res.statusCode === 500);
  expect(blockedRequest).not.toBeUndefined();
});

Test 2: Total Requests Exceed Global Limit

To test this, we need each request to come from a unique device. We'll randomize the X-Forwarded-For header to simulate different IPs:

test('Total requests exceed global limit', async () => {
  const testLimit = requests;
  const responses = [];

  // Send testLimit + 1 requests, each with a unique IP
  for (let i = 0; i < testLimit + 1; i++) {
    responses.push(
      request(server)
        .post('/api/login')
        .set('X-Forwarded-For', `192.168.0.${i}`) // Unique IP per request
        .send({ email: 'test@example.com', password: 'password' })
    );
  }

  await Promise.all(responses);

  // First testLimit requests should succeed
  const successfulRequests = responses.filter(res => res.statusCode === 200);
  expect(successfulRequests.length).toBe(testLimit);

  // The last request should be blocked by the global limit
  const blockedRequest = responses.find(res => res.statusCode === 500);
  expect(blockedRequest).not.toBeUndefined();
});

Test 3: Rate Limiter Resets After Window Expires

This ensures limits reset once the time window passes:

test('Rate limiter resets after window expires', async () => {
  // Send enough requests to trigger the limit
  for (let i = 0; i < uniqueRequests; i++) {
    await request(server)
      .post('/api/login')
      .send({ email: 'test@example.com', password: 'password' })
      .expect(200);
  }

  // Next request should be blocked
  await request(server)
    .post('/api/login')
    .send({ email: 'test@example.com', password: 'password' })
    .expect(500);

  // Wait for the window to expire (add 100ms buffer to be safe)
  await new Promise(resolve => setTimeout(resolve, window + 100));

  // Request should succeed again after reset
  await request(server)
    .post('/api/login')
    .send({ email: 'test@example.com', password: 'password' })
    .expect(200);
});

内容的提问来源于stack exchange,提问作者John

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.01 00:22:30