feat: implement IPC handlers, database services and Extractor UI
- Add SqlServerService and MySqlService for database persistence - Implement IPC handlers for file, extractor, cleaner, and database operations - Define IPC API types and update preload script - Create ExtractorPage UI with OrderNumberInput component - Add unit and integration tests for MySQL and SQL Server - Update vitest config with path aliases
This commit is contained in:
270
tests/integration/mysql.test.ts
Normal file
270
tests/integration/mysql.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Integration tests for MySqlService
|
||||
* These tests require a running MySQL instance
|
||||
* Skip if MySQL is not available: npx vitest run --reporter=verbose
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { MySqlService, MySqlConfig } from '@services/database/mysql'
|
||||
|
||||
// MySQL test configuration
|
||||
// In production, these should come from environment variables
|
||||
const testConfig: MySqlConfig = {
|
||||
host: process.env.MYSQL_HOST || 'localhost',
|
||||
port: parseInt(process.env.MYSQL_PORT || '3306'),
|
||||
user: process.env.MYSQL_USER || 'root',
|
||||
password: process.env.MYSQL_PASSWORD || 'password',
|
||||
database: process.env.MYSQL_DATABASE || 'test_db'
|
||||
}
|
||||
|
||||
describe('MySqlService Integration Tests', () => {
|
||||
let service: MySqlService
|
||||
let isMysqlAvailable = true
|
||||
|
||||
beforeAll(async () => {
|
||||
service = new MySqlService(testConfig)
|
||||
|
||||
// Try to connect, skip tests if MySQL is not available
|
||||
try {
|
||||
await service.connect()
|
||||
} catch (error) {
|
||||
console.warn('MySQL not available, skipping integration tests')
|
||||
isMysqlAvailable = false
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (service && isMysqlAvailable) {
|
||||
await service.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
describe('connect()', () => {
|
||||
it('should connect to MySQL database', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
expect(service.isConnected()).toBe(true)
|
||||
})
|
||||
|
||||
it('should throw error when already connected', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
// Already connected in beforeAll
|
||||
await expect(service.connect()).rejects.toThrow('Already connected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isConnected()', () => {
|
||||
it('should return true when connected', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
expect(service.isConnected()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when not connected', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
const newService = new MySqlService(testConfig)
|
||||
expect(newService.isConnected()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnect()', () => {
|
||||
it('should disconnect from MySQL database', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
await service.disconnect()
|
||||
expect(service.isConnected()).toBe(false)
|
||||
|
||||
// Reconnect for other tests
|
||||
await service.connect()
|
||||
})
|
||||
|
||||
it('should not throw when already disconnected', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
const newService = new MySqlService(testConfig)
|
||||
await expect(newService.disconnect()).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('query()', () => {
|
||||
beforeAll(async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure connected
|
||||
if (!service.isConnected()) {
|
||||
await service.connect()
|
||||
}
|
||||
|
||||
// Create test table
|
||||
await service.query(`
|
||||
CREATE TABLE IF NOT EXISTS test_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
|
||||
// Clean up test data
|
||||
await service.query('DELETE FROM test_users WHERE email LIKE ?', ['%test%'])
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
// Drop test table
|
||||
try {
|
||||
await service.query('DROP TABLE IF EXISTS test_users')
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute SELECT 1', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await service.query('SELECT 1 as value')
|
||||
|
||||
expect(result.columns).toContain('value')
|
||||
expect(result.rowCount).toBe(1)
|
||||
expect(result.rows[0]).toEqual({ value: 1 })
|
||||
})
|
||||
|
||||
it('should execute INSERT with parameters', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await service.query('INSERT INTO test_users (name, email) VALUES (?, ?)', [
|
||||
'Test User',
|
||||
'test@example.com'
|
||||
])
|
||||
|
||||
expect(result.rowCount).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
|
||||
it('should execute SELECT with parameters', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await service.query('SELECT id, name, email FROM test_users WHERE email = ?', [
|
||||
'test@example.com'
|
||||
])
|
||||
|
||||
expect(result.columns).toEqual(['id', 'name', 'email'])
|
||||
expect(result.rowCount).toBeGreaterThanOrEqual(0)
|
||||
expect(result.rows[0]?.name).toBe('Test User')
|
||||
})
|
||||
|
||||
it('should throw error when not connected', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
const newService = new MySqlService(testConfig)
|
||||
await expect(newService.query('SELECT 1')).rejects.toThrow('Not connected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction()', () => {
|
||||
beforeAll(async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure connected and table exists
|
||||
if (!service.isConnected()) {
|
||||
await service.connect()
|
||||
}
|
||||
|
||||
await service.query(`
|
||||
CREATE TABLE IF NOT EXISTS test_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
balance DECIMAL(10, 2) NOT NULL DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
// Clean up and initialize
|
||||
await service.query('DELETE FROM test_accounts')
|
||||
await service.query('INSERT INTO test_accounts (balance) VALUES (100), (50)')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await service.query('DROP TABLE IF EXISTS test_accounts')
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute multiple queries in a transaction', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
// Transfer 20 from account 1 to account 2
|
||||
await service.transaction([
|
||||
{ sql: 'UPDATE test_accounts SET balance = balance - ? WHERE id = ?', params: [20, 1] },
|
||||
{ sql: 'UPDATE test_accounts SET balance = balance + ? WHERE id = ?', params: [20, 2] }
|
||||
])
|
||||
|
||||
const result = await service.query('SELECT balance FROM test_accounts ORDER BY id')
|
||||
|
||||
expect(result.rows[0]?.balance).toEqual(expect.anything())
|
||||
expect(result.rows[1]?.balance).toEqual(expect.anything())
|
||||
})
|
||||
|
||||
it('should rollback on error', async () => {
|
||||
if (!isMysqlAvailable) {
|
||||
console.log('Skipped: MySQL not available')
|
||||
return
|
||||
}
|
||||
|
||||
// Get initial balances
|
||||
const initial = await service.query('SELECT balance FROM test_accounts ORDER BY id')
|
||||
|
||||
// Try invalid transaction (syntax error)
|
||||
await expect(service.transaction([{ sql: 'INVALID SQL STATEMENT' }])).rejects.toThrow()
|
||||
|
||||
// Verify balances unchanged
|
||||
const result = await service.query('SELECT balance FROM test_accounts ORDER BY id')
|
||||
expect(result.rows).toEqual(initial.rows)
|
||||
})
|
||||
})
|
||||
})
|
||||
294
tests/integration/sql-server.test.ts
Normal file
294
tests/integration/sql-server.test.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Integration tests for SqlServerService
|
||||
* These tests require a running SQL Server instance
|
||||
* Skip if SQL Server is not available
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { SqlServerService, SqlServerConfig } from '@main/services/database/sql-server'
|
||||
import * as sql from 'mssql'
|
||||
|
||||
// SQL Server test configuration
|
||||
const testConfig: SqlServerConfig = {
|
||||
server: process.env.SQL_SERVER_HOST || 'localhost',
|
||||
port: parseInt(process.env.SQL_SERVER_PORT || '1433'),
|
||||
user: process.env.SQL_SERVER_USER || 'sa',
|
||||
password: process.env.SQL_SERVER_PASSWORD || 'password',
|
||||
database: process.env.SQL_SERVER_DATABASE || 'testdb',
|
||||
options: {
|
||||
encrypt: false, // Set to true for Azure SQL
|
||||
trustServerCertificate: true // Set to false in production with valid cert
|
||||
}
|
||||
}
|
||||
|
||||
describe('SqlServerService Integration Tests', () => {
|
||||
let service: SqlServerService
|
||||
let isSqlServerAvailable = true
|
||||
|
||||
beforeAll(async () => {
|
||||
service = new SqlServerService(testConfig)
|
||||
|
||||
// Try to connect, skip tests if SQL Server is not available
|
||||
try {
|
||||
await service.connect()
|
||||
} catch (error) {
|
||||
console.warn('SQL Server not available, skipping integration tests')
|
||||
isSqlServerAvailable = false
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (service && isSqlServerAvailable) {
|
||||
await service.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
describe('connect()', () => {
|
||||
it('should connect to SQL Server database', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
expect(service.isConnected()).toBe(true)
|
||||
})
|
||||
|
||||
it('should throw error when already connected', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
// Already connected in beforeAll
|
||||
await expect(service.connect()).rejects.toThrow('Already connected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isConnected()', () => {
|
||||
it('should return true when connected', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
expect(service.isConnected()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when not connected', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
const newService = new SqlServerService(testConfig)
|
||||
expect(newService.isConnected()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnect()', () => {
|
||||
it('should disconnect from SQL Server database', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
await service.disconnect()
|
||||
expect(service.isConnected()).toBe(false)
|
||||
|
||||
// Reconnect for other tests
|
||||
await service.connect()
|
||||
})
|
||||
|
||||
it('should not throw when already disconnected', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
const newService = new SqlServerService(testConfig)
|
||||
await expect(newService.disconnect()).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('query()', () => {
|
||||
beforeAll(async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure connected
|
||||
if (!service.isConnected()) {
|
||||
await service.connect()
|
||||
}
|
||||
|
||||
// Create test table
|
||||
await service.query(`
|
||||
IF OBJECT_ID('dbo.TestUsers', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.TestUsers;
|
||||
|
||||
CREATE TABLE dbo.TestUsers (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
name NVARCHAR(100) NOT NULL,
|
||||
email NVARCHAR(100),
|
||||
created_at DATETIME DEFAULT GETDATE()
|
||||
)
|
||||
`)
|
||||
|
||||
// Clean up test data
|
||||
await service.query('DELETE FROM dbo.TestUsers WHERE email LIKE @email', {
|
||||
email: { value: '%test%', type: sql.NVarChar }
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
// Drop test table
|
||||
try {
|
||||
await service.query('DROP TABLE IF EXISTS dbo.TestUsers')
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute SELECT 1', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await service.query('SELECT 1 as value')
|
||||
|
||||
expect(result.columns).toContain('value')
|
||||
expect(result.rowCount).toBe(1)
|
||||
expect(result.rows[0]).toEqual({ value: 1 })
|
||||
})
|
||||
|
||||
it('should execute INSERT with parameters', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await service.query(
|
||||
'INSERT INTO dbo.TestUsers (name, email) VALUES (@name, @email)',
|
||||
{
|
||||
name: { value: 'Test User', type: sql.NVarChar },
|
||||
email: { value: 'test@example.com', type: sql.NVarChar }
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.rowCount).toBe(1)
|
||||
})
|
||||
|
||||
it('should execute SELECT with parameters', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await service.query(
|
||||
'SELECT id, name, email FROM dbo.TestUsers WHERE email = @email',
|
||||
{
|
||||
email: { value: 'test@example.com', type: sql.NVarChar }
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.columns).toEqual(['id', 'name', 'email'])
|
||||
expect(result.rowCount).toBeGreaterThanOrEqual(0)
|
||||
expect(result.rows[0]?.name).toBe('Test User')
|
||||
})
|
||||
|
||||
it('should throw error when not connected', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
const newService = new SqlServerService(testConfig)
|
||||
await expect(newService.query('SELECT 1')).rejects.toThrow('Not connected to SQL Server')
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction()', () => {
|
||||
beforeAll(async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure connected and table exists
|
||||
if (!service.isConnected()) {
|
||||
await service.connect()
|
||||
}
|
||||
|
||||
await service.query(`
|
||||
IF OBJECT_ID('dbo.TestAccounts', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.TestAccounts;
|
||||
|
||||
CREATE TABLE dbo.TestAccounts (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
balance DECIMAL(10, 2) NOT NULL DEFAULT 0
|
||||
)
|
||||
`)
|
||||
|
||||
// Clean up and initialize
|
||||
await service.query('DELETE FROM dbo.TestAccounts')
|
||||
await service.query('INSERT INTO dbo.TestAccounts (balance) VALUES (100), (50)')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await service.query('DROP TABLE IF EXISTS dbo.TestAccounts')
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute multiple queries in a transaction', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
// Transfer 20 from account 1 to account 2
|
||||
await service.transaction([
|
||||
{
|
||||
sql: 'UPDATE dbo.TestAccounts SET balance = balance - @amount WHERE id = @id',
|
||||
params: { amount: { value: 20, type: sql.Decimal }, id: { value: 1, type: sql.Int } }
|
||||
},
|
||||
{
|
||||
sql: 'UPDATE dbo.TestAccounts SET balance = balance + @amount WHERE id = @id',
|
||||
params: { amount: { value: 20, type: sql.Decimal }, id: { value: 2, type: sql.Int } }
|
||||
}
|
||||
])
|
||||
|
||||
const result = await service.query('SELECT balance FROM dbo.TestAccounts ORDER BY id')
|
||||
|
||||
expect(result.rows[0]?.balance).toBeDefined()
|
||||
expect(result.rows[1]?.balance).toBeDefined()
|
||||
})
|
||||
|
||||
it('should rollback on error', async () => {
|
||||
if (!isSqlServerAvailable) {
|
||||
console.log('Skipped: SQL Server not available')
|
||||
return
|
||||
}
|
||||
|
||||
// Get initial balances
|
||||
const initial = await service.query('SELECT balance FROM dbo.TestAccounts ORDER BY id')
|
||||
|
||||
// Try invalid transaction (syntax error)
|
||||
await expect(service.transaction([{ sql: 'INVALID SQL STATEMENT' }])).rejects.toThrow()
|
||||
|
||||
// Verify balances unchanged
|
||||
const result = await service.query('SELECT balance FROM dbo.TestAccounts ORDER BY id')
|
||||
expect(result.rows).toEqual(initial.rows)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user