fix(test): improve logger mock path and add behavior-based repository tests

- Fix logger-performance test mock path to use bare module specifier
- Replace meaningless "should be defined" assertions in repositories test
  with behavior-based tests covering upsert, batch operations, queries,
  deletes, and error handling for both MaterialsToBeDeletedRepository
  and DiscreteMaterialPlanRepository

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-05 08:16:40 +08:00
parent 406a8dfd2f
commit ae29f38d24
2 changed files with 586 additions and 63 deletions

View File

@@ -1,59 +1,45 @@
/**
* Repository Unit Tests
*
* Tests for TypeORM repository patterns.
* Note: These tests mock the database connections.
* 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 } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { createMockRepository, createMockQueryBuilder } from '../mocks'
// Mock TypeORM
// --- Shared mock state ---
let mockRepo: ReturnType<typeof createMockRepository>
// Mock TypeORM decorators (entities still need them)
vi.mock('typeorm', () => {
// Create mock decorator functions
const Entity = vi.fn()
const PrimaryGeneratedColumn = vi.fn()
const Column = vi.fn()
const ManyToOne = vi.fn()
const OneToMany = vi.fn()
const ManyToMany = vi.fn()
const JoinColumn = vi.fn()
const JoinTable = vi.fn()
const CreateDateColumn = vi.fn()
const UpdateDateColumn = vi.fn()
const DeleteDateColumn = vi.fn()
const Index = vi.fn()
const Unique = vi.fn()
const Check = vi.fn()
const Exclusion = vi.fn()
const Generated = vi.fn()
const decorator = vi.fn()
return {
DataSource: vi.fn(() => ({
initialize: vi.fn().mockResolvedValue({}),
isInitialized: false,
getRepository: vi.fn(),
initialize: vi.fn().mockResolvedValue(undefined),
isInitialized: true,
getRepository: vi.fn(() => mockRepo),
destroy: vi.fn()
})),
Repository: vi.fn(),
In: vi.fn((arr) => arr),
// Add all the decorators that entities use
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
OneToMany,
ManyToMany,
JoinColumn,
JoinTable,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
Unique,
Check,
Exclusion,
Generated,
// Other TypeORM exports
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(),
@@ -76,40 +62,577 @@ vi.mock('../../src/main/services/logger', () => ({
}))
}))
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', () => {
beforeEach(() => {
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
})
it('should be defined', async () => {
const { MaterialsToBeDeletedRepository } =
await import('../../src/main/services/database/repositories/MaterialsToBeDeletedRepository')
expect(MaterialsToBeDeletedRepository).toBeDefined()
afterEach(() => {
vi.resetModules()
})
it('should create repository instance', async () => {
const { MaterialsToBeDeletedRepository } =
await import('../../src/main/services/database/repositories/MaterialsToBeDeletedRepository')
// --- upsert ---
it('upsert: creates new entity when not found', async () => {
const repo = new MaterialsToBeDeletedRepository()
expect(repo).toBeDefined()
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', () => {
beforeEach(() => {
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
})
it('should be defined', async () => {
const { DiscreteMaterialPlanRepository } =
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
expect(DiscreteMaterialPlanRepository).toBeDefined()
afterEach(() => {
vi.resetModules()
})
it('should create repository instance', async () => {
const { DiscreteMaterialPlanRepository } =
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
// --- queryAll ---
it('queryAll: returns find results', async () => {
const repo = new DiscreteMaterialPlanRepository()
expect(repo).toBeDefined()
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<string, unknown>).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)
})
})