forked from wangziqi/gongxue-base
feat: add tenant theme templates
This commit is contained in:
@@ -4836,6 +4836,129 @@ async function testTenantAdminOps() {
|
||||
});
|
||||
assert.equal(branding.item?.brandName, '集成测试品牌', 'tenant admin should update branding');
|
||||
|
||||
const themeTemplatesDenied = await request('/api/tenant-admin/theme-templates', {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(themeTemplatesDenied.code, 'TENANT_ADMIN_REQUIRED', 'student should not list tenant theme templates');
|
||||
|
||||
const themeTemplatesPermissionDenied = await request('/api/tenant-admin/theme-templates', {
|
||||
userId: TENANT_OPERATOR_USER_ID,
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(themeTemplatesPermissionDenied.code, 'TENANT_PERMISSION_REQUIRED', 'operator without theme permission should not list theme templates');
|
||||
|
||||
const themeTemplates = await request('/api/tenant-admin/theme-templates', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
assert.equal(themeTemplates.items?.length >= 3, true, 'tenant admin should list platform theme templates');
|
||||
assert.ok(themeTemplates.items?.some(item => item.code === 'classic'), 'theme templates should include classic default');
|
||||
|
||||
const invalidThemeKey = await request('/api/tenant-admin/theme/preview', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
templateCode: 'classic',
|
||||
theme: {
|
||||
rawCss: 'body{display:none}',
|
||||
},
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidThemeKey.code, 'INVALID_THEME_KEY', 'theme preview should reject unknown theme keys');
|
||||
|
||||
const unsafeThemeAsset = await request('/api/tenant-admin/theme/preview', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
templateCode: 'classic',
|
||||
publicAssets: {
|
||||
shareImageUrl: 'javascript:alert(1)',
|
||||
},
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(unsafeThemeAsset.code, 'PUBLIC_CONFIG_SECRET_REJECTED', 'theme public assets should reject unsafe public strings');
|
||||
|
||||
const unsafeCssVar = await request('/api/tenant-admin/theme/preview', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
templateCode: 'classic',
|
||||
theme: {
|
||||
customCssVars: {
|
||||
'--tiku-card-bg': 'url(https://example.test/tracker.png)',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(unsafeCssVar.code, 'PUBLIC_CONFIG_SECRET_REJECTED', 'theme custom CSS variables should reject unsafe CSS values');
|
||||
|
||||
const previewTheme = await request('/api/tenant-admin/theme/preview', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
templateCode: 'focus',
|
||||
theme: {
|
||||
primaryColor: '#123abc',
|
||||
accentColor: '#f59e0b',
|
||||
borderRadius: 10,
|
||||
buttonRadius: 10,
|
||||
customCssVars: {
|
||||
'--tiku-focus-ring': '#123abc',
|
||||
},
|
||||
icons: {
|
||||
home: 'book-open',
|
||||
},
|
||||
},
|
||||
publicAssets: {
|
||||
logoUrl: '/assets/tenant/logo.png',
|
||||
iconSet: 'focus',
|
||||
shareCardStyle: 'study',
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(previewTheme.item?.draftTemplateCode, 'focus', 'theme preview should persist draft template code');
|
||||
assert.equal(previewTheme.item?.draftTheme?.primaryColor, '#123abc', 'theme preview should merge color overrides');
|
||||
assert.equal(previewTheme.item?.draftPublicAssets?.logoUrl, '/assets/tenant/logo.png', 'theme preview should persist safe public asset references');
|
||||
|
||||
const tenantTheme = await request('/api/tenant-admin/theme', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
assert.equal(tenantTheme.item?.draftTemplateCode, 'focus', 'tenant theme detail should expose draft theme');
|
||||
assert.equal(tenantTheme.item?.draftTheme?.customCssVars?.['--tiku-focus-ring'], '#123abc', 'tenant theme detail should expose safe CSS variables');
|
||||
|
||||
const publishedTheme = await request('/api/tenant-admin/theme/publish', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
useDraft: true,
|
||||
},
|
||||
});
|
||||
assert.equal(publishedTheme.item?.activeTemplateCode, 'focus', 'theme publish should activate draft template');
|
||||
assert.equal(publishedTheme.item?.activeTheme?.primaryColor, '#123abc', 'theme publish should activate draft tokens');
|
||||
assert.equal(publishedTheme.item?.status, 'published', 'theme publish should mark config published');
|
||||
|
||||
const tenantThemeAfterPublish = await request('/api/tenant-admin/theme', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
assert.equal(tenantThemeAfterPublish.item?.draftTemplateCode, null, 'theme publish should clear draft template');
|
||||
assert.equal(tenantThemeAfterPublish.item?.activePublicAssets?.iconSet, 'focus', 'theme publish should expose active public assets');
|
||||
|
||||
const resolvedTenantTheme = await request('/api/tenant/resolve', {
|
||||
userId: false,
|
||||
tenantId: false,
|
||||
query: { tenantCode: 'master' },
|
||||
});
|
||||
assert.equal(resolvedTenantTheme.branding?.theme?.primaryColor, '#123abc', 'tenant resolve should return published theme tokens to frontend');
|
||||
|
||||
const themeAuditLogs = await request('/api/tenant-admin/audit-logs', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { action: 'tenant.theme', limit: 20 },
|
||||
});
|
||||
assert.ok(themeAuditLogs.items?.some(item => item.action === 'tenant.theme.previewed'), 'audit logs should include theme preview');
|
||||
assert.ok(themeAuditLogs.items?.some(item => item.action === 'tenant.theme.published'), 'audit logs should include theme publish');
|
||||
|
||||
const publicSecretRejected = await request('/api/tenant-admin/auth-providers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
|
||||
Reference in New Issue
Block a user