/** * 模板列表 API 测试 * * 测试 app/api/templates/route.ts 的核心功能 * * 主要测试点: * 1. 内置模板生成逻辑 * 2. 自定义模板查询(排除内置模板) * 3. 模板合并和排序 */ // 内置模板配置(与 route.ts 保持一致) const BUILTIN_TEMPLATE_KEYS = [ '3dmax-material', 'psd-material', 'audio-material', 'video-material', 'model-material', 'other-material', ] const BUILTIN_TEMPLATE_NAMES: Record = { '3dmax-material': '3DMAX 素材模板', 'psd-material': 'PSD 素材模板', 'audio-material': '音频素材模板', 'video-material': '视频素材模板', 'model-material': '模特素材模板', 'other-material': '其他素材模板', } function createBuiltinTemplate(key: string) { return { id: `builtin-${key}`, name: BUILTIN_TEMPLATE_NAMES[key] || key, isBuiltin: true, } } describe('模板列表 API - 内置模板配置', () => { describe('BUILTIN_TEMPLATE_KEYS', () => { it('应该包含6个内置模板key', () => { expect(BUILTIN_TEMPLATE_KEYS).toHaveLength(6) }) it('应该包含所有预期的模板key', () => { const expectedKeys = [ '3dmax-material', 'psd-material', 'audio-material', 'video-material', 'model-material', 'other-material', ] expect(BUILTIN_TEMPLATE_KEYS).toEqual(expectedKeys) }) }) describe('BUILTIN_TEMPLATE_NAMES', () => { it('每个模板key都应该有对应的名称', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { expect(BUILTIN_TEMPLATE_NAMES[key]).toBeDefined() expect(typeof BUILTIN_TEMPLATE_NAMES[key]).toBe('string') expect(BUILTIN_TEMPLATE_NAMES[key].length).toBeGreaterThan(0) } }) }) describe('createBuiltinTemplate', () => { it('应该生成正确格式的内置模板对象', () => { const template = createBuiltinTemplate('3dmax-material') expect(template).toEqual({ id: 'builtin-3dmax-material', name: '3DMAX 素材模板', isBuiltin: true, }) }) it('内置模板ID应该以builtin-开头', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const template = createBuiltinTemplate(key) expect(template.id).toMatch(/^builtin-/) expect(template.id).toBe(`builtin-${key}`) } }) it('所有内置模板应该标记isBuiltin为true', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const template = createBuiltinTemplate(key) expect(template.isBuiltin).toBe(true) } }) it('未知key应该使用key作为名称', () => { const template = createBuiltinTemplate('unknown-key') expect(template.name).toBe('unknown-key') }) }) }) describe('模板列表 API - 模板过滤逻辑', () => { /** * 测试内置模板过滤逻辑 * 验证:当 enabledBuiltinTemplates 为空数组时,应该返回所有内置模板 */ describe('内置模板启用过滤', () => { it('enabledBuiltinTemplates为空数组时应返回所有内置模板', () => { const enabledBuiltinTemplates: string[] = [] const builtinTemplates = BUILTIN_TEMPLATE_KEYS .filter(key => enabledBuiltinTemplates.length === 0 || enabledBuiltinTemplates.includes(key)) .map(key => createBuiltinTemplate(key)) expect(builtinTemplates).toHaveLength(6) }) it('enabledBuiltinTemplates有值时应只返回启用的模板', () => { const enabledBuiltinTemplates = ['3dmax-material', 'psd-material'] const builtinTemplates = BUILTIN_TEMPLATE_KEYS .filter(key => enabledBuiltinTemplates.length === 0 || enabledBuiltinTemplates.includes(key)) .map(key => createBuiltinTemplate(key)) expect(builtinTemplates).toHaveLength(2) expect(builtinTemplates.map(t => t.id)).toEqual([ 'builtin-3dmax-material', 'builtin-psd-material', ]) }) it('enabledBuiltinTemplates只包含一个时应只返回该模板', () => { const enabledBuiltinTemplates = ['video-material'] const builtinTemplates = BUILTIN_TEMPLATE_KEYS .filter(key => enabledBuiltinTemplates.length === 0 || enabledBuiltinTemplates.includes(key)) .map(key => createBuiltinTemplate(key)) expect(builtinTemplates).toHaveLength(1) expect(builtinTemplates[0].id).toBe('builtin-video-material') }) }) /** * 测试自定义模板排除内置模板的逻辑 * 这是本次修改的核心功能 */ describe('自定义模板排除内置模板', () => { it('应该正确识别内置模板ID格式', () => { const builtinIds = [ 'builtin-3dmax-material', 'builtin-psd-material', 'builtin-audio-material', ] for (const id of builtinIds) { expect(id.startsWith('builtin-')).toBe(true) } }) it('自定义模板ID不应该以builtin-开头', () => { const customIds = [ 'custom-template-1', 'my-template', 'user-created-template', ] for (const id of customIds) { expect(id.startsWith('builtin-')).toBe(false) } }) it('模拟过滤逻辑:应该排除builtin-开头的模板', () => { // 模拟数据库中的模板数据 const allDbTemplates = [ { id: 'builtin-3dmax-material', name: '3DMAX 素材模板', status: 'active' }, { id: 'builtin-psd-material', name: 'PSD 素材模板', status: 'active' }, { id: 'custom-template-1', name: '自定义模板1', status: 'active' }, { id: 'my-template', name: '我的模板', status: 'active' }, ] // 模拟 Prisma 查询的 NOT startsWith 逻辑 const customTemplates = allDbTemplates.filter(t => t.status === 'active' && !t.id.startsWith('builtin-') ) expect(customTemplates).toHaveLength(2) expect(customTemplates.map(t => t.id)).toEqual([ 'custom-template-1', 'my-template', ]) }) it('当数据库中没有自定义模板时应返回空数组', () => { const allDbTemplates = [ { id: 'builtin-3dmax-material', name: '3DMAX 素材模板', status: 'active' }, { id: 'builtin-psd-material', name: 'PSD 素材模板', status: 'active' }, ] const customTemplates = allDbTemplates.filter(t => t.status === 'active' && !t.id.startsWith('builtin-') ) expect(customTemplates).toHaveLength(0) }) it('应该排除非active状态的模板', () => { const allDbTemplates = [ { id: 'custom-template-1', name: '自定义模板1', status: 'active' }, { id: 'custom-template-2', name: '自定义模板2', status: 'draft' }, { id: 'custom-template-3', name: '自定义模板3', status: 'archived' }, ] const customTemplates = allDbTemplates.filter(t => t.status === 'active' && !t.id.startsWith('builtin-') ) expect(customTemplates).toHaveLength(1) expect(customTemplates[0].id).toBe('custom-template-1') }) }) /** * 测试模板合并逻辑 */ describe('模板合并', () => { it('合并后内置模板应该在前面', () => { const builtinTemplates = [ { id: 'builtin-3dmax-material', name: '3DMAX', isBuiltin: true }, ] const customTemplates = [ { id: 'custom-1', name: '自定义1' }, ] const allTemplates = [ ...builtinTemplates, ...customTemplates.map(t => ({ ...t, isBuiltin: false })), ] expect(allTemplates[0].isBuiltin).toBe(true) expect(allTemplates[1].isBuiltin).toBe(false) }) it('自定义模板应该标记isBuiltin为false', () => { const customTemplates = [ { id: 'custom-1', name: '自定义1' }, { id: 'custom-2', name: '自定义2' }, ] const merged = customTemplates.map(t => ({ ...t, isBuiltin: false })) for (const t of merged) { expect(t.isBuiltin).toBe(false) } }) it('合并后不应该有重复的模板', () => { const builtinTemplates = BUILTIN_TEMPLATE_KEYS.map(key => createBuiltinTemplate(key)) const customTemplates = [ { id: 'custom-1', name: '自定义1', isBuiltin: false }, ] const allTemplates = [...builtinTemplates, ...customTemplates] const ids = allTemplates.map(t => t.id) const uniqueIds = new Set(ids) expect(uniqueIds.size).toBe(ids.length) }) }) }) describe('模板列表 API - 边界情况', () => { it('空的enabledBuiltinTemplates数组应该表示全部启用', () => { const enabledBuiltinTemplates: string[] = [] const isAllEnabled = enabledBuiltinTemplates.length === 0 expect(isAllEnabled).toBe(true) }) it('null或undefined的enabledBuiltinTemplates应该被处理为全部启用', () => { // 模拟从数据库获取的值 const settingsValue: string | null = null let enabledBuiltinTemplates: string[] = [] if (settingsValue) { try { const parsed = JSON.parse(settingsValue) if (Array.isArray(parsed) && parsed.length > 0) { enabledBuiltinTemplates = parsed } } catch { // 解析失败,保持空数组 } } // 空数组表示全部启用 const builtinTemplates = BUILTIN_TEMPLATE_KEYS .filter(key => enabledBuiltinTemplates.length === 0 || enabledBuiltinTemplates.includes(key)) expect(builtinTemplates).toHaveLength(6) }) it('无效的JSON应该被处理为全部启用', () => { const invalidJson = 'not-valid-json' let enabledBuiltinTemplates: string[] = [] try { const parsed = JSON.parse(invalidJson) if (Array.isArray(parsed) && parsed.length > 0) { enabledBuiltinTemplates = parsed } } catch { // 解析失败,保持空数组 } expect(enabledBuiltinTemplates).toHaveLength(0) }) })