Files
doodle-store/apps/server/test/worksheet-import-database.test.ts

195 lines
7.9 KiB
TypeScript

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,
);
},
);
},
);