forked from wangziqi/gongxue-base
feat: add public question bank sync
This commit is contained in:
@@ -3579,6 +3579,137 @@ async function testPublicQuestionBankAdoption() {
|
||||
});
|
||||
assert.equal(session.item?.questionCount, 2, 'adopted public bank collection should be usable for practice sessions');
|
||||
|
||||
const studentSyncDenied = await request('/api/tenant-content/public-question-banks/sync', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
method: 'POST',
|
||||
body: { adoptionId: adopted.item.id },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentSyncDenied.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not sync adopted public banks');
|
||||
|
||||
const crossTenantSyncDenied = await request('/api/tenant-content/public-question-banks/sync', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { adoptionId: adopted.item.id },
|
||||
expectStatus: 404,
|
||||
});
|
||||
assert.equal(crossTenantSyncDenied.code, 'QUESTION_BANK_ADOPTION_NOT_FOUND', 'public bank sync must be tenant isolated');
|
||||
|
||||
const syncSourceHash = `public-sync-source-${Date.now()}`;
|
||||
const sourceQuestion = await request('/api/tenant-content/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
questionBankId: ids.questionBank,
|
||||
subjectId: ids.subject,
|
||||
categoryId: ids.category,
|
||||
entryId: ids.contentEntry,
|
||||
contentNodeId: ids.contentNodeSchoolTarget,
|
||||
primaryCollectionId: ids.questionCollection,
|
||||
legacyId: `public-sync-source-${Date.now()}`,
|
||||
type: 'choice',
|
||||
typeLabel: '单选题',
|
||||
difficulty: 1,
|
||||
content: '平台公共题库新增同步题:5 + 5 = ?',
|
||||
options: [
|
||||
{ label: 'A', text: '9' },
|
||||
{ label: 'B', text: '10' },
|
||||
],
|
||||
correctOptionIndex: 1,
|
||||
correctOptionIndices: [1],
|
||||
answerText: '10',
|
||||
explanation: '新增公共题应同步到已采纳租户。',
|
||||
status: 'published',
|
||||
sourceHash: syncSourceHash,
|
||||
},
|
||||
});
|
||||
assert.ok(sourceQuestion.item?.id, 'platform content editor should create a new source public question');
|
||||
|
||||
const synced = await request('/api/tenant-content/public-question-banks/sync', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { adoptionId: adopted.item.id, copyLimit: 20 },
|
||||
});
|
||||
assert.equal(synced.sync?.status, 'synced', 'public bank sync should finish without conflicts for platform-only changes');
|
||||
assert.equal(synced.item?.syncStatus, 'synced', 'adoption sync status should be synced');
|
||||
assert.ok(
|
||||
synced.sync?.results?.some(item => item.sourceQuestionId === sourceQuestion.item.id && item.action === 'inserted'),
|
||||
'public bank sync should insert newly published source questions',
|
||||
);
|
||||
|
||||
const collectionAfterSync = await request('/api/catalog/question-collections/questions', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: false,
|
||||
query: { collectionId: adopted.item.targetCollectionId, limit: 50 },
|
||||
});
|
||||
const syncedLegacyId = `public:${MAIN_TENANT_ID}:${sourceQuestion.item.id}`;
|
||||
assert.ok(
|
||||
collectionAfterSync.items?.some(item => item.legacyId === syncedLegacyId && item.content === '平台公共题库新增同步题:5 + 5 = ?'),
|
||||
'tenant collection should expose newly synced public question copy',
|
||||
);
|
||||
|
||||
const conflictTarget = collectionAfterSync.items?.find(item => item.legacyId === `public:${MAIN_TENANT_ID}:${ids.question}`);
|
||||
assert.ok(conflictTarget?.id, 'adopted tenant collection should include a copied seed question for conflict checks');
|
||||
const tenantCustomContent = `租户自定义公共题副本 ${Date.now()}`;
|
||||
await request('/api/tenant-content/questions', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
questionId: conflictTarget.id,
|
||||
createVersion: true,
|
||||
content: tenantCustomContent,
|
||||
options: conflictTarget.options || [],
|
||||
correctOptionIndex: conflictTarget.correctOptionIndex,
|
||||
correctOptionIndices: conflictTarget.correctOptionIndices || [],
|
||||
answerText: conflictTarget.answerText,
|
||||
explanation: '租户已经对采纳题目做了本地改写,同步时必须保留。',
|
||||
sourceHash: `tenant-custom-${Date.now()}`,
|
||||
},
|
||||
});
|
||||
|
||||
const platformChangedHash = `platform-source-update-${Date.now()}`;
|
||||
await request('/api/tenant-content/questions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
questionId: ids.question,
|
||||
createVersion: true,
|
||||
content: '平台公共题库更新:1 + 1 = ?',
|
||||
options: [
|
||||
{ label: 'A', text: '1' },
|
||||
{ label: 'B', text: '2' },
|
||||
],
|
||||
correctOptionIndex: 1,
|
||||
correctOptionIndices: [1],
|
||||
answerText: '2',
|
||||
explanation: '平台公共题库发布了新版本。',
|
||||
sourceHash: platformChangedHash,
|
||||
},
|
||||
});
|
||||
|
||||
const conflictSync = await request('/api/tenant-content/public-question-banks/sync', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { adoptionId: adopted.item.id, copyLimit: 20 },
|
||||
});
|
||||
assert.equal(conflictSync.sync?.status, 'conflict', 'public bank sync should report conflicts when tenant changed copied content');
|
||||
assert.equal(conflictSync.item?.syncStatus, 'failed', 'adoption should keep failed sync status when conflicts need operator review');
|
||||
assert.ok(
|
||||
conflictSync.sync?.results?.some(item => item.sourceQuestionId === ids.question && item.action === 'conflict'),
|
||||
'public bank sync should identify the source question that conflicts with tenant edits',
|
||||
);
|
||||
|
||||
const collectionAfterConflict = await request('/api/catalog/question-collections/questions', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: false,
|
||||
query: { collectionId: adopted.item.targetCollectionId, limit: 50 },
|
||||
});
|
||||
const conflictTargetAfter = collectionAfterConflict.items?.find(item => item.id === conflictTarget.id);
|
||||
assert.equal(conflictTargetAfter?.content, tenantCustomContent, 'public bank sync must not overwrite tenant-customized copied question');
|
||||
|
||||
const mainTenantNotAdopted = await request('/api/tenant-content/public-question-banks', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { onlyNotAdopted: 'true' },
|
||||
|
||||
Reference in New Issue
Block a user