ai-chatbot/tests/routes/legacy-oss-recovery.test.ts

138 lines
8.4 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { NextRequest, NextResponse } from "next/server";
import { serverLoader } from "../helpers/canvas-download-server-loader";
import { normalizeLegacyAliyunStorageUrl, isLegacyAliyunStorageConfig, RETIRED_STORAGE_MESSAGE } from "@/lib/storage/retired-storage";
import { collectImageArchiveSources, replaceArchivedImageUrls } from "@/lib/storage/image-archive-urls";
import { buildImageProxyUrl } from "@/lib/image-delivery";
const legacy = "https://demo-sparkai.oss-cn-shanghai.aliyuncs.com/uploads/product.png";
const permanent = "https://scqnycdn.jiansun.vip/legacy-oss/demo-sparkai/uploads/product.png";
test("legacy identity preserves image variants while dropping expired OSS credentials", () => {
const signed = legacy.replace("https:", "http:") + "?Signature=expired&Expires=1&OSSAccessKeyId=old&x-oss-process=image%2Fresize%2Cw_100#preview";
expect(normalizeLegacyAliyunStorageUrl(signed)).toBe(legacy + "?x-oss-process=image%2Fresize%2Cw_100");
expect(normalizeLegacyAliyunStorageUrl("/api/image-proxy?url=" + encodeURIComponent(signed))).toBe(normalizeLegacyAliyunStorageUrl(signed));
for (const source of [permanent, "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/image.png", "https://other-bucket.oss-cn-shanghai.aliyuncs.com/a.png"]) {
expect(normalizeLegacyAliyunStorageUrl(source)).toBeNull();
}
expect(isLegacyAliyunStorageConfig({ bucket: "demo-sparkai", endpoint: "https://oss-cn-shanghai.aliyuncs.com" })).toBe(true);
expect(isLegacyAliyunStorageConfig({ bucket: "other-bucket", endpoint: "https://oss-cn-shanghai.aliyuncs.com" })).toBe(false);
});
test("late canvas saves replace legacy URLs embedded in SVG, HTML and Markdown originals", () => {
const content = { svg: `<svg><image href="${legacy}"/></svg>`, html: `<img src='${legacy}'>`, text: `![thumb|||${legacy}|||2K](${permanent})` };
expect(collectImageArchiveSources(content)).toContain(legacy);
const result = replaceArchivedImageUrls(content, { [legacy]: permanent });
expect(JSON.stringify(result)).not.toContain("https://demo-sparkai.");
expect(result.svg).toContain(permanent);
expect(result.text).toBe(`![thumb|||${permanent}|||2K](${permanent})`);
});
test("legacy URLs in comma-separated reference descriptions keep sentence punctuation", () => {
const text = `Reference: ${legacy}, next reference`;
expect(collectImageArchiveSources(text)).toContain(legacy);
expect(replaceArchivedImageUrls(text, { [legacy]: permanent })).toBe(`Reference: ${permanent}, next reference`);
});
test("legacy display URLs go through archive resolution while other Alibaba buckets keep working", () => {
const url = buildImageProxyUrl(legacy, { width: 400 });
expect(url).toMatch(/^\/api\/image-proxy\?/);
expect(new URL(url, "https://app.example").searchParams.get("url")).toBe(legacy);
expect(buildImageProxyUrl("https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/result.png", { width: 400 })).toMatch(/^\/api\/image-proxy\?/);
});
test("browser image fallbacks remain on one stable archive URL instead of retrying the old source", () => {
const delivery = serverLoader({ window: { location: { origin: "https://app.example" } } })("lib/image-delivery.ts");
const fallback = delivery.buildImageFallbackUrl(legacy);
expect(fallback).toMatch(/^https:\/\/app\.example\/api\/image-proxy\?/);
expect(delivery.buildImageFallbackUrl(fallback)).toBe(fallback);
expect(delivery.buildImageFallbackUrl(permanent)).toBe(permanent);
});
function archiveLoader(rows: Array<{ sourceUrl: string; targetUrl: string | null }>) {
return serverLoader({
"@/lib/db": { db: { select: () => ({ from: () => ({ where: async () => rows }) }), insert: () => { throw new Error("Legacy sources must not be queued"); } } },
"@/lib/db/schema": { imageArchiveTask: { sourceUrl: "source", targetUrl: "target" } },
})("lib/storage/image-archive-db.ts");
}
test("archive lookup serves signed legacy links from the durable Qiniu alias", async () => {
const archive = archiveLoader([{ sourceUrl: legacy, targetUrl: permanent }]);
expect(await archive.resolveLegacyAliyunStorageUrl(legacy + "?Expires=1&Signature=expired")).toBe(permanent);
expect(await archive.resolveLegacyAliyunStorageUrl(permanent)).toBe(permanent);
expect(await archive.enqueueImageArchiveTask({ sourceUrl: legacy })).toBeNull();
});
test("legacy files without a durable copy fail permanently instead of fetching the old bucket", async () => {
const archive = archiveLoader([]);
await expect(archive.resolveLegacyAliyunStorageUrl(legacy)).rejects.toThrow("HTTP 410");
let fetched = 0;
const bytes = serverLoader({
"./image-archive-db": { resolveLegacyAliyunStorageUrl: archive.resolveLegacyAliyunStorageUrl },
fetch: () => { fetched++; throw new Error("Unexpected old-bucket request"); },
})("lib/storage/image-byte-cache.ts");
await expect(bytes.fetchImageBytes(legacy)).rejects.toThrow(RETIRED_STORAGE_MESSAGE);
expect(fetched).toBe(0);
});
test("byte reads use the resolved Qiniu object and preserve its bytes", async () => {
const seen: string[] = [];
const target = permanent + "?acceptance=" + crypto.randomUUID();
const original = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 1]);
const bytes = serverLoader({
"./image-archive-db": { resolveLegacyAliyunStorageUrl: async () => target },
fetch: async (url: string) => { seen.push(url); return new Response(original, { headers: { "content-type": "image/png" } }); },
})("lib/storage/image-byte-cache.ts");
expect((await bytes.fetchImageBytes(legacy)).buffer.equals(original)).toBe(true);
expect(seen).toEqual([target]);
});
test("preview requests redirect to Qiniu after resolving a legacy alias", async () => {
const route = serverLoader({
"next/server": { NextResponse },
"@/lib/storage/image-archive-db": { resolveLegacyAliyunStorageUrl: async () => permanent },
fetch: () => { throw new Error("Legacy previews must not fetch the old bucket"); },
})("app/api/image-proxy/route.ts");
const response = await route.GET(new NextRequest("https://app.example/api/image-proxy?url=" + encodeURIComponent(legacy), { headers: { "sec-fetch-dest": "image" } }));
expect(response.status).toBe(307);
expect(response.headers.get("location")).toBe(permanent);
});
test("original downloads resolve the archived file without reading the legacy source", async () => {
const route = serverLoader({
"next/server": { NextResponse },
"@/lib/storage/image-archive-db": { resolveLegacyAliyunStorageUrl: async () => permanent },
"@/lib/storage/original-image-download": { resolveOriginalCdnDownload: async (url: string) => {
expect(url).toBe(permanent);
return { delivery: "cdn", downloadUrl: permanent + "?attname=original.png", filename: "original.png" };
} },
fetch: () => { throw new Error("Unexpected source request"); },
})("app/api/image-proxy/route.ts");
const response = await route.GET(new NextRequest("https://app.example/api/image-proxy?original=1&download=resolve&url=" + encodeURIComponent(legacy)));
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({ delivery: "cdn", filename: "original.png" });
});
test("unrecoverable preview and download requests return a cause and recommendation immediately", async () => {
let fetched = 0;
const route = serverLoader({
"next/server": { NextResponse },
"@/lib/storage/image-archive-db": { resolveLegacyAliyunStorageUrl: async () => { throw new Error("HTTP 410: " + RETIRED_STORAGE_MESSAGE); } },
fetch: () => { fetched++; throw new Error("Unexpected source request"); },
})("app/api/image-proxy/route.ts");
for (const query of ["", "original=1&download=resolve&"]) {
const response = await route.GET(new NextRequest("https://app.example/api/image-proxy?" + query + "url=" + encodeURIComponent(legacy)));
const data = await response.json();
expect(response.status).toBe(410);
expect(data.failure).toMatchObject({ code: "STORAGE_RETIRED", retryable: false });
expect(data.failure.recommendation).toContain("重新上传");
expect(data.error).toContain(data.requestId);
}
expect(fetched).toBe(0);
});
test("retired model inputs are reported as missing files rather than a model-service failure", async () => {
const { getRequestFailureDetails } = await import("@/lib/request-error");
expect(getRequestFailureDetails(new Error("HTTP 410: " + RETIRED_STORAGE_MESSAGE))).toMatchObject({ code: "SOURCE_STORAGE_RETIRED", outcome: "failed" });
});