/** * Test utilities and mock factories * Following TDD best practices with reusable test data generators */ import { vi } from 'vitest'; import type { PostData } from '../../src/main/engine/PostEngine'; import type { MediaData } from '../../src/main/engine/MediaEngine'; import type { ProjectData } from '../../src/main/engine/ProjectEngine'; import type { Task, TaskProgress } from '../../src/main/engine/TaskManager'; // ============================================ // Post Mock Factory // ============================================ let postIdCounter = 1; export function createMockPost(overrides?: Partial): PostData { const id = `post-${postIdCounter++}`; const now = new Date(); return { id, projectId: 'default', title: `Test Post ${id}`, slug: `test-post-${id}`, excerpt: 'This is a test excerpt', content: '# Test Content\n\nThis is test content.', status: 'draft', author: 'Test Author', createdAt: now, updatedAt: now, publishedAt: undefined, tags: ['test', 'mock'], categories: ['testing'], ...overrides, }; } export function createMockPublishedPost(overrides?: Partial): PostData { const now = new Date(); return createMockPost({ status: 'published', publishedAt: now, ...overrides, }); } // ============================================ // Media Mock Factory // ============================================ let mediaIdCounter = 1; export function createMockMedia(overrides?: Partial): MediaData { const id = `media-${mediaIdCounter++}`; const now = new Date(); return { id, filename: `${id}.jpg`, originalName: `original-${id}.jpg`, mimeType: 'image/jpeg', size: 1024 * 100, // 100KB width: 800, height: 600, alt: 'Test image', caption: 'A test image caption', createdAt: now, updatedAt: now, tags: ['test', 'image'], ...overrides, }; } export function createMockPdfMedia(overrides?: Partial): MediaData { return createMockMedia({ filename: 'document.pdf', originalName: 'document.pdf', mimeType: 'application/pdf', width: undefined, height: undefined, ...overrides, }); } // ============================================ // Project Mock Factory // ============================================ let projectIdCounter = 1; export function createMockProject(overrides?: Partial): ProjectData { const id = overrides?.id ?? `project-${projectIdCounter++}`; const now = new Date(); return { id, name: `Test Project ${id}`, slug: `test-project-${id}`, description: 'A test project', dataPath: undefined, createdAt: now, updatedAt: now, isActive: false, ...overrides, }; } export function createMockActiveProject(overrides?: Partial): ProjectData { return createMockProject({ isActive: true, ...overrides, }); } // ============================================ // Task Mock Factory // ============================================ let taskIdCounter = 1; export function createMockTask( executor?: (onProgress: (progress: number, message: string) => void) => Promise, overrides?: Partial> ): Task { const id = `task-${taskIdCounter++}`; return { id, name: `Test Task ${id}`, execute: executor || (async (onProgress) => { onProgress(50, 'Processing...'); onProgress(100, 'Done'); return undefined as T; }), ...overrides, }; } export function createMockSlowTask(durationMs: number): Task { return createMockTask(async (onProgress) => { const steps = 10; const stepDuration = durationMs / steps; for (let i = 1; i <= steps; i++) { await new Promise(resolve => setTimeout(resolve, stepDuration)); onProgress(i * 10, `Step ${i}/${steps}`); } }); } export function createMockFailingTask(errorMessage: string = 'Task failed'): Task { return createMockTask(async () => { throw new Error(errorMessage); }); } export function createMockTaskProgress(overrides?: Partial): TaskProgress { return { taskId: `task-${taskIdCounter++}`, status: 'pending', progress: 0, message: 'Waiting...', startTime: new Date(), ...overrides, }; } // ============================================ // Database Mock Utilities // ============================================ export function createMockDatabase() { const data = { posts: new Map(), media: new Map(), }; return { data, 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', })), initializeLocal: vi.fn(), initializeRemote: vi.fn(), close: vi.fn(), }; } // ============================================ // File System Mock Utilities // ============================================ export function createMockFileSystem() { const files = new Map(); const directories = new Set(['/mock', '/mock/userData', '/mock/userData/posts', '/mock/userData/media']); return { files, directories, readFile: vi.fn(async (path: string) => { const content = files.get(path); if (content === undefined) { const error = new Error(`ENOENT: no such file or directory, open '${path}'`); (error as NodeJS.ErrnoException).code = 'ENOENT'; throw error; } return content; }), writeFile: vi.fn(async (path: string, content: string | Buffer) => { files.set(path, content); }), unlink: vi.fn(async (path: string) => { if (!files.has(path)) { const error = new Error(`ENOENT: no such file or directory, unlink '${path}'`); (error as NodeJS.ErrnoException).code = 'ENOENT'; throw error; } files.delete(path); }), mkdir: vi.fn(async (path: string) => { directories.add(path); }), readdir: vi.fn(async (path: string) => { const entries: string[] = []; for (const filePath of files.keys()) { if (filePath.startsWith(path + '/')) { const relativePath = filePath.slice(path.length + 1); const firstSegment = relativePath.split('/')[0]; if (!entries.includes(firstSegment)) { entries.push(firstSegment); } } } return entries; }), stat: vi.fn(async (path: string) => { if (files.has(path)) { const content = files.get(path)!; return { isFile: () => true, isDirectory: () => false, size: typeof content === 'string' ? content.length : content.length, }; } if (directories.has(path)) { return { isFile: () => false, isDirectory: () => true, size: 0, }; } const error = new Error(`ENOENT: no such file or directory, stat '${path}'`); (error as NodeJS.ErrnoException).code = 'ENOENT'; throw error; }), copyFile: vi.fn(async (src: string, dest: string) => { const content = files.get(src); if (content === undefined) { const error = new Error(`ENOENT: no such file or directory, copyfile '${src}'`); (error as NodeJS.ErrnoException).code = 'ENOENT'; throw error; } files.set(dest, content); }), access: vi.fn(async (path: string) => { if (!files.has(path) && !directories.has(path)) { const error = new Error(`ENOENT: no such file or directory, access '${path}'`); (error as NodeJS.ErrnoException).code = 'ENOENT'; throw error; } }), }; } // ============================================ // Dropbox Mock Factory // ============================================ import type { DropboxSyncConfig, DropboxConflict } from '../../src/main/engine/DropboxSyncEngine'; let dropboxConflictIdCounter = 1; export function createMockDropboxConfig(overrides?: Partial): DropboxSyncConfig { return { accessToken: 'mock-dropbox-access-token', refreshToken: 'mock-dropbox-refresh-token', appKey: 'mock-app-key', appSecret: 'mock-app-secret', syncEnabled: true, syncInterval: 60, localPostsDir: '/mock/userData/projects/default/posts', localMediaDir: '/mock/userData/projects/default/media', remoteBasePath: '/bds', ...overrides, }; } export function createMockDropboxClient() { return { filesUpload: vi.fn().mockResolvedValue({ result: { name: 'test.md', path_lower: '/bds/posts/2026/01/test.md', content_hash: 'mockhash123', server_modified: '2026-01-15T10:00:00Z', size: 100, }, }), filesDownload: vi.fn().mockResolvedValue({ result: { name: 'test.md', path_lower: '/bds/posts/2026/01/test.md', fileBinary: Buffer.from('downloaded content'), content_hash: 'mockhash123', server_modified: '2026-01-15T10:00:00Z', size: 18, }, }), filesDeleteV2: vi.fn().mockResolvedValue({ result: { metadata: { '.tag': 'file', name: 'test.md' } }, }), filesListFolder: vi.fn().mockResolvedValue({ result: { entries: [], cursor: 'mock-cursor', has_more: false }, }), filesListFolderContinue: vi.fn().mockResolvedValue({ result: { entries: [], cursor: 'mock-cursor-next', has_more: false }, }), filesListFolderGetLatestCursor: vi.fn().mockResolvedValue({ result: { cursor: 'mock-latest-cursor' }, }), filesListFolderLongpoll: vi.fn().mockResolvedValue({ result: { changes: false, backoff: 0 }, }), filesGetMetadata: vi.fn().mockResolvedValue({ result: { '.tag': 'file', name: 'test.md', path_lower: '/bds/posts/2026/01/test.md', content_hash: 'mockhash123', server_modified: '2026-01-15T10:00:00Z', size: 100, }, }), setRefreshToken: vi.fn(), getRefreshToken: vi.fn().mockReturnValue('mock-refresh-token'), getAccessToken: vi.fn().mockReturnValue('mock-access-token'), }; } export function createMockDropboxConflict(overrides?: Partial): DropboxConflict { const id = `conflict-${dropboxConflictIdCounter++}`; return { id, localPath: '/mock/userData/projects/default/posts/2026/01/test.md', remotePath: '/bds/posts/2026/01/test.md', localModified: new Date('2026-01-20T15:00:00Z'), remoteModified: new Date('2026-01-20T14:00:00Z'), localHash: 'localhash123', remoteHash: 'remotehash456', ...overrides, }; } // ============================================ // Import Analysis Report Mock Factory // ============================================ import type { ImportAnalysisReport, AnalyzedPost, AnalyzedMedia, AnalyzedCategory, AnalyzedTag, PostAnalysisStatus, MediaAnalysisStatus, } from '../../src/main/engine/ImportAnalysisEngine'; import type { WxrPost, WxrMedia, WxrSiteInfo } from '../../src/main/engine/WxrParser'; let wxrPostIdCounter = 1; let wxrMediaIdCounter = 1; export function createMockWxrSiteInfo(overrides?: Partial): WxrSiteInfo { return { title: 'Test WordPress Site', link: 'https://example.com', description: 'A test WordPress site', language: 'en-US', ...overrides, }; } export function createMockWxrPost(overrides?: Partial): WxrPost { const id = wxrPostIdCounter++; return { wpId: id, title: `Test Post ${id}`, slug: `test-post-${id}`, status: 'publish', content: `

Test content for post ${id}

`, excerpt: `Excerpt for post ${id}`, pubDate: '2024-01-15T10:00:00Z', postDate: '2024-01-15T10:00:00Z', postModified: '2024-01-16T12:00:00Z', creator: 'testauthor', postType: 'post', categories: ['Test Category'], tags: ['test-tag'], ...overrides, }; } export function createMockWxrMedia(overrides?: Partial): WxrMedia { const id = wxrMediaIdCounter++; return { wpId: id, title: `Test Media ${id}`, filename: `test-image-${id}.jpg`, url: `https://example.com/wp-content/uploads/2024/01/test-image-${id}.jpg`, relativePath: `2024/01/test-image-${id}.jpg`, pubDate: '2024-01-15T10:00:00Z', parentId: 0, mimeType: 'image/jpeg', description: `Description for media ${id}`, ...overrides, }; } export function createMockAnalyzedPost( overrides?: Partial, wxrOverrides?: Partial ): AnalyzedPost { const wxrPost = createMockWxrPost(wxrOverrides); return { wxrPost, status: 'new' as PostAnalysisStatus, contentHash: `hash-${wxrPost.wpId}`, markdownPreview: `# ${wxrPost.title}\n\nTest content for post ${wxrPost.wpId}`, ...overrides, }; } export function createMockAnalyzedMedia( overrides?: Partial, wxrOverrides?: Partial ): AnalyzedMedia { const wxrMedia = createMockWxrMedia(wxrOverrides); return { wxrMedia, status: 'new' as MediaAnalysisStatus, fileHash: `filehash-${wxrMedia.wpId}`, ...overrides, }; } export function createMockAnalyzedCategory(overrides?: Partial): AnalyzedCategory { return { name: 'Test Category', slug: 'test-category', existsInProject: false, ...overrides, }; } export function createMockAnalyzedTag(overrides?: Partial): AnalyzedTag { return { name: 'test-tag', slug: 'test-tag', existsInProject: false, ...overrides, }; } export function createMockImportAnalysisReport( overrides?: Partial ): ImportAnalysisReport { const posts = overrides?.posts?.items || [createMockAnalyzedPost()]; const pages = overrides?.pages?.items || []; const mediaItems = overrides?.media?.items || [createMockAnalyzedMedia()]; const categories = overrides?.categories || [createMockAnalyzedCategory()]; const tags = overrides?.tags || [createMockAnalyzedTag()]; return { sourceFile: '/path/to/export.xml', site: createMockWxrSiteInfo(), analyzedAt: new Date('2024-01-15T12:00:00Z'), posts: { total: posts.length, new: posts.filter(p => p.status === 'new').length, updates: posts.filter(p => p.status === 'update').length, conflicts: posts.filter(p => p.status === 'conflict').length, contentDuplicates: posts.filter(p => p.status === 'content-duplicate').length, items: posts, ...overrides?.posts, }, pages: { total: pages.length, new: pages.filter(p => p.status === 'new').length, updates: pages.filter(p => p.status === 'update').length, conflicts: pages.filter(p => p.status === 'conflict').length, contentDuplicates: pages.filter(p => p.status === 'content-duplicate').length, items: pages, ...overrides?.pages, }, media: { total: mediaItems.length, new: mediaItems.filter(m => m.status === 'new').length, updates: mediaItems.filter(m => m.status === 'update').length, conflicts: mediaItems.filter(m => m.status === 'conflict').length, contentDuplicates: mediaItems.filter(m => m.status === 'content-duplicate').length, missing: mediaItems.filter(m => m.status === 'missing').length, items: mediaItems, ...overrides?.media, }, categories, tags, macros: overrides?.macros || { total: 0, mappedCount: 0, unmappedCount: 0, discovered: [], }, ...overrides, }; } // ============================================ // Reset Utilities // ============================================ export function resetMockCounters(): void { postIdCounter = 1; mediaIdCounter = 1; projectIdCounter = 1; taskIdCounter = 1; dropboxConflictIdCounter = 1; wxrPostIdCounter = 1; wxrMediaIdCounter = 1; } // ============================================ // Assertion Helpers // ============================================ export function expectPostToMatchPartial( actual: PostData, expected: Partial ): void { for (const [key, value] of Object.entries(expected)) { if (value instanceof Date) { expect((actual as Record)[key]).toBeInstanceOf(Date); } else { expect((actual as Record)[key]).toEqual(value); } } } export function expectMediaToMatchPartial( actual: MediaData, expected: Partial ): void { for (const [key, value] of Object.entries(expected)) { if (value instanceof Date) { expect((actual as Record)[key]).toBeInstanceOf(Date); } else { expect((actual as Record)[key]).toEqual(value); } } }