382 lines
15 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|