// services/api/test/reset.ts // e2e 共用隔离工具:每个 e2e spec 的 beforeAll 调用 resetDb(),防止 spec 间 // 唯一约束冲突与脏数据。 // 实现:SET FOREIGN_KEY_CHECKS=0 包裹逐表 TRUNCATE(users 被 sessions/audit_logs // 引用,即便子表已清空,存在 FK 约束本身即令 TRUNCATE 失败,必须关检查); // 表序仍按 FK 安全顺序排列(子表在前),关检查失败时也不至于中途卡死。 // 顺序 await 在 Prisma 连接池上复用同一空闲连接,会话级 FOREIGN_KEY_CHECKS // 对后续 TRUNCATE 生效;finally 恢复检查并断开,失败也不泄漏连接。 // Task 18 起 e2e 会向 MinIO 写入真实对象(staging/originals):同一函数内追加 // `photo-live/dev` 前缀对象清理(reset.ts 文件头 TODO 的落实——只清本前缀, // packages/storage 的 test/-/ 前缀对象不受影响,两包测试可并行)。 import { DeleteObjectsCommand, ListObjectsV2Command, S3Client, } from '@aws-sdk/client-s3'; import { PrismaClient } from '@photo/db-cn'; export const RESET_TABLES: readonly string[] = [ // 照片与上传域 + outbox(Task 10):均为扁平字符串外键(无 FK 约束), // 顺序按逻辑依赖排列(事件/作业/变体/会话先于 photos)。 'outbox_events', 'photo_jobs', 'photo_variants', 'upload_sessions', 'photos', // 水印域(P4a Task 3):任务/配置账本先于 albums(watermarkVersion 等位列 // 随 albums 清零)。 'watermark_jobs', 'watermark_configs', // 相册域(Task 9):子表(翻译/资产/成员)在前,albums 在后;相册域为扁平 // 字符串外键(无 FK 约束),此处顺序仅为逻辑可读,不依赖 FK 检查开关。 'category_translations', 'categories', 'album_assets', 'album_translations', 'album_members', 'albums', // 身份域(Task 8)。 'audit_logs', 'sessions', 'users', ]; /** e2e 对象隔离边界:与 src/uploads/support.ts 的 OBJECT_KEY_ROOT 一致。 */ const RESET_OBJECT_PREFIX = 'photo-live/dev/'; /** 删除 MinIO 中 RESET_OBJECT_PREFIX 前缀的全部对象(分页列举 + 批量删除)。 */ async function resetMinioObjects(): Promise { const required = [ 'S3_ENDPOINT', 'S3_REGION', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'S3_BUCKET', ] as const; const missing = required.filter((key) => !process.env[key]); if (missing.length > 0) { throw new Error(`resetMinioObjects: 缺少环境变量 ${missing.join(', ')}`); } const client = new S3Client({ endpoint: process.env.S3_ENDPOINT, region: process.env.S3_REGION, credentials: { accessKeyId: process.env.S3_ACCESS_KEY as string, secretAccessKey: process.env.S3_SECRET_KEY as string, }, forcePathStyle: true, }); const bucket = process.env.S3_BUCKET as string; try { for (;;) { const listed = await client.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: RESET_OBJECT_PREFIX, MaxKeys: 1000, }), ); const objects = (listed.Contents ?? []).filter( (obj) => typeof obj.Key === 'string', ); if (objects.length === 0) { break; } await client.send( new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: objects.map((obj) => ({ Key: obj.Key as string })), Quiet: true, }, }), ); if (!listed.IsTruncated) { break; } } } finally { await client.destroy(); } } export async function resetDb(): Promise { await resetMinioObjects(); const prisma = new PrismaClient(); try { await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0'); for (const table of RESET_TABLES) { await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${table}`); } } finally { await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); await prisma.$disconnect(); } }