102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import dotenv from "dotenv";
|
|
import getPool from "../lib/server/db";
|
|
import { ApiError } from "../lib/server/errors";
|
|
import { enforceAuthRateLimit, enforceUserWriteRateLimit } from "../lib/server/rate-limit";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const envLoaded = dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
|
|
const hasDb = Boolean(process.env.DATABASE_URL);
|
|
|
|
async function ensureRateLimitTable() {
|
|
const pool = getPool();
|
|
await pool.query(`
|
|
create table if not exists rate_limits(
|
|
key text primary key,
|
|
window_start timestamptz not null,
|
|
count integer not null default 0,
|
|
updated_at timestamptz not null default now()
|
|
)
|
|
`);
|
|
}
|
|
|
|
test("auth rate limit blocks when threshold is exceeded", async t => {
|
|
if (!hasDb) {
|
|
t.skip("DATABASE_URL not set");
|
|
return;
|
|
}
|
|
if (envLoaded.error) t.diagnostic(String(envLoaded.error));
|
|
|
|
await ensureRateLimitTable();
|
|
const pool = getPool();
|
|
const marker = Date.now();
|
|
const ip = `test-ip-${marker}`;
|
|
const identifier = `rate_limit_${marker}@example.com`;
|
|
try {
|
|
await enforceAuthRateLimit({
|
|
route: "login",
|
|
ip,
|
|
identifier,
|
|
ipLimit: 1,
|
|
identifierLimit: 1,
|
|
windowMs: 60_000
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => enforceAuthRateLimit({
|
|
route: "login",
|
|
ip,
|
|
identifier,
|
|
ipLimit: 1,
|
|
identifierLimit: 1,
|
|
windowMs: 60_000
|
|
}),
|
|
(error: unknown) => error instanceof ApiError && error.code === "RATE_LIMITED"
|
|
);
|
|
} finally {
|
|
await pool.query("delete from rate_limits where key like $1 or key like $2", [
|
|
`auth:login:ip:${ip}%`,
|
|
`auth:login:identifier:${identifier}%`
|
|
]);
|
|
}
|
|
});
|
|
|
|
test("user write rate limit blocks when threshold is exceeded", async t => {
|
|
if (!hasDb) {
|
|
t.skip("DATABASE_URL not set");
|
|
return;
|
|
}
|
|
if (envLoaded.error) t.diagnostic(String(envLoaded.error));
|
|
|
|
await ensureRateLimitTable();
|
|
const pool = getPool();
|
|
const userId = 987654;
|
|
const scope = `test_scope_${Date.now()}`;
|
|
try {
|
|
await enforceUserWriteRateLimit({
|
|
userId,
|
|
scope,
|
|
limit: 1,
|
|
windowMs: 60_000
|
|
});
|
|
|
|
await assert.rejects(
|
|
() => enforceUserWriteRateLimit({
|
|
userId,
|
|
scope,
|
|
limit: 1,
|
|
windowMs: 60_000
|
|
}),
|
|
(error: unknown) => error instanceof ApiError && error.code === "RATE_LIMITED"
|
|
);
|
|
} finally {
|
|
await pool.query("delete from rate_limits where key = $1", [
|
|
`write:user:${userId}:scope:${scope}`
|
|
]);
|
|
}
|
|
});
|