Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ BOUNTY_AUDIT_STORE_PATH=./data/audit.json

# [OPTIONAL] Defines the environment (e.g., development, test, production).
# Default: development
# Example: NODE_ENV=development
NODE_ENV=development

# [OPTIONAL] CORS allowed origins (comma-separated). Default: http://localhost:5173
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ This project uses Vitest for testing. Tests are organized by type and located in

### Running Tests

Backend tests run with `NODE_ENV=test`. In that environment the API rate-limit
middleware is intentionally replaced with a no-op passthrough so high-volume
integration tests can exercise routes without flaky `429` responses. Production
and development keep the configured read and mutation limits active.

**Run all tests:**
```bash
npm test
Expand Down
63 changes: 63 additions & 0 deletions backend/test/rateLimit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import express from "express";
import request from "supertest";
import { afterEach, describe, expect, it, vi } from "vitest";

async function loadLimiters() {
return import("../src/utils");
}

afterEach(() => {
delete process.env.NODE_ENV;
delete process.env.RATE_LIMIT_WINDOW_MS;
delete process.env.RATE_LIMIT_READ_MAX;
delete process.env.RATE_LIMIT_MUTATION_MAX;
vi.resetModules();
});

describe("rate limiting environment behavior", () => {
it("does not return 429 for high-volume requests in NODE_ENV=test", async () => {
process.env.NODE_ENV = "test";
process.env.RATE_LIMIT_MUTATION_MAX = "1";
process.env.RATE_LIMIT_WINDOW_MS = "60000";
vi.resetModules();

const { mutationLimiter } = await loadLimiters();
const app = express();
app.post("/limited", mutationLimiter, (_req, res) => {
res.status(200).json({ ok: true });
});

for (let i = 0; i < 200; i += 1) {
await request(app).post("/limited").send({ count: i }).expect(200);
}
});

it("keeps read and mutation rate limits active in NODE_ENV=production", async () => {
process.env.NODE_ENV = "production";
process.env.RATE_LIMIT_READ_MAX = "2";
process.env.RATE_LIMIT_MUTATION_MAX = "2";
process.env.RATE_LIMIT_WINDOW_MS = "60000";
vi.resetModules();

const { mutationLimiter, readLimiter } = await loadLimiters();
const app = express();
app.use(readLimiter);
app.get("/limited", (_req, res) => {
res.status(200).json({ ok: true });
});
app.post("/limited", mutationLimiter, (_req, res) => {
res.status(200).json({ ok: true });
});

await request(app).get("/limited").expect(200);
await request(app).get("/limited").expect(200);
await request(app).get("/limited").expect(429);

await request(app).post("/limited").send({ attempt: 1 }).expect(200);
await request(app).post("/limited").send({ attempt: 2 }).expect(200);
const limited = await request(app).post("/limited").send({ attempt: 3 }).expect(429);

expect(limited.headers["retry-after"]).toBe("60");
expect(limited.body.error).toMatch(/too many requests/i);
});
});
Loading