- Add MaterialTypeManagementDialog component for managing material type keywords - Add MaterialsTypeToBeDeletedDAO for database operations - Add material-type-handler IPC handlers - Update CleanerPage with type management button - Add database fix scripts for AUTO_INCREMENT - Update documentation for settings partial save and validation flow Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
272 lines
7.8 KiB
TypeScript
272 lines
7.8 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { ConfigManager } from '@services/config/config-manager'
|
|
import type { SettingsData } from '@types/settings.types'
|
|
|
|
describe('ConfigManager - deep merge utilities', () => {
|
|
it('should deep merge objects, updating only specified fields', async () => {
|
|
const manager = ConfigManager.getInstance()
|
|
await manager.initialize()
|
|
// Reload to ensure clean state from previous tests
|
|
await manager['loadEnvFile']()
|
|
|
|
// Setup initial state
|
|
const initial: SettingsData = {
|
|
erp: {
|
|
url: 'http://old.com',
|
|
username: 'user1',
|
|
password: 'pass1',
|
|
headless: true,
|
|
ignoreHttpsErrors: true,
|
|
autoCloseBrowser: true
|
|
},
|
|
database: {
|
|
dbType: 'mysql',
|
|
server: '',
|
|
mysqlHost: 'localhost',
|
|
mysqlPort: 3306,
|
|
database: 'db',
|
|
username: 'user',
|
|
password: ''
|
|
},
|
|
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'validation.xlsx' },
|
|
extraction: {
|
|
batchSize: 100,
|
|
verbose: true,
|
|
autoConvert: true,
|
|
mergeBatches: true,
|
|
enableDbPersistence: true
|
|
},
|
|
validation: {
|
|
dataSource: 'database_full',
|
|
batchSize: 2000,
|
|
matchMode: 'substring',
|
|
enableCrud: false,
|
|
defaultManager: ''
|
|
},
|
|
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
|
execution: { dryRun: false }
|
|
}
|
|
|
|
// Load initial settings
|
|
await manager.saveAllSettings(initial)
|
|
// Reload from disk to populate cache
|
|
await manager['loadEnvFile']()
|
|
|
|
// Partial update
|
|
const partial = {
|
|
erp: { url: 'http://new.com' }
|
|
}
|
|
|
|
const result = await manager.savePartialSettings(partial)
|
|
|
|
expect(result.success).toBe(true)
|
|
|
|
const current = manager.getAllSettings()
|
|
|
|
// Updated field
|
|
expect(current.erp.url).toBe('http://new.com')
|
|
|
|
// Preserved fields
|
|
expect(current.erp.username).toBe('user1')
|
|
expect(current.database.dbType).toBe('mysql')
|
|
expect(current.paths.dataDir).toBe('/data')
|
|
})
|
|
})
|
|
|
|
describe('ConfigManager - backup and restore', () => {
|
|
it('should create backup before saving', async () => {
|
|
const manager = ConfigManager.getInstance()
|
|
await manager.initialize()
|
|
|
|
const backupSuccess = await manager['backupEnvFile']()
|
|
|
|
expect(backupSuccess).toBe(true)
|
|
|
|
// Check backup file exists (in same location as .env file, which is src/main/)
|
|
const fs = await import('fs')
|
|
const path = await import('path')
|
|
const backupPath = path.resolve(process.cwd(), 'src/main/.env.backup')
|
|
|
|
expect(fs.existsSync(backupPath)).toBe(true)
|
|
})
|
|
|
|
// Note: Skipping fs.writeFileSync mock test due to ESM limitations in Vitest
|
|
// The restoreBackup functionality is tested indirectly through the savePartialSettings rollback test
|
|
})
|
|
|
|
describe('ConfigManager.savePartialSettings', () => {
|
|
it('should save only specified fields and preserve others', async () => {
|
|
const manager = ConfigManager.getInstance()
|
|
await manager.initialize()
|
|
|
|
// Setup initial state with multiple categories
|
|
await manager.saveAllSettings({
|
|
erp: {
|
|
url: 'http://old.com',
|
|
username: 'user1',
|
|
password: 'pass1',
|
|
headless: true,
|
|
ignoreHttpsErrors: true,
|
|
autoCloseBrowser: true
|
|
},
|
|
database: {
|
|
dbType: 'mysql',
|
|
server: '',
|
|
mysqlHost: '192.168.1.1',
|
|
mysqlPort: 3306,
|
|
database: 'testdb',
|
|
username: 'dbuser',
|
|
password: ''
|
|
},
|
|
paths: { dataDir: '/old/path', defaultOutput: 'out.xlsx', validationOutput: 'val.xlsx' },
|
|
extraction: {
|
|
batchSize: 50,
|
|
verbose: true,
|
|
autoConvert: true,
|
|
mergeBatches: true,
|
|
enableDbPersistence: true
|
|
},
|
|
validation: {
|
|
dataSource: 'database_full',
|
|
batchSize: 1000,
|
|
matchMode: 'exact',
|
|
enableCrud: false,
|
|
defaultManager: ''
|
|
},
|
|
ui: { fontFamily: 'Tahoma', fontSize: 14, productionIdInputWidth: 25 },
|
|
execution: { dryRun: true }
|
|
})
|
|
// Reload from disk to populate cache
|
|
await manager['loadEnvFile']()
|
|
|
|
// Update only ERP URL
|
|
const result = await manager.savePartialSettings({
|
|
erp: { url: 'http://new.com' }
|
|
})
|
|
|
|
expect(result.success).toBe(true)
|
|
|
|
const current = manager.getAllSettings()
|
|
|
|
// Verify updated field
|
|
expect(current.erp.url).toBe('http://new.com')
|
|
|
|
// Verify preserved ERP fields
|
|
expect(current.erp.username).toBe('user1')
|
|
expect(current.erp.password).toBe('pass1')
|
|
|
|
// Verify preserved other categories
|
|
expect(current.database.dbType).toBe('mysql')
|
|
expect(current.database.mysqlHost).toBe('192.168.1.1')
|
|
expect(current.paths.dataDir).toBe('/old/path')
|
|
expect(current.extraction.batchSize).toBe(50)
|
|
expect(current.ui.fontFamily).toBe('Tahoma')
|
|
})
|
|
|
|
it('should reject updates to non-whitelisted fields', async () => {
|
|
const manager = ConfigManager.getInstance()
|
|
await manager.initialize()
|
|
// Reset to ensure clean state
|
|
manager.resetToDefaults()
|
|
await manager.save()
|
|
|
|
const result = await manager.savePartialSettings({
|
|
database: { dbType: 'postgres' }
|
|
})
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toContain('不允许修改')
|
|
expect(result.error).toContain('database.dbType')
|
|
})
|
|
|
|
it('should handle nested object updates correctly', async () => {
|
|
const manager = ConfigManager.getInstance()
|
|
await manager.initialize()
|
|
// Reset to ensure clean state
|
|
manager.resetToDefaults()
|
|
await manager.save()
|
|
|
|
await manager.saveAllSettings({
|
|
erp: {
|
|
url: 'http://test.com',
|
|
username: 'u',
|
|
password: 'p',
|
|
headless: false,
|
|
ignoreHttpsErrors: false,
|
|
autoCloseBrowser: false
|
|
},
|
|
database: {
|
|
dbType: 'mysql',
|
|
server: '',
|
|
mysqlHost: 'localhost',
|
|
mysqlPort: 3306,
|
|
database: 'db',
|
|
username: 'user',
|
|
password: ''
|
|
},
|
|
paths: { dataDir: '/data', defaultOutput: 'out.xlsx', validationOutput: 'val.xlsx' },
|
|
extraction: {
|
|
batchSize: 100,
|
|
verbose: true,
|
|
autoConvert: true,
|
|
mergeBatches: true,
|
|
enableDbPersistence: true
|
|
},
|
|
validation: {
|
|
dataSource: 'database_full',
|
|
batchSize: 2000,
|
|
matchMode: 'substring',
|
|
enableCrud: false,
|
|
defaultManager: ''
|
|
},
|
|
ui: { fontFamily: 'Arial', fontSize: 12, productionIdInputWidth: 20 },
|
|
execution: { dryRun: false }
|
|
})
|
|
// Reload from disk to populate cache
|
|
await manager['loadEnvFile']()
|
|
|
|
// Update multiple ERP fields at once
|
|
const result = await manager.savePartialSettings({
|
|
erp: {
|
|
url: 'http://updated.com',
|
|
username: 'newuser',
|
|
password: 'newpass'
|
|
}
|
|
})
|
|
|
|
expect(result.success).toBe(true)
|
|
|
|
const current = manager.getAllSettings()
|
|
|
|
expect(current.erp.url).toBe('http://updated.com')
|
|
expect(current.erp.username).toBe('newuser')
|
|
expect(current.erp.password).toBe('newpass')
|
|
expect(current.erp.headless).toBe(false) // preserved
|
|
})
|
|
|
|
it('should restore backup on save failure', async () => {
|
|
const manager = ConfigManager.getInstance()
|
|
await manager.initialize()
|
|
// Reset to ensure clean state
|
|
manager.resetToDefaults()
|
|
await manager.save()
|
|
|
|
const originalUrl = manager.getAllSettings().erp.url
|
|
|
|
// Mock save to fail
|
|
vi.spyOn(manager, 'save').mockResolvedValueOnce(false)
|
|
|
|
const result = await manager.savePartialSettings({
|
|
erp: { url: 'http://should-not-apply.com' }
|
|
})
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toContain('保存配置失败')
|
|
|
|
// Verify rollback
|
|
expect(manager.getAllSettings().erp.url).toBe(originalUrl)
|
|
|
|
manager.save.mockRestore()
|
|
})
|
|
})
|