/** * 音频处理模块测试 * 测试 lib/audio-processor.ts 的核心功能 */ // Get the mocked parseBuffer from jest.setup.js const musicMetadataMock = jest.requireMock('music-metadata') const mockParseBuffer = musicMetadataMock.parseBuffer as jest.Mock const mockParseFile = musicMetadataMock.parseFile as jest.Mock import { formatDuration, isAudioFile, parseAudioMetadata, parseAudioMetadataFromFile, SUPPORTED_AUDIO_FORMATS, AudioMetadata, } from '../audio-processor' describe('音频处理模块', () => { describe('formatDuration', () => { it('应该正确格式化0秒', () => { expect(formatDuration(0)).toBe('00:00') }) it('应该正确格式化小于1分钟的时长', () => { expect(formatDuration(30)).toBe('00:30') expect(formatDuration(59)).toBe('00:59') }) it('应该正确格式化整分钟', () => { expect(formatDuration(60)).toBe('01:00') expect(formatDuration(120)).toBe('02:00') expect(formatDuration(600)).toBe('10:00') }) it('应该正确格式化分钟和秒的组合', () => { expect(formatDuration(90)).toBe('01:30') expect(formatDuration(125)).toBe('02:05') expect(formatDuration(3661)).toBe('61:01') // 超过1小时 }) it('应该正确处理小数秒(向下取整)', () => { expect(formatDuration(30.5)).toBe('00:30') expect(formatDuration(59.9)).toBe('00:59') expect(formatDuration(90.7)).toBe('01:30') }) it('应该正确处理负数(边界情况)', () => { // Math.floor 对负数的行为 expect(formatDuration(-1)).toBe('-1:-1') }) }) describe('isAudioFile', () => { it('应该识别常见音频MIME类型', () => { expect(isAudioFile('audio/mpeg')).toBe(true) expect(isAudioFile('audio/wav')).toBe(true) expect(isAudioFile('audio/flac')).toBe(true) expect(isAudioFile('audio/aac')).toBe(true) expect(isAudioFile('audio/ogg')).toBe(true) expect(isAudioFile('audio/mp4')).toBe(true) expect(isAudioFile('audio/webm')).toBe(true) }) it('应该拒绝非音频MIME类型', () => { expect(isAudioFile('video/mp4')).toBe(false) expect(isAudioFile('image/png')).toBe(false) expect(isAudioFile('application/pdf')).toBe(false) expect(isAudioFile('text/plain')).toBe(false) }) it('应该处理空字符串', () => { expect(isAudioFile('')).toBe(false) }) it('应该处理不完整的MIME类型', () => { expect(isAudioFile('audio')).toBe(false) expect(isAudioFile('audio/')).toBe(true) // 以 audio/ 开头 }) }) describe('SUPPORTED_AUDIO_FORMATS', () => { it('应该包含MP3格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/mpeg') }) it('应该包含WAV格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/wav') expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/x-wav') }) it('应该包含FLAC格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/flac') }) it('应该包含AAC格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/aac') }) it('应该包含M4A格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/mp4') expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/x-m4a') }) it('应该包含OGG格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/ogg') }) it('应该包含WebM格式', () => { expect(SUPPORTED_AUDIO_FORMATS).toContain('audio/webm') }) it('应该有正确的格式数量', () => { expect(SUPPORTED_AUDIO_FORMATS.length).toBe(9) }) }) describe('parseAudioMetadata', () => { beforeEach(() => { jest.clearAllMocks() }) it('应该正确解析音频元数据', async () => { mockParseBuffer.mockResolvedValue({ format: { duration: 180.5, container: 'MPEG', bitrate: 320000, sampleRate: 44100, numberOfChannels: 2, }, } as any) const buffer = Buffer.from('test audio data') const result = await parseAudioMetadata(buffer, 'audio/mpeg') expect(result).not.toBeNull() expect(result?.duration).toBe(180.5) expect(result?.durationFormatted).toBe('03:00') expect(result?.format).toBe('MPEG') expect(result?.bitrate).toBe(320000) expect(result?.sampleRate).toBe(44100) expect(result?.channels).toBe(2) }) it('应该处理缺少duration的情况', async () => { mockParseBuffer.mockResolvedValue({ format: { container: 'WAV', }, } as any) const buffer = Buffer.from('test') const result = await parseAudioMetadata(buffer) expect(result).not.toBeNull() expect(result?.duration).toBe(0) expect(result?.durationFormatted).toBe('00:00') }) it('应该处理缺少container的情况', async () => { mockParseBuffer.mockResolvedValue({ format: { duration: 60, }, } as any) const buffer = Buffer.from('test') const result = await parseAudioMetadata(buffer) expect(result).not.toBeNull() expect(result?.format).toBe('unknown') }) it('应该优雅处理解析错误', async () => { mockParseBuffer.mockRejectedValue(new Error('Invalid audio format')) const buffer = Buffer.from('invalid data') const result = await parseAudioMetadata(buffer, 'audio/mpeg') expect(result).toBeNull() }) it('应该传递正确的mimeType参数', async () => { mockParseBuffer.mockResolvedValue({ format: { duration: 30, container: 'MP3' }, } as any) const buffer = Buffer.from('test') await parseAudioMetadata(buffer, 'audio/mpeg') expect(mockParseBuffer).toHaveBeenCalledWith(buffer, { mimeType: 'audio/mpeg' }) }) }) describe('parseAudioMetadataFromFile', () => { beforeEach(() => { jest.clearAllMocks() }) it('应该从临时文件路径解析音频元数据', async () => { mockParseFile.mockResolvedValue({ format: { duration: 75, container: 'MP3', bitrate: 192000, sampleRate: 48000, numberOfChannels: 2, }, } as any) const result = await parseAudioMetadataFromFile('/tmp/audio.mp3') expect(mockParseFile).toHaveBeenCalledWith('/tmp/audio.mp3') expect(result).toEqual({ duration: 75, durationFormatted: '01:15', format: 'MP3', bitrate: 192000, sampleRate: 48000, channels: 2, }) }) it('文件解析失败时应返回 null', async () => { mockParseFile.mockRejectedValue(new Error('bad file')) await expect(parseAudioMetadataFromFile('/tmp/bad.mp3')).resolves.toBeNull() }) }) describe('AudioMetadata 类型验证', () => { it('应该有正确的类型结构', () => { const mockMetadata: AudioMetadata = { duration: 180, durationFormatted: '03:00', format: 'mp3', bitrate: 320000, sampleRate: 44100, channels: 2, } expect(mockMetadata.duration).toBe(180) expect(mockMetadata.durationFormatted).toBe('03:00') expect(mockMetadata.format).toBe('mp3') expect(mockMetadata.bitrate).toBe(320000) expect(mockMetadata.sampleRate).toBe(44100) expect(mockMetadata.channels).toBe(2) }) it('可选字段应该可以为undefined', () => { const minimalMetadata: AudioMetadata = { duration: 60, durationFormatted: '01:00', format: 'wav', } expect(minimalMetadata.bitrate).toBeUndefined() expect(minimalMetadata.sampleRate).toBeUndefined() expect(minimalMetadata.channels).toBeUndefined() }) }) })