import { test, expect } from "@playwright/test"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import vm from "node:vm"; import * as crypto from "node:crypto"; import ts from "typescript"; import sharp from "sharp"; import JSZip from "jszip"; const root = path.resolve(__dirname, "../.."); function serverLoader(mocks: Record, env: Record = {}) { const modules = new Map(); const runtimeGlobal = {}; const load = (relative: string): any => { const filename = path.resolve(root, relative); if (modules.has(filename)) return modules.get(filename)!.exports; const module = { exports: {} }; modules.set(filename, module); const source = ts.transpileModule(readFileSync(filename, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true, }, }).outputText; const nativeRequire = createRequire(filename); const requireMock = (name: string) => { if (name === "server-only") return {}; if (Object.prototype.hasOwnProperty.call(mocks, name)) return mocks[name]; if (name.startsWith("@/")) return load(`${name.slice(2)}.ts`); if (name.startsWith(".")) return load(path.relative(root, path.resolve(path.dirname(filename), `${name}.ts`))); return nativeRequire(name); }; const context = vm.createContext({ module, exports: module.exports, require: requireMock, globalThis: runtimeGlobal, process: { env }, Buffer, URL, URLSearchParams, DOMException, Response, crypto, setTimeout, clearTimeout, setImmediate, AbortSignal, AbortController, ReadableStream, Headers, Blob, Date: mocks.Date || Date, document: mocks.document, window: mocks.window, console: { log() {}, warn() {}, error() {}, info() {} }, fetch: mocks.fetch || (() => { throw new Error("Unexpected network call"); }), }); new vm.Script(source, { filename }).runInContext(context); return module.exports; }; return load; } test("simultaneous archive and download fetch the source once; stored aliases survive module restart", async () => { let requests = 0; const source = `https://provider.example/${crypto.randomUUID()}.png`; const bytes = await sharp({ create: { width: 16, height: 24, channels: 4, background: "#12345680" } }).png().toBuffer(); const loader = serverLoader({ fetch: async () => { requests++; await new Promise(r => setTimeout(r, 40)); return new Response(bytes, { headers: { "content-type": "image/png" } }); } }); const cache = loader("lib/storage/image-byte-cache.ts"); const results = await Promise.all(Array.from({ length: 10 }, () => cache.fetchImageBytes(source))); expect(requests).toBe(1); expect(results.every((r: any) => r.buffer.equals(bytes))).toBe(true); const alias = `https://cdn.example/${crypto.randomUUID()}.png`; await cache.cacheImageBytes(`source::${alias}`, results[0]); const restarted = serverLoader({})("lib/storage/image-byte-cache.ts"); expect((await restarted.fetchImageBytes(alias)).buffer.equals(bytes)).toBe(true); }); test("source body timeout aborts a slow body even after successful headers", async () => { const cache = serverLoader({ fetch: async (_url: string, options: any) => ({ ok: true, headers: new Headers({ "content-type": "image/png" }), arrayBuffer: () => new Promise((_resolve, reject) => options.signal.addEventListener("abort", () => reject(new Error("body timeout")), { once: true })), }) })("lib/storage/image-byte-cache.ts"); await expect(cache.fetchImageBytes(`https://provider.example/${crypto.randomUUID()}`, { timeoutMs: 40 })).rejects.toThrow("body timeout"); }); test("a provider error page with HTTP 200 never poisons the image cache", async () => { let requests = 0; const bytes = await sharp({ create: { width: 25, height: 39, channels: 3, background: "red" } }).png().toBuffer(); const module = serverLoader({ fetch: async () => { requests++; return requests === 1 ? new Response("upstream timeout", { headers: { "content-type": "image/png" } }) : new Response(bytes, { headers: { "content-type": "image/png" } }); } })("lib/storage/image-byte-cache.ts"); const url = `https://provider.example/${crypto.randomUUID()}.png`; await expect(module.fetchImageBytes(url)).rejects.toThrow("error document"); expect((await module.fetchImageBytes(url)).buffer.equals(bytes)).toBe(true); expect(requests).toBe(2); }); test("compatible PNG remains byte-identical; JPEG becomes RGB PNG without resizing", async () => { const module = serverLoader({})("lib/canvas/photoshop-safe-download.ts"); const bytes = await sharp({ create: { width: 32, height: 48, channels: 4, background: "#12345680" } }).png().toBuffer(); expect((await module.convertImageToPhotoshopPng(bytes)).equals(bytes)).toBe(true); const jpeg = await sharp(bytes).jpeg().toBuffer(); const converted = await module.convertImageToPhotoshopPng(jpeg); const metadata = await sharp(converted).metadata(); expect(metadata).toMatchObject({ format: "png", space: "srgb", width: 32, height: 48, depth: "uchar" }); }); test("CDN delivery requires a ready compatible asset and a configured HTTPS storage origin", async () => { let url = "https://cdn.example/uploads/test.png"; let status = "ready"; const module = serverLoader({ "@/lib/db/queries": { getDefaultStorageConfig: async () => ({ type: "qiniu-kodo", publicUrl: "https://cdn.example", isEnabled: true }) }, "./image-archive-db": { findImageArchiveDownloadTask: async () => ({ metadata: { downloadAsset: { status, url, contentType: "image/png", bytes: 321 } } }) }, })("lib/storage/canvas-cdn-download.ts"); const ready = await module.resolveCanvasCdnDownload(["https://provider.example/out.jpg"], "花瓶.png"); expect(ready.direct.bytes).toBe(321); expect(new URL(ready.direct.downloadUrl).searchParams.get("attname")).toBe("花瓶.png"); status = "pending"; expect((await module.resolveCanvasCdnDownload([url], "a.png")).direct).toBeNull(); status = "ready"; url = "https://evil.example/uploads/test.png"; expect((await module.resolveCanvasCdnDownload([url], "a.png")).direct).toBeNull(); }); test("PNG preparation failure leaves the original archived and schedules a retry", async () => { let saved: any; const module = serverLoader({ "./image-archive-db": { claimImageDownloadTask: async () => ({ id: "task", targetUrl: "https://cdn.example/original.png", metadata: { downloadAsset: { attempts: 2 } } }), saveImageDownloadAsset: async (_id: string, asset: any) => { saved = asset; } }, "./image-byte-cache": { fetchImageBytes: async () => { throw new Error("storage offline"); } }, "./service": {}, })("lib/storage/image-download-preparation.ts"); await module.prepareImageArchiveDownload("task"); expect(saved.status).toBe("failed"); expect(saved.attempts).toBe(2); expect(Date.parse(saved.retryAt)).toBeGreaterThan(Date.now()); }); test("ZIP streams retain original bytes and Unicode names without recompression", async () => { const module = serverLoader({})("lib/canvas/download-zip-stream.ts"); const bytes = await sharp({ create: { width: 8, height: 8, channels: 3, background: "red" } }).png().toBuffer(); const zip = new JSZip(); zip.file("作品/花瓶.png", bytes); const data = Buffer.from(await new Response(module.streamDownloadZip(zip)).arrayBuffer()); const unpacked = await JSZip.loadAsync(data); expect((await unpacked.file("作品/花瓶.png")!.async("nodebuffer")).equals(bytes)).toBe(true); // Local-file compression method 0 means STORE. expect(data.readUInt16LE(8)).toBe(0); }); test("ready downloads hand the CDN URL to the browser without buffering the file", async () => { let frame: any; let payload: any; let appended = false; const module = serverLoader({ "@/lib/image-delivery": { buildImageProxyUrl: (url: string) => url }, fetch: async (_url: string, init: any) => { payload = JSON.parse(init.body); return Response.json({ delivery: "cdn", downloadUrl: "https://cdn.example/a.png?attname=a.png", filename: "a.png", bytes: 123 }); }, document: { createElement: (tag: string) => { expect(tag).toBe("iframe"); return frame = { remove() {} }; }, body: { appendChild: () => { appended = true; } } }, window: { setTimeout() {} }, })("lib/client/canvas-image-download.ts"); const result = await module.downloadCanvasImageAsPng({ url: "https://provider.example/a.png" }); expect(payload.preferDirect).toBe(true); expect(appended).toBe(true); expect(frame.hidden).toBe(true); expect(frame.src).toContain("https://cdn.example/"); expect(result.bytes).toBe(123); }); test("single-image route preserves authentication and returns a small CDN response only when requested", async () => { let signedIn = false; let lookups = 0; const module = serverLoader({ "@/app/(auth)/auth": { auth: async () => signedIn ? { user: { id: "owner" } } : null }, "@/lib/storage/canvas-cdn-download": { resolveCanvasCdnDownload: async () => { lookups++; return { direct: { delivery: "cdn", downloadUrl: "https://cdn.example/a.png?attname=a.png", filename: "a.png", bytes: 100 }, task: null }; } }, "@/lib/storage/image-download-preparation": {}, "next/server": { after() {}, NextResponse: class extends Response { static json(body: unknown, init?: ResponseInit) { return new Response(JSON.stringify(body), { ...init, headers: { "Content-Type": "application/json", ...(init?.headers || {}) } }); } } }, })("app/api/canvas/download-image/route.ts"); const request = { url: "https://app.example/api/canvas/download-image", headers: new Headers(), json: async () => ({ url: "https://provider.example/a.png", preferDirect: true }) }; expect((await module.POST(request)).status).toBe(401); expect(lookups).toBe(0); signedIn = true; const response = await module.POST(request); expect(response.status).toBe(200); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect((await response.json()).delivery).toBe("cdn"); expect(lookups).toBe(1); }); test("batch downloads use the durable prepared PNG even when the source cache is cold", async () => { const original = `https://provider.example/${crypto.randomUUID()}.jpg`; const prepared = `https://cdn.example/${crypto.randomUUID()}.png`; const bytes = await sharp({ create: { width: 12, height: 20, channels: 4, background: "#abcdef80" } }).png().toBuffer(); const fetched: string[] = []; const module = serverLoader({ "@/lib/storage/image-archive-db": { findImageArchiveDownloadTask: async () => ({ targetUrl: original, metadata: { downloadAsset: { status: "ready", url: prepared, contentType: "image/png" } } }) }, fetch: async (url: string) => { fetched.push(url); expect(url).toBe(prepared); return new Response(bytes, { headers: { "Content-Type": "image/png" } }); }, }, { POSTGRES_URL: "mock-only" })("lib/canvas/photoshop-safe-download.ts"); const result = await module.fetchCanvasDownloadAsset({ file: { url: original, name: "作品" }, index: 0, requestOrigin: "https://app.example", cookie: "", retryCount: 1 }); expect(result.ok).toBe(true); expect(result.bytes.equals(bytes)).toBe(true); expect(result.filename).toBe("001-作品.png"); expect(fetched).toEqual([prepared]); }); test("an archive completed during a source failure is resolved before the next attempt", async () => { const source = `https://provider.example/${crypto.randomUUID()}.png`; const archived = `https://cdn.example/${crypto.randomUUID()}.png`; const bytes = await sharp({ create: { width: 75, height: 113, channels: 4, background: "#abcdef80" } }).png().toBuffer(); let targetUrl: string | undefined; const fetched: string[] = []; const module = serverLoader({ "@/lib/storage/image-archive-db": { findImageArchiveDownloadTask: async () => ({ targetUrl }) }, fetch: async (url: string) => { fetched.push(url); if (url === source) { targetUrl = archived; throw new Error("Old provider URL timed out"); } return new Response(bytes, { headers: { "content-type": "image/png" } }); }, }, { POSTGRES_URL: "mock-only" })("lib/canvas/photoshop-safe-download.ts"); const result = await module.fetchCanvasDownloadAsset({ file: { url: source }, index: 0, requestOrigin: "https://app.example", cookie: "", retryCount: 2 }); expect(result.ok).toBe(true); expect(result.bytes.equals(bytes)).toBe(true); expect(fetched).toEqual([source, archived]); }); test("parallel preparation polls share one task, retaining bytes if the disk cache is unavailable", async () => { const { createDownloadPreparationQueue } = serverLoader({})("lib/canvas/download-preparation.ts"); const runs: Array<() => Promise> = []; const asset = { buffer: Buffer.from("test-normalized-png"), contentType: "image/png" }; let release!: () => void; let loads = 0; const gate = new Promise(resolve => { release = resolve; }); const queue = createDownloadPreparationQueue({ load: async () => { loads++; await gate; return asset; }, read: async () => null, write: async () => { throw new Error("Disk full"); }, schedule: (run: () => Promise) => runs.push(run), }); const input = { userId: "owner", urls: ["https://provider.example/a.png"], requestOrigin: "https://app.example", cookie: "session=private" }; const initial = await Promise.all(Array.from({ length: 10 }, () => queue.request(input))); expect(initial.every(item => item.status === "preparing")).toBe(true); expect(runs.length).toBe(1); const running = runs[0](); expect((await queue.request(input)).status).toBe("preparing"); expect(loads).toBe(1); release(); await running; const result = await queue.request(input); expect(result.status).toBe("ready"); expect(result.asset.buffer.equals(asset.buffer)).toBe(true); }); test("preparation cache survives a process restart and isolates authenticated users", async () => { const { createDownloadPreparationQueue } = serverLoader({})("lib/canvas/download-preparation.ts"); const runs: Array<() => Promise> = []; const cache = new Map(); let loads = 0; const deps = { load: async () => { loads++; return { buffer: Buffer.from("png-original"), contentType: "image/png" }; }, read: async (key: string) => cache.get(key) || null, write: async (key: string, value: any) => { cache.set(key, value); }, schedule: (run: () => Promise) => runs.push(run) }; const input = { userId: "owner-a", urls: ["https://app.example/private.png"], requestOrigin: "https://app.example", cookie: "session=a" }; const queue = createDownloadPreparationQueue(deps); expect((await queue.request(input)).status).toBe("preparing"); await runs.shift()!(); const restarted = createDownloadPreparationQueue(deps); expect((await restarted.request(input)).status).toBe("ready"); expect(loads).toBe(1); expect((await restarted.request({ ...input, userId: "owner-b", cookie: "session=b" })).status).toBe("preparing"); await runs.shift()!(); expect(loads).toBe(2); expect([...cache.keys()].every(key => !key.includes("private.png") && !key.includes("session="))).toBe(true); }); test("preparations have bounded concurrency and a deadline for unreachable originals", async () => { const { createDownloadPreparationQueue } = serverLoader({})("lib/canvas/download-preparation.ts"); const runs: Array<() => Promise> = []; let active = 0; let maximum = 0; const releases: Array<() => void> = []; const queue = createDownloadPreparationQueue({ concurrency: 2, load: async () => { active++; maximum = Math.max(maximum, active); await new Promise(resolve => releases.push(resolve)); active--; return { buffer: Buffer.from("png"), contentType: "image/png" }; }, read: async () => null, write: async () => {}, schedule: (run: () => Promise) => runs.push(run) }); for (let i = 0; i < 3; i++) await queue.request({ userId: `owner-${i}`, urls: [`https://provider.example/${i}.png`], requestOrigin: "https://app.example", cookie: "" }); const running = Promise.all(runs.map(run => run())); expect(active).toBe(2); releases.shift()!(); releases.shift()!(); await new Promise(resolve => setImmediate(resolve)); releases.shift()!(); await running; expect(maximum).toBe(2); const failedRuns: Array<() => Promise> = []; const deadlineQueue = createDownloadPreparationQueue({ timeoutMs: 25, load: () => new Promise(() => {}), read: async () => null, write: async () => {}, schedule: (run: () => Promise) => failedRuns.push(run) }); const input = { userId: "owner", urls: ["https://provider.example/unreachable.png"], requestOrigin: "https://app.example", cookie: "" }; await deadlineQueue.request(input); await failedRuns[0](); expect((await deadlineQueue.request(input)).status).toBe("failed"); }); test("the download route returns promptly then serves a full-size PNG while archive upload is unfinished", async () => { const bytes = await sharp({ create: { width: 122, height: 186, channels: 4, background: "#12345680" } }).png().toBuffer(); const runs: Array<() => Promise> = []; let calls = 0; let targetUrl: string | undefined; const module = serverLoader({ "@/app/(auth)/auth": { auth: async () => ({ user: { id: "owner" } }) }, "@/lib/storage/canvas-cdn-download": { resolveCanvasCdnDownload: async () => ({ task: targetUrl ? { id: "archived", targetUrl } : null, direct: null }) }, "@/lib/storage/image-download-preparation": { prepareImageArchiveDownload: async () => {} }, "next/server": { NextResponse: Response, after: (run: () => Promise) => runs.push(run) }, fetch: async () => { calls++; return new Response(bytes, { headers: { "content-type": "image/png" } }); }, })("app/api/canvas/download-image/route.ts"); const request = { url: "https://app.example/api/canvas/download-image", headers: new Headers(), json: async () => ({ url: `https://provider.example/${crypto.randomUUID()}.png`, preparation: "async", name: "原图" }) }; const body = await request.json(); request.json = async () => body; const first = await module.POST(request); expect(first.status).toBe(202); expect(calls).toBe(0); expect((await first.json()).delivery).toBe("preparing"); await runs[0](); targetUrl = "https://cdn.example/archive-completed.png"; const second = await module.POST(request); expect(second.status).toBe(200); const downloaded = Buffer.from(await second.arrayBuffer()); expect(downloaded.equals(bytes)).toBe(true); expect(await sharp(downloaded).metadata()).toMatchObject({ width: 122, height: 186, hasAlpha: true }); expect(calls).toBe(1); }); test("the browser waits for preparation and cancellation never starts a second request", async () => { let calls = 0; const messages: string[] = []; let appended = false; const module = serverLoader({ fetch: async (_url: string, options: any) => { expect(JSON.parse(options.body).preparation).toBe("async"); calls++; return calls === 1 ? Response.json({ delivery: "preparing", retryAfter: 1 }, { status: 202 }) : Response.json({ delivery: "cdn", downloadUrl: "https://cdn.example/ready.png", filename: "ready.png" }); }, document: { createElement: () => ({ remove() {} }), body: { appendChild: () => { appended = true; } } }, window: { setTimeout() {} }, })("lib/client/canvas-image-download.ts"); const running = module.downloadCanvasImageAsPng({ url: "https://provider.example/a.png", onProgress: (p: any) => messages.push(p.message) }); await new Promise(resolve => setTimeout(resolve, 20)); expect(appended).toBe(false); await running; expect(calls).toBe(2); expect(appended).toBe(true); expect(messages.some(message => message.includes("准备"))).toBe(true); const abort = new AbortController(); calls = 0; const cancelled = module.downloadCanvasImageAsPng({ url: "https://provider.example/a.png", signal: abort.signal }); setTimeout(() => abort.abort(), 10); await expect(cancelled).rejects.toMatchObject({ name: "AbortError" }); expect(calls).toBe(1); }); test("preparation polling has a finite deadline instead of an endless spinner", async () => { let time = 0; const module = serverLoader({ Date: { now: () => time }, fetch: async () => { time = 181_000; return Response.json({ delivery: "preparing" }, { status: 202 }); }, })("lib/client/canvas-image-download.ts"); await expect(module.downloadCanvasImageAsPng({ url: "https://provider.example/a.png" })) .rejects.toMatchObject({ code: "preparation_timeout" }); });