ai-chatbot/tests/routes/canvas-download-rate-limit.test.ts

109 lines
3.8 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import {
CanvasImageDownloadError,
downloadCanvasImageAsPng,
getCanvasImageDownloadErrorMessage,
} from "@/lib/client/canvas-image-download";
function readSource(path: string) {
return readFileSync(resolve(process.cwd(), path), "utf8");
}
test.describe("canvas image download rate limiting", () => {
test("isolates downloads by authenticated user and moves polling reads out of the generic bucket", () => {
const proxySource = readSource("proxy.ts");
const limiterSource = readSource("lib/security/rate-limiter.ts");
const downloadBranchIndex = proxySource.indexOf(
'pathname === "/api/canvas/download-image"'
);
const genericBranchIndex = proxySource.indexOf(
'} else if (pathname.startsWith("/api/")) {'
);
expect(limiterSource).toContain("download: new RateLimiter(60000, 60)");
expect(proxySource).toContain(
"`user:" + ["$", "{userId}"].join("") + ":canvas-download`"
);
expect(proxySource).toContain("rateLimiters.download");
expect(downloadBranchIndex).toBeGreaterThan(0);
expect(genericBranchIndex).toBeGreaterThan(downloadBranchIndex);
expect(proxySource).toContain(
'if (/^\\/api\\/canvas\\/[^/]+\\/revision$/.test(pathname)) return true;'
);
expect(proxySource).toContain('"/api/ai-wash/config"');
expect(proxySource).toContain('"/api/canvas/model-aliases"');
expect(proxySource).toContain('"/api/featured"');
expect(proxySource).toContain('"/api/scene-fusion/config"');
expect(proxySource).toContain('headers: { "Retry-After": String(retryAfter) }');
});
test("retries one 429 response and keeps rate limiting separate from source expiry", async () => {
const originalFetch = globalThis.fetch;
let requestCount = 0;
globalThis.fetch = async () => {
requestCount++;
return new Response(
JSON.stringify({ error: "Too many requests", retryAfter: 0 }),
{
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": "0",
},
}
);
};
try {
await expect(
downloadCanvasImageAsPng({
url: "https://example.com/image.png",
})
).rejects.toMatchObject({
code: "rate_limited",
status: 429,
});
expect(requestCount).toBe(2);
} finally {
globalThis.fetch = originalFetch;
}
const rateLimitMessage = getCanvasImageDownloadErrorMessage(
new CanvasImageDownloadError("Too many requests", {
code: "rate_limited",
status: 429,
})
);
const sourceMessage = getCanvasImageDownloadErrorMessage(
new CanvasImageDownloadError("图片源文件暂时不可访问", {
code: "source_unavailable",
status: 502,
})
);
expect(rateLimitMessage).toBe("图片下载请求过于频繁,请稍后重试");
expect(rateLimitMessage).not.toContain("源图");
expect(sourceMessage).toContain("图片仍保留在画布中");
expect(sourceMessage).not.toContain("重新生成");
});
test("uses the shared error formatter in every canvas image download entry", () => {
for (const path of [
"components/canvas/nodes/ImageNode.tsx",
"components/canvas/CanvasChatPanel.tsx",
"components/canvas/AiWashWorkspace.tsx",
"components/style-pro/StyleProWorkspace.tsx",
]) {
const source = readSource(path);
expect(source, path).toContain("getCanvasImageDownloadErrorMessage(error)");
}
const imageNodeSource = readSource("components/canvas/nodes/ImageNode.tsx");
expect(imageNodeSource).not.toContain(
"`" + ["$", "{error.message}"].join("") + ",源图可能已失效"
);
});
});