forked from wangziqi/gongxue-base
feat: add question export foundation
This commit is contained in:
@@ -2253,6 +2253,16 @@ async function testTenantContentAdmin() {
|
||||
correctOptionIndices: [1],
|
||||
answerText: '4',
|
||||
explanation: '基础加法。',
|
||||
subQuestions: [
|
||||
{
|
||||
type: 'choice',
|
||||
content: '集成测试子题:1 + 1 = ?',
|
||||
options: ['1', '2'],
|
||||
correctOptionIndices: [1],
|
||||
answerText: '2',
|
||||
explanation: '子题基础加法。',
|
||||
},
|
||||
],
|
||||
status: 'published',
|
||||
examMarkers: { schoolName: '集成测试学院', salesIntent: true },
|
||||
},
|
||||
@@ -2312,6 +2322,118 @@ async function testTenantContentAdmin() {
|
||||
});
|
||||
assert.ok(publicCreatedCollections.items?.some(item => item.id === collection.item.id), 'catalog should expose created collection');
|
||||
|
||||
const exportDenied = await request('/api/tenant-content/exports/questions', {
|
||||
method: 'POST',
|
||||
body: { scopeType: 'collection', scopeId: collection.item.id },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(exportDenied.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not export tenant question bank content');
|
||||
|
||||
const exportNoAnswers = await request('/api/tenant-content/exports/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
scopeType: 'collection',
|
||||
scopeId: collection.item.id,
|
||||
format: 'json',
|
||||
exportType: 'questions',
|
||||
includeAnswers: false,
|
||||
includeExplanations: false,
|
||||
options: { title: '集成测试无答案题库导出' },
|
||||
},
|
||||
});
|
||||
assert.equal(exportNoAnswers.job?.status, 'completed', 'question export should create a completed export job');
|
||||
assert.equal(exportNoAnswers.export?.summary?.questionCount, 1, 'question export should include collection questions');
|
||||
assert.ok(exportNoAnswers.export?.files?.[0]?.contentBase64, 'question export should return a downloadable JSON payload');
|
||||
assert.equal(
|
||||
Object.hasOwn(exportNoAnswers.export?.questions?.[0] || {}, 'answerText'),
|
||||
false,
|
||||
'question export should omit answers when includeAnswers is false',
|
||||
);
|
||||
assert.equal(
|
||||
Object.hasOwn(exportNoAnswers.export?.questions?.[0] || {}, 'explanation'),
|
||||
false,
|
||||
'question export should omit explanations when includeExplanations is false',
|
||||
);
|
||||
assert.equal(
|
||||
Object.hasOwn(exportNoAnswers.export?.questions?.[0]?.subQuestions?.[0] || {}, 'correctOptionIndices'),
|
||||
false,
|
||||
'question export should omit sub question answers when includeAnswers is false',
|
||||
);
|
||||
assert.equal(
|
||||
Object.hasOwn(exportNoAnswers.export?.questions?.[0]?.subQuestions?.[0] || {}, 'answerText'),
|
||||
false,
|
||||
'question export should omit sub question answer text when includeAnswers is false',
|
||||
);
|
||||
assert.equal(
|
||||
Object.hasOwn(exportNoAnswers.export?.questions?.[0]?.subQuestions?.[0] || {}, 'explanation'),
|
||||
false,
|
||||
'question export should omit sub question explanations when includeExplanations is false',
|
||||
);
|
||||
const decodedExport = JSON.parse(Buffer.from(exportNoAnswers.export.files[0].contentBase64, 'base64').toString('utf8'));
|
||||
assert.equal(decodedExport._tikuExport, '3.0', 'question export JSON should use the new export contract');
|
||||
assert.equal(decodedExport.questions?.[0]?.content, '集成测试题:2 + 2 = ?', 'question export JSON should include question content');
|
||||
|
||||
const exportBadLimit = await request('/api/tenant-content/exports/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
scopeType: 'collection',
|
||||
scopeId: collection.item.id,
|
||||
limit: 'not-a-number',
|
||||
},
|
||||
});
|
||||
assert.equal(exportBadLimit.export?.summary?.questionCount, 1, 'question export should tolerate invalid limit input with a safe default');
|
||||
|
||||
const paperExport = await request('/api/tenant-content/exports/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
scopeType: 'content_node',
|
||||
scopeId: childNode.item.id,
|
||||
format: 'paper_json',
|
||||
exportType: 'paper',
|
||||
includeAnswers: true,
|
||||
includeExplanations: true,
|
||||
options: {
|
||||
title: '集成测试试卷版导出',
|
||||
durationMinutes: 90,
|
||||
watermarkText: '仅供内部测试',
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(paperExport.export?.renderHints?.pdfLayout, 'paper', 'paper export should expose paper render hints');
|
||||
assert.ok(
|
||||
paperExport.export?.sections?.some(section => section.questions?.some(item => item.id === question.item.id)),
|
||||
'paper export should group the created question by section',
|
||||
);
|
||||
const exportedQuestionWithAnswer = paperExport.export?.questions?.find(item => item.id === question.item.id);
|
||||
assert.equal(exportedQuestionWithAnswer?.answerText, '4', 'paper export should include answers when requested');
|
||||
|
||||
const partnerExportDenied = await request('/api/tenant-content/exports/questions', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { scopeType: 'collection', scopeId: collection.item.id },
|
||||
expectStatus: 404,
|
||||
});
|
||||
assert.equal(partnerExportDenied.code, 'QUESTION_COLLECTION_NOT_FOUND', 'question export must not read another tenant collection');
|
||||
|
||||
const exportJobs = await request('/api/tenant-content/exports/jobs', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { scopeType: 'collection', scopeId: collection.item.id, limit: 10 },
|
||||
});
|
||||
assert.ok(
|
||||
exportJobs.items?.some(item => item.id === exportNoAnswers.job.id && item.questionCount === 1),
|
||||
'question export jobs should list completed export audit records',
|
||||
);
|
||||
const invalidExportJobsScope = await request('/api/tenant-content/exports/jobs', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { scopeType: 'not_supported' },
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidExportJobsScope.code, 'INVALID_EXPORT_SCOPE', 'question export jobs should validate scope type filters');
|
||||
|
||||
const createdBlueprintSession = await request('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
body: { userId: USER_ID, blueprintId: blueprint.item.id },
|
||||
|
||||
Reference in New Issue
Block a user