/** * 系统数据完整性模块测试 * 测试 lib/system-integrity.ts 的核心功能 */ import { BUILTIN_TEMPLATE_KEYS, BUILTIN_TEMPLATE_CONFIG, type BuiltinTemplateKey, checkSystemIntegrity, repairSystemIntegrity, ensureBuiltinTemplates, } from '../system-integrity' import { prisma } from '@/lib/prisma' // Mock prisma jest.mock('@/lib/prisma', () => ({ prisma: { uploadTemplate: { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), count: jest.fn(), }, uploadField: { create: jest.fn(), }, material: { findMany: jest.fn(), count: jest.fn(), updateMany: jest.fn(), }, $transaction: jest.fn(), }, })) const mockPrisma = prisma as jest.Mocked describe('系统数据完整性模块', () => { 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) }) it('所有key应该是字符串类型', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { expect(typeof key).toBe('string') } }) }) describe('BUILTIN_TEMPLATE_CONFIG', () => { it('每个模板key都应该有对应的配置', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { expect(BUILTIN_TEMPLATE_CONFIG[key]).toBeDefined() } }) it('每个模板配置应该包含必要字段', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] expect(config.name).toBeTruthy() expect(typeof config.name).toBe('string') expect(config.description).toBeTruthy() expect(typeof config.description).toBe('string') expect(config.materialType).toBeTruthy() expect(typeof config.materialType).toBe('string') expect(config.materialKind).toBeTruthy() expect(Array.isArray(config.fields)).toBe(true) expect(config.fields.length).toBeGreaterThan(0) } }) it('materialKind 应该是有效值 (IMAGE/VIDEO/AUDIO/OTHER)', () => { const validKinds = ['IMAGE', 'VIDEO', 'AUDIO', 'OTHER'] for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] expect(validKinds).toContain(config.materialKind) } }) it('3DMAX模板应该包含renderer字段', () => { const config = BUILTIN_TEMPLATE_CONFIG['3dmax-material'] const fieldKeys = config.fields.map(f => f.key) expect(fieldKeys).toContain('renderer') }) it('视频模板的materialKind应该是VIDEO', () => { const config = BUILTIN_TEMPLATE_CONFIG['video-material'] expect(config.materialKind).toBe('VIDEO') }) it('其他模板的materialKind应该是OTHER', () => { const config = BUILTIN_TEMPLATE_CONFIG['other-material'] expect(config.materialKind).toBe('OTHER') }) it('所有IMAGE类型模板应该正确标记', () => { const imageTemplates: BuiltinTemplateKey[] = [ '3dmax-material', 'psd-material', 'model-material', ] for (const key of imageTemplates) { expect(BUILTIN_TEMPLATE_CONFIG[key].materialKind).toBe('IMAGE') } }) it('音频模板的materialKind应该是AUDIO', () => { const config = BUILTIN_TEMPLATE_CONFIG['audio-material'] expect(config.materialKind).toBe('AUDIO') }) }) describe('字段配置验证', () => { it('每个字段应该有完整的配置', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] for (const field of config.fields) { expect(field.key).toBeTruthy() expect(typeof field.key).toBe('string') expect(field.label).toBeTruthy() expect(typeof field.label).toBe('string') expect(field.type).toBeTruthy() expect(typeof field.required).toBe('boolean') expect(field.layoutWidth).toBeTruthy() expect(typeof field.visibleOnHomeFilter).toBe('boolean') expect(typeof field.isSearchable).toBe('boolean') } } }) it('字段类型应该是有效值', () => { const validTypes = ['text', 'select', 'number', 'textarea', 'date', 'checkbox'] for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] for (const field of config.fields) { expect(validTypes).toContain(field.type) } } }) it('所有模板都应该包含productModel字段', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] const fieldKeys = config.fields.map(f => f.key) expect(fieldKeys).toContain('productModel') } }) it('所有模板都应该包含分类字段(categoryName或audioCategory)', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] const fieldKeys = config.fields.map(f => f.key) // 音频模板使用 audioCategory,其他模板使用 categoryName const hasCategoryField = fieldKeys.includes('categoryName') || fieldKeys.includes('audioCategory') expect(hasCategoryField).toBe(true) } }) it('所有模板都应该包含场景字段(sceneName或audioScene)', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] const fieldKeys = config.fields.map(f => f.key) // 音频模板使用 audioScene,其他模板使用 sceneName const hasSceneField = fieldKeys.includes('sceneName') || fieldKeys.includes('audioScene') expect(hasSceneField).toBe(true) } }) it('分类和场景字段应该标记addToCategory', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] // 查找分类字段(categoryName 或 audioCategory) const categoryField = config.fields.find(f => f.key === 'categoryName' || f.key === 'audioCategory') // 查找场景字段(sceneName 或 audioScene) const sceneField = config.fields.find(f => f.key === 'sceneName' || f.key === 'audioScene') expect(categoryField?.addToCategory).toBe(true) expect(sceneField?.addToCategory).toBe(true) } }) it('select类型字段应该标记addToCategory', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] const selectFields = config.fields.filter(f => f.type === 'select') for (const field of selectFields) { expect(field.addToCategory).toBe(true) } } }) }) describe('模板ID生成规则', () => { it('内置模板ID应该以builtin-开头', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const templateId = `builtin-${key}` expect(templateId).toMatch(/^builtin-/) } }) it('生成的模板ID应该唯一', () => { const ids = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`) const uniqueIds = new Set(ids) expect(uniqueIds.size).toBe(ids.length) }) it('模板ID格式应该符合规范', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const templateId = `builtin-${key}` // 只包含字母、数字和连字符 expect(templateId).toMatch(/^[a-z0-9-]+$/) } }) }) describe('模板配置一致性', () => { it('所有模板的materialType应该唯一', () => { const materialTypes = BUILTIN_TEMPLATE_KEYS.map( key => BUILTIN_TEMPLATE_CONFIG[key].materialType ) const uniqueTypes = new Set(materialTypes) expect(uniqueTypes.size).toBe(materialTypes.length) }) it('模板名称应该唯一', () => { const names = BUILTIN_TEMPLATE_KEYS.map( key => BUILTIN_TEMPLATE_CONFIG[key].name ) const uniqueNames = new Set(names) expect(uniqueNames.size).toBe(names.length) }) it('每个模板的字段key应该唯一', () => { for (const key of BUILTIN_TEMPLATE_KEYS) { const config = BUILTIN_TEMPLATE_CONFIG[key] const fieldKeys = config.fields.map(f => f.key) const uniqueKeys = new Set(fieldKeys) expect(uniqueKeys.size).toBe(fieldKeys.length) } }) }) }) describe('checkSystemIntegrity', () => { beforeEach(() => { jest.clearAllMocks() }) it('当所有内置模板存在且无孤儿素材时应返回healthy=true', async () => { // Mock: 所有内置模板都存在 const expectedIds = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`) ;(mockPrisma.uploadTemplate.findMany as jest.Mock) .mockResolvedValueOnce(expectedIds.map(id => ({ id }))) // 内置模板检查 .mockResolvedValueOnce([]) // 引用模板检查 // Mock: 没有素材引用模板 ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([]) const result = await checkSystemIntegrity() expect(result.healthy).toBe(true) expect(result.checks.builtinTemplates.healthy).toBe(true) expect(result.checks.builtinTemplates.missing).toHaveLength(0) expect(result.checks.orphanedMaterials.healthy).toBe(true) expect(result.recommendations).toHaveLength(0) }) it('当缺少内置模板时应返回healthy=false', async () => { // Mock: 只有部分模板存在 ;(mockPrisma.uploadTemplate.findMany as jest.Mock) .mockResolvedValueOnce([ { id: 'builtin-3dmax-material' }, { id: 'builtin-psd-material' }, ]) .mockResolvedValueOnce([]) ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([]) const result = await checkSystemIntegrity() expect(result.healthy).toBe(false) expect(result.checks.builtinTemplates.healthy).toBe(false) expect(result.checks.builtinTemplates.existing).toBe(2) expect(result.checks.builtinTemplates.missing.length).toBe(4) expect(result.recommendations.length).toBeGreaterThan(0) }) it('当存在孤儿素材时应返回healthy=false', async () => { // Mock: 所有内置模板存在 const expectedIds = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`) ;(mockPrisma.uploadTemplate.findMany as jest.Mock) .mockResolvedValueOnce(expectedIds.map(id => ({ id }))) .mockResolvedValueOnce([]) // 引用的模板不存在 // Mock: 有素材引用不存在的模板 ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([ { templateId: 'deleted-template-1' }, { templateId: 'deleted-template-2' }, ]) ;(mockPrisma.material.count as jest.Mock).mockResolvedValue(5) const result = await checkSystemIntegrity() expect(result.healthy).toBe(false) expect(result.checks.orphanedMaterials.healthy).toBe(false) expect(result.checks.orphanedMaterials.count).toBe(5) expect(result.checks.orphanedMaterials.templateIds).toContain('deleted-template-1') }) it('应该返回正确的时间戳格式', async () => { ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([]) const result = await checkSystemIntegrity() expect(result.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) }) }) describe('repairSystemIntegrity', () => { beforeEach(() => { jest.clearAllMocks() }) it('应该创建缺失的内置模板', async () => { // Mock transaction 执行创建逻辑 ;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => { const tx = { uploadTemplate: { findUnique: jest.fn().mockResolvedValue(null), // 模板不存在 create: jest.fn().mockResolvedValue({ id: 'test' }), }, uploadField: { create: jest.fn().mockResolvedValue({}), }, } return callback(tx) }) // Mock 孤儿素材检查 ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) const result = await repairSystemIntegrity({ createMissingBuiltinTemplates: true, fixOrphanedMaterials: false, }) expect(result.success).toBe(true) expect(result.repairs.templatesCreated.length).toBe(6) expect(result.errors).toHaveLength(0) }) it('当模板已存在时应跳过创建', async () => { ;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => { const tx = { uploadTemplate: { findUnique: jest.fn().mockResolvedValue({ id: 'existing' }), // 模板已存在 create: jest.fn(), }, uploadField: { create: jest.fn(), }, } return callback(tx) }) ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) const result = await repairSystemIntegrity({ createMissingBuiltinTemplates: true, fixOrphanedMaterials: false, }) expect(result.success).toBe(true) expect(result.repairs.templatesCreated).toHaveLength(0) }) it('应该修复孤儿素材', async () => { ;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => { const tx = { uploadTemplate: { findUnique: jest.fn().mockResolvedValue({ id: 'existing' }), create: jest.fn(), }, uploadField: { create: jest.fn(), }, } return callback(tx) }) // Mock: 有素材引用不存在的自定义模板 ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([ { templateId: 'custom-deleted-template' }, ]) ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.material.updateMany as jest.Mock).mockResolvedValue({ count: 3 }) const result = await repairSystemIntegrity({ createMissingBuiltinTemplates: false, fixOrphanedMaterials: true, }) expect(result.success).toBe(true) expect(result.repairs.orphanedMaterialsFixed).toBe(3) }) it('创建模板失败时应记录错误', async () => { ;(mockPrisma.$transaction as jest.Mock).mockRejectedValue(new Error('Database error')) ;(mockPrisma.material.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) const result = await repairSystemIntegrity({ createMissingBuiltinTemplates: true, fixOrphanedMaterials: false, }) expect(result.success).toBe(false) expect(result.errors.length).toBeGreaterThan(0) expect(result.repairs.templatesFailed.length).toBe(6) }) }) describe('ensureBuiltinTemplates', () => { beforeEach(() => { jest.clearAllMocks() }) it('当所有模板已存在时应返回空created数组', async () => { const expectedIds = BUILTIN_TEMPLATE_KEYS.map(key => `builtin-${key}`) ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue( expectedIds.map(id => ({ id })) ) const result = await ensureBuiltinTemplates() expect(result.checked).toBe(true) expect(result.created).toHaveLength(0) expect(result.errors).toHaveLength(0) }) it('应该只创建缺失的模板', async () => { // Mock: 只有2个模板存在 ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([ { id: 'builtin-3dmax-material' }, { id: 'builtin-psd-material' }, ]) ;(mockPrisma.$transaction as jest.Mock).mockImplementation(async (callback) => { const tx = { uploadTemplate: { findUnique: jest.fn().mockResolvedValue(null), create: jest.fn().mockResolvedValue({}), }, uploadField: { create: jest.fn().mockResolvedValue({}), }, } await callback(tx) }) const result = await ensureBuiltinTemplates() expect(result.checked).toBe(true) expect(result.created.length).toBe(4) // 6 - 2 = 4 }) it('并发创建时应忽略唯一约束冲突', async () => { ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.$transaction as jest.Mock).mockRejectedValue( new Error('Unique constraint failed') ) const result = await ensureBuiltinTemplates() expect(result.checked).toBe(true) expect(result.errors).toHaveLength(0) // 唯一约束错误被忽略 }) it('其他错误应该被记录', async () => { ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockResolvedValue([]) ;(mockPrisma.$transaction as jest.Mock).mockRejectedValue( new Error('Connection timeout') ) const result = await ensureBuiltinTemplates() expect(result.checked).toBe(true) expect(result.errors.length).toBeGreaterThan(0) expect(result.errors[0]).toContain('Connection timeout') }) it('初始查询失败时应返回checked=false', async () => { ;(mockPrisma.uploadTemplate.findMany as jest.Mock).mockRejectedValue( new Error('Database unavailable') ) const result = await ensureBuiltinTemplates() expect(result.checked).toBe(false) expect(result.errors).toContain('Database unavailable') }) })