homeio
PublikFork dari doctor-io/homeio
rate-limit.test.ts
import { afterEach, describe, expect, it } from "vitest";
import {
_resetPartialAuthGuardsForTesting,
_resetTotpReplayGuardForTesting,
isPartialAuthTokenBlocked,
isPartialAuthTokenConsumed,
isTotpCodeReplayed,
markPartialAuthTokenConsumed,
markTotpCodeUsed,
recordPartialAuthFailure,
} from "@/lib/server/modules/auth/rate-limit";
afterEach(() => {
_resetTotpReplayGuardForTesting();
_resetPartialAuthGuardsForTesting();
});
describe("totp replay guard", () => {
it("returns false before any code has been marked used", () => {
expect(isTotpCodeReplayed("user-1", "123456")).toBe(false);
});
it("returns true when the exact same code is replayed within the window", () => {
markTotpCodeUsed("user-1", "123456", 1_000);
expect(isTotpCodeReplayed("user-1", "123456", 1_500)).toBe(true);
});
it("returns false for a different code from the same user", () => {
markTotpCodeUsed("user-1", "123456", 1_000);
expect(isTotpCodeReplayed("user-1", "654321", 1_500)).toBe(false);
});
it("isolates the guard per user", () => {
markTotpCodeUsed("user-1", "123456", 1_000);
expect(isTotpCodeReplayed("user-2", "123456", 1_500)).toBe(false);
});
it("expires the record after its TTL elapses", () => {
markTotpCodeUsed("user-1", "123456", 1_000);
// 120 s TTL → record valid through 121_000 but not at 1_000 + 120_001.
expect(isTotpCodeReplayed("user-1", "123456", 121_001)).toBe(false);
// Calling once with an expired record should also drop it from memory.
expect(isTotpCodeReplayed("user-1", "123456", 121_001)).toBe(false);
});
it("replacing the stored code with a newer one moves the window forward", () => {
markTotpCodeUsed("user-1", "111111", 1_000);
markTotpCodeUsed("user-1", "222222", 30_000);
// Old code is no longer the "last used" — replay check should miss.
expect(isTotpCodeReplayed("user-1", "111111", 30_500)).toBe(false);
expect(isTotpCodeReplayed("user-1", "222222", 30_500)).toBe(true);
});
});
describe("partial-auth brute-force guard", () => {
const TOKEN = "partial.user-1.future.sig";
// Pick an explicit `now` and a token expiry several minutes into its
// future so we exercise the failure-count threshold without the
// "record expired" path tripping.
const NOW_MS = 1_000_000_000_000;
const TOKEN_EXP_SECONDS = Math.floor(NOW_MS / 1000) + 5 * 60;
it("returns false before any failures are recorded", () => {
expect(isPartialAuthTokenBlocked(TOKEN, NOW_MS)).toBe(false);
});
it("trips at the 5th failure for the same token", () => {
for (let i = 0; i < 4; i++) {
recordPartialAuthFailure(TOKEN, TOKEN_EXP_SECONDS, NOW_MS);
}
expect(isPartialAuthTokenBlocked(TOKEN, NOW_MS)).toBe(false);
recordPartialAuthFailure(TOKEN, TOKEN_EXP_SECONDS, NOW_MS);
expect(isPartialAuthTokenBlocked(TOKEN, NOW_MS)).toBe(true);
});
it("isolates the guard per token (different raw tokens cannot pool)", () => {
for (let i = 0; i < 5; i++) {
recordPartialAuthFailure(TOKEN, TOKEN_EXP_SECONDS, NOW_MS);
}
expect(isPartialAuthTokenBlocked(TOKEN, NOW_MS)).toBe(true);
expect(
isPartialAuthTokenBlocked("partial.other.future.sig", NOW_MS),
).toBe(false);
});
it("self-evicts once the token's own expiry has passed", () => {
for (let i = 0; i < 5; i++) {
recordPartialAuthFailure(TOKEN, TOKEN_EXP_SECONDS, NOW_MS);
}
expect(isPartialAuthTokenBlocked(TOKEN, NOW_MS)).toBe(true);
// After expiry + buffer: the read-path sweep drops the record and the
// block disappears. Real-world equivalent: token has expired anyway, so
// the caller would already see partial_auth_expired before this check.
const wellPastExpiry = TOKEN_EXP_SECONDS * 1000 + 60_000;
expect(isPartialAuthTokenBlocked(TOKEN, wellPastExpiry)).toBe(false);
});
});
describe("partial-auth single-use guard", () => {
const TOKEN = "partial.user-1.future.sig";
// Use a real "near-future" epoch for these tests since they don't pass an
// explicit `now` and the default Date.now() must not have already passed
// the token's expiry.
const TOKEN_EXP_SECONDS = Math.floor(Date.now() / 1000) + 60 * 60;
it("returns false before the token has been consumed", () => {
expect(isPartialAuthTokenConsumed(TOKEN)).toBe(false);
});
it("returns true after markPartialAuthTokenConsumed", () => {
markPartialAuthTokenConsumed(TOKEN, TOKEN_EXP_SECONDS);
expect(isPartialAuthTokenConsumed(TOKEN)).toBe(true);
});
it("isolates per token", () => {
markPartialAuthTokenConsumed(TOKEN, TOKEN_EXP_SECONDS);
expect(isPartialAuthTokenConsumed("partial.other.future.sig")).toBe(false);
});
it("clears once the token's own expiry has passed", () => {
markPartialAuthTokenConsumed(TOKEN, TOKEN_EXP_SECONDS);
expect(isPartialAuthTokenConsumed(TOKEN)).toBe(true);
const wellPastExpiry = TOKEN_EXP_SECONDS * 1000 + 60_000;
expect(isPartialAuthTokenConsumed(TOKEN, wellPastExpiry)).toBe(false);
});
});