Files
ai-drama-platform/scripts/smoke-api-rate-limit.mjs
T

82 lines
3.6 KiB
JavaScript

import assert from "node:assert/strict";
import { dbRun, withTransaction } from "../server/db.mjs";
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
const scope = {
"x-organization-id": "org-studio-lab",
"x-workspace-id": "ws-local-aidrama",
"x-project-id": "thunder-mouth"
};
async function request(path, headers = {}, options = {}) {
const response = await fetch(`${api}${path}`, {
...options,
headers: { "content-type": "application/json", ...headers, ...(options.headers || {}) }
});
const payload = await response.json().catch(() => ({}));
return { response, payload };
}
async function login() {
const result = await request("/api/auth/login", {}, { method: "POST", body: JSON.stringify({ email: "producer@local.test", password: "Demo@123456" }) });
assert.equal(result.response.ok, true, "限流回归登录失败");
return { authorization: `Bearer ${result.payload.session.token}` };
}
const owner = { ...(await login()), ...scope };
const restoreOwner = { ...(await login()), ...scope };
let clientId = "";
let originalLimit = 120;
let changedLimit = false;
try {
const config = await request("/api/system/config", owner);
assert.equal(config.response.ok, true, "读取系统配置失败");
const setting = config.payload.settings.find((item) => item.key === "api.rate_limit_per_minute");
originalLimit = Number(setting?.value ?? 120);
const created = await request("/api/system/api-clients", owner, {
method: "POST",
body: JSON.stringify({ name: `Smoke Rate Limit Client ${Date.now()}`, scopes: ["jobs:read"] })
});
assert.equal(created.response.status, 201, "限流回归 API 客户端创建失败");
clientId = created.payload.client.id;
const clientHeaders = { authorization: `Bearer ${created.payload.clientKey}`, ...scope };
const updated = await request("/api/system/config", owner, {
method: "POST",
body: JSON.stringify({ settings: [{ key: "api.rate_limit_per_minute", value: 2 }] })
});
assert.equal(updated.response.ok, true, "降低 API 限流配置失败");
changedLimit = true;
const first = await request("/api/jobs", clientHeaders);
const second = await request("/api/jobs", clientHeaders);
const third = await request("/api/jobs", clientHeaders);
assert.equal(first.response.status, 200, "限流窗口内第一次请求不应被阻断");
assert.equal(second.response.status, 200, "限流窗口内第二次请求不应被阻断");
assert.equal(first.response.headers.get("x-ratelimit-limit"), "2", "响应必须返回限流上限");
assert.equal(second.response.headers.get("x-ratelimit-remaining"), "0", "第二次请求后剩余额度应为 0");
assert.equal(third.response.status, 429, "超过限流上限必须返回 429");
assert.equal(third.payload.error, "rate_limit_exceeded", "限流错误码必须稳定");
assert.ok(Number(third.response.headers.get("retry-after")) > 0, "429 必须返回 Retry-After");
assert.equal(third.response.headers.get("x-ratelimit-remaining"), "0", "429 响应剩余额度应为 0");
console.log("api rate-limit smoke passed: configured limit, headers, 429, retry-after");
} finally {
if (changedLimit) {
const restored = await request("/api/system/config", restoreOwner, {
method: "POST",
body: JSON.stringify({ settings: [{ key: "api.rate_limit_per_minute", value: originalLimit }] })
});
assert.equal(restored.response.ok, true, "限流回归未能恢复原配置");
}
if (clientId) {
withTransaction(() => {
dbRun("DELETE FROM api_clients WHERE id = ?", [clientId]);
dbRun("DELETE FROM audit_logs WHERE target_id = ?", [clientId]);
});
}
}