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
61 changes: 61 additions & 0 deletions tests/schema/__snapshots__/rate-limit.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`GET /api/rate-limit — admin listing snapshot should maintain a stable forbidden error shape when unauthenticated 1`] = `
{
"error": {
"code": "forbidden",
},
}
`;

exports[`GET /api/rate-limit — admin listing snapshot should maintain a stable response shape for a successful paginated response 1`] = `
{
"data": [
{
"action": "rate_limit.blocked",
"correlationId": "corr-1",
"createdAt": "2026-07-28T10:00:00.000Z",
"id": "log-1",
"ip": "192.168.1.1",
"rateLimitContext": null,
"walletAddress": "GAHK7EYR7AQ5B56K2RRYUWWC7EJ5CWWWURC2Q4GQRHBDQY7ZLMQVB6TF",
},
],
"nextCursor": "eyJzb3J0VmFsdWUiOiIyMDI2LTA3LTI4VDEwOjAwOjAwLjAwMFoifQ==",
}
`;

exports[`GET /api/rate-limit — admin listing snapshot should maintain a stable validation error shape 1`] = `
{
"error": {
"code": "validation_error",
"message": "limit must be a positive integer",
"requestId": "test-req-id",
},
}
`;

exports[`GET /api/rate-limit/status — public status snapshot should maintain a stable anonymous status shape with zero usage 1`] = `
{
"data": {
"clientIp": "127.0.0.1",
"limit": 60,
"remaining": 60,
"resetAt": Any<String>,
"type": "anonymous",
"used": 0,
"windowMs": 60000,
},
}
`;

exports[`GET /api/rate-limit/status — public status snapshot should maintain a stable authenticated status shape 1`] = `
{
"data": {
"bypasses": true,
"limit": 60,
"type": "authenticated",
"windowMs": 60000,
},
}
`;
168 changes: 168 additions & 0 deletions tests/schema/rate-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import express from "express";
import request from "supertest";
import { rateLimitRouter } from "../../src/routes/rate-limit";
import { requestContextStorage } from "../../src/lib/requestContext";
import { anonRateLimitStore, createRateLimitAnon } from "../../src/middleware/rateLimitAnon";

jest.mock("../../src/middleware/requireAdmin", () => ({
requireAdmin: jest.fn((_req: any, _res: any, next: any) => {
next();
}),
}));

jest.mock("../../src/repositories/auditLogRepo");

jest.mock("../../src/config/logger", () => ({
logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
}));

jest.mock("../../src/metrics/registry", () => {
const actual = jest.requireActual("../../src/metrics/registry");
return {
...actual,
rateLimitRequestDuration: {
observe: jest.fn(),
},
};
});

import { getAuditLogs } from "../../src/repositories/auditLogRepo";

const mockGetAuditLogs = getAuditLogs as jest.MockedFunction<typeof getAuditLogs>;

function makeApp(): express.Express {
const app = express();
app.use(express.json());
app.use((_req, _res, next) => {
requestContextStorage.run({ requestId: "test-req-id" }, next);
});
app.use("/api/rate-limit", rateLimitRouter);
return app;
}

const app = makeApp();

beforeEach(() => {
jest.clearAllMocks();
anonRateLimitStore.clear();
});

describe("GET /api/rate-limit — admin listing snapshot", () => {
it("should maintain a stable response shape for a successful paginated response", async () => {
mockGetAuditLogs.mockResolvedValueOnce({
data: [
{
id: "log-1",
action: "rate_limit.blocked",
walletAddress: "GAHK7EYR7AQ5B56K2RRYUWWC7EJ5CWWWURC2Q4GQRHBDQY7ZLMQVB6TF",
ip: "192.168.1.1",
correlationId: "corr-1",
rateLimitContext: null,
createdAt: new Date("2026-07-28T10:00:00.000Z"),
},
],
nextCursor: "eyJzb3J0VmFsdWUiOiIyMDI2LTA3LTI4VDEwOjAwOjAwLjAwMFoifQ==",
});

const response = await request(app)
.get("/api/rate-limit")
.set("Authorization", "Bearer valid-admin-token");

expect(response.status).toBe(200);
expect(response.body).toMatchSnapshot();
});

it("should maintain a stable validation error shape", async () => {
const response = await request(app)
.get("/api/rate-limit")
.set("Authorization", "Bearer valid-admin-token")
.query({ limit: "not-a-number" });

expect(response.status).toBe(400);
expect(response.body).toMatchSnapshot();
});

it("should maintain a stable forbidden error shape when unauthenticated", async () => {
const mockRequireAdmin = jest.requireMock("../../src/middleware/requireAdmin").requireAdmin as jest.Mock;
mockRequireAdmin.mockImplementationOnce((_req: any, res: any, _next: any) => {
res.status(403).json({ error: { code: "forbidden" } });
});

const response = await request(app).get("/api/rate-limit");

expect(response.status).toBe(403);
expect(response.body).toMatchSnapshot();
});

it("should have the expected top-level fields on success", async () => {
mockGetAuditLogs.mockResolvedValueOnce({
data: [],
nextCursor: null,
});

const response = await request(app)
.get("/api/rate-limit")
.set("Authorization", "Bearer valid-admin-token");

expect(response.body).toHaveProperty("data");
expect(response.body).toHaveProperty("nextCursor");
expect(Array.isArray(response.body.data)).toBe(true);
});

it("should return an empty data array when no audit logs exist", async () => {
mockGetAuditLogs.mockResolvedValueOnce({
data: [],
nextCursor: null,
});

const response = await request(app)
.get("/api/rate-limit")
.set("Authorization", "Bearer valid-admin-token");

expect(response.body.data).toHaveLength(0);
expect(response.body.nextCursor).toBeNull();
});
});

describe("GET /api/rate-limit/status — public status snapshot", () => {
it("should maintain a stable anonymous status shape with zero usage", async () => {
const response = await request(app).get("/api/rate-limit/status");

expect(response.status).toBe(200);
expect(response.body).toMatchSnapshot({
data: { resetAt: expect.any(String) },
});
});

it("should maintain a stable authenticated status shape", async () => {
const response = await request(app)
.get("/api/rate-limit/status")
.set("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.dGVzdA");

expect(response.status).toBe(200);
expect(response.body).toMatchSnapshot();
});

it("should report correct usage after making requests through the real limiter", async () => {
const localApp = express();
localApp.use((_req, _res, next) => {
requestContextStorage.run({ requestId: "test-req-id" }, next);
});
localApp.use("/api/rate-limit", rateLimitRouter);
localApp.use(
createRateLimitAnon({ windowMs: 60_000, max: 60, store: anonRateLimitStore }),
);
localApp.get("/api/markets", (_req, res) => {
res.json({ data: [] });
});

await request(localApp).get("/api/markets");
await request(localApp).get("/api/markets");

const response = await request(localApp).get("/api/rate-limit/status");

expect(response.status).toBe(200);
expect(response.body.data.used).toBe(2);
expect(response.body.data.remaining).toBe(58);
});
});
Loading