forked from wangziqi/gongxue-base
feat: add public question bank adoption
This commit is contained in:
@@ -15,6 +15,7 @@ const TENANT_SALES_USER_ID = '00000000-0000-0000-0000-000000000104';
|
||||
const TENANT_AGENT_USER_ID = '00000000-0000-0000-0000-000000000105';
|
||||
const TENANT_TEACHER_USER_ID = '00000000-0000-0000-0000-000000000106';
|
||||
const SECOND_STUDENT_USER_ID = '00000000-0000-0000-0000-000000000107';
|
||||
const PARTNER_TENANT_ADMIN_USER_ID = '00000000-0000-0000-0000-000000000907';
|
||||
const AUTH_USER_ID = '00000000-0000-0000-0000-00000000a101';
|
||||
const AUTH_TENANT_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a102';
|
||||
const AUTH_PLATFORM_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a999';
|
||||
@@ -48,6 +49,8 @@ const ids = {
|
||||
tenantClassOther: '00000000-0000-0000-0000-000000000852',
|
||||
examDate: '00000000-0000-0000-0000-000000000861',
|
||||
tenantExamDate: '00000000-0000-0000-0000-000000000862',
|
||||
questionBank: '00000000-0000-0000-0000-000000000400',
|
||||
publicQuestionBankGrant: '00000000-0000-0000-0000-000000000906',
|
||||
};
|
||||
|
||||
const paymentFixture = (() => {
|
||||
@@ -2404,6 +2407,115 @@ async function testTenantContentAssetsAndImports() {
|
||||
assert.equal(partnerImports.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'import jobs must be tenant isolated');
|
||||
}
|
||||
|
||||
async function testPublicQuestionBankAdoption() {
|
||||
const studentDenied = await request('/api/tenant-content/public-question-banks', {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentDenied.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not browse adoptable public banks');
|
||||
|
||||
const platformBanks = await request('/api/platform-admin/question-banks', {
|
||||
userId: false,
|
||||
headers: { 'x-platform-admin-key': 'local-platform-admin-key' },
|
||||
query: { q: '烟测公共题库' },
|
||||
});
|
||||
assert.ok(platformBanks.items?.some(item => item.id === ids.questionBank && item.sourceScope === 'platform'), 'platform admin should list platform public banks');
|
||||
|
||||
const grant = await request('/api/platform-admin/question-bank-grants', {
|
||||
userId: false,
|
||||
headers: { 'x-platform-admin-key': 'local-platform-admin-key' },
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.publicQuestionBankGrant,
|
||||
sourceQuestionBankId: ids.questionBank,
|
||||
grantScope: 'plans',
|
||||
allowedPlanCodes: ['starter_yearly', 'pro_yearly'],
|
||||
status: 'active',
|
||||
metadata: { source: 'integration-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(grant.item?.sourceQuestionBankId, ids.questionBank, 'platform admin should upsert public bank grant');
|
||||
|
||||
const grants = await request('/api/platform-admin/question-bank-grants', {
|
||||
userId: false,
|
||||
headers: { 'x-platform-admin-key': 'local-platform-admin-key' },
|
||||
query: { questionBankId: ids.questionBank },
|
||||
});
|
||||
assert.ok(grants.items?.some(item => item.id === ids.publicQuestionBankGrant), 'platform admin should list public bank grants');
|
||||
|
||||
const partnerBanks = await request('/api/tenant-content/public-question-banks', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
const adoptable = partnerBanks.items?.find(item => item.grantId === ids.publicQuestionBankGrant);
|
||||
assert.ok(adoptable, 'partner tenant should see public bank granted by SaaS plan');
|
||||
assert.equal(adoptable.adoptedId, null, 'public bank should start as not adopted after smoke seed');
|
||||
|
||||
const adopted = await request('/api/tenant-content/public-question-banks/adopt', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
grantId: ids.publicQuestionBankGrant,
|
||||
entryName: '合作商采纳烟测公共题库',
|
||||
collectionName: '合作商公共题库题目',
|
||||
copyLimit: 3,
|
||||
metadata: { source: 'integration-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(adopted.item?.sourceQuestionBankId, ids.questionBank, 'adoption should bind source public question bank');
|
||||
assert.equal(adopted.item?.copiedQuestionCount, 3, 'adoption should copy a snapshot of published questions');
|
||||
assert.ok(adopted.item?.targetEntryId, 'adoption should create tenant content entry');
|
||||
assert.ok(adopted.item?.targetCollectionId, 'adoption should create tenant question collection');
|
||||
|
||||
const repeated = await request('/api/tenant-content/public-question-banks/adopt', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { grantId: ids.publicQuestionBankGrant },
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(repeated.code, 'QUESTION_BANK_ALREADY_ADOPTED', 'tenant should not adopt the same public bank twice');
|
||||
|
||||
const adoptedList = await request('/api/tenant-content/public-question-banks', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
const adoptedItem = adoptedList.items?.find(item => item.grantId === ids.publicQuestionBankGrant);
|
||||
assert.equal(adoptedItem?.adoptionStatus, 'active', 'adoptable list should expose adoption status');
|
||||
|
||||
const entries = await request('/api/catalog/content-entries', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: false,
|
||||
query: { entryType: 'question_practice' },
|
||||
});
|
||||
assert.ok(entries.items?.some(item => item.id === adopted.item.targetEntryId), 'adopted public bank should appear in tenant catalog entries');
|
||||
|
||||
const collections = await request('/api/catalog/question-collections', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: false,
|
||||
query: { entryId: adopted.item.targetEntryId },
|
||||
});
|
||||
assert.ok(collections.items?.some(item => item.id === adopted.item.targetCollectionId && item.questionCount === 3), 'adopted public bank should expose a tenant collection');
|
||||
|
||||
const session = await request('/api/learning/practice-sessions', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
collectionId: adopted.item.targetCollectionId,
|
||||
mode: 'sequential',
|
||||
questionLimit: 2,
|
||||
},
|
||||
});
|
||||
assert.equal(session.item?.questionCount, 2, 'adopted public bank collection should be usable for practice sessions');
|
||||
|
||||
const mainTenantNotAdopted = await request('/api/tenant-content/public-question-banks', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { onlyNotAdopted: 'true' },
|
||||
});
|
||||
assert.ok(!mainTenantNotAdopted.items?.some(item => item.grantId === ids.publicQuestionBankGrant), 'platform-owned main tenant should not see plan grant without matching SaaS subscription');
|
||||
}
|
||||
|
||||
async function testTenantAdminOps() {
|
||||
const fakeWechat = await startFakeWechatServer();
|
||||
const denied = await request('/api/tenant-admin/branding', {
|
||||
@@ -3567,6 +3679,7 @@ async function main() {
|
||||
await check('tenant isolation', testTenantIsolation);
|
||||
await check('tenant content admin', testTenantContentAdmin);
|
||||
await check('tenant content assets and imports', testTenantContentAssetsAndImports);
|
||||
await check('public question bank adoption', testPublicQuestionBankAdoption);
|
||||
await check('tenant admin operations', testTenantAdminOps);
|
||||
await check('tenant member permissions and audit', testTenantMemberPermissionsAndAudit);
|
||||
await check('tenant class and student scopes', testTenantClassStudentScopes);
|
||||
|
||||
@@ -75,6 +75,8 @@ const ids = {
|
||||
partnerInvoice: '00000000-0000-0000-0000-000000000903',
|
||||
partnerInvoiceItem: '00000000-0000-0000-0000-000000000904',
|
||||
partnerInvoicePayment: '00000000-0000-0000-0000-000000000905',
|
||||
publicQuestionBankGrant: '00000000-0000-0000-0000-000000000906',
|
||||
partnerTenantAdminUser: '00000000-0000-0000-0000-000000000907',
|
||||
platformAdminUser: '00000000-0000-0000-0000-000000000999',
|
||||
};
|
||||
|
||||
@@ -504,6 +506,20 @@ async function main() {
|
||||
[ids.platformAdminUser, ids.authPlatformAdminUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.platform_users (id, auth_user_id, username, phone, name, primary_role, raw_profile)
|
||||
values ($1, null, 'smoke_partner_admin', '13800000907', 'Smoke Partner Admin', 'tenant_admin', '{"source":"smoke-seed"}'::jsonb)
|
||||
on conflict (id)
|
||||
do update set username = excluded.username,
|
||||
phone = excluded.phone,
|
||||
name = excluded.name,
|
||||
primary_role = excluded.primary_role,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.partnerTenantAdminUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.user_identities (user_id, provider, provider_subject, phone)
|
||||
@@ -526,6 +542,16 @@ async function main() {
|
||||
[tenantId, ids.user],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.tenant_memberships
|
||||
where tenant_id = $1
|
||||
and user_id = $2
|
||||
and role = 'tenant_admin'
|
||||
`,
|
||||
[ids.partnerTenant, ids.tenantAdminUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.tenant_memberships (tenant_id, user_id, role, status, permissions)
|
||||
@@ -650,10 +676,13 @@ async function main() {
|
||||
await client.query(
|
||||
`
|
||||
insert into public.question_banks (id, tenant_id, region_id, name, source_scope, status, metadata)
|
||||
values ($1, $2, $3, '烟测题库', 'tenant', 'active', '{"source":"smoke-seed"}'::jsonb)
|
||||
values ($1, $2, $3, '烟测公共题库', 'platform', 'active', '{"source":"smoke-seed","commercialScope":"public_region_bank"}'::jsonb)
|
||||
on conflict (id)
|
||||
do update set name = excluded.name,
|
||||
region_id = excluded.region_id,
|
||||
source_scope = excluded.source_scope,
|
||||
status = excluded.status,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.questionBank, tenantId, ids.region],
|
||||
@@ -1784,6 +1813,109 @@ async function main() {
|
||||
[ids.partnerInvoicePayment, ids.partnerTenant, ids.partnerInvoice],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.tenant_memberships (tenant_id, user_id, role, status, permissions)
|
||||
values ($1, $2, 'tenant_admin', 'active', '{"content:*":true}'::jsonb)
|
||||
on conflict (tenant_id, user_id, role)
|
||||
do update set status = 'active',
|
||||
permissions = excluded.permissions,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.partnerTenant, ids.partnerTenantAdminUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.question_bank_grants (
|
||||
id, source_question_bank_id, grant_scope, allowed_plan_codes,
|
||||
allowed_tenant_ids, status, starts_at, expires_at, metadata
|
||||
)
|
||||
values (
|
||||
$1, $2, 'plans', array['starter_yearly','pro_yearly']::text[],
|
||||
'{}'::uuid[], 'active', '2026-06-21T00:00:00Z', null,
|
||||
'{"source":"smoke-seed","business":"public_question_bank_authorization"}'::jsonb
|
||||
)
|
||||
on conflict (id)
|
||||
do update set grant_scope = excluded.grant_scope,
|
||||
allowed_plan_codes = excluded.allowed_plan_codes,
|
||||
status = excluded.status,
|
||||
starts_at = excluded.starts_at,
|
||||
expires_at = excluded.expires_at,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.publicQuestionBankGrant, ids.questionBank],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.tenant_question_bank_adoptions
|
||||
where tenant_id = $1
|
||||
and source_question_bank_id = $2
|
||||
`,
|
||||
[ids.partnerTenant, ids.questionBank],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.question_collection_items
|
||||
where tenant_id = $1
|
||||
and metadata->>'source' = 'public_question_bank_adoption'
|
||||
`,
|
||||
[ids.partnerTenant],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.question_versions
|
||||
where tenant_id = $1
|
||||
and question_id in (
|
||||
select id
|
||||
from public.questions
|
||||
where tenant_id = $1
|
||||
and legacy_id like 'public:%'
|
||||
)
|
||||
`,
|
||||
[ids.partnerTenant],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.questions
|
||||
where tenant_id = $1
|
||||
and legacy_id like 'public:%'
|
||||
`,
|
||||
[ids.partnerTenant],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.question_collections
|
||||
where tenant_id = $1
|
||||
and metadata->>'source' = 'public_question_bank_adoption'
|
||||
`,
|
||||
[ids.partnerTenant],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.content_entries
|
||||
where tenant_id = $1
|
||||
and entry_key like 'public-%'
|
||||
`,
|
||||
[ids.partnerTenant],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.question_banks
|
||||
where tenant_id = $1
|
||||
and metadata->>'source' = 'public_question_bank_adoption'
|
||||
`,
|
||||
[ids.partnerTenant],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
delete from public.tenant_usage_records
|
||||
|
||||
Reference in New Issue
Block a user