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('hasPremiumModule'); expect(source).toContain('PREMIUM_MODULE_IDS.CUSTOM_TEMPLATES_CORE'); expect(source).toContain('PREMIUM_MODULE_IDS.INTRANET_PENETRATION_CORE'); }); 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('getAuthorizedUsageLimits'); expect(source).toContain('授权限额模块不可用'); }); 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); } }); });