ai-chatbot/tests/routes/canvas-zip-download.test.ts

299 lines
24 KiB
TypeScript
Raw Permalink 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 { NextRequest, NextResponse } from "next/server";
import { mkdtemp, rm, readFile, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import JSZip from "jszip";
import { serverLoader } from "../helpers/canvas-download-server-loader";
import { parseDownloadByteRange } from "@/lib/canvas/download-byte-range";
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, HeadObjectCommand, PutObjectCommand, UploadPartCommand } from "@aws-sdk/client-s3";
const runtimeWork = { trackRuntimeWork: async (run: () => Promise<void>) => run() };
test("ranges reject invalid offsets and preserve normal, open and suffix byte ranges", () => {
expect(parseDownloadByteRange("bytes=0-3", 20)).toEqual({start:0,end:3});
expect(parseDownloadByteRange("bytes=10-", 20)).toEqual({start:10,end:19});
expect(parseDownloadByteRange("bytes=-4", 20)).toEqual({start:16,end:19});
for (const range of ["bytes=-0","bytes=20-","bytes=4-2","bytes=1-2,5-6","invalid"])
expect(()=>parseDownloadByteRange(range,20)).toThrow();
});
test("multi-select ZIP admission is immediate; complete archives survive restart and support authorized range retries", async () => {
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-test-')); let owner:string|null='owner';let fetches=0;
const callbacks:Array<()=>Promise<void>>=[];
const mocks={
'@/lib/observability/active-work':runtimeWork,'@/app/(auth)/auth': {auth:async()=>owner?{user:{id:owner}}:null},
'next/server': {NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
'@/lib/canvas/photoshop-safe-download':{fetchCanvasDownloadAsset:async({index}:any)=>{
fetches++;return {ok:true,filename:`${index}-花瓶.png`,bytes:Buffer.from(`image-${index}`),contentType:'image/png'};
}},
'@/lib/storage/canvas-zip-delivery':{publishCanvasZip:async()=>null,removeCanvasZipObject:async()=>{}},
};
const loader=()=>serverLoader(mocks,{CANVAS_DOWNLOAD_ZIP_DIR:dir})('app/api/canvas/download-zip-task/route.ts');
const request=(query:string,headers:Record<string,string>={})=>new NextRequest('http://localhost/api/canvas/download-zip-task?'+query,{headers});
const taskId=randomUUID();const files=[{url:'https://provider.example/1.png'},{url:'https://provider.example/2.png'}];
try {
const route=loader();
const response=await route.POST(new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId,folderName:'作品',files})}));
expect(response.status).toBe(202);expect(fetches).toBe(0);
expect((await route.GET(request(`taskId=${taskId}&download=1`))).status).toBe(409);
while (callbacks.length) await callbacks.shift()!();
const state=await (await route.GET(request(`taskId=${taskId}`))).json();
expect(state.task).toMatchObject({status:'completed',successCount:2,failedCount:0});
const complete=await route.GET(request(`taskId=${taskId}&download=1`));
const data=Buffer.from(await complete.arrayBuffer());const zip=await JSZip.loadAsync(data);
expect((await zip.file('作品/0-花瓶.png')!.async('nodebuffer')).toString()).toBe('image-0');
expect(complete.headers.get('content-length')).toBe(String(data.length));
const range=await route.GET(request(`taskId=${taskId}&download=1`,{range:'bytes=0-3'}));
expect(range.status).toBe(206);expect(Buffer.from(await range.arrayBuffer())).toEqual(data.subarray(0,4));
expect((await route.GET(request(`taskId=${taskId}&download=1`,{range:'bytes=999999-'}))).status).toBe(416);
expect((await route.GET(request(`taskId=${taskId}&download=1`,{range:'bytes=0-3','if-range':'"old-file"'}))).status).toBe(200);
const fresh=loader();expect((await (await fresh.GET(request(`taskId=${taskId}`))).json()).task.status).toBe('completed');expect(fetches).toBe(2);
const manifest=await readFile(join(dir,taskId,'task.json'),'utf8');expect(manifest).not.toContain('cookie');
owner='stranger';expect((await fresh.GET(request(`taskId=${taskId}`))).status).toBe(404);
owner=null;expect((await fresh.GET(request(`taskId=${taskId}`))).status).toBe(401);
} finally {await rm(dir,{recursive:true,force:true});}
});
test("all-source failures never produce an empty successful ZIP; retries reuse the task and resolve sources again", async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-retry-'));const callbacks:Array<()=>Promise<void>>=[];let available=false;
const route=serverLoader({
'@/lib/observability/active-work':runtimeWork,'@/app/(auth)/auth':{auth:async()=>({user:{id:'owner'}})},
'next/server':{NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
'@/lib/canvas/photoshop-safe-download':{fetchCanvasDownloadAsset:async()=>available?{ok:true,filename:'one.png',bytes:Buffer.from('one'),contentType:'image/png'}:{ok:false,error:'expired provider URL'}},
'@/lib/storage/canvas-zip-delivery':{publishCanvasZip:async()=>null},
},{CANVAS_DOWNLOAD_ZIP_DIR:dir})('app/api/canvas/download-zip-task/route.ts');
const id=randomUUID();const post=()=>new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId:id,retry:true,files:[{url:'https://provider.example/expired.png'}]})});
try {
await route.POST(post());while (callbacks.length) await callbacks.shift()!();
let state=await(await route.GET(new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}`))).json();
expect(state.task.status).toBe('failed');expect(state.task.downloadUrl).toBe('');
available=true;await route.POST(post());while (callbacks.length) await callbacks.shift()!();
state=await(await route.GET(new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}`))).json();
expect(state.task).toMatchObject({id,status:'completed',successCount:1});
} finally {await rm(dir,{recursive:true,force:true});}
});
test("a stale process lock can be recovered, while a live worker cannot be duplicated",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-lock-'));const id=randomUUID();
const store=serverLoader({},{CANVAS_DOWNLOAD_ZIP_DIR:dir})('lib/canvas/download-zip-task-store.ts');
try{
const release=await store.claimZipTask(id);expect(release).toBeTruthy();expect(await store.claimZipTask(id)).toBeNull();await release();
await mkdir(join(dir,id,'lock'));await writeFile(join(dir,id,'lock','owner.json'),JSON.stringify({pid:2147483647}));
const recovered=await store.claimZipTask(id);expect(recovered).toBeTruthy();await recovered();
}finally{await rm(dir,{recursive:true,force:true});}
});
test("the browser polls a complete ZIP, preserves an idempotent task after a lost POST response, and waits to start the browser download",async()=>{
let calls=0;let firstId='';let frames:any[]=[];const stored=new Map<string,string>();
const start=serverLoader({
sessionStorage:{getItem:(key:string)=>stored.get(key),setItem:(key:string,value:string)=>stored.set(key,value)},
document:{createElement:()=>({remove(){}}),body:{appendChild:(frame:any)=>frames.push(frame)}},
window:{setTimeout:()=>0},
setTimeout:(fn:()=>void,ms:number)=>{if(ms<10000)queueMicrotask(fn);return 0;},
fetch:async(url:string,init:any)=>{
calls++;
if(init?.method==='POST'){
const payload=JSON.parse(init.body);
if(calls===1){firstId=payload.taskId;throw new Error('response disconnected');}
expect(payload.taskId).toBe(firstId);
return Response.json({task:{id:firstId,status:'pending',progress:0}});
}
expect(frames).toHaveLength(0);
return Response.json({task:{id:firstId,status:'completed',progress:100,successCount:2,failedCount:0,downloadUrl:'/api/canvas/download-zip-task?taskId='+firstId+'&download=1'}});
},
})('lib/client/canvas-zip-download.ts');
const result=await start.downloadCanvasSelectionZip({folderName:'作品',files:[{url:'https://cdn.example/a.png',name:'a'}],selectionKey:'project:node-1',onProgress:()=>{}});
expect(calls).toBe(3);expect(result.successCount).toBe(2);expect(frames).toHaveLength(1);expect(frames[0].src).toContain('download=1');
});
test("partial source failures are explicit; CDN redirects retain an authenticated local range fallback",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-partial-'));const callbacks:Array<()=>Promise<void>>=[];let owner='owner';
const route=serverLoader({
'@/lib/observability/active-work':runtimeWork,'@/app/(auth)/auth':{auth:async()=>({user:{id:owner}})},
'next/server':{NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
'@/lib/canvas/photoshop-safe-download':{fetchCanvasDownloadAsset:async({index}:any)=>index?{ok:false,error:'source expired'}:{ok:true,filename:'original.png',bytes:Buffer.from('original'),contentType:'image/png'}},
'@/lib/storage/canvas-zip-delivery':{publishCanvasZip:async()=>({url:'https://cdn.example/complete.zip',key:'canvas-downloads/x.zip',storageConfigId:'config'})},
},{CANVAS_DOWNLOAD_ZIP_DIR:dir})('app/api/canvas/download-zip-task/route.ts');
const id=randomUUID();const request=(extra='')=>new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}${extra}`);
try{
await route.POST(new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId:id,files:[{url:'https://provider.example/1'},{url:'https://provider.example/2'}]})}));
while (callbacks.length) await callbacks.shift()!();
const state=await(await route.GET(request())).json();expect(state.task).toMatchObject({status:'completed',successCount:1,failedCount:1});
const cdn=await route.GET(request('&download=1'));expect(cdn.status).toBe(307);expect(cdn.headers.get('location')).toBe('https://cdn.example/complete.zip');
const local=await route.GET(request('&download=1&local=1'));const zip=await JSZip.loadAsync(await local.arrayBuffer());
expect(Object.keys(zip.files).some(name=>name.endsWith('_download-failures.txt'))).toBe(true);
owner='stranger';expect((await route.GET(request('&download=1'))).status).toBe(404);
}finally{await rm(dir,{recursive:true,force:true});}
});
test("recovery validates staged files instead of treating missing bytes as successful images",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-missing-'));const callbacks:Array<()=>Promise<void>>=[];const id=randomUUID();
const load=serverLoader({
'@/lib/observability/active-work':runtimeWork,'@/app/(auth)/auth':{auth:async()=>({user:{id:'owner'}})},
'next/server':{NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
'@/lib/canvas/photoshop-safe-download':{fetchCanvasDownloadAsset:async()=>({ok:false,error:'source expired'})},
'@/lib/storage/canvas-zip-delivery':{publishCanvasZip:async()=>null},
},{CANVAS_DOWNLOAD_ZIP_DIR:dir});
try{
const store=load('lib/canvas/download-zip-task-store.ts');
await store.saveZipTask({id,userId:'owner',folderName:'test',files:[{url:'https://provider.example/a',prepared:'0.image',filename:'a.png'}],totalCount:1,processedCount:1,successCount:1,failedCount:0,compressPercent:0,status:'running',stage:'preparing',createdAt:Date.now(),updatedAt:Date.now(),expiresAt:Date.now()+60000});
const route=load('app/api/canvas/download-zip-task/route.ts');const req=()=>new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}`);
await route.GET(req());while (callbacks.length) await callbacks.shift()!();
const state=await(await route.GET(req())).json();expect(state.task).toMatchObject({status:'failed',successCount:0,failedCount:1,downloadUrl:''});
}finally{await rm(dir,{recursive:true,force:true});}
});
test("large ZIP writing checks current free space as staged input bytes are released",async()=>{
const fs=await import('node:fs/promises');const {createReadStream}=await import('node:fs');
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-space-'));let checks=0;
const module=serverLoader({'node:fs/promises':{...fs,statfs:async()=>{checks++;return {bsize:1,bavail:1024**3+33*1024**2};}}},{CANVAS_DOWNLOAD_ZIP_DIR:dir})('lib/canvas/download-zip-stream.ts');
try{
const source=join(dir,'source.bin');const handle=await fs.open(source,'w');await handle.truncate(48*1024**2);await handle.close();
const zip=new JSZip();zip.file('large.bin',createReadStream(source));
const result=await module.writeDownloadZip(zip,randomUUID(),()=>{});
expect(result.bytes).toBeGreaterThan(48*1024**2);expect(checks).toBeGreaterThan(4);
}finally{await rm(dir,{recursive:true,force:true});}
});
test("slow expired-object cleanup cannot keep the ZIP admission request open",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-cleanup-'));const callbacks:Array<()=>Promise<void>>=[];
const mocks:Record<string,unknown>={
'@/lib/observability/active-work':runtimeWork,'@/app/(auth)/auth':{auth:async()=>({user:{id:'owner'}})},
'next/server':{NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
'@/lib/canvas/photoshop-safe-download':{},'@/lib/storage/canvas-zip-delivery':{},
};
const load=serverLoader(mocks,{CANVAS_DOWNLOAD_ZIP_DIR:dir});const store=load('lib/canvas/download-zip-task-store.ts');
mocks['@/lib/canvas/download-zip-task-store']={...store,cleanupZipTasks:()=>new Promise(()=>{})};
try{
const route=load('app/api/canvas/download-zip-task/route.ts');
const result=await Promise.race([route.POST(new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({files:[{url:'https://cdn.example/a.png'}]})})),new Promise(resolve=>setTimeout(()=>resolve('blocked'),1000))]);
expect(result).not.toBe('blocked');expect((result as Response).status).toBe(202);
}finally{await rm(dir,{recursive:true,force:true});}
});
test("large ZIPs use bounded parallel parts and publish only the complete ordered archive", async () => {
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-parts-'));
const bytes=Buffer.alloc(25*1024**2+19,0x5a);bytes[16*1024**2]=0xff;
const file=join(dir,'large.zip');await writeFile(file,bytes);
const parts=new Map<number,Buffer>();let inFlight=0;let peak=0;let finished=false;let destroyed=false;let trailers='';
const progress:number[]=[];
const client={config:{requestChecksumCalculation:async()=>''},destroy(){destroyed=true;},async send(command:any){
if(command instanceof HeadObjectCommand)throw Object.assign(new Error('missing'),{$metadata:{httpStatusCode:404}});
if(command instanceof CreateMultipartUploadCommand){expect(command.input.ContentDisposition).toContain('attachment');return {UploadId:'upload'};}
if(command instanceof UploadPartCommand){
const input=command.input;inFlight++;peak=Math.max(peak,inFlight);expect(input.ContentLength).toBeLessThanOrEqual(8*1024**2);
await new Promise(resolve=>setTimeout(resolve,input.PartNumber===1?30:20));
expect(Buffer.isBuffer(input.Body)).toBe(true);
parts.set(input.PartNumber!,Buffer.from(input.Body as Buffer));inFlight--;return {ETag:`part-${input.PartNumber}`};
}
expect(command).toBeInstanceOf(CompleteMultipartUploadCommand);
expect(command.input.MultipartUpload.Parts.map((p:any)=>p.PartNumber)).toEqual([1,2,3,4]);
expect(Buffer.concat(command.input.MultipartUpload.Parts.map((p:any)=>parts.get(p.PartNumber)!)).equals(bytes)).toBe(true);
finished=true;trailers=await client.config.requestChecksumCalculation();return {};
}};
const publish=serverLoader({
'@/lib/db/queries':{getDefaultStorageConfig:async()=>({id:'storage'})},
'./service':{toDbS3UploadConfig:()=>({provider:'qiniu-kodo',bucket:'bucket',publicUrl:'https://cdn.example'}),createS3Client:()=>client,buildS3PublicUrl:(_:any,key:string)=>'https://cdn.example/'+key},
fetch:async(url:string,init:any)=>{expect(finished).toBe(true);expect(init.method).toBe('HEAD');expect(new URL(url).searchParams.get('v')).toBe(String(bytes.length));return new Response(null,{headers:{'content-length':String(bytes.length)}});},
})('lib/storage/canvas-zip-delivery.ts').publishCanvasZip;
try{
const result=await publish({id:randomUUID(),path:file,bytes:bytes.length,disposition:'attachment; filename="large.zip"',onProgress:async(n:number)=>{progress.push(n);}});
expect(result.url).toContain('https://cdn.example/canvas-downloads/');expect(peak).toBeGreaterThan(1);expect(peak).toBeLessThanOrEqual(3);expect(progress.at(-1)).toBe(bytes.length);
expect(progress).toEqual([...progress].sort((a,b)=>a-b));expect(trailers).toBe('WHEN_REQUIRED');expect(destroyed).toBe(true);
}finally{await rm(dir,{recursive:true,force:true});}
});
test("failed multipart uploads abort their parts and retain the original ZIP with actionable advice",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-abort-'));const file=join(dir,'large.zip');const bytes=17*1024**2;
const handle=await (await import('node:fs/promises')).open(file,'w');await handle.truncate(bytes);await handle.close();
let aborted=false;let completed=false;
const client={config:{},destroy(){},async send(command:any){
if(command instanceof HeadObjectCommand)throw Object.assign(new Error('missing'),{$metadata:{httpStatusCode:404}});
if(command instanceof CreateMultipartUploadCommand)return {UploadId:'upload'};
if(command instanceof UploadPartCommand){if(command.input.PartNumber===2)throw new DOMException('part timeout','TimeoutError');return {ETag:'part'};}
if(command instanceof AbortMultipartUploadCommand){aborted=true;return {};}
completed=true;return {};
}};
const publish=serverLoader({
'@/lib/db/queries':{getDefaultStorageConfig:async()=>({id:'storage'})},
'./service':{toDbS3UploadConfig:()=>({provider:'qiniu-kodo',bucket:'bucket',publicUrl:'https://cdn.example'}),createS3Client:()=>client},
})('lib/storage/canvas-zip-delivery.ts').publishCanvasZip;
try{
const result=await publish({id:randomUUID(),path:file,bytes,disposition:'attachment'});
expect(result.url).toBeNull();expect(result.error).toContain('传送至七牛时超时');expect(result.error).toContain('继续传送原 ZIP');
expect(aborted).toBe(true);expect(completed).toBe(false);expect((await (await import('node:fs/promises')).stat(file)).size).toBe(bytes);
}finally{await rm(dir,{recursive:true,force:true});}
});
test("retrying CDN verification reuses an already uploaded ZIP without uploading it again",async()=>{
let uploads=0;let available=false;
const client={config:{},destroy(){},async send(command:any){
if(command instanceof HeadObjectCommand)return {ContentLength:100};
if(command instanceof PutObjectCommand||command instanceof CreateMultipartUploadCommand)uploads++;
throw new Error('Unexpected upload');
}};
const publish=serverLoader({
'@/lib/db/queries':{getDefaultStorageConfig:async()=>({id:'storage'})},
'./service':{toDbS3UploadConfig:()=>({provider:'qiniu-kodo',bucket:'bucket',publicUrl:'https://cdn.example'}),createS3Client:()=>client,buildS3PublicUrl:(_:any,key:string)=>'https://cdn.example/'+key},
fetch:async()=>new Response(null,{status:available?200:404,headers:{'content-length':available?'100':'0'}}),
})('lib/storage/canvas-zip-delivery.ts').publishCanvasZip;
const params={id:randomUUID(),path:'unused-already-published.zip',bytes:100,disposition:'attachment'};
const first=await publish(params);expect(first.url).toBeNull();expect(first.error).toContain('七牛下载链接尚未可用');
available=true;expect((await publish(params)).url).toContain('https://cdn.example/');expect(uploads).toBe(0);
});
test("CDN failures never silently start local downloads; retries reuse the ZIP and keep it resumable beyond one hour",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-cdn-retry-'));const callbacks:Array<()=>Promise<void>>=[];
let fetches=0;let publications=0;let available=false;let now=Date.now();
let activeWork=0;
const runtimeWork={trackRuntimeWork:async(run:()=>Promise<void>)=>{activeWork++;try{return await run();}finally{activeWork--;}}};
class Clock extends Date{static now(){return now;}}
const load=serverLoader({
Date:Clock,'@/lib/observability/active-work':runtimeWork,'@/app/(auth)/auth':{auth:async()=>({user:{id:'owner'}})},
'next/server':{NextResponse,after:(run:()=>Promise<void>)=>callbacks.push(run)},
'@/lib/canvas/photoshop-safe-download':{fetchCanvasDownloadAsset:async()=>{fetches++;return {ok:true,filename:'original.png',bytes:Buffer.from('original'),contentType:'image/png'};}},
'@/lib/storage/canvas-zip-delivery':{publishCanvasZip:async()=>{expect(activeWork).toBe(1);publications++;return {url:available?'https://cdn.example/complete.zip':null,key:'canvas-downloads/key.zip',storageConfigId:'storage',error:'ZIP 已打包,传送至七牛时超时;请再次点击下载继续原 ZIP,不会重新生成图片。'};},removeCanvasZipObject:async()=>{}},
},{CANVAS_DOWNLOAD_ZIP_DIR:dir});
const route=load('app/api/canvas/download-zip-task/route.ts');const id=randomUUID();
const post=()=>new NextRequest('http://localhost/api/canvas/download-zip-task',{method:'POST',body:JSON.stringify({taskId:id,retry:true,files:[{url:'https://cdn.example/original.png'}]})});
const request=(suffix='')=>new NextRequest(`http://localhost/api/canvas/download-zip-task?taskId=${id}${suffix}`,{headers:{range:'bytes=0-3'}});
try{
await route.POST(post());while(callbacks.length)await callbacks.shift()!();
const failed=(await(await route.GET(request())).json()).task;
expect(failed).toMatchObject({id,status:'failed',successCount:1,downloadUrl:''});expect(failed.error).toContain('不会重新生成图片');
expect((await route.GET(request('&download=1'))).status).toBe(409);
const before=await readFile(join(dir,id,'task.json'),'utf8');const original=Buffer.from(await readFile(JSON.parse(before).zipFile.path));
available=true;await route.POST(post());while(callbacks.length)await callbacks.shift()!();
expect(fetches).toBe(1);expect(publications).toBe(2);
const manifest=JSON.parse(await readFile(join(dir,id,'task.json'),'utf8'));expect(await readFile(manifest.zipFile.path)).toEqual(original);
delete manifest.cdnUrl;
await load('lib/canvas/download-zip-task-store.ts').saveZipTask(manifest);
await route.POST(post());while(callbacks.length)await callbacks.shift()!();
expect(fetches).toBe(1);expect(publications).toBe(3);
expect(activeWork).toBe(0);
now+=2*60*60_000;
expect((await route.GET(request('&download=1'))).status).toBe(307);
const local=await route.GET(request('&download=1&local=1'));expect(local.status).toBe(206);expect(Buffer.from(await local.arrayBuffer())).toEqual(original.subarray(0,4));
}finally{await rm(dir,{recursive:true,force:true});}
});
test("small ZIP delivery preserves the complete file and download filename",async()=>{
const dir=await mkdtemp(join(tmpdir(),'canvas-zip-small-'));const file=join(dir,'small.zip');const bytes=Buffer.from('PK small complete archive');await writeFile(file,bytes);
let written=false;
const client={config:{},destroy(){},async send(command:any){
if(command instanceof HeadObjectCommand)throw Object.assign(new Error('missing'),{$metadata:{httpStatusCode:404}});
expect(command).toBeInstanceOf(PutObjectCommand);expect(command.input.ContentLength).toBe(bytes.length);expect(command.input.ContentDisposition).toContain('small.zip');
const chunks:Buffer[]=[];for await(const chunk of command.input.Body)chunks.push(Buffer.from(chunk));
expect(Buffer.concat(chunks).equals(bytes)).toBe(true);written=true;return {};
}};
const publish=serverLoader({
'@/lib/db/queries':{getDefaultStorageConfig:async()=>({id:'storage'})},
'./service':{toDbS3UploadConfig:()=>({provider:'qiniu-kodo',bucket:'bucket',publicUrl:'https://cdn.example'}),createS3Client:()=>client,buildS3PublicUrl:(_:any,key:string)=>'https://cdn.example/'+key},
fetch:async()=>{expect(written).toBe(true);return new Response(null,{headers:{'content-length':String(bytes.length)}});},
})('lib/storage/canvas-zip-delivery.ts').publishCanvasZip;
try{expect((await publish({id:randomUUID(),path:file,bytes:bytes.length,disposition:'attachment; filename="small.zip"'})).url).toContain('https://cdn.example/');}
finally{await rm(dir,{recursive:true,force:true});}
});