Skip to content

Commit 8cda7b7

Browse files
committed
chore: update dependencies and add vitest configuration for testing
1 parent b55c08a commit 8cda7b7

File tree

9 files changed

+1812
-24
lines changed

9 files changed

+1812
-24
lines changed

package-lock.json

Lines changed: 1353 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"dev": "ts-node src/index.ts",
1414
"lint": "eslint 'src/**/*.ts'",
1515
"format": "prettier --write 'src/**/*.ts'",
16-
"test": "echo \"No tests defined\" && exit 0",
16+
"test": "vitest run",
17+
"test:watch": "vitest",
18+
"test:coverage": "vitest run --coverage",
1719
"prepublishOnly": "npm run build"
1820
},
1921
"keywords": [
@@ -42,7 +44,8 @@
4244
"devDependencies": {
4345
"@types/node": "^18.16.1",
4446
"ts-node": "^10.9.1",
45-
"typescript": "^5.0.4"
47+
"typescript": "^5.0.4",
48+
"vitest": "^3.1.1"
4649
},
4750
"files": [
4851
"dist",

src/tools/info.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,15 @@
11
import { z } from "zod";
2-
import { createPool, Pool, PoolOptions } from "mysql2/promise";
2+
import { Pool } from "mysql2/promise";
33
import { Environment, InfoParams, DatabaseInfo } from "../types/index.js";
44
import { config } from "dotenv";
5+
import { pools } from "../db/pools.js";
56

67
config();
78

89
export const infoToolName = "info";
910
export const infoToolDescription = "Get information about MySQL databases";
1011
export const InfoToolSchema = InfoParams;
1112

12-
// Connection pools for each environment
13-
const pools = new Map<Environment, Pool>();
14-
15-
// Initialize pools
16-
Object.values(Environment.enum).forEach((env) => {
17-
const config: PoolOptions = {
18-
host: process.env[`${env.toUpperCase()}_DB_HOST`],
19-
user: process.env[`${env.toUpperCase()}_DB_USER`],
20-
password: process.env[`${env.toUpperCase()}_DB_PASS`],
21-
database: process.env[`${env.toUpperCase()}_DB_NAME`],
22-
ssl: process.env[`${env.toUpperCase()}_DB_SSL`] === "true" ? {} : undefined,
23-
connectionLimit: 5,
24-
};
25-
26-
if (config.host && config.user && config.password && config.database) {
27-
pools.set(env, createPool(config));
28-
}
29-
});
30-
3113
export async function runInfoTool(params: z.infer<typeof InfoToolSchema>): Promise<{ content: { type: string; text: string }[] }> {
3214
const { environment } = params;
3315

src/tools/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const queryToolDescription = "Execute read-only SQL queries against MySQL
1111
export const QueryToolSchema = QueryParams;
1212

1313
// Validate query is read-only
14-
function isReadOnlyQuery(sql: string): boolean {
14+
export function isReadOnlyQuery(sql: string): boolean {
1515
const upperSql = sql.trim().toUpperCase();
1616
return upperSql.startsWith("SELECT") || upperSql.startsWith("SHOW") ||
1717
upperSql.startsWith("DESCRIBE") || upperSql.startsWith("DESC");

tests/tools/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# MySQL MCP Server Test Fixes
2+
3+
This document describes the issues that were fixed in the test suite of the MySQL MCP Server project.
4+
5+
## Issues Fixed
6+
7+
1. **Exported the `isReadOnlyQuery` function in query.ts**:
8+
- The function was being imported in the tests but wasn't exported from the source file
9+
- Fixed by adding the `export` keyword to the function declaration
10+
11+
2. **Fixed the Mocking Approach**:
12+
- Previous test implementation was using `vi.mocked()` which was causing type errors
13+
- The mocks were not properly set up for the connection and query methods
14+
- We simplified the mock implementation with pure JavaScript functions instead of relying on jest/vitest mocking APIs
15+
16+
3. **Simplified Test State Management**:
17+
- Added global variables to track mock state (e.g., `releaseCalled`, `queryCount`)
18+
- Used these variables to verify that the expected operations were performed
19+
- Made tests more readable and less brittle
20+
21+
4. **Fixed the `info.ts` Module**:
22+
- The module was creating its own pools instead of importing from db/pools.js
23+
- Updated to import the pools from the correct location, making it consistent with other modules
24+
25+
## Improved Test Patterns
26+
27+
The tests now use a more reliable approach:
28+
29+
1. **Manual Mocking**: Instead of relying on mock functions with `mockResolvedValueOnce`, etc., we use pure JavaScript functions with conditional logic.
30+
31+
2. **Clear State Management**: Each test resets the mock state before running, preventing test interdependence.
32+
33+
3. **Fixed Environmental Setup**: Tests now properly set up the environment and clean up after themselves.
34+
35+
## Running Tests
36+
37+
Tests can be run with:
38+
39+
```bash
40+
npm test
41+
```
42+
43+
All tests now pass successfully.

tests/tools/environments.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { runEnvironmentsTool } from '../../src/tools/environments.js';
3+
import { Environment } from '../../src/types/index.js';
4+
5+
describe('environments tool', () => {
6+
const originalEnv = process.env;
7+
8+
beforeEach(() => {
9+
vi.resetModules();
10+
process.env = { ...originalEnv };
11+
12+
// Clean any environment-specific variables
13+
Object.keys(process.env).forEach(key => {
14+
if (key.includes('_DB_')) {
15+
delete process.env[key];
16+
}
17+
});
18+
19+
// Mock stderr to suppress debug output during tests
20+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
21+
});
22+
23+
afterEach(() => {
24+
process.env = originalEnv;
25+
vi.restoreAllMocks();
26+
});
27+
28+
it('should return empty environments when no DB configs are set', async () => {
29+
const result = await runEnvironmentsTool();
30+
expect(result).toHaveProperty('content');
31+
expect(result.content).toHaveLength(1);
32+
expect(result.content[0].type).toBe('text');
33+
34+
const parsedContent = JSON.parse(result.content[0].text);
35+
expect(parsedContent.environments).toEqual([]);
36+
expect(parsedContent.count).toBe(0);
37+
});
38+
39+
it('should detect local environment when LOCAL_DB_* vars are set', async () => {
40+
// Set up local environment variables
41+
process.env.LOCAL_DB_HOST = 'localhost';
42+
process.env.LOCAL_DB_USER = 'root';
43+
process.env.LOCAL_DB_NAME = 'test';
44+
45+
const result = await runEnvironmentsTool();
46+
const parsedContent = JSON.parse(result.content[0].text);
47+
48+
expect(parsedContent.environments).toContain('local');
49+
expect(parsedContent.count).toBe(1);
50+
expect(parsedContent.debug.envVars).toHaveProperty('LOCAL_DB_HOST', 'localhost');
51+
});
52+
53+
it('should detect multiple environments when multiple DB configs are set', async () => {
54+
// Set up environment variables for multiple environments
55+
process.env.LOCAL_DB_HOST = 'localhost';
56+
process.env.LOCAL_DB_USER = 'root';
57+
process.env.LOCAL_DB_NAME = 'test';
58+
59+
process.env.DEVELOPMENT_DB_HOST = 'dev.example.com';
60+
process.env.DEVELOPMENT_DB_USER = 'dev_user';
61+
process.env.DEVELOPMENT_DB_NAME = 'dev_db';
62+
63+
const result = await runEnvironmentsTool();
64+
const parsedContent = JSON.parse(result.content[0].text);
65+
66+
expect(parsedContent.environments).toContain('local');
67+
expect(parsedContent.environments).toContain('development');
68+
expect(parsedContent.environments).not.toContain('staging');
69+
expect(parsedContent.environments).not.toContain('production');
70+
expect(parsedContent.count).toBe(2);
71+
});
72+
73+
it('should handle errors gracefully', async () => {
74+
// Mock Environment.enum to throw an error
75+
vi.spyOn(Environment, 'enum', 'get').mockImplementation(() => {
76+
throw new Error('Test error');
77+
});
78+
79+
await expect(runEnvironmentsTool()).rejects.toThrow('Test error');
80+
});
81+
});

tests/tools/info.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { runInfoTool } from '../../src/tools/info.js';
3+
import { RowDataPacket, FieldPacket } from 'mysql2/promise';
4+
5+
// Track mock state
6+
let releaseCalled = false;
7+
let queryCount = 0;
8+
let mockQueryShouldFail = false;
9+
10+
// Mock responses for each query call
11+
const mockResponses = [
12+
// Version query
13+
[[{ version: '8.0.32' } as RowDataPacket], [] as FieldPacket[]],
14+
15+
// Status query
16+
[[
17+
{ Variable_name: 'Uptime', Value: '12345' } as RowDataPacket,
18+
{ Variable_name: 'Threads_connected', Value: '10' } as RowDataPacket
19+
], [] as FieldPacket[]],
20+
21+
// Variables query
22+
[[
23+
{ Variable_name: 'max_connections', Value: '151' } as RowDataPacket,
24+
{ Variable_name: 'version', Value: '8.0.32' } as RowDataPacket
25+
], [] as FieldPacket[]],
26+
27+
// Process list query
28+
[[
29+
{ Id: 1, User: 'root', Host: 'localhost', Command: 'Query', Time: 0 } as RowDataPacket
30+
], [] as FieldPacket[]],
31+
32+
// Databases query
33+
[[
34+
{ Database: 'mysql' } as RowDataPacket,
35+
{ Database: 'information_schema' } as RowDataPacket,
36+
{ Database: 'test' } as RowDataPacket
37+
], [] as FieldPacket[]]
38+
];
39+
40+
// Mock the DB pools module
41+
vi.mock('../../src/db/pools.js', () => {
42+
const mockPools = new Map();
43+
44+
// Mock connection with simple functions
45+
const mockConnection = {
46+
query: (sql) => {
47+
if (mockQueryShouldFail) {
48+
return Promise.reject(new Error('Connection error'));
49+
}
50+
51+
// Return the appropriate response based on the query count
52+
const response = mockResponses[queryCount];
53+
queryCount++;
54+
return Promise.resolve(response);
55+
},
56+
release: () => {
57+
releaseCalled = true;
58+
return Promise.resolve();
59+
}
60+
};
61+
62+
// Mock pool
63+
const mockPool = {
64+
getConnection: () => Promise.resolve(mockConnection),
65+
end: () => Promise.resolve()
66+
};
67+
68+
// Add mock pool for local environment
69+
mockPools.set('local', mockPool);
70+
71+
return {
72+
pools: mockPools
73+
};
74+
});
75+
76+
describe('info tool', () => {
77+
const originalEnv = process.env;
78+
79+
beforeEach(() => {
80+
vi.resetModules();
81+
process.env = { ...originalEnv };
82+
83+
// Reset mock state
84+
releaseCalled = false;
85+
queryCount = 0;
86+
mockQueryShouldFail = false;
87+
88+
// Mock stderr to suppress debug output during tests
89+
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
90+
});
91+
92+
afterEach(() => {
93+
process.env = originalEnv;
94+
vi.restoreAllMocks();
95+
});
96+
97+
it('should retrieve and format database information', async () => {
98+
// Run the info tool
99+
const result = await runInfoTool({ environment: 'local' });
100+
101+
// Verify result structure
102+
expect(result).toHaveProperty('content');
103+
expect(result.content).toHaveLength(1);
104+
expect(result.content[0].type).toBe('text');
105+
106+
// Parse and verify content
107+
const parsedContent = JSON.parse(result.content[0].text);
108+
expect(parsedContent).toHaveProperty('version', '8.0.32');
109+
expect(parsedContent).toHaveProperty('status', 'Up 12345 seconds');
110+
expect(parsedContent.variables).toHaveProperty('max_connections', '151');
111+
expect(parsedContent.processlist).toHaveLength(1);
112+
expect(parsedContent.databases).toContain('mysql');
113+
expect(parsedContent.databases).toContain('test');
114+
115+
// Verify connection was released
116+
expect(releaseCalled).toBe(true);
117+
118+
// Verify all queries were called
119+
expect(queryCount).toBe(5);
120+
});
121+
122+
it('should throw error when environment not found', async () => {
123+
await expect(runInfoTool({ environment: 'production' }))
124+
.rejects.toThrow('No connection pool available for environment: production');
125+
});
126+
127+
it('should handle database query errors', async () => {
128+
mockQueryShouldFail = true;
129+
130+
await expect(runInfoTool({ environment: 'local' }))
131+
.rejects.toThrow('Failed to get database info: Connection error');
132+
133+
// Verify connection was released even after error
134+
expect(releaseCalled).toBe(true);
135+
});
136+
});

0 commit comments

Comments
 (0)