420 lines
13 KiB
TypeScript
420 lines
13 KiB
TypeScript
/**
|
|
* MediaEngine Unit Tests
|
|
*
|
|
* Tests for media file management including:
|
|
* - Media import and storage
|
|
* - Sidecar metadata file handling
|
|
* - MIME type detection
|
|
* - Checksum calculation
|
|
* - Filename generation
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { createMockMedia, createMockPdfMedia, resetMockCounters } from '../utils/factories';
|
|
|
|
// Mock dependencies
|
|
vi.mock('../../src/main/database', () => ({
|
|
getDatabase: vi.fn(() => ({
|
|
getLocal: vi.fn(() => ({
|
|
select: vi.fn(() => ({
|
|
from: vi.fn(() => ({
|
|
where: vi.fn(() => Promise.resolve([])),
|
|
orderBy: vi.fn(() => Promise.resolve([])),
|
|
})),
|
|
})),
|
|
insert: vi.fn(() => ({
|
|
values: vi.fn(() => Promise.resolve()),
|
|
})),
|
|
update: vi.fn(() => ({
|
|
set: vi.fn(() => ({
|
|
where: vi.fn(() => Promise.resolve()),
|
|
})),
|
|
})),
|
|
delete: vi.fn(() => ({
|
|
where: vi.fn(() => Promise.resolve()),
|
|
})),
|
|
})),
|
|
getRemote: vi.fn(() => null),
|
|
getDataPaths: vi.fn(() => ({
|
|
database: '/mock/userData/bds.db',
|
|
posts: '/mock/userData/posts',
|
|
media: '/mock/userData/media',
|
|
})),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('fs/promises');
|
|
|
|
describe('MediaEngine', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
resetMockCounters();
|
|
});
|
|
|
|
describe('Media Data Validation', () => {
|
|
it('should create valid media with required fields', () => {
|
|
const media = createMockMedia();
|
|
|
|
expect(media.id).toBeDefined();
|
|
expect(media.filename).toBeDefined();
|
|
expect(media.originalName).toBeDefined();
|
|
expect(media.mimeType).toBeDefined();
|
|
expect(media.size).toBeGreaterThan(0);
|
|
expect(media.createdAt).toBeInstanceOf(Date);
|
|
expect(media.updatedAt).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it('should allow optional dimension fields', () => {
|
|
const imageMedia = createMockMedia({ width: 1920, height: 1080 });
|
|
const pdfMedia = createMockPdfMedia();
|
|
|
|
expect(imageMedia.width).toBe(1920);
|
|
expect(imageMedia.height).toBe(1080);
|
|
expect(pdfMedia.width).toBeUndefined();
|
|
expect(pdfMedia.height).toBeUndefined();
|
|
});
|
|
|
|
it('should allow optional alt and caption', () => {
|
|
const withAlt = createMockMedia({ alt: 'Description', caption: 'A caption' });
|
|
const withoutAlt = createMockMedia({ alt: undefined, caption: undefined });
|
|
|
|
expect(withAlt.alt).toBe('Description');
|
|
expect(withAlt.caption).toBe('A caption');
|
|
expect(withoutAlt.alt).toBeUndefined();
|
|
expect(withoutAlt.caption).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('MIME Type Handling', () => {
|
|
it('should recognize common image MIME types', () => {
|
|
const imageMimeTypes = [
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/gif',
|
|
'image/webp',
|
|
'image/svg+xml',
|
|
];
|
|
|
|
imageMimeTypes.forEach(mimeType => {
|
|
const media = createMockMedia({ mimeType });
|
|
expect(media.mimeType).toBe(mimeType);
|
|
expect(media.mimeType.startsWith('image/')).toBe(true);
|
|
});
|
|
});
|
|
|
|
it('should recognize document MIME types', () => {
|
|
const docMimeTypes = [
|
|
'application/pdf',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
];
|
|
|
|
docMimeTypes.forEach(mimeType => {
|
|
const media = createMockMedia({ mimeType });
|
|
expect(media.mimeType).toBe(mimeType);
|
|
});
|
|
});
|
|
|
|
it('should identify image types by MIME prefix', () => {
|
|
const isImage = (mimeType: string) => mimeType.startsWith('image/');
|
|
|
|
expect(isImage('image/jpeg')).toBe(true);
|
|
expect(isImage('image/png')).toBe(true);
|
|
expect(isImage('application/pdf')).toBe(false);
|
|
expect(isImage('video/mp4')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Filename Generation', () => {
|
|
it('should generate unique filenames', () => {
|
|
const generateFilename = (originalName: string, id: string): string => {
|
|
const ext = originalName.split('.').pop() || '';
|
|
return `${id}.${ext}`;
|
|
};
|
|
|
|
const filename1 = generateFilename('photo.jpg', 'media-1');
|
|
const filename2 = generateFilename('photo.jpg', 'media-2');
|
|
|
|
expect(filename1).toBe('media-1.jpg');
|
|
expect(filename2).toBe('media-2.jpg');
|
|
expect(filename1).not.toBe(filename2);
|
|
});
|
|
|
|
it('should preserve file extension', () => {
|
|
const getExtension = (filename: string): string => {
|
|
const parts = filename.split('.');
|
|
return parts.length > 1 ? parts.pop()!.toLowerCase() : '';
|
|
};
|
|
|
|
expect(getExtension('photo.jpg')).toBe('jpg');
|
|
expect(getExtension('document.PDF')).toBe('pdf');
|
|
expect(getExtension('archive.tar.gz')).toBe('gz');
|
|
expect(getExtension('noextension')).toBe('');
|
|
});
|
|
|
|
it('should sanitize original filename', () => {
|
|
const sanitizeFilename = (filename: string): string => {
|
|
return filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
};
|
|
|
|
expect(sanitizeFilename('my file (1).jpg')).toBe('my_file__1_.jpg');
|
|
expect(sanitizeFilename('résumé.pdf')).toBe('r_sum_.pdf');
|
|
expect(sanitizeFilename('normal-file.png')).toBe('normal-file.png');
|
|
});
|
|
});
|
|
|
|
describe('Sidecar Metadata', () => {
|
|
it('should generate sidecar path from media path', () => {
|
|
const getSidecarPath = (mediaPath: string): string => `${mediaPath}.meta`;
|
|
|
|
expect(getSidecarPath('/media/photo.jpg')).toBe('/media/photo.jpg.meta');
|
|
expect(getSidecarPath('/media/doc.pdf')).toBe('/media/doc.pdf.meta');
|
|
});
|
|
|
|
it('should create correct sidecar content structure', () => {
|
|
const media = createMockMedia({
|
|
id: 'media-123',
|
|
originalName: 'vacation-photo.jpg',
|
|
mimeType: 'image/jpeg',
|
|
size: 1024000,
|
|
width: 1920,
|
|
height: 1080,
|
|
alt: 'Vacation photo',
|
|
caption: 'Summer vacation 2024',
|
|
tags: ['vacation', 'summer'],
|
|
});
|
|
|
|
const sidecarContent = {
|
|
id: media.id,
|
|
originalName: media.originalName,
|
|
mimeType: media.mimeType,
|
|
size: media.size,
|
|
width: media.width,
|
|
height: media.height,
|
|
alt: media.alt,
|
|
caption: media.caption,
|
|
createdAt: media.createdAt.toISOString(),
|
|
updatedAt: media.updatedAt.toISOString(),
|
|
tags: media.tags,
|
|
};
|
|
|
|
expect(sidecarContent.id).toBe('media-123');
|
|
expect(sidecarContent.width).toBe(1920);
|
|
expect(sidecarContent.tags).toEqual(['vacation', 'summer']);
|
|
});
|
|
|
|
it('should handle missing optional fields in sidecar', () => {
|
|
const media = createMockPdfMedia({
|
|
alt: undefined,
|
|
caption: undefined,
|
|
});
|
|
|
|
const sidecarContent = {
|
|
id: media.id,
|
|
originalName: media.originalName,
|
|
mimeType: media.mimeType,
|
|
size: media.size,
|
|
width: media.width,
|
|
height: media.height,
|
|
alt: media.alt,
|
|
caption: media.caption,
|
|
createdAt: media.createdAt.toISOString(),
|
|
updatedAt: media.updatedAt.toISOString(),
|
|
tags: media.tags,
|
|
};
|
|
|
|
expect(sidecarContent.width).toBeUndefined();
|
|
expect(sidecarContent.height).toBeUndefined();
|
|
expect(sidecarContent.alt).toBeUndefined();
|
|
expect(sidecarContent.caption).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Checksum Calculation', () => {
|
|
it('should calculate consistent checksum for same content', () => {
|
|
const crypto = require('crypto');
|
|
const calculateChecksum = (buffer: Buffer): string => {
|
|
return crypto.createHash('md5').update(buffer).digest('hex');
|
|
};
|
|
|
|
const buffer = Buffer.from('test content');
|
|
const checksum1 = calculateChecksum(buffer);
|
|
const checksum2 = calculateChecksum(buffer);
|
|
|
|
expect(checksum1).toBe(checksum2);
|
|
});
|
|
|
|
it('should calculate different checksums for different content', () => {
|
|
const crypto = require('crypto');
|
|
const calculateChecksum = (buffer: Buffer): string => {
|
|
return crypto.createHash('md5').update(buffer).digest('hex');
|
|
};
|
|
|
|
const buffer1 = Buffer.from('content A');
|
|
const buffer2 = Buffer.from('content B');
|
|
|
|
expect(calculateChecksum(buffer1)).not.toBe(calculateChecksum(buffer2));
|
|
});
|
|
});
|
|
|
|
describe('File Size Formatting', () => {
|
|
it('should format bytes correctly', () => {
|
|
const formatFileSize = (bytes: number): string => {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
};
|
|
|
|
expect(formatFileSize(0)).toBe('0 B');
|
|
expect(formatFileSize(500)).toBe('500 B');
|
|
expect(formatFileSize(1024)).toBe('1 KB');
|
|
expect(formatFileSize(1536)).toBe('1.5 KB');
|
|
expect(formatFileSize(1048576)).toBe('1 MB');
|
|
expect(formatFileSize(1073741824)).toBe('1 GB');
|
|
});
|
|
});
|
|
|
|
describe('Media Tags', () => {
|
|
it('should handle empty tags array', () => {
|
|
const media = createMockMedia({ tags: [] });
|
|
expect(media.tags).toEqual([]);
|
|
});
|
|
|
|
it('should preserve tag order', () => {
|
|
const tags = ['first', 'second', 'third'];
|
|
const media = createMockMedia({ tags });
|
|
expect(media.tags).toEqual(tags);
|
|
});
|
|
|
|
it('should serialize tags to JSON', () => {
|
|
const tags = ['photo', 'landscape', '2024'];
|
|
const serialized = JSON.stringify(tags);
|
|
const deserialized = JSON.parse(serialized);
|
|
|
|
expect(deserialized).toEqual(tags);
|
|
});
|
|
});
|
|
|
|
describe('Image Dimensions', () => {
|
|
it('should store width and height for images', () => {
|
|
const media = createMockMedia({
|
|
mimeType: 'image/jpeg',
|
|
width: 3840,
|
|
height: 2160,
|
|
});
|
|
|
|
expect(media.width).toBe(3840);
|
|
expect(media.height).toBe(2160);
|
|
});
|
|
|
|
it('should calculate aspect ratio', () => {
|
|
const getAspectRatio = (width: number, height: number): string => {
|
|
const gcd = (a: number, b: number): number => b === 0 ? a : gcd(b, a % b);
|
|
const divisor = gcd(width, height);
|
|
return `${width / divisor}:${height / divisor}`;
|
|
};
|
|
|
|
expect(getAspectRatio(1920, 1080)).toBe('16:9');
|
|
expect(getAspectRatio(1024, 768)).toBe('4:3');
|
|
expect(getAspectRatio(1080, 1080)).toBe('1:1');
|
|
});
|
|
|
|
it('should not require dimensions for non-images', () => {
|
|
const pdf = createMockPdfMedia();
|
|
expect(pdf.width).toBeUndefined();
|
|
expect(pdf.height).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MediaEngine Integration Helpers', () => {
|
|
describe('Database Record Conversion', () => {
|
|
it('should convert MediaData to database record', () => {
|
|
const media = createMockMedia({
|
|
tags: ['tag1', 'tag2'],
|
|
});
|
|
|
|
const dbRecord = {
|
|
id: media.id,
|
|
filename: media.filename,
|
|
originalName: media.originalName,
|
|
mimeType: media.mimeType,
|
|
size: media.size,
|
|
width: media.width,
|
|
height: media.height,
|
|
alt: media.alt,
|
|
caption: media.caption,
|
|
filePath: `/mock/userData/media/${media.filename}`,
|
|
sidecarPath: `/mock/userData/media/${media.filename}.meta`,
|
|
createdAt: media.createdAt,
|
|
updatedAt: media.updatedAt,
|
|
syncStatus: 'pending' as const,
|
|
syncedAt: null,
|
|
checksum: 'abc123',
|
|
tags: JSON.stringify(media.tags),
|
|
};
|
|
|
|
expect(dbRecord.tags).toBe('["tag1","tag2"]');
|
|
expect(dbRecord.sidecarPath).toContain('.meta');
|
|
});
|
|
|
|
it('should convert database record to MediaData', () => {
|
|
const dbRecord = {
|
|
id: 'media-1',
|
|
filename: 'photo.jpg',
|
|
originalName: 'original.jpg',
|
|
mimeType: 'image/jpeg',
|
|
size: 102400,
|
|
width: 800,
|
|
height: 600,
|
|
alt: 'A photo',
|
|
caption: 'Caption text',
|
|
filePath: '/mock/path/photo.jpg',
|
|
sidecarPath: '/mock/path/photo.jpg.meta',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
syncStatus: 'pending' as const,
|
|
syncedAt: null,
|
|
checksum: 'abc123',
|
|
tags: '["tag1","tag2"]',
|
|
};
|
|
|
|
const mediaData = {
|
|
id: dbRecord.id,
|
|
filename: dbRecord.filename,
|
|
originalName: dbRecord.originalName,
|
|
mimeType: dbRecord.mimeType,
|
|
size: dbRecord.size,
|
|
width: dbRecord.width,
|
|
height: dbRecord.height,
|
|
alt: dbRecord.alt,
|
|
caption: dbRecord.caption,
|
|
createdAt: dbRecord.createdAt,
|
|
updatedAt: dbRecord.updatedAt,
|
|
tags: JSON.parse(dbRecord.tags) as string[],
|
|
};
|
|
|
|
expect(mediaData.tags).toEqual(['tag1', 'tag2']);
|
|
});
|
|
});
|
|
|
|
describe('File Path Generation', () => {
|
|
it('should generate correct media file path', () => {
|
|
const mediaDir = '/mock/userData/media';
|
|
const filename = 'media-123.jpg';
|
|
|
|
const filePath = `${mediaDir}/${filename}`;
|
|
expect(filePath).toBe('/mock/userData/media/media-123.jpg');
|
|
});
|
|
|
|
it('should generate correct sidecar path', () => {
|
|
const filePath = '/mock/userData/media/media-123.jpg';
|
|
const sidecarPath = `${filePath}.meta`;
|
|
|
|
expect(sidecarPath).toBe('/mock/userData/media/media-123.jpg.meta');
|
|
});
|
|
});
|
|
});
|