// packages/storage/test/minio.adapter.spec.ts —— MinIO 真实集成测试(依赖 // docker compose 的 photo-live-minio-1 与仓库根 .env 的 S3_* 配置)。 // 每次运行使用唯一前缀 test/-/,避免历史残留对象干扰断言。 import { randomBytes, randomUUID } from 'node:crypto'; import { ErrorCodes } from '@photo/contracts'; import { StorageError, createStorageFromEnv, type MinioStorageAdapter } from '../src/index'; const PREFIX = `test/${Date.now()}-${randomUUID()}`; describe('MinioStorageAdapter(真实 MinIO 集成)', () => { let storage: MinioStorageAdapter; beforeAll(() => { storage = createStorageFromEnv(); }); it('presignPut: 客户端按凭证直接 PUT 后,head 返回精确字节数与 etag', async () => { const key = `${PREFIX}/presign-put.bin`; const body = randomBytes(2048); const credential = await storage.presignPut(key, body.length, 'application/octet-stream', 300); expect(credential.mode).toBe('presigned'); expect(credential.method).toBe('PUT'); expect(credential.headers['Content-Type']).toBe('application/octet-stream'); expect(new Date(credential.expiresAt).getTime()).toBeGreaterThan(Date.now()); const res = await fetch(credential.url, { method: credential.method, headers: credential.headers, body }); expect(res.ok).toBe(true); const head = await storage.head(key); expect(head).not.toBeNull(); expect(head?.bytes).toBe(body.length); expect(head?.etag).toBeTruthy(); expect(head?.contentType).toBe('application/octet-stream'); }); it('put + getStream: 写入后流式读回的字节与写入一致', async () => { const key = `${PREFIX}/put-get.bin`; const body = randomBytes(4096); const { etag } = await storage.put(key, body, 'application/octet-stream'); expect(etag).toBeTruthy(); const stream = await storage.getStream(key); const chunks: Buffer[] = []; for await (const chunk of stream) { chunks.push(Buffer.from(chunk as Uint8Array)); } expect(Buffer.concat(chunks).equals(body)).toBe(true); }); it('copyIfAbsent: 首次 created:true;同参数重复调用幂等 created:false 且 etag 不变', async () => { const srcKey = `${PREFIX}/copy-src.bin`; const destKey = `${PREFIX}/copy-dest.bin`; const body = randomBytes(512); await storage.put(srcKey, body, 'application/octet-stream'); const first = await storage.copyIfAbsent(srcKey, destKey, body.length); expect(first.created).toBe(true); expect(first.etag).toBeTruthy(); const second = await storage.copyIfAbsent(srcKey, destKey, body.length); expect(second.created).toBe(false); expect(second.etag).toBe(first.etag); }); it('copyIfAbsent: dest 已存在但字节数不符 → StorageError(UPLOAD_VERIFY_FAILED)', async () => { const srcKey = `${PREFIX}/mismatch-src.bin`; const destKey = `${PREFIX}/mismatch-dest.bin`; const srcBody = randomBytes(64); await storage.put(srcKey, srcBody, 'application/octet-stream'); // 预置一个不同大小的 dest 对象,制造核验失败 await storage.put(destKey, randomBytes(128), 'application/octet-stream'); const promise = storage.copyIfAbsent(srcKey, destKey, srcBody.length); await expect(promise).rejects.toBeInstanceOf(StorageError); await expect(promise).rejects.toMatchObject({ code: ErrorCodes.UPLOAD_VERIFY_FAILED }); }); it('head: 缺失键返回 null(不抛错)', async () => { const head = await storage.head(`${PREFIX}/missing.bin`); expect(head).toBeNull(); }); it('presignGet: 预签名 URL 取回的字节与对象一致', async () => { const key = `${PREFIX}/presign-get.bin`; const body = randomBytes(1024); await storage.put(key, body, 'application/octet-stream'); const url = await storage.presignGet(key, 300); const res = await fetch(url); expect(res.ok).toBe(true); expect(Buffer.from(await res.arrayBuffer()).equals(body)).toBe(true); }); });