feat: add TypeORM, logger, schemas, hooks and stores
- Add TypeORM integration with data-source, entities and repositories - Add logger service for structured logging - Add Zod validation schemas for auth, cleaner and extractor - Add custom React hooks (useAuth, useCleaner, useExtractor, useValidation) - Add Zustand stores (useAppStore, useUserStore) - Add UI components (Button, Modal, Toast) - Add error types and ErpBrowserManager - Refactor IPC handlers and services - Add unit tests for new modules Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
208
tests/unit/errors.test.ts
Normal file
208
tests/unit/errors.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Error Types Unit Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
BaseError,
|
||||
ErpConnectionError,
|
||||
DatabaseQueryError,
|
||||
ValidationError,
|
||||
ERP_ERROR_CODES,
|
||||
DATABASE_ERROR_CODES,
|
||||
VALIDATION_ERROR_CODES,
|
||||
isBaseError,
|
||||
isErpConnectionError,
|
||||
isDatabaseQueryError,
|
||||
isValidationError,
|
||||
getErrorMessage,
|
||||
getErrorCode
|
||||
} from '../../src/main/types/errors'
|
||||
|
||||
// Concrete implementation for testing abstract BaseError
|
||||
class TestError extends BaseError {
|
||||
constructor(message: string, code: string, cause?: Error) {
|
||||
super('TestError', message, code, cause)
|
||||
}
|
||||
}
|
||||
|
||||
describe('Error Types', () => {
|
||||
describe('BaseError', () => {
|
||||
it('should create an error with name, message, and code', () => {
|
||||
const error = new TestError('Test message', 'TEST_CODE')
|
||||
|
||||
expect(error.name).toBe('TestError')
|
||||
expect(error.message).toBe('Test message')
|
||||
expect(error.code).toBe('TEST_CODE')
|
||||
expect(error.cause).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should capture cause when provided', () => {
|
||||
const cause = new Error('Original error')
|
||||
const error = new TestError('Test message', 'TEST_CODE', cause)
|
||||
|
||||
expect(error.cause).toBe(cause)
|
||||
})
|
||||
|
||||
it('should serialize to JSON correctly', () => {
|
||||
const error = new TestError('Test message', 'TEST_CODE')
|
||||
const json = error.toJSON()
|
||||
|
||||
expect(json).toEqual({
|
||||
name: 'TestError',
|
||||
message: 'Test message',
|
||||
code: 'TEST_CODE',
|
||||
cause: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should be an instance of Error', () => {
|
||||
const error = new TestError('Test message', 'TEST_CODE')
|
||||
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ErpConnectionError', () => {
|
||||
it('should create with default code', () => {
|
||||
const error = new ErpConnectionError('Connection failed')
|
||||
|
||||
expect(error.name).toBe('ErpConnectionError')
|
||||
expect(error.code).toBe(ERP_ERROR_CODES.CONNECTION_FAILED)
|
||||
})
|
||||
|
||||
it('should create with specific code', () => {
|
||||
const error = new ErpConnectionError('Login failed', ERP_ERROR_CODES.LOGIN_FAILED)
|
||||
|
||||
expect(error.code).toBe(ERP_ERROR_CODES.LOGIN_FAILED)
|
||||
})
|
||||
|
||||
it('should accept cause', () => {
|
||||
const cause = new Error('Network timeout')
|
||||
const error = new ErpConnectionError('Timeout', ERP_ERROR_CODES.TIMEOUT, cause)
|
||||
|
||||
expect(error.cause).toBe(cause)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DatabaseQueryError', () => {
|
||||
it('should create with default code', () => {
|
||||
const error = new DatabaseQueryError('Query failed')
|
||||
|
||||
expect(error.name).toBe('DatabaseQueryError')
|
||||
expect(error.code).toBe(DATABASE_ERROR_CODES.QUERY_FAILED)
|
||||
})
|
||||
|
||||
it('should create with specific code', () => {
|
||||
const error = new DatabaseQueryError(
|
||||
'Connection failed',
|
||||
DATABASE_ERROR_CODES.CONNECTION_FAILED
|
||||
)
|
||||
|
||||
expect(error.code).toBe(DATABASE_ERROR_CODES.CONNECTION_FAILED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ValidationError', () => {
|
||||
it('should create with default code', () => {
|
||||
const error = new ValidationError('Invalid input')
|
||||
|
||||
expect(error.name).toBe('ValidationError')
|
||||
expect(error.code).toBe(VALIDATION_ERROR_CODES.INVALID_INPUT)
|
||||
})
|
||||
|
||||
it('should create with specific code', () => {
|
||||
const error = new ValidationError('Missing field', VALIDATION_ERROR_CODES.MISSING_REQUIRED)
|
||||
|
||||
expect(error.code).toBe(VALIDATION_ERROR_CODES.MISSING_REQUIRED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type Guards', () => {
|
||||
it('isBaseError should return true for BaseError instances', () => {
|
||||
const error = new ValidationError('Test')
|
||||
|
||||
expect(isBaseError(error)).toBe(true)
|
||||
expect(isBaseError(new Error('Test'))).toBe(false)
|
||||
expect(isBaseError('string')).toBe(false)
|
||||
})
|
||||
|
||||
it('isErpConnectionError should return true only for ErpConnectionError', () => {
|
||||
const erpError = new ErpConnectionError('Test')
|
||||
const dbError = new DatabaseQueryError('Test')
|
||||
|
||||
expect(isErpConnectionError(erpError)).toBe(true)
|
||||
expect(isErpConnectionError(dbError)).toBe(false)
|
||||
})
|
||||
|
||||
it('isDatabaseQueryError should return true only for DatabaseQueryError', () => {
|
||||
const dbError = new DatabaseQueryError('Test')
|
||||
const valError = new ValidationError('Test')
|
||||
|
||||
expect(isDatabaseQueryError(dbError)).toBe(true)
|
||||
expect(isDatabaseQueryError(valError)).toBe(false)
|
||||
})
|
||||
|
||||
it('isValidationError should return true only for ValidationError', () => {
|
||||
const valError = new ValidationError('Test')
|
||||
const erpError = new ErpConnectionError('Test')
|
||||
|
||||
expect(isValidationError(valError)).toBe(true)
|
||||
expect(isValidationError(erpError)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Helper Functions', () => {
|
||||
it('getErrorMessage should return message from BaseError', () => {
|
||||
const error = new ValidationError('Invalid input')
|
||||
|
||||
expect(getErrorMessage(error)).toBe('Invalid input')
|
||||
})
|
||||
|
||||
it('getErrorMessage should return message from standard Error', () => {
|
||||
const error = new Error('Standard error')
|
||||
|
||||
// In non-production, it returns the actual message
|
||||
expect(getErrorMessage(error)).toBe('Standard error')
|
||||
})
|
||||
|
||||
it('getErrorMessage should handle unknown types', () => {
|
||||
expect(getErrorMessage('string error')).toBe('string error')
|
||||
expect(getErrorMessage(null)).toBe('An unknown error occurred')
|
||||
expect(getErrorMessage(undefined)).toBe('An unknown error occurred')
|
||||
})
|
||||
|
||||
it('getErrorCode should return code from BaseError', () => {
|
||||
const error = new ValidationError('Test', VALIDATION_ERROR_CODES.MISSING_REQUIRED)
|
||||
|
||||
expect(getErrorCode(error)).toBe(VALIDATION_ERROR_CODES.MISSING_REQUIRED)
|
||||
})
|
||||
|
||||
it('getErrorCode should return UNKNOWN_ERROR for non-BaseError', () => {
|
||||
const error = new Error('Test')
|
||||
|
||||
expect(getErrorCode(error)).toBe('UNKNOWN_ERROR')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Codes', () => {
|
||||
it('ERP_ERROR_CODES should have all expected codes', () => {
|
||||
expect(ERP_ERROR_CODES.CONNECTION_FAILED).toBe('ERP_CONNECTION_FAILED')
|
||||
expect(ERP_ERROR_CODES.LOGIN_FAILED).toBe('ERP_LOGIN_FAILED')
|
||||
expect(ERP_ERROR_CODES.TIMEOUT).toBe('ERP_TIMEOUT')
|
||||
expect(ERP_ERROR_CODES.SESSION_EXPIRED).toBe('ERP_SESSION_EXPIRED')
|
||||
})
|
||||
|
||||
it('DATABASE_ERROR_CODES should have all expected codes', () => {
|
||||
expect(DATABASE_ERROR_CODES.CONNECTION_FAILED).toBe('DB_CONNECTION_FAILED')
|
||||
expect(DATABASE_ERROR_CODES.QUERY_FAILED).toBe('DB_QUERY_FAILED')
|
||||
expect(DATABASE_ERROR_CODES.TIMEOUT).toBe('DB_TIMEOUT')
|
||||
})
|
||||
|
||||
it('VALIDATION_ERROR_CODES should have all expected codes', () => {
|
||||
expect(VALIDATION_ERROR_CODES.INVALID_INPUT).toBe('VAL_INVALID_INPUT')
|
||||
expect(VALIDATION_ERROR_CODES.MISSING_REQUIRED).toBe('VAL_MISSING_REQUIRED')
|
||||
expect(VALIDATION_ERROR_CODES.INVALID_FORMAT).toBe('VAL_INVALID_FORMAT')
|
||||
})
|
||||
})
|
||||
})
|
||||
78
tests/unit/logger.test.ts
Normal file
78
tests/unit/logger.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Logger Unit Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
// Mock winston since we don't need actual file logging in tests
|
||||
vi.mock('winston', () => ({
|
||||
default: {
|
||||
createLogger: vi.fn(() => ({
|
||||
add: vi.fn(),
|
||||
child: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn()
|
||||
})),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn()
|
||||
})),
|
||||
format: {
|
||||
combine: vi.fn(),
|
||||
timestamp: vi.fn(),
|
||||
colorize: vi.fn(),
|
||||
printf: vi.fn(),
|
||||
json: vi.fn()
|
||||
},
|
||||
transports: {
|
||||
Console: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('winston-daily-rotate-file', () => ({
|
||||
default: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
isReady: vi.fn(() => false),
|
||||
getPath: vi.fn(() => './logs')
|
||||
}
|
||||
}))
|
||||
|
||||
describe('Logger', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('should create a logger with context', async () => {
|
||||
const { createLogger } = await import('../../src/main/services/logger')
|
||||
const logger = createLogger('TestContext')
|
||||
|
||||
expect(logger).toBeDefined()
|
||||
expect(logger.child).toBeDefined()
|
||||
})
|
||||
|
||||
it('should have log methods', async () => {
|
||||
const { createLogger } = await import('../../src/main/services/logger')
|
||||
const logger = createLogger('TestContext')
|
||||
|
||||
expect(typeof logger.info).toBe('function')
|
||||
expect(typeof logger.error).toBe('function')
|
||||
expect(typeof logger.warn).toBe('function')
|
||||
expect(typeof logger.debug).toBe('function')
|
||||
})
|
||||
|
||||
it('should export default logger', async () => {
|
||||
const logger = await import('../../src/main/services/logger')
|
||||
expect(logger.default).toBeDefined()
|
||||
})
|
||||
})
|
||||
67
tests/unit/repositories.test.ts
Normal file
67
tests/unit/repositories.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Repository Unit Tests
|
||||
*
|
||||
* Tests for TypeORM repository patterns.
|
||||
* Note: These tests mock the database connections.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
// Mock TypeORM
|
||||
vi.mock('typeorm', () => ({
|
||||
DataSource: vi.fn(() => ({
|
||||
initialize: vi.fn().mockResolvedValue({}),
|
||||
isInitialized: false,
|
||||
getRepository: vi.fn(),
|
||||
destroy: vi.fn()
|
||||
})),
|
||||
Repository: vi.fn(),
|
||||
In: vi.fn((arr) => arr)
|
||||
}))
|
||||
|
||||
vi.mock('../../src/main/services/logger', () => ({
|
||||
createLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('MaterialsToBeDeletedRepository', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should be defined', async () => {
|
||||
const { MaterialsToBeDeletedRepository } =
|
||||
await import('../../src/main/services/database/repositories/MaterialsToBeDeletedRepository')
|
||||
expect(MaterialsToBeDeletedRepository).toBeDefined()
|
||||
})
|
||||
|
||||
it('should create repository instance', async () => {
|
||||
const { MaterialsToBeDeletedRepository } =
|
||||
await import('../../src/main/services/database/repositories/MaterialsToBeDeletedRepository')
|
||||
const repo = new MaterialsToBeDeletedRepository()
|
||||
expect(repo).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('DiscreteMaterialPlanRepository', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should be defined', async () => {
|
||||
const { DiscreteMaterialPlanRepository } =
|
||||
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||
expect(DiscreteMaterialPlanRepository).toBeDefined()
|
||||
})
|
||||
|
||||
it('should create repository instance', async () => {
|
||||
const { DiscreteMaterialPlanRepository } =
|
||||
await import('../../src/main/services/database/repositories/DiscreteMaterialPlanRepository')
|
||||
const repo = new DiscreteMaterialPlanRepository()
|
||||
expect(repo).toBeDefined()
|
||||
})
|
||||
})
|
||||
227
tests/unit/schemas.test.ts
Normal file
227
tests/unit/schemas.test.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Zod Schemas Unit Tests
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
ExtractorInputSchema,
|
||||
validateExtractorInput
|
||||
} from '../../src/main/schemas/extractor.schema'
|
||||
import { CleanerInputSchema, validateCleanerInput } from '../../src/main/schemas/cleaner.schema'
|
||||
import { LoginRequestSchema, validateLoginRequest } from '../../src/main/schemas/auth.schema'
|
||||
|
||||
describe('Extractor Schema', () => {
|
||||
describe('ExtractorInputSchema', () => {
|
||||
it('should validate valid input', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234', 'SC98765432109876'],
|
||||
batchSize: 10
|
||||
}
|
||||
|
||||
const result = ExtractorInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should apply default batchSize', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234']
|
||||
}
|
||||
|
||||
const result = ExtractorInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.batchSize).toBe(10)
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject empty orderNumbers array', () => {
|
||||
const input = {
|
||||
orderNumbers: []
|
||||
}
|
||||
|
||||
const result = ExtractorInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject missing orderNumbers', () => {
|
||||
const input = {}
|
||||
|
||||
const result = ExtractorInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject empty string in orderNumbers', () => {
|
||||
const input = {
|
||||
orderNumbers: ['', 'SC12345678901234']
|
||||
}
|
||||
|
||||
const result = ExtractorInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateExtractorInput', () => {
|
||||
it('should return success for valid input', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234']
|
||||
}
|
||||
|
||||
const result = validateExtractorInput(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return error message for invalid input', () => {
|
||||
const input = {
|
||||
orderNumbers: []
|
||||
}
|
||||
|
||||
const result = validateExtractorInput(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Cleaner Schema', () => {
|
||||
describe('CleanerInputSchema', () => {
|
||||
it('should validate valid input', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234'],
|
||||
materialCodes: ['MAT001', 'MAT002'],
|
||||
dryRun: true
|
||||
}
|
||||
|
||||
const result = CleanerInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should validate with empty materialCodes', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234'],
|
||||
materialCodes: [],
|
||||
dryRun: false
|
||||
}
|
||||
|
||||
const result = CleanerInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject missing dryRun', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234'],
|
||||
materialCodes: ['MAT001']
|
||||
}
|
||||
|
||||
const result = CleanerInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject non-boolean dryRun', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234'],
|
||||
materialCodes: [],
|
||||
dryRun: 'yes'
|
||||
}
|
||||
|
||||
const result = CleanerInputSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateCleanerInput', () => {
|
||||
it('should return success for valid input', () => {
|
||||
const input = {
|
||||
orderNumbers: ['SC12345678901234'],
|
||||
materialCodes: ['MAT001'],
|
||||
dryRun: false
|
||||
}
|
||||
|
||||
const result = validateCleanerInput(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Auth Schema', () => {
|
||||
describe('LoginRequestSchema', () => {
|
||||
it('should validate valid input', () => {
|
||||
const input = {
|
||||
username: 'testuser',
|
||||
password: 'testpass'
|
||||
}
|
||||
|
||||
const result = LoginRequestSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject empty username', () => {
|
||||
const input = {
|
||||
username: '',
|
||||
password: 'testpass'
|
||||
}
|
||||
|
||||
const result = LoginRequestSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject empty password', () => {
|
||||
const input = {
|
||||
username: 'testuser',
|
||||
password: ''
|
||||
}
|
||||
|
||||
const result = LoginRequestSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject missing fields', () => {
|
||||
const input = {}
|
||||
|
||||
const result = LoginRequestSchema.safeParse(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateLoginRequest', () => {
|
||||
it('should return success for valid input', () => {
|
||||
const input = {
|
||||
username: 'admin',
|
||||
password: 'password123'
|
||||
}
|
||||
|
||||
const result = validateLoginRequest(input)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toBeDefined()
|
||||
expect(result.data?.username).toBe('admin')
|
||||
})
|
||||
|
||||
it('should return error for invalid input', () => {
|
||||
const input = {
|
||||
username: ''
|
||||
}
|
||||
|
||||
const result = validateLoginRequest(input)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user