feat: add import post-check templates

This commit is contained in:
Codex
2026-06-29 08:16:48 +08:00
parent eee21f6eed
commit 69f8e28d4e
15 changed files with 954 additions and 28 deletions

View File

@@ -2659,6 +2659,33 @@ async function testTenantContentAssetsAndImports() {
});
assert.equal(deniedImport.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not preview content import');
const deniedTemplate = await request('/api/tenant-content/imports/templates', {
query: { importType: 'questions', format: 'csv' },
expectStatus: 403,
});
assert.equal(deniedTemplate.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not download import templates');
const questionFieldMapping = await request('/api/tenant-content/imports/field-mapping', {
userId: TENANT_ADMIN_USER_ID,
query: { importType: 'questions' },
});
assert.ok(
questionFieldMapping.item?.fields?.some(field => field.field === 'content' && field.required === true),
'question field mapping should describe required content field',
);
assert.ok(
questionFieldMapping.item?.fields?.some(field => field.aliases?.includes('题干')),
'question field mapping should include Chinese aliases for old import operators',
);
const scorelineTemplate = await request('/api/tenant-content/imports/templates', {
userId: TENANT_ADMIN_USER_ID,
query: { importType: 'scoreline', format: 'csv' },
});
assert.equal(scorelineTemplate.item?.fileName, 'scoreline-import-template.csv', 'template endpoint should return deterministic filename');
const scorelineTemplateText = Buffer.from(scorelineTemplate.item?.contentBase64 || '', 'base64').toString('utf8');
assert.ok(scorelineTemplateText.includes('kind,legacyId'), 'CSV template should include scoreline headers');
const oversizedNormalJson = await request('/api/auth/sms/send', {
method: 'POST',
body: {
@@ -2781,11 +2808,29 @@ async function testTenantContentAssetsAndImports() {
'valid import should process all valid questions idempotently',
);
const questionPostCheck = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: { jobId: validPreview.job.id },
});
assert.equal(questionPostCheck.item?.status, 'passed', 'question import post-check should pass after completed import');
assert.equal(questionPostCheck.item?.counts?.processedItemCount, 2, 'question post-check should count processed rows');
assert.equal(questionPostCheck.item?.counts?.collectionBindingCount, 2, 'question post-check should verify collection bindings');
const questionPostCheckStatus = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
query: { jobId: validPreview.job.id },
});
assert.equal(questionPostCheckStatus.item?.importPostCheck?.status, 'passed', 'post-check result should be readable from job summary');
const jobs = await request('/api/tenant-content/imports', {
userId: TENANT_ADMIN_USER_ID,
query: { importType: 'questions', limit: 10 },
});
assert.ok(jobs.items?.some(item => item.id === validPreview.job.id && item.status === 'completed'), 'import job list should include completed job');
assert.ok(
jobs.items?.some(item => item.id === validPreview.job.id && item.status === 'completed' && item.summary?.importPostCheck?.status === 'passed'),
'import job list should include completed job with post-check summary',
);
const importedQuestions = await request('/api/catalog/questions', {
query: { collectionId: ids.questionCollection, limit: 100 },
@@ -2891,6 +2936,14 @@ async function testTenantContentAssetsAndImports() {
});
assert.equal(queuedSyncExecution.code, 'IMPORT_JOB_QUEUED', 'queued import job should not be executed synchronously');
const queuedPostCheck = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: { jobId: asyncQuestionPreview.job.id },
expectStatus: 409,
});
assert.equal(queuedPostCheck.code, 'IMPORT_JOB_NOT_COMPLETED', 'queued import job should not allow post-check before worker completion');
const vocabEntry = await request('/api/tenant-content/content-entries', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',
@@ -2997,6 +3050,14 @@ async function testTenantContentAssetsAndImports() {
(vocabularyImport.item?.insertedCount || 0) + (vocabularyImport.item?.updatedCount || 0) + (vocabularyImport.item?.skippedCount || 0) >= 2,
'vocabulary import should process unit and word idempotently',
);
const vocabularyPostCheck = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: { jobId: vocabularyPreview.job.id },
});
assert.equal(vocabularyPostCheck.item?.status, 'passed', 'vocabulary import post-check should pass');
assert.equal(vocabularyPostCheck.item?.counts?.processedItemCount, 1, 'vocabulary post-check should count unit import items');
assert.equal(vocabularyPostCheck.item?.counts?.vocabularyWordCount >= 1, true, 'vocabulary post-check should verify imported words');
const vocabularyUnits = await request('/api/catalog/vocabulary-units', {
query: { regionId: ids.region },
@@ -3124,6 +3185,13 @@ async function testTenantContentAssetsAndImports() {
(handbookImport.item?.insertedCount || 0) + (handbookImport.item?.updatedCount || 0) + (handbookImport.item?.skippedCount || 0) >= 3,
'handbook import should process subject, chapter, and entry idempotently',
);
const handbookPostCheck = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: { jobId: handbookPreview.job.id },
});
assert.equal(handbookPostCheck.item?.status, 'passed', 'handbook import post-check should pass');
assert.equal(handbookPostCheck.item?.counts?.handbookEntryCount >= 1, true, 'handbook post-check should verify entries');
const handbookSubjects = await request('/api/catalog/handbook-subjects', {
query: { regionId: ids.region },
@@ -3220,6 +3288,13 @@ async function testTenantContentAssetsAndImports() {
(scorelineImport.item?.insertedCount || 0) + (scorelineImport.item?.updatedCount || 0) + (scorelineImport.item?.skippedCount || 0) >= 4,
'scoreline import should process all valid rows',
);
const scorelinePostCheck = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: { jobId: scorelinePreview.job.id },
});
assert.equal(scorelinePostCheck.item?.status, 'passed', 'scoreline import post-check should pass');
assert.equal(scorelinePostCheck.item?.counts?.scorelineRecordExistingCount, 1, 'scoreline post-check should verify record target');
const importedScorelineRecords = await request('/api/scoreline/records', {
query: { regionId: ids.region, year: 2025, pageSize: 50 },
@@ -3374,6 +3449,13 @@ async function testTenantContentAssetsAndImports() {
(videoImport.item?.insertedCount || 0) + (videoImport.item?.updatedCount || 0) + (videoImport.item?.skippedCount || 0) >= 2,
'video import should process video and question binding',
);
const videoPostCheck = await request('/api/tenant-content/imports/post-check', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: { jobId: videoPreview.job.id },
});
assert.equal(videoPostCheck.item?.status, 'passed', 'video import post-check should pass');
assert.equal(videoPostCheck.item?.counts?.videoBindingCount >= 1, true, 'video post-check should verify question video bindings');
const questionVideosAfterImport = await request(`/api/questions/${ids.question}/videos`);
assert.ok(