ai-chatbot/tests/routes/original-image-download.test.ts

221 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from "@playwright/test";
import { serverLoader } from "../helpers/canvas-download-server-loader";
import type { CanvasImageDownloadProgress } from "@/lib/client/canvas-image-download";
const config = { type: "qiniu-kodo", isEnabled: true, publicUrl: "https://cdn.example" };
test("original downloads use the archived JPEG rather than the prepared PNG and transfer only HEAD metadata", async () => {
const calls: Array<{ url: string; method: string }> = [];
const loader = serverLoader({
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
"./image-archive-db": { findImageArchiveDownloadTask: async () => ({ targetUrl: "https://cdn.example/original.jpg", metadata: {
downloadAsset: { status: "ready", url: "https://cdn.example/converted.png", contentType: "image/png" },
} }) },
fetch: async (url: URL, options: { method: string; cache: string }) => {
expect(options.cache).toBe("default");
calls.push({ url: url.toString(), method: options.method });
return { ok: true, headers: new Headers({ "content-type": "image/jpeg", "content-length": "12345" }),
arrayBuffer() { throw new Error("Image bytes must bypass the application"); } };
},
});
const result = await loader("lib/storage/original-image-download.ts").resolveOriginalCdnDownload("https://upstream.example/temporary.png", "产品.png", new AbortController().signal);
expect(calls).toEqual([{ url: "https://cdn.example/original.jpg", method: "HEAD" }]);
expect(result).toMatchObject({ delivery: "cdn", filename: "产品.jpg", bytes: 12345 });
expect(new URL(result.downloadUrl).pathname).toBe("/original.jpg");
});
test("signed original URLs retain the byte-preserving proxy without changing their signature", async () => {
const module = serverLoader({
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
"./image-archive-db": { findImageArchiveDownloadTask: async () => null },
fetch() { throw new Error("Do not add filename parameters to a signed URL"); },
})("lib/storage/original-image-download.ts");
const result = await module.resolveOriginalCdnDownload("https://cdn.example/source.jpg?token=signature%2Fvalue&e=123", "image.jpg", new AbortController().signal);
expect(result).toBeNull();
});
test("existing CDN objects bypass the archive scan and retain the MIME-derived format", async () => {
const module = serverLoader({
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
"./image-archive-db": { findImageArchiveDownloadTask() { throw new Error("An existing CDN object needs no archive scan"); } },
fetch: async () => ({ ok: true, headers: new Headers({ "content-type": "image/webp" }) }),
})("lib/storage/original-image-download.ts");
const result = await module.resolveOriginalCdnDownload("https://cdn.example/source.png", "source.png", new AbortController().signal);
expect(result.filename).toBe("source.webp");
expect(new URL(result.downloadUrl).pathname).toBe("/source.png");
});
test("temporary third-party URLs are never checked with HEAD when no archive is ready", async () => {
const module = serverLoader({
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
"./image-archive-db": { findImageArchiveDownloadTask: async () => null },
fetch() { throw new Error("The upstream file service does not support HEAD"); },
})("lib/storage/original-image-download.ts");
expect(await module.resolveOriginalCdnDownload("https://upstream.example/image.png", "image", new AbortController().signal)).toBeNull();
});
test("a CDN metadata timeout falls back to the original Kodo object without reading its body", async () => {
let closed = false;
const module = serverLoader({
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
"./image-archive-db": {},
"./service": {
toDbS3UploadConfig: () => ({ bucket: "images" }),
createS3Client: () => ({
send: async (command: { input: { Bucket: string; Key: string } }) => {
expect(command.input).toEqual({ Bucket: "images", Key: "uploads/source.png" });
return { ContentType: "image/jpeg", ContentLength: 12345 };
},
destroy: () => { closed = true; },
}),
},
fetch: async () => { throw new DOMException("Timed out", "TimeoutError"); },
})("lib/storage/original-image-download.ts");
const result = await module.resolveOriginalCdnDownload("https://cdn.example/uploads/source.png", "图片.png", new AbortController().signal);
expect(result).toMatchObject({ delivery: "cdn", filename: "图片.jpg", bytes: 12345 });
expect(closed).toBe(true);
});
test("original download metadata reports missing sources with cause, advice and a feedback ID", async () => {
const route = serverLoader({
"next/server": { NextResponse: Response },
"@/lib/storage/original-image-download": { resolveOriginalCdnDownload: async () => { throw new Error("HTTP 404"); } },
})("app/api/image-proxy/route.ts");
const request = { nextUrl: new URL("https://app.example/api/image-proxy?url=https%3A%2F%2Fcdn.example%2Fmissing.jpg&original=1&download=resolve"), signal: new AbortController().signal, headers: new Headers() };
const response = await route.GET(request);
const data = await response.json();
expect(response.status).toBe(404);
expect(data.failure).toMatchObject({ code: "SOURCE_NOT_FOUND", retryable: false });
expect(data.failure.recommendation).toBeTruthy();
expect(data.error).toContain(data.requestId);
});
test("the original proxy returns the first bytes while the upstream is still sending the file", async () => {
const head = Uint8Array.from([255, 216, 255, 224]);
const tail = Uint8Array.from([4, 8, 255, 217]);
let upstream!: ReadableStreamDefaultController<Uint8Array>;
const body = new ReadableStream<Uint8Array>({
start(controller) { upstream = controller; controller.enqueue(head); },
});
const route = serverLoader({
"next/server": { NextResponse: Response },
fetch: async () => new Response(body, { headers: { "content-type": "image/jpeg", "content-length": "8" } }),
})("app/api/image-proxy/route.ts");
const response = await route.GET({
nextUrl: new URL("https://app.example/api/image-proxy?url=https%3A%2F%2Fimages.example%2Foriginal.jpg&original=1"),
signal: new AbortController().signal, headers: new Headers(),
});
expect(response.headers.get("content-length")).toBe("8");
const reader = response.body.getReader();
expect((await reader.read()).value).toEqual(head);
upstream.enqueue(tail);
upstream.close();
expect((await reader.read()).value).toEqual(tail);
expect((await reader.read()).done).toBe(true);
});
function browserMocks() {
const frames: any[] = []; const links: any[] = [];
return { frames, links,
window: { location: { origin: "https://app.example" }, setTimeout() {} },
document: { createElement: (tag: string) => ({ tag, click() { links.push(this); }, remove() {} }),
body: { appendChild: (el: any) => { if (el.tag === "iframe") frames.push(el); }, removeChild() {} } },
};
}
test("chat original downloads use a native CDN download without fetching the image through CORS", async () => {
const dom = browserMocks(); const calls: string[] = [];
const progress: CanvasImageDownloadProgress[] = [];
const client = serverLoader({ ...dom, fetch: async (url: string) => {
calls.push(url);
return Response.json({ delivery: "cdn", downloadUrl: "https://cdn.example/image.jpg?attname=original.jpg", filename: "original.jpg", bytes: 5000000 });
} })("lib/client/canvas-image-download.ts");
const result = await client.downloadOriginalImage({ url: "https://cdn.example/image.jpg", onProgress: (event: CanvasImageDownloadProgress) => progress.push(event) });
expect(result).toEqual({ filename: "original.jpg", bytes: 5000000 });
expect(calls).toHaveLength(1);
expect(new URL(calls[0], "https://app.example").searchParams.get("download")).toBe("resolve");
expect(dom.frames[0].src).toBe("https://cdn.example/image.jpg?attname=original.jpg");
expect(dom.links).toHaveLength(0);
expect(progress.map(event => event.phase)).toEqual(["preparing", "downloading"]);
});
test("original fallback preserves JPEG bytes and the MIME-derived filename", async () => {
const dom = browserMocks(); const bytes = Buffer.from([255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 255, 217]); let calls = 0;
const client = serverLoader({ ...dom, fetch: async () => ++calls === 1 ? Response.json({ delivery: "proxy" }) : new Response(bytes, { headers: { "content-type": "image/jpeg" } }) })("lib/client/canvas-image-download.ts");
expect(await client.downloadOriginalImage({ url: "https://external.example/image.png", name: "original.png" })).toEqual({ filename: "original.jpg", bytes: bytes.length });
expect(calls).toBe(2);
const actual = await fetch(dom.links[0].href).then(r=>r.arrayBuffer());
expect(Buffer.from(actual).equals(bytes)).toBe(true);
URL.revokeObjectURL(dom.links[0].href);
});
test("one click prepares a compatible PNG when the CDN original is WebP", async () => {
const dom = browserMocks();
const calls: string[] = [];
const progress: CanvasImageDownloadProgress[] = [];
const url = "https://cdn.example/image.png";
const client = serverLoader({ ...dom, fetch: async (path: string, init?: RequestInit) => {
calls.push(path);
if (calls.length === 1) {
return Response.json({ delivery: "cdn", downloadUrl: url, filename: "商品.webp", bytes: 500 });
}
expect(init?.method).toBe("POST");
expect(JSON.parse(String(init?.body))).toMatchObject({ url, name: "商品", preparation: "async" });
if (calls.length === 2) {
return Response.json({ delivery: "preparing", retryAfter: 1, message: "正在准备原图" }, { status: 202 });
}
return Response.json({ delivery: "cdn", downloadUrl: "https://cdn.example/prepared.png", filename: "商品.png", bytes: 900 });
} })("lib/client/canvas-image-download.ts");
expect(await client.downloadOriginalImage({ url, name: "商品", onProgress: (event: CanvasImageDownloadProgress) => progress.push(event) }))
.toEqual({ filename: "商品.png", bytes: 900 });
expect(calls.slice(1)).toEqual(["/api/canvas/download-image", "/api/canvas/download-image"]);
expect(progress.map(event => event.phase)).toEqual(["preparing", "converting", "converting", "downloading"]);
for (const event of progress.filter(event => event.phase === "converting")) {
expect(event.message).toContain("正在转换为兼容 PNG");
expect(event.percent).toBeUndefined();
}
expect(dom.frames.map(frame => frame.src)).toEqual(["https://cdn.example/prepared.png"]);
expect(dom.links).toHaveLength(0);
});
test("one click uses the PNG preparation route without buffering an incompatible proxy response", async () => {
const dom = browserMocks();
const calls: string[] = [];
const progress: CanvasImageDownloadProgress[] = [];
let cancelled = false;
const bytes = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
const client = serverLoader({ ...dom, fetch: async (path: string) => {
calls.push(path);
if (calls.length === 1) return Response.json({ delivery: "proxy" });
if (calls.length === 2) {
return new Response(new ReadableStream({ cancel() { cancelled = true; } }),
{ headers: { "content-type": "image/avif" } });
}
return new Response(bytes, { headers: { "content-type": "image/png", "content-length": String(bytes.length) } });
} })("lib/client/canvas-image-download.ts");
expect(await client.downloadOriginalImage({
url: "https://external.example/image", name: "商品", onProgress: (event: CanvasImageDownloadProgress) => progress.push(event),
})).toEqual({ filename: "商品.png", bytes: bytes.length });
expect(cancelled).toBe(true);
expect(calls[2]).toBe("/api/canvas/download-image");
expect(progress).toContainEqual({
phase: "downloading", message: "正在下载 PNG...", loadedBytes: bytes.length, totalBytes: bytes.length, percent: 99,
});
expect(dom.links).toHaveLength(1);
URL.revokeObjectURL(dom.links[0].href);
});
test("original downloads reject processed references before network access", async () => {
const client = serverLoader({ ...browserMocks(), fetch() { throw new Error("Must not fetch a processed image as an original"); } })("lib/client/canvas-image-download.ts");
await expect(client.downloadOriginalImage({ url: "https://cdn.example/image.jpg?imageView2/2/w/320&token=signed" })).rejects.toThrow("无法确认原图");
});
test("chat downloads display the server cause and advice without losing its feedback ID", async () => {
const failure = { code: "SOURCE_FORBIDDEN", message: "图片源站拒绝访问原图。", recommendation: "请联系管理员检查访问权限。", status: 403, retryable: false };
const client = serverLoader({ ...browserMocks(), fetch: async () => Response.json({ failure, requestId: "original-feedback" }, { status: 403 }) })("lib/client/canvas-image-download.ts");
let error: unknown;
try { await client.downloadOriginalImage({ url: "https://cdn.example/image.jpg" }); } catch(e) { error=e; }
expect(client.getCanvasImageDownloadErrorMessage(error)).toContain("下载编号:original-feedback");
expect(client.getCanvasImageDownloadErrorMessage(error)).toContain(failure.recommendation);
});