Implements the core savePartialSettings method that: - Validates fields against UI_EDITABLE_FIELDS whitelist - Deep merges partial updates with current settings - Creates backup before saving - Restores backup on save failure - Reloads .env file to populate cache with correct keys Added comprehensive tests: - Partial update preserves existing fields - Rejects non-whitelisted fields - Handles nested object updates - Restores backup on save failure Fixed cache key inconsistency bug by clearing cache in loadEnvFile() and reloading after save to ensure proper cache population. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
218 lines
7.4 KiB
TypeScript
218 lines
7.4 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()
|
|
})
|
|
})
|