forked from wangziqi/gongxue-base
feat: add public bank sync failure ops
This commit is contained in:
@@ -1184,6 +1184,14 @@ async function testPlatformAdminPermissions() {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(paymentDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'manual service-fee payment must require billing payment permission');
|
||||
|
||||
const publicBankOpsDenied = await request('/api/platform-admin/question-bank-sync-status', {
|
||||
tenantId: false,
|
||||
userId: false,
|
||||
headers: restrictedHeaders,
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(publicBankOpsDenied.code, 'PLATFORM_PERMISSION_REQUIRED', 'public bank sync ops should require dedicated platform question bank ops permission');
|
||||
}
|
||||
|
||||
async function testPlatformStaffManagement() {
|
||||
@@ -6841,6 +6849,22 @@ async function testPublicQuestionBankAdoption() {
|
||||
'public bank sync should insert newly published source questions',
|
||||
);
|
||||
|
||||
const platformSyncStatus = await request('/api/platform-admin/question-bank-sync-status', {
|
||||
userId: false,
|
||||
headers: { 'x-platform-admin-key': 'local-platform-admin-key' },
|
||||
query: { tenantId: PARTNER_TENANT_ID, sourceQuestionBankId: ids.questionBank },
|
||||
});
|
||||
const platformSyncStatusItem = platformSyncStatus.items?.find(item => item.id === adopted.item.id);
|
||||
assert.equal(platformSyncStatusItem?.syncStatus, 'synced', 'platform admin should inspect public bank sync status across tenants');
|
||||
assert.equal(platformSyncStatus.summary?.synced >= 1, true, 'platform sync status should include summary counts');
|
||||
assert.ok(!JSON.stringify(platformSyncStatusItem).includes('新增公共题应同步到已采纳租户'), 'platform sync status must not leak source question explanations');
|
||||
|
||||
const studentPlatformSyncStatusDenied = await request('/api/platform-admin/question-bank-sync-status', {
|
||||
query: { tenantId: PARTNER_TENANT_ID },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentPlatformSyncStatusDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'public bank sync ops endpoint must require platform admin access');
|
||||
|
||||
const syncNotifications = await request('/api/tenant-content/notifications', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
@@ -6945,6 +6969,15 @@ async function testPublicQuestionBankAdoption() {
|
||||
'public bank sync should identify the source question that conflicts with tenant edits',
|
||||
);
|
||||
|
||||
const failedPlatformSyncStatus = await request('/api/platform-admin/question-bank-sync-status', {
|
||||
userId: false,
|
||||
headers: { 'x-platform-admin-key': 'local-platform-admin-key' },
|
||||
query: { tenantId: PARTNER_TENANT_ID, syncStatus: 'failed', onlyOpenIssues: 'true' },
|
||||
});
|
||||
const failedPlatformSyncStatusItem = failedPlatformSyncStatus.items?.find(item => item.id === adopted.item.id);
|
||||
assert.equal(failedPlatformSyncStatusItem?.syncStatus, 'failed', 'platform sync ops should expose failed public bank adoptions');
|
||||
assert.equal(failedPlatformSyncStatusItem?.openConflictNotificationCount >= 1, true, 'platform sync ops should expose open conflict notification count');
|
||||
|
||||
const conflictNotifications = await request('/api/tenant-content/notifications', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
|
||||
@@ -230,8 +230,111 @@ async function main() {
|
||||
const secondOutput = await runWorkerOnce();
|
||||
assert.match(secondOutput, /processed=0/, 'worker should skip already synced public bank when source has not changed');
|
||||
|
||||
await pool.query(
|
||||
'update public.question_bank_grants set status = $2, updated_at = now() where id = $1',
|
||||
[ids.grant, 'disabled'],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
update public.tenant_question_bank_adoptions
|
||||
set status = 'active',
|
||||
sync_status = 'pending',
|
||||
metadata = metadata || '{"source":"public_bank_worker_failure_test"}'::jsonb,
|
||||
updated_at = now()
|
||||
where tenant_id = $1 and id = $2
|
||||
`,
|
||||
[PARTNER_TENANT_ID, ids.adoption],
|
||||
);
|
||||
|
||||
const failedOutput = await runWorkerOnce();
|
||||
assert.match(failedOutput, /processed=1/, 'worker should claim forced pending adoption');
|
||||
assert.match(failedOutput, /failed=1/, 'worker should count unavailable grant as failed');
|
||||
|
||||
const failedAdoption = await pool.query(
|
||||
`
|
||||
select status, sync_status, metadata
|
||||
from public.tenant_question_bank_adoptions
|
||||
where tenant_id = $1 and id = $2
|
||||
`,
|
||||
[PARTNER_TENANT_ID, ids.adoption],
|
||||
);
|
||||
assert.equal(failedAdoption.rows[0]?.status, 'active', 'failed worker sync should return adoption to active');
|
||||
assert.equal(failedAdoption.rows[0]?.sync_status, 'failed', 'failed worker sync should mark adoption failed');
|
||||
assert.equal(
|
||||
failedAdoption.rows[0]?.metadata?.publicBankSyncWorker?.lastError?.code,
|
||||
'QUESTION_BANK_GRANT_NOT_AVAILABLE',
|
||||
'failed worker sync should persist a stable error code',
|
||||
);
|
||||
|
||||
const failureNotifications = await pool.query(
|
||||
`
|
||||
select notification_type, status, severity, message, metadata
|
||||
from public.tenant_content_notifications
|
||||
where tenant_id = $1 and adoption_id = $2 and notification_type = 'public_question_bank_sync_failed'
|
||||
order by created_at desc
|
||||
limit 5
|
||||
`,
|
||||
[PARTNER_TENANT_ID, ids.adoption],
|
||||
);
|
||||
const failureNotification = failureNotifications.rows[0];
|
||||
assert.equal(failureNotification?.status, 'unread', 'failed worker sync should create an unread tenant notification');
|
||||
assert.equal(failureNotification?.severity, 'error', 'failed worker sync notification should use error severity');
|
||||
assert.equal(
|
||||
failureNotification?.metadata?.errorCode,
|
||||
'QUESTION_BANK_GRANT_NOT_AVAILABLE',
|
||||
'failed worker sync notification should expose stable error code metadata',
|
||||
);
|
||||
assert.ok(
|
||||
!JSON.stringify(failureNotification).includes('integration-second-region-v1'),
|
||||
'failed worker sync notification should not leak source question details',
|
||||
);
|
||||
|
||||
await pool.query(
|
||||
'update public.question_bank_grants set status = $2, updated_at = now() where id = $1',
|
||||
[ids.grant, 'active'],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
update public.tenant_question_bank_adoptions
|
||||
set status = 'active', sync_status = 'pending', updated_at = now()
|
||||
where tenant_id = $1 and id = $2
|
||||
`,
|
||||
[PARTNER_TENANT_ID, ids.adoption],
|
||||
);
|
||||
|
||||
const recoveryOutput = await runWorkerOnce();
|
||||
assert.match(recoveryOutput, /processed=1/, 'worker should retry adoption after grant recovers');
|
||||
|
||||
const recoveredAdoption = await pool.query(
|
||||
`
|
||||
select sync_status
|
||||
from public.tenant_question_bank_adoptions
|
||||
where tenant_id = $1 and id = $2
|
||||
`,
|
||||
[PARTNER_TENANT_ID, ids.adoption],
|
||||
);
|
||||
assert.equal(recoveredAdoption.rows[0]?.sync_status, 'synced', 'worker should recover failed sync after grant becomes available');
|
||||
|
||||
const resolvedFailureNotifications = await pool.query(
|
||||
`
|
||||
select status, resolved_at
|
||||
from public.tenant_content_notifications
|
||||
where tenant_id = $1 and adoption_id = $2 and notification_type = 'public_question_bank_sync_failed'
|
||||
order by updated_at desc
|
||||
limit 1
|
||||
`,
|
||||
[PARTNER_TENANT_ID, ids.adoption],
|
||||
);
|
||||
assert.equal(
|
||||
resolvedFailureNotifications.rows[0]?.status,
|
||||
'resolved',
|
||||
'successful recovery should resolve previous public bank sync failure notifications',
|
||||
);
|
||||
assert.ok(resolvedFailureNotifications.rows[0]?.resolved_at, 'resolved failure notification should record resolved_at');
|
||||
|
||||
console.log('Public question bank sync worker integration test complete.');
|
||||
} finally {
|
||||
await pool.query('update public.question_bank_grants set status = $2, updated_at = now() where id = $1', [ids.grant, 'active']).catch(() => {});
|
||||
await cleanup(pool).catch(() => {});
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user