115 lines
7.5 KiB
TypeScript
115 lines
7.5 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
||
import { serverLoader } from "../helpers/canvas-download-server-loader";
|
||
|
||
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 }) => {
|
||
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("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);
|
||
});
|
||
|
||
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 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" });
|
||
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);
|
||
});
|
||
|
||
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("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);
|
||
});
|