From a2546425abfec36b15bd9a58cfb7f507cd05cfa3 Mon Sep 17 00:00:00 2001 From: Graaytech Date: Tue, 28 Jul 2026 08:39:26 -0700 Subject: [PATCH] Updated files Changes made --- .../__snapshots__/rate-limit.test.ts.snap | 61 +++++++ tests/schema/rate-limit.test.ts | 168 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 tests/schema/__snapshots__/rate-limit.test.ts.snap create mode 100644 tests/schema/rate-limit.test.ts diff --git a/tests/schema/__snapshots__/rate-limit.test.ts.snap b/tests/schema/__snapshots__/rate-limit.test.ts.snap new file mode 100644 index 0000000..a3a8385 --- /dev/null +++ b/tests/schema/__snapshots__/rate-limit.test.ts.snap @@ -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, + "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, + }, +} +`; diff --git a/tests/schema/rate-limit.test.ts b/tests/schema/rate-limit.test.ts new file mode 100644 index 0000000..4db8f20 --- /dev/null +++ b/tests/schema/rate-limit.test.ts @@ -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; + +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); + }); +});