import 'reflect-metadata'; import assert from 'node:assert/strict'; import { createHash, randomUUID } from 'node:crypto'; import { test } from 'node:test'; import { PrismaService } from '../src/database/prisma.service'; import { WorksheetImporter } from '../src/imports/worksheet-import'; import { testConfig } from './test-config'; import { worksheetFixture as fixture } from './worksheet-import.fixture'; const databaseUrl = process.env.TEST_DATABASE_URL; test( '真实 PostgreSQL:题型元数据导入、待迁移资产、对账、幂等与整批回滚', { skip: !databaseUrl }, async (context) => { const url = new URL(databaseUrl!); assert.ok(['127.0.0.1', 'localhost'].includes(url.hostname)); assert.match(url.pathname, /^\/doodle_auth_test_[a-f0-9]{32}$/); const db = new PrismaService({ ...testConfig, databaseUrl: databaseUrl!, }); context.after(() => db.onModuleDestroy()); const prisma = db.client; const importer = new WorksheetImporter(db); await prisma.category.create({ data: { status: 'active', id: fixture.category, name: '导入验证', icon: '📐', color: '#fff', }, }); const run = (rows: unknown[], dryRun = false, metadataOnly = true) => { const source = rows.map((row) => JSON.stringify(row)).join('\n'); return importer.importExport(source, { dryRun, metadataOnly, requestId: randomUUID(), sourceSha256: createHash('sha256').update(source).digest('hex'), }); }; const counts = async () => ({ assets: await prisma.asset.count(), worksheets: await prisma.worksheet.count(), audits: await prisma.adminAuditLog.count(), }); await context.test( '默认拒绝未就绪预览;预演及无效分类不写任何数据', async () => { const before = await counts(); await assert.rejects(run([fixture], false, false), { status: 409, }); await assert.rejects( run([{ ...fixture, category: 'missing-category' }]), { status: 400 }, ); const preview = await run([fixture], true); assert.equal(preview.inserted, 1); assert.equal(preview.pendingPreviews, 1); assert.equal(preview.readyForPublicUse, false); assert.deepEqual(await counts(), before); }, ); await context.test( '同批保存原 ID、原始计数、时间、旧年级及 pending 资产,不伪造资源或年级关系', async () => { const report = await run([{ ...fixture, grade: 23 }]); assert.equal(report.inserted, 1); assert.equal(report.assetsInserted, 1); assert.equal(report.gradeRelationsInserted, 0); assert.equal(report.warnings[0]?.originalValue, 23); const row = await prisma.worksheet.findUniqueOrThrow({ where: { id: fixture._id }, }); assert.equal(row.grade, 23); assert.equal(row.downloads, 23n); assert.equal(row.downloadsSeed, 12n); assert.equal( row.createdAt.toISOString(), fixture.createdAt.$date, ); assert.equal( row.updatedAt.toISOString(), fixture.updatedAt.$date, ); assert.equal(row.firstPublishedAt, null); assert.equal(row.contentUpdatedAt, null); assert.equal(row.status, 'active'); const asset = await prisma.asset.findUniqueOrThrow({ where: { id: row.previewAssetId }, }); assert.equal(asset.sourceRef, fixture.previewImg); assert.equal( asset.sourceRefHash, createHash('sha256') .update(fixture.previewImg) .digest('hex'), ); assert.equal(asset.status, 'pending'); assert.equal(asset.objectKey, null); assert.equal(asset.bucket, null); assert.equal(asset.mimeType, null); assert.equal( await prisma.worksheetGrade.count({ where: { worksheetId: fixture._id }, }), 0, ); assert.equal( await prisma.worksheetResource.count({ where: { worksheetId: fixture._id }, }), 0, ); const audit = await prisma.adminAuditLog.findFirstOrThrow({ where: { resourceId: fixture._id, action: 'worksheet.import', }, }); assert.equal(audit.actorType, 'migration'); assert.equal( (audit.afterData as { downloads: string }).downloads, '23', ); }, ); await context.test( '重导不新增或更新时间,共享预览复用资产;不同旧快照禁止覆盖', async () => { const before = await counts(); const stored = await prisma.worksheet.findUniqueOrThrow({ where: { id: fixture._id }, }); const repeated = await run([{ ...fixture, grade: 23 }]); assert.equal(repeated.unchanged, 1); assert.equal(repeated.assetsInserted, 0); assert.deepEqual(await counts(), before); assert.deepEqual( await prisma.worksheet.findUniqueOrThrow({ where: { id: fixture._id }, }), stored, ); const shared = await run([ { ...fixture, _id: 'worksheet-import-shared' }, ]); assert.equal(shared.assetsInserted, 0); assert.equal(shared.assetsReused, 1); assert.equal(await prisma.asset.count(), before.assets); await assert.rejects( run([{ ...fixture, grade: 23, downloads: 24 }]), { status: 409 }, ); assert.equal( ( await prisma.worksheet.findUniqueOrThrow({ where: { id: fixture._id }, }) ).downloads, 23n, ); }, ); await context.test( '后半批冲突会回滚前面新增题型、资产及审计', async () => { const before = await counts(); await assert.rejects( run([ { ...fixture, _id: 'worksheet-import-rollback', previewImg: 'cloud://fixture-env/assets/previews/rollback.jpg', }, { ...fixture, grade: 23, title: '禁止覆盖' }, ]), { status: 409 }, ); assert.deepEqual(await counts(), before); assert.equal( await prisma.worksheet.count({ where: { id: 'worksheet-import-rollback' }, }), 0, ); }, ); }, );