195 lines
7.9 KiB
TypeScript
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,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|