forked from wangziqi/gongxue-base
feat: enforce public bank SaaS scopes
This commit is contained in:
@@ -63,7 +63,13 @@ const ids = {
|
||||
pointExchangeItem: '00000000-0000-0000-0000-000000000879',
|
||||
pointExpensiveExchangeItem: '00000000-0000-0000-0000-000000000880',
|
||||
questionBank: '00000000-0000-0000-0000-000000000400',
|
||||
secondPublicRegion: '00000000-0000-0000-0000-0000000003f1',
|
||||
secondPublicQuestionBank: '00000000-0000-0000-0000-0000000004f1',
|
||||
secondPublicQuestion: '00000000-0000-0000-0000-0000000004f2',
|
||||
secondPublicQuestionVersion: '00000000-0000-0000-0000-0000000004f3',
|
||||
secondPublicQuestionBankGrant: '00000000-0000-0000-0000-0000000009f1',
|
||||
publicQuestionBankGrant: '00000000-0000-0000-0000-000000000906',
|
||||
partnerSubscription: '00000000-0000-0000-0000-000000000902',
|
||||
platformOverdueInvoice: crypto.randomUUID(),
|
||||
};
|
||||
|
||||
@@ -6504,6 +6510,104 @@ async function testPublicQuestionBankAdoption() {
|
||||
});
|
||||
assert.equal(studentDenied.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not browse adoptable public banks');
|
||||
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
try {
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.regions (id, tenant_id, legacy_id, name, code, sort_order, is_active)
|
||||
values ($1, $2, 'integration-second-region', '烟测第二地区', 'SMOKE-2', 2, true)
|
||||
on conflict (id)
|
||||
do update set tenant_id = excluded.tenant_id,
|
||||
name = excluded.name,
|
||||
code = excluded.code,
|
||||
sort_order = excluded.sort_order,
|
||||
is_active = excluded.is_active
|
||||
`,
|
||||
[ids.secondPublicRegion, MAIN_TENANT_ID],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.question_banks (id, tenant_id, region_id, name, source_scope, status, metadata)
|
||||
values ($1, $2, $3, '烟测第二地区公共题库', 'platform', 'active', '{"source":"integration-test","commercialScope":"second_region_bank"}'::jsonb)
|
||||
on conflict (id)
|
||||
do update set tenant_id = excluded.tenant_id,
|
||||
region_id = excluded.region_id,
|
||||
name = excluded.name,
|
||||
source_scope = excluded.source_scope,
|
||||
status = excluded.status,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.secondPublicQuestionBank, MAIN_TENANT_ID, ids.secondPublicRegion],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.questions (
|
||||
id, tenant_id, question_bank_id, subject_id, category_id,
|
||||
legacy_id, type, type_label, difficulty, tags, status
|
||||
)
|
||||
values ($1, $2, $3, $4, $5, 'integration-second-region-question', 'choice', '单选题', 1, '[]'::jsonb, 'published')
|
||||
on conflict (id)
|
||||
do update set question_bank_id = excluded.question_bank_id,
|
||||
subject_id = excluded.subject_id,
|
||||
category_id = excluded.category_id,
|
||||
status = excluded.status,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.secondPublicQuestion, MAIN_TENANT_ID, ids.secondPublicQuestionBank, ids.subject, ids.category],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.question_versions (
|
||||
id, tenant_id, question_id, version_no, content, options,
|
||||
correct_option_index, correct_option_indices, answer_text, explanation, source_hash, created_by
|
||||
)
|
||||
values (
|
||||
$1, $2, $3, 1, '第二地区公共题库题目:2 + 2 = ?',
|
||||
'[{"label":"A","text":"3"},{"label":"B","text":"4"}]'::jsonb,
|
||||
1, '[1]'::jsonb, '4', '第二地区题库只应授权给购买该地区或全国版的租户。',
|
||||
'integration-second-region-v1', $4
|
||||
)
|
||||
on conflict (id)
|
||||
do update set content = excluded.content,
|
||||
options = excluded.options,
|
||||
correct_option_index = excluded.correct_option_index,
|
||||
correct_option_indices = excluded.correct_option_indices,
|
||||
answer_text = excluded.answer_text,
|
||||
explanation = excluded.explanation,
|
||||
source_hash = excluded.source_hash
|
||||
`,
|
||||
[ids.secondPublicQuestionVersion, MAIN_TENANT_ID, ids.secondPublicQuestion, TENANT_ADMIN_USER_ID],
|
||||
);
|
||||
await pool.query(
|
||||
'update public.questions set current_version_id = $2, updated_at = now() where id = $1',
|
||||
[ids.secondPublicQuestion, ids.secondPublicQuestionVersion],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.question_bank_grants (
|
||||
id, source_question_bank_id, grant_scope, allowed_plan_codes,
|
||||
allowed_region_ids, status, metadata
|
||||
)
|
||||
values (
|
||||
$1, $2, 'plans', array['starter_yearly','pro_yearly']::text[],
|
||||
array[$3::uuid]::uuid[], 'active', '{"source":"integration-test","scope":"second_region"}'::jsonb
|
||||
)
|
||||
on conflict (id)
|
||||
do update set source_question_bank_id = excluded.source_question_bank_id,
|
||||
grant_scope = excluded.grant_scope,
|
||||
allowed_plan_codes = excluded.allowed_plan_codes,
|
||||
allowed_region_ids = excluded.allowed_region_ids,
|
||||
status = excluded.status,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.secondPublicQuestionBankGrant, ids.secondPublicQuestionBank, ids.secondPublicRegion],
|
||||
);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
const platformBanks = await request('/api/platform-admin/question-banks', {
|
||||
userId: false,
|
||||
headers: { 'x-platform-admin-key': 'local-platform-admin-key' },
|
||||
@@ -6539,7 +6643,85 @@ async function testPublicQuestionBankAdoption() {
|
||||
});
|
||||
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.accessPlanCode, 'starter_yearly', 'adoptable public bank should expose the subscription plan that grants access');
|
||||
assert.equal(adoptable.accessMode, 'limited_regions', 'adoptable public bank should expose limited region access mode');
|
||||
assert.equal(adoptable.adoptedId, null, 'public bank should start as not adopted after smoke seed');
|
||||
assert.equal(
|
||||
partnerBanks.items?.some(item => item.grantId === ids.secondPublicQuestionBankGrant),
|
||||
false,
|
||||
'starter tenant restricted to one region must not see second-region public banks',
|
||||
);
|
||||
|
||||
const secondRegionAdoptDenied = await request('/api/tenant-content/public-question-banks/adopt', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { grantId: ids.secondPublicQuestionBankGrant },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(secondRegionAdoptDenied.code, 'QUESTION_BANK_GRANT_NOT_AVAILABLE', 'restricted-region SaaS plan must not adopt out-of-scope public banks');
|
||||
|
||||
const upgradePool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
try {
|
||||
const upgradeResult = await upgradePool.query(
|
||||
`
|
||||
update public.tenant_subscriptions
|
||||
set plan_code = 'pro_yearly',
|
||||
metadata = jsonb_build_object(
|
||||
'source', 'integration-test-upgrade',
|
||||
'publicQuestionBankAccess', jsonb_build_object('mode', 'national', 'allowAllRegions', true)
|
||||
),
|
||||
updated_at = now()
|
||||
where id = $1
|
||||
`,
|
||||
[ids.partnerSubscription],
|
||||
);
|
||||
assert.equal(upgradeResult.rowCount, 1, 'integration test should upgrade exactly one partner subscription');
|
||||
const upgradedSubscription = await upgradePool.query(
|
||||
'select plan_code, metadata from public.tenant_subscriptions where id = $1',
|
||||
[ids.partnerSubscription],
|
||||
);
|
||||
assert.equal(upgradedSubscription.rows[0]?.plan_code, 'pro_yearly', 'partner subscription upgrade should persist before public bank list');
|
||||
assert.equal(
|
||||
upgradedSubscription.rows[0]?.metadata?.publicQuestionBankAccess?.mode,
|
||||
'national',
|
||||
'partner subscription upgrade should persist national public bank access mode',
|
||||
);
|
||||
} finally {
|
||||
await upgradePool.end();
|
||||
}
|
||||
|
||||
const upgradedPartnerBanks = await request('/api/tenant-content/public-question-banks', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: PARTNER_TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
const secondRegionVisible = upgradedPartnerBanks.items?.find(item => item.grantId === ids.secondPublicQuestionBankGrant);
|
||||
assert.ok(secondRegionVisible, 'pro/national SaaS subscription should see second-region public banks');
|
||||
assert.equal(secondRegionVisible.accessPlanCode, 'pro_yearly', 'upgraded public bank access should be attributed to pro_yearly');
|
||||
assert.equal(secondRegionVisible.accessMode, 'national', 'upgraded public bank access should expose national mode');
|
||||
|
||||
const restoreSubscriptionPool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
try {
|
||||
const restoreResult = await restoreSubscriptionPool.query(
|
||||
`
|
||||
update public.tenant_subscriptions
|
||||
set plan_code = 'starter_yearly',
|
||||
metadata = jsonb_build_object(
|
||||
'source', 'integration-test-restore',
|
||||
'publicQuestionBankAccess', jsonb_build_object(
|
||||
'mode', 'limited_regions',
|
||||
'allowedRegionIds', jsonb_build_array($2::text)
|
||||
)
|
||||
),
|
||||
updated_at = now()
|
||||
where id = $1
|
||||
`,
|
||||
[ids.partnerSubscription, ids.region],
|
||||
);
|
||||
assert.equal(restoreResult.rowCount, 1, 'integration test should restore partner subscription after national access check');
|
||||
} finally {
|
||||
await restoreSubscriptionPool.end();
|
||||
}
|
||||
|
||||
const adopted = await request('/api/tenant-content/public-question-banks/adopt', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
|
||||
Reference in New Issue
Block a user