116 lines
7.1 KiB
TypeScript
116 lines
7.1 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import sharp from "sharp";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { serverLoader } from "../helpers/canvas-download-server-loader";
|
|
|
|
const config = { type: "qiniu-kodo", provider: "qiniu-kodo", isEnabled: true,
|
|
publicUrl: "https://cdn.example", endpoint: "https://s3.example", region: "cn-east-1",
|
|
bucket: "images", accessKeyId: "test-ak", secretAccessKey: "test-sk" };
|
|
|
|
test("external model results including signed OSS URLs are fetched by Qiniu without app image reads", async () => {
|
|
let source = "";
|
|
let calls = 0;
|
|
const load = serverLoader({
|
|
"@/lib/storage/service": { resolveS3UploadTarget: async () => ({ config }),
|
|
buildS3PublicUrl: (_: unknown, key: string) => `${config.publicUrl}/${key}` },
|
|
fetch: async (url: string, init: RequestInit) => {
|
|
calls++;
|
|
expect(new URL(url).hostname).toBe("iovip-z0.qiniuio.com");
|
|
expect(init.method).toBe("POST");
|
|
const encoded = new URL(url).pathname.split("/fetch/")[1].split("/to/")[0];
|
|
expect(Buffer.from(encoded, "base64url").toString()).toBe(source);
|
|
return Response.json({ key: "uploads/ai-generated-archive-task-1", fsize: 5000, mimeType: "image/jpeg" });
|
|
},
|
|
}, { NEXT_PUBLIC_APP_URL: "https://app.example" });
|
|
const { archiveImageThroughQiniu } = load("lib/storage/qiniu-image-archive.ts");
|
|
for (source of ["https://api.vip98k.com/api/external/assets/image",
|
|
"https://dashscope-a717.oss-accelerate.aliyuncs.com/result.png?Expires=123&Signature=a%2Bb%3D",
|
|
"https://images.provider.example/result.jpg"]) {
|
|
expect(await archiveImageThroughQiniu(source, "task-1")).toMatchObject({ bytes: 5000, contentType: "image/jpeg" });
|
|
}
|
|
for (const local of ["https://cdn.example/uploads/a.png", "https://app.example/api/generated-image-preview/id",
|
|
"http://127.0.0.1/a.png", "data:image/png;base64,abcd"]) {
|
|
expect(await archiveImageThroughQiniu(local, "task-1")).toBeNull();
|
|
}
|
|
expect(calls).toBe(3);
|
|
});
|
|
|
|
test("stored direct uploads prepare once and then download from CDN", async () => {
|
|
let task: any = null;
|
|
const ensured: string[] = [];
|
|
const url = "https://cdn.example/uploads/direct/user-1/image.webp";
|
|
const module = serverLoader({
|
|
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
|
|
"./image-archive-db": {
|
|
findImageArchiveDownloadTask: async () => task,
|
|
ensureStoredImageDownloadTask: async (stored: string) => {
|
|
ensured.push(stored); return task = { id: "stored", targetUrl: stored, metadata: { downloadAsset: { status: "pending" } } };
|
|
},
|
|
},
|
|
})("lib/storage/canvas-cdn-download.ts");
|
|
const pending = await module.resolveCanvasCdnDownload([url], "图片.png");
|
|
expect(pending).toMatchObject({ direct: null, canPrepareDirect: true });
|
|
task.metadata.downloadAsset = { status: "ready", url: "https://cdn.example/uploads/converted.png", contentType: "image/png", bytes: 1234 };
|
|
const ready = await module.resolveCanvasCdnDownload([url], "图片.png");
|
|
expect(ready.direct).toMatchObject({ delivery: "cdn", bytes: 1234 });
|
|
expect(new URL(ready.direct.downloadUrl).searchParams.get("attname")).toBe("图片.png");
|
|
expect(ensured).toEqual([url]);
|
|
});
|
|
|
|
test("download preparation only registers unsigned files in the configured bucket", async () => {
|
|
const stored: string[] = [];
|
|
const module = serverLoader({
|
|
"@/lib/db/queries": { getDefaultStorageConfig: async () => config },
|
|
"./image-archive-db": { findImageArchiveDownloadTask: async () => null,
|
|
ensureStoredImageDownloadTask: async (url: string) => { stored.push(url); return { id: "1", targetUrl: url }; } },
|
|
}, { CANVAS_QINIU_ORIGIN_URL: "https://origin.example" })("lib/storage/canvas-cdn-download.ts");
|
|
for (const url of ["https://other.example/uploads/a.png", "https://cdn.example/uploads/a.png?token=signed",
|
|
"https://cdn.example/uploads/a.png?imageView2/2/w/160", "https://cdn.example/private/a.png"]) {
|
|
expect((await module.resolveCanvasCdnDownload([url], "a.png")).canPrepareDirect).toBe(false);
|
|
}
|
|
expect((await module.resolveCanvasCdnDownload(["https://origin.example/uploads/direct/user/a.png"], "a.png")).canPrepareDirect).toBe(true);
|
|
expect(stored).toEqual(["https://cdn.example/uploads/direct/user/a.png"]);
|
|
});
|
|
|
|
test("nested uploaded PNGs use a bounded header read without stripping their colour profile", async () => {
|
|
const bytes = await sharp({ create: { width: 32, height: 24, channels: 4, background: "#1d785c80" } }).withIccProfile("p3").png().toBuffer();
|
|
let reads = 0;
|
|
const module = serverLoader({
|
|
"@/lib/storage/service": { resolveS3UploadTarget: async () => ({ config }),
|
|
buildS3PublicUrl: (_: unknown, key: string) => `${config.publicUrl}/${key}`,
|
|
createS3Client: () => ({ destroy() {}, send: async (cmd: any) => {
|
|
reads++; expect(cmd.input.Key).toBe("uploads/direct/user/a.png");
|
|
expect(cmd.input.Range).toBe("bytes=0-65535");
|
|
return { $metadata: { httpStatusCode: 206 }, ContentType: "image/png", ContentLength: bytes.length,
|
|
ContentRange: `bytes 0-${bytes.length - 1}/${bytes.length}`, Body: { transformToByteArray: async () => bytes } };
|
|
} }) },
|
|
})("lib/storage/qiniu-image-archive.ts");
|
|
const url = "https://cdn.example/uploads/direct/user/a.png";
|
|
expect(await module.getReadyQiniuImageDownload(url)).toMatchObject({ status: "ready", url, bytes: bytes.length });
|
|
expect(reads).toBe(1);
|
|
});
|
|
|
|
test("preview aliases use the configured CDN and retain processing and signed URL semantics", async () => {
|
|
const module = serverLoader({}, { NEXT_PUBLIC_CANVAS_CDN_ORIGIN: "https://cdn.example",
|
|
NEXT_PUBLIC_CANVAS_STORAGE_ORIGIN: "https://origin.example" })("lib/image-delivery.ts");
|
|
const source = "https://origin.example/uploads/a.png";
|
|
const options = { width: 331, quality: 86, format: "webp" };
|
|
const direct = "https://cdn.example/uploads/a.png?imageView2/2/w/360/format/webp/q/88";
|
|
expect(module.buildImageProxyUrl(source, options)).toBe(direct);
|
|
expect(module.buildImageProxyUrl(`/api/image-proxy?url=${encodeURIComponent(source)}&w=331&q=86&f=webp`)).toBe(direct);
|
|
expect(module.buildImageProxyUrl(source + "?token=signed", options)).toBe(source + "?token=signed");
|
|
expect(module.buildImageProxyUrl(`/api/image-proxy?url=${encodeURIComponent(source)}&proxy=1&w=331`)).toContain("proxy=1");
|
|
});
|
|
|
|
test("saved preview URLs redirect to an archived CDN thumbnail without reading the upstream image", async () => {
|
|
const source = "https://images.provider.example/result.png";
|
|
const route = serverLoader({
|
|
"next/server": { NextResponse },
|
|
"@/lib/storage/image-archive-db": { getArchivedImageUrlMappings: async () => ({ [source]: "https://cdn.example/uploads/a.png" }) },
|
|
fetch: () => { throw Error("No image bytes should pass through the app"); },
|
|
}, { NEXT_PUBLIC_CANVAS_CDN_ORIGIN: "https://cdn.example" })("app/api/image-proxy/route.ts");
|
|
const response = await route.GET(new NextRequest(`https://app.example/api/image-proxy?url=${encodeURIComponent(source)}&w=331&q=86&f=webp`, { headers: { "sec-fetch-dest": "image" } }));
|
|
expect(response.status).toBe(307);
|
|
expect(response.headers.get("location")).toBe("https://cdn.example/uploads/a.png?imageView2/2/w/360/format/webp/q/88");
|
|
});
|