mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add backend unit test framework and modularize settings UI
Introduced a Vitest-based backend unit testing framework with supporting scripts, helpers, and GitHub Actions integration. Refactored the admin settings page to a modular architecture, splitting monolithic logic into feature-specific tabs and hooks for improved maintainability and testability. Updated documentation to reflect the new testing setup and settings architecture, and added new dependencies for testing utilities.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Component: Metadata Tagging Utility Tests
|
||||
* Documentation: documentation/phase3/file-organization.md
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { checkFfmpegAvailable, tagAudioFileMetadata, tagMultipleFiles } from '@/lib/utils/metadata-tagger';
|
||||
|
||||
const execMock = vi.hoisted(() => vi.fn());
|
||||
const fsMock = vi.hoisted(() => ({
|
||||
access: vi.fn(),
|
||||
unlink: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
exec: execMock,
|
||||
}));
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
default: fsMock,
|
||||
...fsMock,
|
||||
}));
|
||||
|
||||
function mockExecSuccess(stdout = 'ok') {
|
||||
execMock.mockImplementation((command: string, options: any, callback?: any) => {
|
||||
const cb = typeof options === 'function' ? options : callback;
|
||||
cb(null, stdout, '');
|
||||
});
|
||||
}
|
||||
|
||||
function mockExecFailure(message = 'ffmpeg error') {
|
||||
execMock.mockImplementation((command: string, options: any, callback?: any) => {
|
||||
const cb = typeof options === 'function' ? options : callback;
|
||||
cb(new Error(message), '', '');
|
||||
});
|
||||
}
|
||||
|
||||
describe('metadata tagger', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns an error for unsupported file formats', async () => {
|
||||
fsMock.access.mockResolvedValue(undefined);
|
||||
|
||||
const result = await tagAudioFileMetadata('/tmp/book.wav', {
|
||||
title: 'Book',
|
||||
author: 'Author',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Unsupported file format');
|
||||
expect(execMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tags an m4b file with metadata', async () => {
|
||||
fsMock.access.mockResolvedValue(undefined);
|
||||
mockExecSuccess('done');
|
||||
|
||||
const result = await tagAudioFileMetadata('/tmp/book.m4b', {
|
||||
title: 'Book',
|
||||
author: 'Author',
|
||||
narrator: 'Narrator',
|
||||
year: 2020,
|
||||
asin: 'ASIN123',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.taggedFilePath).toBe('/tmp/book.m4b.tmp');
|
||||
|
||||
const command = execMock.mock.calls[0][0] as string;
|
||||
expect(command).toContain('-metadata title="Book"');
|
||||
expect(command).toContain('-metadata album_artist="Author"');
|
||||
expect(command).toContain('-metadata composer="Narrator"');
|
||||
expect(command).toContain('-metadata date="2020"');
|
||||
expect(command).toContain('-metadata ----:com.apple.iTunes:ASIN="ASIN123"');
|
||||
expect(command).toContain('-f mp4');
|
||||
});
|
||||
|
||||
it('cleans up temp files and returns errors when ffmpeg fails', async () => {
|
||||
fsMock.access.mockResolvedValue(undefined);
|
||||
fsMock.unlink.mockResolvedValue(undefined);
|
||||
mockExecFailure('exec failed');
|
||||
|
||||
const result = await tagAudioFileMetadata('/tmp/book.mp3', {
|
||||
title: 'Book',
|
||||
author: 'Author',
|
||||
asin: 'ASIN123',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('ffmpeg failed');
|
||||
expect(fsMock.unlink).toHaveBeenCalledWith('/tmp/book.mp3.tmp');
|
||||
});
|
||||
|
||||
it('tags multiple files in sequence', async () => {
|
||||
fsMock.access.mockResolvedValue(undefined);
|
||||
mockExecSuccess('done');
|
||||
|
||||
const results = await tagMultipleFiles(['/tmp/one.m4a', '/tmp/two.m4a'], {
|
||||
title: 'Book',
|
||||
author: 'Author',
|
||||
});
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((result) => result.success)).toBe(true);
|
||||
});
|
||||
|
||||
it('checks ffmpeg availability', async () => {
|
||||
mockExecSuccess('ffmpeg version');
|
||||
await expect(checkFfmpegAvailable()).resolves.toBe(true);
|
||||
|
||||
mockExecFailure('not installed');
|
||||
await expect(checkFfmpegAvailable()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user