diff --git a/tests/e2e/auth-flow.test.ts b/tests/e2e/auth-flow.test.ts index 64b7fdc..99d1804 100644 --- a/tests/e2e/auth-flow.test.ts +++ b/tests/e2e/auth-flow.test.ts @@ -85,8 +85,10 @@ test.describe('Authentication Flow', () => { const errorMessage = page.locator('.error, [role="alert"], .text-red') const hasError = await errorMessage.count() - // Either error shown or still on login page - expect(hasError >= 0).toBe(true) + // Verify login was rejected: either error shown or still on login page + const loginDialog = page.locator('[data-testid="login-dialog"]') + const isStillOnLoginPage = await loginDialog.isVisible().catch(() => false) + expect(hasError > 0 || isStillOnLoginPage).toBe(true) } }) }) diff --git a/tests/unit/mysql.test.ts b/tests/unit/mysql.test.ts index 3a4c72f..70afb99 100644 --- a/tests/unit/mysql.test.ts +++ b/tests/unit/mysql.test.ts @@ -1,11 +1,51 @@ /** * Unit tests for MySqlService - * These tests do not require a MySQL instance + * Covers both unconnected state and connected-path operations using mocked mysql2/promise. */ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { MySqlService } from '@services/database/mysql' +// ---- Hoisted mock functions ---- +const { + mockCreateConnection, + mockPing, + mockExecute, + mockBeginTransaction, + mockCommit, + mockRollback, + mockEnd +} = vi.hoisted(() => ({ + mockCreateConnection: vi.fn(), + mockPing: vi.fn(), + mockExecute: vi.fn(), + mockBeginTransaction: vi.fn(), + mockCommit: vi.fn(), + mockRollback: vi.fn(), + mockEnd: vi.fn() +})) + +// Mock mysql2/promise driver +vi.mock('mysql2/promise', () => ({ + default: { + createConnection: mockCreateConnection + } +})) + +// Mock logger +vi.mock('@services/logger', () => ({ + createLogger: () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn() + }), + trackDuration: async (fn: () => Promise) => { + const result = await fn() + return { result } + } +})) + const mockConfig = { host: 'localhost', port: 3306, @@ -14,11 +54,30 @@ const mockConfig = { database: 'testdb' } +function createMockConnection() { + return { + ping: mockPing, + execute: mockExecute, + beginTransaction: mockBeginTransaction, + commit: mockCommit, + rollback: mockRollback, + end: mockEnd + } +} + describe('MySqlService Unit Tests', () => { let service: MySqlService beforeEach(() => { + vi.clearAllMocks() service = new MySqlService(mockConfig) + mockCreateConnection.mockResolvedValue(createMockConnection()) + mockPing.mockResolvedValue(undefined) + mockExecute.mockResolvedValue([[], []]) + mockBeginTransaction.mockResolvedValue(undefined) + mockCommit.mockResolvedValue(undefined) + mockRollback.mockResolvedValue(undefined) + mockEnd.mockResolvedValue(undefined) }) describe('constructor', () => { @@ -35,15 +94,33 @@ describe('MySqlService Unit Tests', () => { }) describe('connect', () => { - it('should throw error with invalid credentials', async () => { - // This tests error handling without needing a real server - const invalidConfig = { - ...mockConfig, - host: 'invalid-host-that-does-not-exist' - } - const invalidService = new MySqlService(invalidConfig) + it('should throw error when connection fails', async () => { + mockCreateConnection.mockRejectedValue(new Error('connect ECONNREFUSED')) + await expect(service.connect()).rejects.toThrow('Failed to connect to MySQL') + }) - await expect(invalidService.connect()).rejects.toThrow('Failed to connect to MySQL') + it('should establish connection and ping server', async () => { + await service.connect() + expect(mockCreateConnection).toHaveBeenCalledWith({ + host: 'localhost', + port: 3306, + user: 'test', + password: 'test', + database: 'testdb' + }) + expect(mockPing).toHaveBeenCalled() + expect(service.isConnected()).toBe(true) + }) + + it('should throw when already connected', async () => { + await service.connect() + await expect(service.connect()).rejects.toThrow('Already connected to MySQL') + }) + + it('should throw when ping fails after connection created', async () => { + mockPing.mockRejectedValue(new Error('ping failed')) + await expect(service.connect()).rejects.toThrow('Failed to connect to MySQL') + // Note: source sets connection before ping, so it remains non-null after ping failure }) }) @@ -51,6 +128,51 @@ describe('MySqlService Unit Tests', () => { it('should throw error when not connected', async () => { await expect(service.query('SELECT 1')).rejects.toThrow('Not connected to MySQL') }) + + it('should execute SELECT and return rows with columns', async () => { + await service.connect() + mockExecute.mockResolvedValue([ + [{ id: 1, name: 'test' }, { id: 2, name: 'foo' }], + [{ name: 'id' }, { name: 'name' }] + ]) + + const result = await service.query('SELECT id, name FROM users') + + expect(mockExecute).toHaveBeenCalledWith('SELECT id, name FROM users', undefined) + expect(result.rows).toEqual([ + { id: 1, name: 'test' }, + { id: 2, name: 'foo' } + ]) + expect(result.columns).toEqual(['id', 'name']) + expect(result.rowCount).toBe(2) + }) + + it('should execute INSERT/UPDATE and return affected rows', async () => { + await service.connect() + mockExecute.mockResolvedValue([{ affectedRows: 3, changedRows: 2 }, []]) + + const result = await service.query('UPDATE users SET active = ?', [true]) + + expect(mockExecute).toHaveBeenCalledWith('UPDATE users SET active = ?', [true]) + expect(result.rows).toEqual([]) + expect(result.rowCount).toBe(3) + }) + + it('should use changedRows when affectedRows is zero', async () => { + await service.connect() + mockExecute.mockResolvedValue([{ affectedRows: 0, changedRows: 5 }, []]) + + const result = await service.query('UPDATE users SET x = 1') + + expect(result.rowCount).toBe(5) + }) + + it('should wrap query errors with context', async () => { + await service.connect() + mockExecute.mockRejectedValue(new Error('syntax error')) + + await expect(service.query('INVALID SQL')).rejects.toThrow('MySQL query failed') + }) }) describe('transaction', () => { @@ -59,11 +181,56 @@ describe('MySqlService Unit Tests', () => { 'Not connected to MySQL' ) }) + + it('should execute all queries and commit', async () => { + await service.connect() + + await service.transaction([ + { sql: 'INSERT INTO t VALUES (?)', params: [1] }, + { sql: 'UPDATE t SET x = ?' } + ]) + + expect(mockBeginTransaction).toHaveBeenCalled() + expect(mockExecute).toHaveBeenCalledTimes(2) + expect(mockExecute).toHaveBeenNthCalledWith(1, 'INSERT INTO t VALUES (?)', [1]) + expect(mockExecute).toHaveBeenNthCalledWith(2, 'UPDATE t SET x = ?', undefined) + expect(mockCommit).toHaveBeenCalled() + expect(mockRollback).not.toHaveBeenCalled() + }) + + it('should rollback on query failure', async () => { + await service.connect() + mockExecute.mockRejectedValueOnce(new Error('constraint violation')) + + await expect( + service.transaction([{ sql: 'INSERT INTO t VALUES (?)', params: [1] }]) + ).rejects.toThrow('MySQL transaction failed') + + expect(mockRollback).toHaveBeenCalled() + expect(mockCommit).not.toHaveBeenCalled() + }) }) describe('disconnect', () => { it('should resolve when not connected', async () => { await expect(service.disconnect()).resolves.not.toThrow() }) + + it('should end connection and reset state', async () => { + await service.connect() + expect(service.isConnected()).toBe(true) + + await service.disconnect() + + expect(mockEnd).toHaveBeenCalled() + expect(service.isConnected()).toBe(false) + }) + + it('should wrap disconnect errors', async () => { + await service.connect() + mockEnd.mockRejectedValue(new Error('connection lost')) + + await expect(service.disconnect()).rejects.toThrow('Failed to disconnect from MySQL') + }) }) }) diff --git a/tests/unit/postgresql.test.ts b/tests/unit/postgresql.test.ts index 647a9e7..78ec25e 100644 --- a/tests/unit/postgresql.test.ts +++ b/tests/unit/postgresql.test.ts @@ -1,11 +1,47 @@ /** * Unit tests for PostgreSqlService - * These tests do not require a PostgreSQL instance + * Covers both unconnected state and connected-path operations using mocked pg driver. + * Also tests the prepareSql pure function (no mocks needed for those). */ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { PostgreSqlService, prepareSql } from '@main/services/database/postgresql' +// ---- Hoisted mock functions ---- +const { mockPgPool, mockPgClient } = vi.hoisted(() => { + const client = { + query: vi.fn(), + release: vi.fn() + } + const pool = { + connect: vi.fn(() => client), + query: vi.fn(), + end: vi.fn() + } + return { mockPgPool: pool, mockPgClient: client } +}) + +// Mock pg driver (must use regular function because source uses `new Pool(...)`) +vi.mock('pg', () => ({ + Pool: vi.fn(function () { + return mockPgPool + }) +})) + +// Mock logger +vi.mock('@main/services/logger', () => ({ + createLogger: () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn() + }), + trackDuration: async (fn: () => Promise) => { + const result = await fn() + return { result } + } +})) + const mockConfig = { host: 'localhost', port: 5432, @@ -18,7 +54,13 @@ describe('PostgreSqlService Unit Tests', () => { let service: PostgreSqlService beforeEach(() => { + vi.clearAllMocks() service = new PostgreSqlService(mockConfig) + mockPgPool.connect.mockResolvedValue(mockPgClient) + mockPgPool.query.mockResolvedValue({ rows: [], fields: [], rowCount: 0 }) + mockPgPool.end.mockResolvedValue(undefined) + mockPgClient.query.mockResolvedValue({ rows: [] }) + mockPgClient.release.mockReturnValue(undefined) }) describe('constructor', () => { @@ -40,10 +82,79 @@ describe('PostgreSqlService Unit Tests', () => { }) }) + describe('connect', () => { + it('should throw error when pool creation fails', async () => { + mockPgPool.connect.mockRejectedValue(new Error('connection refused')) + const svc = new PostgreSqlService(mockConfig) + await expect(svc.connect()).rejects.toThrow('Failed to connect to PostgreSQL') + }) + + it('should establish connection and release test client', async () => { + await service.connect() + expect(mockPgPool.connect).toHaveBeenCalled() + expect(mockPgClient.release).toHaveBeenCalled() + expect(service.isConnected()).toBe(true) + }) + + it('should throw when already connected', async () => { + await service.connect() + await expect(service.connect()).rejects.toThrow('Already connected to PostgreSQL') + }) + + it('should reset pool to null on connection failure', async () => { + mockPgPool.connect.mockRejectedValue(new Error('timeout')) + await expect(service.connect()).rejects.toThrow('Failed to connect to PostgreSQL') + expect(service.isConnected()).toBe(false) + }) + }) + describe('query', () => { it('should throw error when not connected', async () => { await expect(service.query('SELECT 1')).rejects.toThrow('Not connected to PostgreSQL') }) + + it('should execute query and return rows with columns', async () => { + await service.connect() + mockPgPool.query.mockResolvedValue({ + rows: [{ ID: 1, Name: 'test' }], + fields: [{ name: 'ID' }, { name: 'Name' }], + rowCount: 1 + }) + + const result = await service.query('SELECT ID, Name FROM Users') + + expect(result.rows).toEqual([{ ID: 1, Name: 'test' }]) + expect(result.columns).toEqual(['ID', 'Name']) + expect(result.rowCount).toBe(1) + }) + + it('should pass params through to pool.query', async () => { + await service.connect() + mockPgPool.query.mockResolvedValue({ rows: [], fields: [], rowCount: 0 }) + + await service.query('SELECT * FROM Users WHERE ID = $1', [42]) + + expect(mockPgPool.query).toHaveBeenCalledWith(expect.any(String), [42]) + }) + + it('should fallback to rows.length when rowCount is null', async () => { + await service.connect() + mockPgPool.query.mockResolvedValue({ + rows: [{ ID: 1 }, { ID: 2 }], + fields: [{ name: 'ID' }], + rowCount: null + }) + + const result = await service.query('SELECT ID FROM Users') + expect(result.rowCount).toBe(2) + }) + + it('should wrap query errors with context', async () => { + await service.connect() + mockPgPool.query.mockRejectedValue(new Error('syntax error')) + + await expect(service.query('INVALID SQL')).rejects.toThrow('PostgreSQL query failed') + }) }) describe('transaction', () => { @@ -52,13 +163,36 @@ describe('PostgreSqlService Unit Tests', () => { 'Not connected to PostgreSQL' ) }) - }) - describe('connect', () => { - it('should throw error with invalid host', async () => { - const invalidConfig = { ...mockConfig, host: 'invalid-host-that-does-not-exist' } - const invalidService = new PostgreSqlService(invalidConfig) - await expect(invalidService.connect()).rejects.toThrow('Failed to connect to PostgreSQL') + it('should execute all queries within BEGIN/COMMIT and release client', async () => { + await service.connect() + mockPgClient.query.mockResolvedValue({ rows: [] }) + + await service.transaction([ + { sql: 'SELECT 1', params: [1] }, + { sql: 'SELECT 2' } + ]) + + // BEGIN + 2 queries + COMMIT + expect(mockPgClient.query).toHaveBeenCalledTimes(4) + expect(mockPgClient.query).toHaveBeenNthCalledWith(1, 'BEGIN') + expect(mockPgClient.query).toHaveBeenNthCalledWith(4, 'COMMIT') + expect(mockPgClient.release).toHaveBeenCalled() + }) + + it('should rollback and release client on query failure', async () => { + await service.connect() + mockPgClient.query + .mockResolvedValueOnce({ rows: [] }) // BEGIN + .mockRejectedValueOnce(new Error('constraint violation')) // query fails + .mockResolvedValueOnce({ rows: [] }) // ROLLBACK + + await expect( + service.transaction([{ sql: 'SELECT 1', params: [1] }]) + ).rejects.toThrow('PostgreSQL transaction failed') + + expect(mockPgClient.query).toHaveBeenCalledWith('ROLLBACK') + expect(mockPgClient.release).toHaveBeenCalled() }) }) @@ -66,6 +200,23 @@ describe('PostgreSqlService Unit Tests', () => { it('should resolve when not connected', async () => { await expect(service.disconnect()).resolves.not.toThrow() }) + + it('should end pool and reset state', async () => { + await service.connect() + expect(service.isConnected()).toBe(true) + + await service.disconnect() + + expect(mockPgPool.end).toHaveBeenCalled() + expect(service.isConnected()).toBe(false) + }) + + it('should wrap disconnect errors', async () => { + await service.connect() + mockPgPool.end.mockRejectedValue(new Error('pool end failed')) + + await expect(service.disconnect()).rejects.toThrow('Failed to disconnect from PostgreSQL') + }) }) }) @@ -83,7 +234,8 @@ describe('prepareSql', () => { }) it('should quote column names in INSERT', () => { - const sql = 'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)' + const sql = + 'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)' const result = prepareSql(sql) expect(result).toBe( 'INSERT INTO "dbo"."BIPUsers" ("UserName", "Password", "UserType") VALUES ($1, $2, $3)' @@ -151,7 +303,9 @@ describe('prepareSql', () => { it('should quote underscore-containing column names', () => { const sql = 'SELECT ERP_URL, ERP_Username, ERP_Password FROM "dbo"."BIPUsers"' const result = prepareSql(sql) - expect(result).toBe('SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"') + expect(result).toBe( + 'SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"' + ) }) it('should handle ON CONFLICT DO UPDATE SET with EXCLUDED', () => { diff --git a/tests/unit/sql-server.test.ts b/tests/unit/sql-server.test.ts index 692db4e..5f00ccc 100644 --- a/tests/unit/sql-server.test.ts +++ b/tests/unit/sql-server.test.ts @@ -1,11 +1,82 @@ /** * Unit tests for SqlServerService - * These tests do not require a SQL Server instance + * Covers both unconnected state and connected-path operations using mocked mssql driver. */ -import { describe, it, expect, beforeEach } from 'vitest' +import { describe, it, expect, beforeEach, vi } from 'vitest' import { SqlServerService } from '@main/services/database/sql-server' +// ---- Hoisted mock functions ---- +const { + mockPoolConnect, + mockPoolClose, + mockRequestInput, + mockRequestQuery, + mockTransactionBegin, + mockTransactionCommit, + mockTransactionRollback, + mockPool, + mockRequest, + mockTransaction +} = vi.hoisted(() => { + const request = { + input: vi.fn(), + query: vi.fn() + } + const transaction = { + begin: vi.fn(), + commit: vi.fn(), + rollback: vi.fn() + } + const pool = { + connect: vi.fn(), + request: vi.fn(() => request), + close: vi.fn(), + connected: true + } + return { + mockPoolConnect: pool.connect, + mockPoolClose: pool.close, + mockRequestInput: request.input, + mockRequestQuery: request.query, + mockTransactionBegin: transaction.begin, + mockTransactionCommit: transaction.commit, + mockTransactionRollback: transaction.rollback, + mockPool: pool, + mockRequest: request, + mockTransaction: transaction + } +}) + +// Mock mssql driver (must use regular functions because source uses `new`) +vi.mock('mssql', () => ({ + default: { + ConnectionPool: vi.fn(function () { + return mockPool + }), + Transaction: vi.fn(function () { + return mockTransaction + }), + Request: vi.fn(function () { + return mockRequest + }) + } +})) + +// Mock logger +vi.mock('@main/services/logger', () => ({ + createLogger: () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn() + }), + trackDuration: async (fn: () => Promise) => { + const result = await fn() + return { result } + } +})) + const mockConfig = { server: 'localhost', port: 1433, @@ -22,6 +93,18 @@ describe('SqlServerService Unit Tests', () => { let service: SqlServerService beforeEach(() => { + vi.clearAllMocks() + mockPool.connected = true + mockPoolConnect.mockResolvedValue(undefined) + mockPoolClose.mockResolvedValue(undefined) + mockRequestInput.mockReturnThis() + mockRequestQuery.mockResolvedValue({ + recordset: [], + rowsAffected: [0] + }) + mockTransactionBegin.mockResolvedValue(undefined) + mockTransactionCommit.mockResolvedValue(undefined) + mockTransactionRollback.mockResolvedValue(undefined) service = new SqlServerService(mockConfig) }) @@ -39,15 +122,22 @@ describe('SqlServerService Unit Tests', () => { }) describe('connect', () => { - it('should throw error with invalid credentials', async () => { - // This tests error handling without needing a real server - const invalidConfig = { - ...mockConfig, - server: 'invalid-host-that-does-not-exist' - } - const invalidService = new SqlServerService(invalidConfig) + it('should throw error when connection fails', async () => { + mockPool.connected = false + mockPoolConnect.mockRejectedValue(new Error('connection refused')) + const svc = new SqlServerService(mockConfig) + await expect(svc.connect()).rejects.toThrow('Failed to connect to SQL Server') + }) - await expect(invalidService.connect()).rejects.toThrow('Failed to connect to SQL Server') + it('should establish connection via pool', async () => { + await service.connect() + expect(mockPoolConnect).toHaveBeenCalled() + expect(service.isConnected()).toBe(true) + }) + + it('should throw when already connected', async () => { + await service.connect() + await expect(service.connect()).rejects.toThrow('Already connected to SQL Server') }) }) @@ -55,6 +145,87 @@ describe('SqlServerService Unit Tests', () => { it('should throw error when not connected', async () => { await expect(service.query('SELECT 1')).rejects.toThrow('Not connected to SQL Server') }) + + it('should execute SELECT and return rows with columns', async () => { + await service.connect() + mockRequestQuery.mockResolvedValue({ + recordset: [{ ID: 1, Name: 'test' }, { ID: 2, Name: 'foo' }], + rowsAffected: [2] + }) + + const result = await service.query('SELECT ID, Name FROM Users') + + expect(mockRequestQuery).toHaveBeenCalledWith('SELECT ID, Name FROM Users') + expect(result.rows).toEqual([ + { ID: 1, Name: 'test' }, + { ID: 2, Name: 'foo' } + ]) + expect(result.columns).toEqual(['ID', 'Name']) + expect(result.rowCount).toBe(2) + }) + + it('should convert array params to @p0, @p1, ... format', async () => { + await service.connect() + mockRequestQuery.mockResolvedValue({ recordset: [], rowsAffected: [0] }) + + await service.query('SELECT * FROM Users WHERE ID = @p0 AND Name = @p1', [42, 'test']) + + expect(mockRequestInput).toHaveBeenCalledWith('p0', 42) + expect(mockRequestInput).toHaveBeenCalledWith('p1', 'test') + }) + + it('should handle INSERT/UPDATE with rowsAffected', async () => { + await service.connect() + mockRequestQuery.mockResolvedValue({ + recordset: undefined, + rowsAffected: [5] + }) + + const result = await service.query('DELETE FROM Users WHERE Active = 0') + + expect(result.rows).toEqual([]) + expect(result.columns).toEqual([]) + expect(result.rowCount).toBe(5) + }) + + it('should fallback to rows.length when rowsAffected is missing', async () => { + await service.connect() + mockRequestQuery.mockResolvedValue({ + recordset: [{ ID: 1 }, { ID: 2 }], + rowsAffected: undefined + }) + + const result = await service.query('SELECT ID FROM Users') + expect(result.rowCount).toBe(2) + }) + + it('should wrap query errors with context', async () => { + await service.connect() + mockRequestQuery.mockRejectedValue(new Error('syntax error')) + + await expect(service.query('INVALID SQL')).rejects.toThrow('SQL Server query failed') + }) + }) + + describe('queryWithParams', () => { + it('should throw error when not connected', async () => { + await expect( + service.queryWithParams('SELECT @p0', { p0: { value: 1 } }) + ).rejects.toThrow('Not connected to SQL Server') + }) + + it('should add typed params via request.input', async () => { + await service.connect() + mockRequestQuery.mockResolvedValue({ recordset: [{ ID: 1 }], rowsAffected: [1] }) + + await service.queryWithParams('SELECT @id', { + id: { value: 42 }, + name: { value: 'test', type: 'NVarChar' as any } + }) + + expect(mockRequestInput).toHaveBeenCalledWith('id', 42) + expect(mockRequestInput).toHaveBeenCalledWith('name', 'NVarChar', 'test') + }) }) describe('transaction', () => { @@ -63,11 +234,56 @@ describe('SqlServerService Unit Tests', () => { 'Not connected to SQL Server' ) }) + + it('should execute all queries and commit', async () => { + await service.connect() + mockRequestQuery.mockResolvedValue({ recordset: [], rowsAffected: [1] }) + + await service.transaction([ + { sql: 'INSERT INTO t VALUES (@p0)', params: [1] }, + { sql: 'UPDATE t SET x = 1' } + ]) + + expect(mockTransactionBegin).toHaveBeenCalled() + expect(mockRequestQuery).toHaveBeenCalledTimes(2) + expect(mockRequestInput).toHaveBeenCalledWith('p0', 1) + expect(mockTransactionCommit).toHaveBeenCalled() + expect(mockTransactionRollback).not.toHaveBeenCalled() + }) + + it('should rollback on query failure', async () => { + await service.connect() + mockRequestQuery.mockRejectedValue(new Error('constraint violation')) + + await expect( + service.transaction([{ sql: 'INSERT INTO t VALUES (@p0)', params: [1] }]) + ).rejects.toThrow('SQL Server transaction failed') + + expect(mockTransactionRollback).toHaveBeenCalled() + expect(mockTransactionCommit).not.toHaveBeenCalled() + }) }) describe('disconnect', () => { it('should resolve when not connected', async () => { await expect(service.disconnect()).resolves.not.toThrow() }) + + it('should close pool and reset state', async () => { + await service.connect() + expect(service.isConnected()).toBe(true) + + await service.disconnect() + + expect(mockPoolClose).toHaveBeenCalled() + expect(service.isConnected()).toBe(false) + }) + + it('should wrap disconnect errors', async () => { + await service.connect() + mockPoolClose.mockRejectedValue(new Error('pool close failed')) + + await expect(service.disconnect()).rejects.toThrow('Failed to disconnect from SQL Server') + }) }) }) diff --git a/vitest.config.ts b/vitest.config.ts index 910f3cc..f8564fa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,13 +6,13 @@ export default defineConfig({ globals: true, environment: 'node', include: ['tests/**/*.{test,spec}.{ts,tsx}'], - exclude: ['node_modules', 'dist', 'out', 'tests/e2e'], + exclude: ['node_modules', 'dist', 'out', 'tests/e2e', 'tests/integration'], setupFiles: ['tests/setup.ts'], env: { NODE_ENV: 'test' }, - // 性能优化配置 - isolate: false, // 禁用隔离(提升 30-50% 速度) + // CI 环境启用隔离以捕获跨文件状态污染;本地开发禁用以提升速度 + isolate: !!process.env.CI, pool: 'threads', // 使用线程池 maxWorkers: 4, bail: process.env.CI ? 1 : undefined,