flnxi-Wu/lib/__tests__/audio-processor.test.ts
2026-05-28 23:27:15 +08:00

265 lines
7.7 KiB
TypeScript

/**
* 音频处理模块测试
* 测试 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()
})
})
})