109 lines
3.8 KiB
TypeScript
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("") + ",源图可能已失效"
|
|
);
|
|
});
|
|
});
|