/** * ImportDefinitionEngine Unit Tests * * Tests the REAL ImportDefinitionEngine class with mocked database. * Following TDD best practices: mock external dependencies, test real implementation. */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; // Store for mock data const mockDefinitions = new Map(); // Create chainable mock for Drizzle ORM that is thenable (can be awaited) function createSelectChain(getData: () => any[]) { const chain: any = { from: vi.fn().mockImplementation(() => chain), where: vi.fn().mockImplementation(() => chain), orderBy: vi.fn().mockImplementation(() => chain), limit: vi.fn().mockImplementation(() => chain), // Make the chain thenable so it can be awaited directly then: (resolve: (value: any[]) => void, reject?: (reason: any) => void) => { return Promise.resolve(getData()).then(resolve, reject); }, }; return chain; } // Track what data Drizzle queries should return let mockDrizzleSelectResults: any[][] = []; const mockLocalDb = { select: vi.fn(() => createSelectChain(() => mockDrizzleSelectResults.shift() || [])), insert: vi.fn(() => ({ values: vi.fn((data: any) => { if (data && data.id) { mockDefinitions.set(data.id, { id: data.id, projectId: data.projectId, name: data.name, wxrFilePath: data.wxrFilePath, uploadsFolderPath: data.uploadsFolderPath, lastAnalysisResult: data.lastAnalysisResult, createdAt: data.createdAt, updatedAt: data.updatedAt, }); } return Promise.resolve(); }), })), update: vi.fn(() => ({ set: vi.fn(() => ({ where: vi.fn(() => Promise.resolve()), })), })), delete: vi.fn(() => ({ where: vi.fn(() => Promise.resolve()), })), }; // Mock the database module vi.mock('../../src/main/database', () => ({ getDatabase: vi.fn(() => ({ getLocal: vi.fn(() => mockLocalDb), getLocalClient: vi.fn(() => null), 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(), })), })); // Mock electron app vi.mock('electron', () => ({ app: { getPath: vi.fn(() => '/mock/userData'), }, })); import { ImportDefinitionEngine } from '../../src/main/engine/ImportDefinitionEngine'; describe('ImportDefinitionEngine', () => { let engine: ImportDefinitionEngine; beforeEach(() => { vi.clearAllMocks(); mockDefinitions.clear(); mockDrizzleSelectResults = []; engine = new ImportDefinitionEngine(); engine.setProjectContext('test-project'); }); afterEach(() => { vi.restoreAllMocks(); }); describe('constructor', () => { it('should create a new instance', () => { expect(engine).toBeDefined(); expect(engine).toBeInstanceOf(ImportDefinitionEngine); }); }); describe('setProjectContext', () => { it('should set and return the current project ID', () => { engine.setProjectContext('project-abc'); expect(engine.getProjectContext()).toBe('project-abc'); }); }); describe('createDefinition', () => { it('should create a definition with default name', async () => { const def = await engine.createDefinition(); expect(def).toBeDefined(); expect(def.id).toMatch(/^import_/); expect(def.name).toBe('Untitled Import'); expect(def.projectId).toBe('test-project'); expect(def.wxrFilePath).toBeNull(); expect(def.uploadsFolderPath).toBeNull(); expect(def.lastAnalysisResult).toBeNull(); expect(def.createdAt).toBeDefined(); expect(def.updatedAt).toBeDefined(); }); it('should create a definition with custom name', async () => { const def = await engine.createDefinition('My WordPress Blog'); expect(def.name).toBe('My WordPress Blog'); }); it('should insert into the database', async () => { await engine.createDefinition('Test Import'); expect(mockLocalDb.insert).toHaveBeenCalledTimes(1); }); }); describe('getDefinition', () => { it('should return a definition by ID', async () => { const created = await engine.createDefinition('My Import'); // Set up mock to return the created definition mockDrizzleSelectResults = [[{ id: created.id, projectId: 'test-project', name: 'My Import', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }]]; const def = await engine.getDefinition(created.id); expect(def).toBeDefined(); expect(def!.id).toBe(created.id); expect(def!.name).toBe('My Import'); }); it('should return null for non-existent ID', async () => { mockDrizzleSelectResults = [[]]; const def = await engine.getDefinition('non-existent-id'); expect(def).toBeNull(); }); it('should not return definitions from other projects', async () => { const created = await engine.createDefinition('My Import'); engine.setProjectContext('other-project'); mockDrizzleSelectResults = [[]]; // would be filtered by project const def = await engine.getDefinition(created.id); expect(def).toBeNull(); }); it('should parse lastAnalysisResult JSON', async () => { const created = await engine.createDefinition('My Import'); // Set up mock to return the definition with analysis result mockDrizzleSelectResults = [[{ id: created.id, projectId: 'test-project', name: 'My Import', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: JSON.stringify({ posts: { total: 5 } }), createdAt: new Date(), updatedAt: new Date(), }]]; const def = await engine.getDefinition(created.id); expect(def!.lastAnalysisResult).toEqual({ posts: { total: 5 } }); }); }); describe('getAllForProject', () => { it('should return empty array when no definitions exist', async () => { mockDrizzleSelectResults = [[]]; const defs = await engine.getAllForProject(); expect(defs).toEqual([]); }); it('should return all definitions for the current project', async () => { await engine.createDefinition('Import 1'); await engine.createDefinition('Import 2'); // Mock returning both definitions mockDrizzleSelectResults = [[ { id: 'id1', projectId: 'test-project', name: 'Import 1', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date() }, { id: 'id2', projectId: 'test-project', name: 'Import 2', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date() }, ]]; const defs = await engine.getAllForProject(); expect(defs).toHaveLength(2); }); it('should not include definitions from other projects', async () => { await engine.createDefinition('Import A'); engine.setProjectContext('other-project'); await engine.createDefinition('Import B'); engine.setProjectContext('test-project'); // Mock returning only the test-project definition mockDrizzleSelectResults = [[ { id: 'id1', projectId: 'test-project', name: 'Import A', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date() }, ]]; const defs = await engine.getAllForProject(); expect(defs).toHaveLength(1); expect(defs[0].name).toBe('Import A'); }); it('should return definitions ordered by updatedAt DESC', async () => { const olderDate = new Date('2024-01-01'); const newerDate = new Date('2024-02-01'); mockDrizzleSelectResults = [[ { id: 'id2', projectId: 'test-project', name: 'Newer', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: newerDate, updatedAt: newerDate }, { id: 'id1', projectId: 'test-project', name: 'Older', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: olderDate, updatedAt: olderDate }, ]]; const defs = await engine.getAllForProject(); expect(defs[0].name).toBe('Newer'); expect(defs[1].name).toBe('Older'); }); }); describe('updateDefinition', () => { it('should update the name', async () => { const created = await engine.createDefinition('Old Name'); // First call for getDefinition check, second for returning updated data mockDrizzleSelectResults = [ [{ // getDefinition call inside updateDefinition id: created.id, projectId: 'test-project', name: 'Old Name', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], [{ // return after update id: created.id, projectId: 'test-project', name: 'New Name', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], ]; const updated = await engine.updateDefinition(created.id, { name: 'New Name' }); expect(updated).toBeDefined(); expect(updated!.name).toBe('New Name'); }); it('should update wxrFilePath', async () => { const created = await engine.createDefinition('Test'); // First call for check, second for returning updated data mockDrizzleSelectResults = [ [{ // getDefinition check id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], [{ // return after update id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: '/path/to/export.xml', uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], ]; const updated = await engine.updateDefinition(created.id, { wxrFilePath: '/path/to/export.xml' }); expect(updated!.wxrFilePath).toBe('/path/to/export.xml'); }); it('should update uploadsFolderPath', async () => { const created = await engine.createDefinition('Test'); mockDrizzleSelectResults = [ [{ // getDefinition check id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], [{ // return after update id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: null, uploadsFolderPath: '/path/to/uploads', lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], ]; const updated = await engine.updateDefinition(created.id, { uploadsFolderPath: '/path/to/uploads' }); expect(updated!.uploadsFolderPath).toBe('/path/to/uploads'); }); it('should update lastAnalysisResult as JSON', async () => { const created = await engine.createDefinition('Test'); const report = { posts: { total: 10, new: 5 } }; mockDrizzleSelectResults = [ [{ // getDefinition check id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }], [{ // return after update id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: JSON.stringify(report), createdAt: new Date(), updatedAt: new Date(), }], ]; const updated = await engine.updateDefinition(created.id, { lastAnalysisResult: JSON.stringify(report) }); expect(updated).toBeDefined(); }); it('should return null for non-existent definition', async () => { mockDrizzleSelectResults = [[]]; const updated = await engine.updateDefinition('non-existent', { name: 'Test' }); expect(updated).toBeNull(); }); it('should not update definitions from other projects', async () => { const created = await engine.createDefinition('Test'); engine.setProjectContext('other-project'); mockDrizzleSelectResults = [[]]; // Would be filtered by project const updated = await engine.updateDefinition(created.id, { name: 'Hacked' }); expect(updated).toBeNull(); }); }); describe('deleteDefinition', () => { it('should delete an existing definition', async () => { const created = await engine.createDefinition('To Delete'); mockDrizzleSelectResults = [[{ id: created.id, projectId: 'test-project', name: 'To Delete', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }]]; const result = await engine.deleteDefinition(created.id); expect(result).toBe(true); }); it('should return false for non-existent definition', async () => { mockDrizzleSelectResults = [[]]; const result = await engine.deleteDefinition('non-existent'); expect(result).toBe(false); }); it('should not delete definitions from other projects', async () => { const created = await engine.createDefinition('Test'); engine.setProjectContext('other-project'); mockDrizzleSelectResults = [[]]; // Would be filtered by project const result = await engine.deleteDefinition(created.id); expect(result).toBe(false); }); it('should remove the definition from the database', async () => { const created = await engine.createDefinition('Test'); // First call returns the definition for delete mockDrizzleSelectResults = [[{ id: created.id, projectId: 'test-project', name: 'Test', wxrFilePath: null, uploadsFolderPath: null, lastAnalysisResult: null, createdAt: new Date(), updatedAt: new Date(), }]]; await engine.deleteDefinition(created.id); // Second call returns empty for get mockDrizzleSelectResults = [[]]; const def = await engine.getDefinition(created.id); expect(def).toBeNull(); }); }); });