55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { cache } from "~/utils/cache.server";
|
|
import { checkAuthRateLimit, getAuthRateLimitKey } from "./auth-rate-limit.server";
|
|
|
|
describe("auth rate limiting", () => {
|
|
beforeEach(() => {
|
|
cache.clear();
|
|
vi.spyOn(Date, "now").mockReturnValue(new Date("2026-05-25T12:00:00.000Z").getTime());
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
cache.clear();
|
|
});
|
|
|
|
it("builds stable keys from action, client ip, and normalized identifier", () => {
|
|
const request = new Request("https://example.test/login", {
|
|
headers: { "x-forwarded-for": "203.0.113.10, 198.51.100.5" },
|
|
});
|
|
|
|
expect(getAuthRateLimitKey({ request, action: "login", identifier: " Ada@Example.COM " })).toBe(
|
|
getAuthRateLimitKey({ request, action: "login", identifier: "ada@example.com" })
|
|
);
|
|
expect(getAuthRateLimitKey({ request, action: "login", identifier: "ada@example.com" })).not.toBe(
|
|
getAuthRateLimitKey({ request, action: "forgot-password", identifier: "ada@example.com" })
|
|
);
|
|
});
|
|
|
|
it("allows requests up to the limit and returns retry timing after the limit", () => {
|
|
const request = new Request("https://example.test/login", {
|
|
headers: { "x-client-ip": "203.0.113.10" },
|
|
});
|
|
const options = { request, action: "login" as const, identifier: "ada@example.com", limit: 2, windowMs: 30_000 };
|
|
|
|
expect(checkAuthRateLimit(options)).toEqual({ allowed: true });
|
|
expect(checkAuthRateLimit(options)).toEqual({ allowed: true });
|
|
expect(checkAuthRateLimit(options)).toEqual({ allowed: false, retryAfterSeconds: 30 });
|
|
});
|
|
|
|
it("starts a new window after the cached counter expires", () => {
|
|
const request = new Request("https://example.test/mfa", {
|
|
headers: { "x-client-ip": "203.0.113.10" },
|
|
});
|
|
const options = { request, action: "mfa" as const, identifier: "ada@example.com", limit: 1, windowMs: 10_000 };
|
|
|
|
expect(checkAuthRateLimit(options)).toEqual({ allowed: true });
|
|
expect(checkAuthRateLimit(options)).toEqual({ allowed: false, retryAfterSeconds: 10 });
|
|
|
|
vi.mocked(Date.now).mockReturnValue(new Date("2026-05-25T12:00:11.000Z").getTime());
|
|
|
|
expect(checkAuthRateLimit(options)).toEqual({ allowed: true });
|
|
});
|
|
});
|