flnxi-Wu/lib/__tests__/license-route-coverage.test.ts
2026-06-03 18:41:52 +08:00

382 lines
15 KiB
TypeScript

import fs from 'fs';
import path from 'path';
const rootDir = path.resolve(__dirname, '..', '..');
function readProjectFile(relativePath: string): string {
return fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
}
describe('许可证业务入口覆盖', () => {
const requiredChecks: Array<{
file: string;
helper: string;
reason: string;
}> = [
{
file: 'app/api/auth/register/route.ts',
helper: 'canCreateUser',
reason: '免费版用户数限制必须在注册入口服务端执行',
},
{
file: 'app/api/materials/upload/route.ts',
helper: 'canUploadMaterial',
reason: '免费版素材数限制必须在上传入口服务端执行',
},
{
file: 'app/api/admin/upload-templates/route.ts',
helper: 'canUseCustomTemplates',
reason: '自定义模板新增必须走许可证功能开关',
},
{
file: 'app/api/admin/upload-templates/[id]/route.ts',
helper: 'canUseCustomTemplates',
reason: '自定义模板编辑和删除必须走许可证功能开关',
},
{
file: 'app/api/admin/upload-templates/[id]/migrate/route.ts',
helper: 'canUseCustomTemplates',
reason: '模板迁移必须走许可证功能开关',
},
{
file: 'app/api/templates/route.ts',
helper: 'canUseCustomTemplates',
reason: '首页模板筛选不能在降级后继续暴露历史自定义模板',
},
{
file: 'app/api/upload-templates/route.ts',
helper: 'canUseCustomTemplates',
reason: '上传页模板选择不能在降级后继续使用历史自定义模板',
},
{
file: 'app/api/material-types/route.ts',
helper: 'canUseCustomTemplates',
reason: '素材类型筛选不能在降级后继续暴露历史自定义模板',
},
{
file: 'app/api/departments/route.ts',
helper: 'canUseDepartmentIsolation',
reason: '部门隔离相关写操作必须走许可证功能开关',
},
{
file: 'app/api/system-settings/route.ts',
helper: 'canUseSystemSettings',
reason: '系统设置写操作必须走许可证功能开关',
},
{
file: 'app/api/system-settings/upload-logo/route.ts',
helper: 'canUseSystemSettings',
reason: '系统 LOGO 上传属于系统设置写操作,必须走许可证功能开关',
},
{
file: 'app/api/admin/cleanup-database/route.ts',
helper: 'canUseSystemSettings',
reason: '数据库清理属于高风险维护操作,必须走许可证功能开关',
},
{
file: 'app/api/admin/reset-material-rankings/route.ts',
helper: 'canUseSystemSettings',
reason: '排行榜重置属于高风险维护操作,必须走许可证功能开关',
},
{
file: 'app/api/admin/migrate-material-fields/route.ts',
helper: 'canUseSystemSettings',
reason: '素材字段迁移属于高风险维护操作,必须走许可证功能开关',
},
{
file: 'app/api/tunnel/start/route.ts',
helper: 'canUseIntranetPenetration',
reason: '内网穿透启动必须走专业版功能开关',
},
{
file: 'app/api/tunnel/status/route.ts',
helper: 'canUseIntranetPenetration',
reason: '内网穿透状态查询不能绕过专业版功能开关',
},
{
file: 'app/api/tunnel/stop/route.ts',
helper: 'canUseIntranetPenetration',
reason: '内网穿透停止入口也必须维持同一许可证边界',
},
];
it.each(requiredChecks)('$file 应调用 $helper', ({ file, helper, reason }) => {
const source = readProjectFile(file);
expect(source.includes(helper)).toBe(true);
if (!source.includes(helper)) {
throw new Error(reason);
}
});
it('高价值付费能力必须同时检查签名模块凭证', () => {
const source = readProjectFile('lib/license/check-limits.ts');
expect(source).toContain('authorizePremiumModuleAction');
expect(source).toContain('PREMIUM_MODULE_IDS.PAID_CORE');
expect(source).toContain('customTemplates:use');
expect(source).toContain('intranetPenetration:use');
expect(source).toContain('apiAccess:use');
});
it('内网穿透入口通过许可证后必须代理到模块服务', () => {
for (const file of [
'app/api/tunnel/start/route.ts',
'app/api/tunnel/status/route.ts',
'app/api/tunnel/stop/route.ts',
]) {
const source = readProjectFile(file);
expect(source).toContain('proxyIntranetModule');
}
});
it('用户数和素材数限制必须来自基础镜像本地签名许可证', () => {
const source = readProjectFile('lib/license/check-limits.ts');
expect(source).toContain('checkLimit(type)');
expect(source).toContain("checkAuthorizedLimit('users')");
expect(source).toContain("checkAuthorizedLimit('materials')");
});
it('自定义模板保存请求标准化必须交给 paid-core', () => {
for (const file of ['app/api/admin/upload-templates/route.ts']) {
const source = readProjectFile(file);
expect(source).toContain('normalizeUploadTemplateSaveWithPaidCore');
expect(source).not.toContain('validateAndFormatUploadTemplateFields');
expect(source).not.toContain('validateUploadTemplateFieldsWithPaidCore');
expect(source).not.toContain('isValidUploadTemplateStatus');
}
const typeSource = readProjectFile('lib/upload-template-save-validation.ts');
expect(typeSource).not.toContain('validateAndFormatUploadTemplateFields');
expect(typeSource).not.toContain('UPLOAD_TEMPLATE_FIELD_LIMITS');
expect(typeSource).not.toContain('ALLOWED_TEMPLATE_STATUSES');
});
it('自定义模板创建和更新必须使用正确的 paid-core 入口', () => {
const createSource = readProjectFile('app/api/admin/upload-templates/route.ts');
const updateSource = readProjectFile('app/api/admin/upload-templates/[id]/route.ts');
expect(createSource).toContain("normalizeUploadTemplateSaveWithPaidCore(body || {}, 'create')");
expect(updateSource).toContain('planUploadTemplateUpdateWithPaidCore');
expect(updateSource).not.toContain("normalizeUploadTemplateSaveWithPaidCore(body || {}, 'update')");
expect(updateSource).not.toContain('new Set(existing.fields.map');
expect(updateSource).not.toContain("errorCode: 'FIELD_NOT_IN_TEMPLATE'");
expect(updateSource).not.toContain("errorCode: 'VERSION_CONFLICT'");
});
it('自定义模板删除规划必须交给 paid-core', () => {
const source = readProjectFile('app/api/admin/upload-templates/[id]/route.ts');
expect(source).toContain('planUploadTemplateDeleteWithPaidCore');
expect(source).not.toContain('因为有 ${materialCount} 个素材正在使用该模板');
expect(source).not.toContain("errorCode: 'TEMPLATE_IN_USE'");
});
it('自定义模板素材字段提交校验必须交给 paid-core', () => {
const source = readProjectFile('lib/template-field-validation.ts');
expect(source).toContain('validateSubmittedTemplateFieldsWithPaidCore');
expect(source).toContain('isBuiltinTemplate');
expect(source).toContain('validateSubmittedTemplateFieldsLocally');
});
it('自定义模板素材 metadata 准备必须交给 paid-core', () => {
for (const file of [
'app/api/materials/upload/route.ts',
'app/api/materials/update/route.ts',
'app/api/materials/[id]/route.ts',
]) {
const source = readProjectFile(file);
expect(source).toContain('prepareMaterialMetadataWithPaidCore');
expect(source).toContain('isBuiltinTemplate');
}
});
it('自定义模板结构变更风险判断必须交给 paid-core 更新计划', () => {
const source = readProjectFile('app/api/admin/upload-templates/[id]/route.ts');
const paidCoreSource = readProjectFile('premium-modules/paid-core/template-validation.mjs');
expect(source).toContain('planUploadTemplateUpdateWithPaidCore');
expect(source).not.toContain('detectTemplateStructureChangesWithPaidCore');
expect(source).not.toContain('TEMPLATE_STRUCTURE_LOCKED');
expect(paidCoreSource).toContain('detectDangerousTemplateChanges');
expect(paidCoreSource).toContain('planUploadTemplateUpdateRequest');
expect(source).not.toContain('function detectDangerousTemplateChanges');
expect(source).not.toContain('function stableStringify');
});
it('自定义模板迁移 metadata 准备必须交给 paid-core', () => {
const source = readProjectFile('app/api/admin/upload-templates/[id]/migrate/route.ts');
expect(source).toContain('planTemplateMigrationBatchWithPaidCore');
expect(source).toContain('buildTemplateMigrationPreviewWithPaidCore');
expect(source).toContain('prepareTemplateMigrationMetadataWithPaidCore');
expect(source).not.toContain('DEFAULT_MIGRATION_SAFE_LIMIT');
expect(source).not.toContain('MAX_MIGRATION_FORCE_LIMIT');
expect(source).not.toContain('parsePositiveInteger');
expect(source).not.toContain('const STANDARD_FIELD_LABELS');
expect(source).not.toContain('function buildFieldSummary');
expect(source).not.toContain('function addPreviewFieldImpact');
expect(source).not.toContain('function buildMigrationMetadata');
expect(source).not.toContain('function getDroppedStandardFields');
expect(source).not.toContain('checkMetadataSize(metadata)');
expect(source).not.toContain('prepareMaterialData(');
expect(source).not.toContain('hasFieldValue');
expect(source).not.toContain('nextTemplateFieldValues');
expect(source).not.toContain('function clearNonTargetStandardFields');
expect(source).not.toContain('CLEARABLE_NON_TARGET_FIELD_DEFAULTS');
expect(source).not.toContain('templateMigrationBackups =');
expect(source).not.toContain('normalizeOptionalString');
expect(source).not.toContain("if ('audioDuration' in migrationMetadata.templateFields)");
});
it('自定义模板公开列表投影必须交给 paid-core', () => {
for (const file of [
'app/api/templates/route.ts',
'app/api/upload-templates/route.ts',
'app/api/material-types/route.ts',
]) {
const source = readProjectFile(file);
expect(source).toContain('projectPublicTemplateOptionsWithPaidCore');
expect(source).not.toContain("status: 'active'");
expect(source).not.toContain("startsWith('builtin-'");
expect(source).not.toContain("startsWith: 'builtin-'");
expect(source).not.toContain('replace(/\\s*素材模板');
expect(source).not.toContain('`template:${template.id}:');
expect(source).not.toContain('isReadOnly: false');
expect(source).not.toContain("key: { not: 'materialType' }");
}
});
it('自定义模板字段填写率统计必须交给 paid-core', () => {
const source = readProjectFile('app/api/admin/upload-templates/[id]/route.ts');
expect(source).toContain('calculateTemplateFieldStatsWithPaidCore');
expect(source).toContain('MAX_SYNC_FIELD_STATS_MATERIALS');
expect(source).not.toContain('function calculateFieldStats');
expect(source).not.toContain('JSON.parse(material.metadata)');
expect(source).not.toContain('isLowFillRate: fillRate < 30');
expect(source).not.toContain('stats.sort((a, b) => a.fillRate - b.fillRate)');
});
it('素材列表自定义字段搜索规划必须交给 paid-core', () => {
const source = readProjectFile('app/api/materials/route.ts');
expect(source).toContain('planMaterialListQueryWithPaidCore');
expect(source).not.toContain('planMaterialCustomSearchWithPaidCore');
expect(source).not.toContain('ALLOWED_DYNAMIC_FILTER_FIELDS');
expect(source).not.toContain('isStandardField');
expect(source).not.toContain("filter((key) => !isStandardField(key))");
});
it('首页自定义筛选字段统计规划必须交给 paid-core', () => {
const source = readProjectFile('app/api/home-filter-fields/route.ts');
expect(source).toContain('planHomeFilterFieldCountsWithPaidCore');
expect(source).not.toContain('planHomeFilterCustomFieldCountsWithPaidCore');
expect(source).toContain("source === 'metadata'");
expect(source).not.toContain('customFieldKeys');
});
it('后端部门隔离管理范围规划必须交给 paid-core', () => {
const delegatedFiles: Array<{ file: string; helpers: string[] }> = [
{
file: 'lib/material-permissions.ts',
helpers: ['planDepartmentScopeWithPaidCore'],
},
{
file: 'app/api/admin/users/route.ts',
helpers: ['planDepartmentScopeWithPaidCore'],
},
{
file: 'app/api/admin/users/update/route.ts',
helpers: ['planDepartmentScopeWithPaidCore'],
},
{
file: 'app/api/materials/route.ts',
helpers: ['buildMaterialManagementScopeWhere', 'canManageMaterial'],
},
{
file: 'app/api/materials/restore/route.ts',
helpers: ['canManageMaterial'],
},
{
file: 'app/api/materials/premium/route.ts',
helpers: ['canManageMaterial'],
},
{
file: 'app/api/materials/stats/route.ts',
helpers: ['planDepartmentScopeWithPaidCore', 'buildMaterialManagementScopeWhere'],
},
];
for (const { file, helpers } of delegatedFiles) {
const source = readProjectFile(file);
for (const helper of helpers) {
expect(source).toContain(helper);
}
expect(source).not.toContain('buildDepartmentDescendants');
expect(source).not.toContain('isDepartmentWithinScope');
}
});
it('部门结构变更规划必须交给 paid-core', () => {
const source = readProjectFile('app/api/departments/route.ts');
expect(source).toContain('planDepartmentStructureChangeWithPaidCore');
expect(source).not.toContain('oldPrefix');
expect(source).not.toContain('newPrefix');
expect(source).not.toContain('renameValue');
expect(source).not.toContain('conflicting');
expect(source).not.toContain('startsWith: prefix');
});
it('本地许可证变更必须写入审计日志', () => {
const source = readProjectFile('lib/license/license-service.ts');
expect(source).toContain('writeLicenseAuditLog');
expect(source).toContain('license_saved');
expect(source).toContain('license_updated');
expect(source).toContain('license_cleared');
expect(source).toContain("nextType === 'PRO'");
});
it('许可证异常必须写入审计日志', () => {
const source = readProjectFile('lib/license/license-service.ts');
expect(source).toContain('license_validation_failed');
expect(source).toContain('license_clock_integrity_failed');
expect(source).toContain('license_clock_rollback_detected');
expect(source).toContain('license_server_signature_failed');
expect(source).toContain('license_server_verification_failed');
expect(source).toContain('license_heartbeat_failed');
});
it('高风险后台维护入口必须要求确认短语', () => {
const destructiveRoutes = [
{
file: 'app/api/admin/cleanup-database/route.ts',
phrase: '清理全部素材',
},
{
file: 'app/api/admin/reset-material-rankings/route.ts',
phrase: '重置排行榜数据',
},
{
file: 'app/api/admin/migrate-material-fields/route.ts',
phrase: '执行字段迁移',
},
];
for (const { file, phrase } of destructiveRoutes) {
const source = readProjectFile(file);
expect(source).toContain('confirmPhrase');
expect(source).toContain(phrase);
}
});
});