/** * Repository Unit Tests * * Behavior-based tests for MaterialsToBeDeletedRepository and DiscreteMaterialPlanRepository. * Verifies correct delegation to TypeORM methods, return values, and safe defaults on error. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { createMockRepository, createMockQueryBuilder } from '../mocks' // --- Shared mock state --- let mockRepo: ReturnType // Mock TypeORM decorators (entities still need them) vi.mock('typeorm', () => { const decorator = vi.fn() return { DataSource: vi.fn(() => ({ initialize: vi.fn().mockResolvedValue(undefined), isInitialized: true, getRepository: vi.fn(() => mockRepo), destroy: vi.fn() })), Repository: vi.fn(), In: vi.fn((arr: unknown[]) => arr), Entity: decorator, PrimaryGeneratedColumn: decorator, Column: decorator, ManyToOne: decorator, OneToMany: decorator, ManyToMany: decorator, JoinColumn: decorator, JoinTable: decorator, CreateDateColumn: decorator, UpdateDateColumn: decorator, DeleteDateColumn: decorator, Index: decorator, Unique: decorator, Check: decorator, Exclusion: decorator, Generated: decorator, Between: vi.fn(), LessThan: vi.fn(), LessThanOrEqual: vi.fn(), MoreThan: vi.fn(), MoreThanOrEqual: vi.fn(), Equal: vi.fn(), Like: vi.fn(), ILike: vi.fn(), IsNull: vi.fn(), Not: vi.fn() } }) vi.mock('../../src/main/services/logger', () => ({ createLogger: vi.fn(() => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() })) })) vi.mock('../../src/main/services/database/data-source', () => ({ getDataSource: vi.fn(() => ({ isInitialized: true, initialize: vi.fn().mockResolvedValue(undefined), getRepository: vi.fn(() => mockRepo) })) })) // --------------------------------------------------------------------------- // MaterialsToBeDeletedRepository // --------------------------------------------------------------------------- describe('MaterialsToBeDeletedRepository', () => { let MaterialsToBeDeletedRepository: typeof import( '../../src/main/services/database/repositories/MaterialsToBeDeletedRepository' ).MaterialsToBeDeletedRepository beforeEach(async () => { vi.clearAllMocks() mockRepo = createMockRepository() const mod = await import( '../../src/main/services/database/repositories/MaterialsToBeDeletedRepository' ) MaterialsToBeDeletedRepository = mod.MaterialsToBeDeletedRepository }) afterEach(() => { vi.resetModules() }) // --- upsert --- it('upsert: creates new entity when not found', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.findOne!.mockResolvedValue(null) mockRepo.create!.mockReturnValue({ materialCode: 'MAT01', managerName: 'Alice' }) const result = await repo.upsert('MAT01', 'Alice') expect(result).toBe(true) expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { materialCode: 'MAT01' } }) expect(mockRepo.create).toHaveBeenCalledWith({ materialCode: 'MAT01', managerName: 'Alice' }) expect(mockRepo.save).toHaveBeenCalled() }) it('upsert: updates existing entity', async () => { const repo = new MaterialsToBeDeletedRepository() const existing = { materialCode: 'MAT01', managerName: 'Bob' } mockRepo.findOne!.mockResolvedValue(existing) const result = await repo.upsert('MAT01', 'Alice') expect(result).toBe(true) expect(existing.managerName).toBe('Alice') expect(mockRepo.save).toHaveBeenCalledWith(existing) }) it('upsert: returns false on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.findOne!.mockRejectedValue(new Error('db fail')) const result = await repo.upsert('MAT01', 'Alice') expect(result).toBe(false) }) // --- upsertBatch --- it('upsertBatch: processes valid materials', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.findOne!.mockResolvedValue(null) mockRepo.create!.mockImplementation((data) => data) const stats = await repo.upsertBatch([ { materialCode: 'M1', managerName: 'A' }, { materialCode: 'M2', managerName: 'B' } ]) expect(stats.total).toBe(2) expect(stats.success).toBe(2) expect(stats.failed).toBe(0) }) it('upsertBatch: skips empty materialCodes', async () => { const repo = new MaterialsToBeDeletedRepository() const stats = await repo.upsertBatch([ { materialCode: '', managerName: 'A' }, { materialCode: ' ', managerName: 'B' }, { materialCode: 'M1', managerName: 'C' } ]) expect(stats.total).toBe(3) expect(stats.success).toBe(1) expect(stats.failed).toBe(2) }) it('upsertBatch: returns stats with partial failures', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.findOne!.mockRejectedValue(new Error('fail')) const stats = await repo.upsertBatch([{ materialCode: 'M1', managerName: 'A' }]) expect(stats.total).toBe(1) expect(stats.failed).toBe(1) }) // --- getAllMaterialCodes --- it('getAllMaterialCodes: returns Set via QueryBuilder', async () => { const repo = new MaterialsToBeDeletedRepository() const qb = createMockQueryBuilder({ result: [{ materialCode: 'M1' }, { materialCode: 'M2' }] }) mockRepo.createQueryBuilder!.mockReturnValue(qb) const result = await repo.getAllMaterialCodes() expect(result).toBeInstanceOf(Set) expect(result.size).toBe(2) expect(result.has('M1')).toBe(true) expect(result.has('M2')).toBe(true) }) it('getAllMaterialCodes: returns empty Set on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.createQueryBuilder!.mockImplementation(() => { throw new Error('db fail') }) const result = await repo.getAllMaterialCodes() expect(result).toBeInstanceOf(Set) expect(result.size).toBe(0) }) // --- getAllRecords --- it('getAllRecords: returns ordered records', async () => { const repo = new MaterialsToBeDeletedRepository() const records = [{ materialCode: 'M1' }, { materialCode: 'M2' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.getAllRecords() expect(result).toEqual(records) expect(mockRepo.find).toHaveBeenCalledWith({ order: { managerName: 'ASC', materialCode: 'ASC' } }) }) it('getAllRecords: returns empty array on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.getAllRecords() expect(result).toEqual([]) }) // --- getByManager --- it('getByManager: filters by managerName', async () => { const repo = new MaterialsToBeDeletedRepository() const records = [{ materialCode: 'M1', managerName: 'Alice' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.getByManager('Alice') expect(result).toEqual(records) expect(mockRepo.find).toHaveBeenCalledWith({ where: { managerName: 'Alice' }, order: { materialCode: 'ASC' } }) }) it('getByManager: returns empty array on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.getByManager('Alice') expect(result).toEqual([]) }) // --- getManagers --- it('getManagers: returns distinct names via QueryBuilder', async () => { const repo = new MaterialsToBeDeletedRepository() const qb = createMockQueryBuilder({ result: [{ managerName: 'Alice' }, { managerName: 'Bob' }] }) mockRepo.createQueryBuilder!.mockReturnValue(qb) const result = await repo.getManagers() expect(result).toEqual(['Alice', 'Bob']) }) it('getManagers: returns empty array on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.createQueryBuilder!.mockImplementation(() => { throw new Error('db fail') }) const result = await repo.getManagers() expect(result).toEqual([]) }) // --- deleteByMaterialCode --- it('deleteByMaterialCode: returns true when affected > 0', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.delete!.mockResolvedValue({ affected: 1 }) const result = await repo.deleteByMaterialCode('M1') expect(result).toBe(true) expect(mockRepo.delete).toHaveBeenCalledWith({ materialCode: 'M1' }) }) it('deleteByMaterialCode: returns false when affected is 0', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.delete!.mockResolvedValue({ affected: 0 }) const result = await repo.deleteByMaterialCode('M1') expect(result).toBe(false) }) it('deleteByMaterialCode: returns false on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.delete!.mockRejectedValue(new Error('db fail')) const result = await repo.deleteByMaterialCode('M1') expect(result).toBe(false) }) // --- deleteByMaterialCodes --- it('deleteByMaterialCodes: returns 0 for empty array', async () => { const repo = new MaterialsToBeDeletedRepository() const result = await repo.deleteByMaterialCodes([]) expect(result).toBe(0) expect(mockRepo.delete).not.toHaveBeenCalled() }) it('deleteByMaterialCodes: returns affected count', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.delete!.mockResolvedValue({ affected: 3 }) const result = await repo.deleteByMaterialCodes(['M1', 'M2', 'M3']) expect(result).toBe(3) }) it('deleteByMaterialCodes: returns 0 on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.delete!.mockRejectedValue(new Error('db fail')) const result = await repo.deleteByMaterialCodes(['M1']) expect(result).toBe(0) }) // --- exists --- it('exists: returns true when count > 0', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.count!.mockResolvedValue(1) const result = await repo.exists('M1') expect(result).toBe(true) }) it('exists: returns false when count is 0', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.count!.mockResolvedValue(0) const result = await repo.exists('M1') expect(result).toBe(false) }) it('exists: returns false on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.count!.mockRejectedValue(new Error('db fail')) const result = await repo.exists('M1') expect(result).toBe(false) }) // --- countAll --- it('countAll: returns count', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.count!.mockResolvedValue(42) const result = await repo.countAll() expect(result).toBe(42) }) it('countAll: returns 0 on error', async () => { const repo = new MaterialsToBeDeletedRepository() mockRepo.count!.mockRejectedValue(new Error('db fail')) const result = await repo.countAll() expect(result).toBe(0) }) }) // --------------------------------------------------------------------------- // DiscreteMaterialPlanRepository // --------------------------------------------------------------------------- describe('DiscreteMaterialPlanRepository', () => { let DiscreteMaterialPlanRepository: typeof import( '../../src/main/services/database/repositories/DiscreteMaterialPlanRepository' ).DiscreteMaterialPlanRepository beforeEach(async () => { vi.clearAllMocks() mockRepo = createMockRepository() const mod = await import( '../../src/main/services/database/repositories/DiscreteMaterialPlanRepository' ) DiscreteMaterialPlanRepository = mod.DiscreteMaterialPlanRepository }) afterEach(() => { vi.resetModules() }) // --- queryAll --- it('queryAll: returns find results', async () => { const repo = new DiscreteMaterialPlanRepository() const records = [{ sourceNumber: 'S1' }, { sourceNumber: 'S2' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.queryAll() expect(result).toEqual(records) }) it('queryAll: returns empty array on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.queryAll() expect(result).toEqual([]) }) // --- queryAllDistinctByMaterialCode --- it('queryAllDistinctByMaterialCode: calls repo.query() with raw SQL', async () => { const repo = new DiscreteMaterialPlanRepository() const mockQueryResult = [{ MaterialCode: 'M1', rn: 1 }] mockRepo = { ...createMockRepository(), query: vi.fn().mockResolvedValue(mockQueryResult) } // Re-import to pick up new mockRepo vi.resetModules() const mod = await import( '../../src/main/services/database/repositories/DiscreteMaterialPlanRepository' ) const freshRepo = new mod.DiscreteMaterialPlanRepository() const result = await freshRepo.queryAllDistinctByMaterialCode() expect(result).toEqual(mockQueryResult) }) it('queryAllDistinctByMaterialCode: returns empty array on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo = { ...createMockRepository(), query: vi.fn().mockRejectedValue(new Error('db fail')) } vi.resetModules() const mod = await import( '../../src/main/services/database/repositories/DiscreteMaterialPlanRepository' ) const freshRepo = new mod.DiscreteMaterialPlanRepository() const result = await freshRepo.queryAllDistinctByMaterialCode() expect(result).toEqual([]) }) // --- queryBySourceNumbers --- it('queryBySourceNumbers: returns empty array for empty input', async () => { const repo = new DiscreteMaterialPlanRepository() const result = await repo.queryBySourceNumbers([]) expect(result).toEqual([]) expect(mockRepo.find).not.toHaveBeenCalled() }) it('queryBySourceNumbers: batches in groups of 2000', async () => { const repo = new DiscreteMaterialPlanRepository() // Create 2500 source numbers to trigger 2 batches const sourceNumbers = Array.from({ length: 2500 }, (_, i) => `S${i}`) mockRepo.find!.mockResolvedValue([]) await repo.queryBySourceNumbers(sourceNumbers) // Should be called twice: once for 2000, once for 500 expect(mockRepo.find).toHaveBeenCalledTimes(2) }) it('queryBySourceNumbers: returns combined results', async () => { const repo = new DiscreteMaterialPlanRepository() const records = [{ sourceNumber: 'S1' }, { sourceNumber: 'S2' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.queryBySourceNumbers(['S1', 'S2']) expect(result).toEqual(records) }) it('queryBySourceNumbers: returns empty array on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.queryBySourceNumbers(['S1']) expect(result).toEqual([]) }) // --- queryBySourceNumbersDistinct --- it('queryBySourceNumbersDistinct: returns empty array for empty input', async () => { const repo = new DiscreteMaterialPlanRepository() const result = await repo.queryBySourceNumbersDistinct([]) expect(result).toEqual([]) }) it('queryBySourceNumbersDistinct: calls repo.query() per batch', async () => { mockRepo = { ...createMockRepository(), query: vi.fn().mockResolvedValue([{ M: 'X' }]) } vi.resetModules() const mod = await import( '../../src/main/services/database/repositories/DiscreteMaterialPlanRepository' ) const repo = new mod.DiscreteMaterialPlanRepository() const result = await repo.queryBySourceNumbersDistinct(['S1', 'S2']) expect(result.length).toBeGreaterThan(0) expect((mockRepo as Record).query).toHaveBeenCalled() }) it('queryBySourceNumbersDistinct: returns empty array on error', async () => { mockRepo = { ...createMockRepository(), query: vi.fn().mockRejectedValue(new Error('db fail')) } vi.resetModules() const mod = await import( '../../src/main/services/database/repositories/DiscreteMaterialPlanRepository' ) const repo = new mod.DiscreteMaterialPlanRepository() const result = await repo.queryBySourceNumbersDistinct(['S1']) expect(result).toEqual([]) }) // --- queryBySourceNumber --- it('queryBySourceNumber: calls find with where clause', async () => { const repo = new DiscreteMaterialPlanRepository() const records = [{ sourceNumber: 'S1', planNumber: 'P1' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.queryBySourceNumber('S1') expect(result).toEqual(records) expect(mockRepo.find).toHaveBeenCalledWith({ where: { sourceNumber: 'S1' } }) }) it('queryBySourceNumber: returns empty array on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.queryBySourceNumber('S1') expect(result).toEqual([]) }) // --- queryByPlanNumber --- it('queryByPlanNumber: calls find with where clause', async () => { const repo = new DiscreteMaterialPlanRepository() const records = [{ sourceNumber: 'S1', planNumber: 'P1' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.queryByPlanNumber('P1') expect(result).toEqual(records) expect(mockRepo.find).toHaveBeenCalledWith({ where: { planNumber: 'P1' } }) }) it('queryByPlanNumber: returns empty array on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.queryByPlanNumber('P1') expect(result).toEqual([]) }) // --- queryByPlanNumbers --- it('queryByPlanNumbers: returns empty array for empty input', async () => { const repo = new DiscreteMaterialPlanRepository() const result = await repo.queryByPlanNumbers([]) expect(result).toEqual([]) expect(mockRepo.find).not.toHaveBeenCalled() }) it('queryByPlanNumbers: calls find with In()', async () => { const repo = new DiscreteMaterialPlanRepository() const records = [{ planNumber: 'P1' }, { planNumber: 'P2' }] mockRepo.find!.mockResolvedValue(records) const result = await repo.queryByPlanNumbers(['P1', 'P2']) expect(result).toEqual(records) expect(mockRepo.find).toHaveBeenCalledWith({ where: { planNumber: ['P1', 'P2'] } // In() mock returns the array as-is }) }) it('queryByPlanNumbers: returns empty array on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.find!.mockRejectedValue(new Error('db fail')) const result = await repo.queryByPlanNumbers(['P1']) expect(result).toEqual([]) }) // --- countAll --- it('countAll: returns count', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.count!.mockResolvedValue(99) const result = await repo.countAll() expect(result).toBe(99) }) it('countAll: returns 0 on error', async () => { const repo = new DiscreteMaterialPlanRepository() mockRepo.count!.mockRejectedValue(new Error('db fail')) const result = await repo.countAll() expect(result).toBe(0) }) })