chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
import { redisTest } from "@internal/testcontainers";
|
||||
import { describe, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.setConfig({ testTimeout: 30_000 }); // 30 seconds timeout
|
||||
|
||||
// Mock the logger
|
||||
vi.mock("./logger.server", () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import type { Express } from "express";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { authorizationRateLimitMiddleware } from "../app/services/authorizationRateLimitMiddleware.server.js";
|
||||
|
||||
describe.skipIf(process.env.GITHUB_ACTIONS)("authorizationRateLimitMiddleware", () => {
|
||||
let app: Express;
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
});
|
||||
|
||||
redisTest("should allow requests within the rate limit", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 10,
|
||||
interval: "1m",
|
||||
maxTokens: 100,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
log: {
|
||||
rejections: false,
|
||||
requests: false,
|
||||
},
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => {
|
||||
res.status(200).json({ message: "Success" });
|
||||
});
|
||||
|
||||
const response = await request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ message: "Success" });
|
||||
expect(response.headers["x-ratelimit-limit"]).toBeDefined();
|
||||
expect(response.headers["x-ratelimit-remaining"]).toBeDefined();
|
||||
expect(response.headers["x-ratelimit-reset"]).toBeDefined();
|
||||
});
|
||||
|
||||
redisTest("should reject requests without an Authorization header", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 10,
|
||||
interval: "1m",
|
||||
maxTokens: 100,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => {
|
||||
res.status(200).json({ message: "Success" });
|
||||
});
|
||||
|
||||
const response = await request(app).get("/api/test");
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body).toHaveProperty("title", "Unauthorized");
|
||||
});
|
||||
|
||||
redisTest("should reject requests that exceed the rate limit", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 1,
|
||||
interval: "1m",
|
||||
maxTokens: 1,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => {
|
||||
res.status(200).json({ message: "Success" });
|
||||
});
|
||||
|
||||
// First request should succeed
|
||||
await request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
// Second request should be rate limited
|
||||
const response = await request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
expect(response.status).toBe(429);
|
||||
expect(response.body).toHaveProperty("title", "Rate Limit Exceeded");
|
||||
});
|
||||
|
||||
redisTest("should not apply rate limiting to whitelisted paths", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 10,
|
||||
interval: "1m",
|
||||
maxTokens: 100,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
pathWhiteList: ["/api/whitelist"],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/whitelist", (req, res) => {
|
||||
res.status(200).json({ message: "Whitelisted" });
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/whitelist")
|
||||
.set("Authorization", "Bearer test-token");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ message: "Whitelisted" });
|
||||
expect(response.headers["x-ratelimit-limit"]).toBeUndefined();
|
||||
});
|
||||
|
||||
redisTest(
|
||||
"should apply different rate limits based on limiterConfigOverride",
|
||||
async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 1,
|
||||
interval: "1m",
|
||||
maxTokens: 1,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
limiterConfigOverride: async (authorizationValue) => {
|
||||
if (authorizationValue === "Bearer premium-token") {
|
||||
return {
|
||||
type: "tokenBucket",
|
||||
refillRate: 10,
|
||||
interval: "1m",
|
||||
maxTokens: 100,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => {
|
||||
res.status(200).json({ message: "Success" });
|
||||
});
|
||||
|
||||
// Regular user should be rate limited after 1 request
|
||||
await request(app).get("/api/test").set("Authorization", "Bearer regular-token");
|
||||
const regularResponse = await request(app)
|
||||
.get("/api/test")
|
||||
.set("Authorization", "Bearer regular-token");
|
||||
expect(regularResponse.status).toBe(429);
|
||||
|
||||
// Premium user should be able to make multiple requests
|
||||
const premiumResponse1 = await request(app)
|
||||
.get("/api/test")
|
||||
.set("Authorization", "Bearer premium-token");
|
||||
expect(premiumResponse1.status).toBe(200);
|
||||
const premiumResponse2 = await request(app)
|
||||
.get("/api/test")
|
||||
.set("Authorization", "Bearer premium-token");
|
||||
expect(premiumResponse2.status).toBe(200);
|
||||
}
|
||||
);
|
||||
|
||||
describe("Advanced Cases", () => {
|
||||
// 1. Test different rate limit configurations
|
||||
redisTest("should enforce fixed window rate limiting", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test-fixed",
|
||||
defaultLimiter: {
|
||||
type: "fixedWindow",
|
||||
window: "10s",
|
||||
tokens: 3,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));
|
||||
|
||||
const makeRequest = () =>
|
||||
request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
// Should allow 3 requests
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await makeRequest();
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
// 4th request should be rate limited
|
||||
const limitedResponse = await makeRequest();
|
||||
expect(limitedResponse.status).toBe(429);
|
||||
|
||||
// Wait for the window to reset
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
// Should allow requests again
|
||||
const newResponse = await makeRequest();
|
||||
expect(newResponse.status).toBe(200);
|
||||
});
|
||||
|
||||
redisTest("should enforce sliding window rate limiting", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test-sliding",
|
||||
defaultLimiter: {
|
||||
type: "slidingWindow",
|
||||
window: "10s",
|
||||
tokens: 3,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));
|
||||
|
||||
const makeRequest = () =>
|
||||
request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
// Should allow 3 requests
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await makeRequest();
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
// 4th request should be rate limited
|
||||
const limitedResponse = await makeRequest();
|
||||
expect(limitedResponse.status).toBe(429);
|
||||
|
||||
// Wait for part of the window to pass
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Should still be limited
|
||||
const stillLimitedResponse = await makeRequest();
|
||||
expect(stillLimitedResponse.status).toBe(429);
|
||||
|
||||
// Wait for the full window to pass
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
// Should allow requests again
|
||||
const newResponse = await makeRequest();
|
||||
expect(newResponse.status).toBe(200);
|
||||
});
|
||||
|
||||
// 2. Test edge cases around rate limit calculations
|
||||
redisTest("should handle token refill correctly", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test-refill",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 1,
|
||||
interval: "5s",
|
||||
maxTokens: 3,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));
|
||||
|
||||
const makeRequest = () =>
|
||||
request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
// Use up all tokens
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const response = await makeRequest();
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
// Next request should be limited
|
||||
const limitedResponse = await makeRequest();
|
||||
expect(limitedResponse.status).toBe(429);
|
||||
|
||||
// Wait for one token to be refilled
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
// Should allow one request
|
||||
const newResponse = await makeRequest();
|
||||
expect(newResponse.status).toBe(200);
|
||||
|
||||
// But the next one should be limited again
|
||||
const limitedAgainResponse = await makeRequest();
|
||||
expect(limitedAgainResponse.status).toBe(429);
|
||||
});
|
||||
|
||||
redisTest("should handle near-zero remaining tokens correctly", async ({ redisOptions }) => {
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test-near-zero",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 1, // 1 token every 5 seconds
|
||||
interval: "5s",
|
||||
maxTokens: 1,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));
|
||||
|
||||
const makeRequest = () =>
|
||||
request(app).get("/api/test").set("Authorization", "Bearer test-token");
|
||||
|
||||
// First request should succeed
|
||||
const firstResponse = await makeRequest();
|
||||
expect(firstResponse.status).toBe(200);
|
||||
|
||||
// Immediate second request should fail
|
||||
const secondResponse = await makeRequest();
|
||||
expect(secondResponse.status).toBe(429);
|
||||
|
||||
// Wait for almost one token to be refilled (4.9 seconds)
|
||||
await new Promise((resolve) => setTimeout(resolve, 4900));
|
||||
|
||||
// This request should still fail as we're just shy of a full token
|
||||
const thirdResponse = await makeRequest();
|
||||
expect(thirdResponse.status).toBe(429);
|
||||
|
||||
// Wait for the full token to be refilled (additional 200ms)
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
// This request should now succeed
|
||||
const fourthResponse = await makeRequest();
|
||||
expect(fourthResponse.status).toBe(200);
|
||||
|
||||
// Immediate next request should fail again
|
||||
const fifthResponse = await makeRequest();
|
||||
expect(fifthResponse.status).toBe(429);
|
||||
});
|
||||
|
||||
// 3. Test the limiterCache functionality
|
||||
redisTest("should use cached limiter configurations", async ({ redisOptions }) => {
|
||||
let configOverrideCalls = 0;
|
||||
const rateLimitMiddleware = authorizationRateLimitMiddleware({
|
||||
redis: redisOptions,
|
||||
keyPrefix: "test-cache",
|
||||
defaultLimiter: {
|
||||
type: "tokenBucket",
|
||||
refillRate: 1,
|
||||
interval: "1m",
|
||||
maxTokens: 10,
|
||||
},
|
||||
pathMatchers: [/^\/api/],
|
||||
limiterCache: {
|
||||
fresh: 1000, // 1 second
|
||||
stale: 2000, // 2 seconds
|
||||
maxItems: 1000,
|
||||
},
|
||||
limiterConfigOverride: async (authorizationValue) => {
|
||||
configOverrideCalls++;
|
||||
if (authorizationValue === "Bearer premium-token") {
|
||||
return {
|
||||
type: "tokenBucket",
|
||||
refillRate: 10,
|
||||
interval: "1m",
|
||||
maxTokens: 100,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
app.use(rateLimitMiddleware);
|
||||
app.get("/api/test", (req, res) => res.status(200).json({ message: "Success" }));
|
||||
|
||||
const makeRequest = () =>
|
||||
request(app).get("/api/test").set("Authorization", "Bearer premium-token");
|
||||
|
||||
// First request should call the override
|
||||
await makeRequest();
|
||||
expect(configOverrideCalls).toBe(1);
|
||||
|
||||
// Subsequent requests within 1 second should use the cache
|
||||
await makeRequest();
|
||||
await makeRequest();
|
||||
expect(configOverrideCalls).toBe(1);
|
||||
|
||||
// Wait for the cache to become stale
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
// This should still use the cache, but also trigger a refresh
|
||||
await makeRequest();
|
||||
expect(configOverrideCalls).toBe(2);
|
||||
|
||||
// Wait for the cache to expire completely
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// This should trigger a new override call
|
||||
await makeRequest();
|
||||
expect(configOverrideCalls).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user