Skip to main content
Glama
IBM

IBM i MCP Server

Official
by IBM
logic.test.ts9.45 kB
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { ExecuteSqlInputSchema, executeSqlLogic, ExecuteSqlResponseSchema, } from "../../../../src/ibmi-mcp-server/tools/executeSql/logic.js"; import { McpError } from "../../../../src/types-global/errors.js"; import { IBMiConnectionPool } from "../../../../src/ibmi-mcp-server/services/connectionPool.js"; // Mock the IBMiConnectionPool vi.mock("../../../../src/ibmi-mcp-server/services/connectionPool.js"); describe("executeSqlLogic", () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("Input Validation", () => { it("should validate valid SQL input", () => { const validInput = { sql: "SELECT * FROM SYSIBM.SYSDUMMY1" }; const validation = ExecuteSqlInputSchema.safeParse(validInput); expect(validation.success).toBe(true); }); it("should reject empty SQL input", () => { const invalidInput = { sql: "" }; const validation = ExecuteSqlInputSchema.safeParse(invalidInput); expect(validation.success).toBe(false); expect(validation.error?.issues[0]?.message).toContain("cannot be empty"); }); it("should reject SQL input that's too long", () => { const invalidInput = { sql: "SELECT * ".repeat(2000) }; // Creates a very long string const validation = ExecuteSqlInputSchema.safeParse(invalidInput); expect(validation.success).toBe(false); expect(validation.error?.issues[0]?.message).toContain("cannot exceed"); }); }); describe("Security Validation", () => { it("should reject SQL with restricted keywords at start", async () => { const restrictedQueries = [ "DROP TABLE test", "DELETE FROM users", "TRUNCATE TABLE logs", "ALTER TABLE users ADD COLUMN", "CREATE TABLE test", "INSERT INTO users VALUES", "UPDATE users SET name", ]; for (const sql of restrictedQueries) { const input = { sql }; await expect(executeSqlLogic(input)).rejects.toThrow(McpError); await expect(executeSqlLogic(input)).rejects.toThrow( "restricted keyword", ); } }); it("should reject SQL with dangerous patterns", async () => { const dangerousQueries = [ "SELECT * FROM users; DROP TABLE users;", "SELECT * FROM users UNION SELECT password FROM admin INTO temp", "EXEC('DROP TABLE users')", "CALL destructive_procedure()", ]; for (const sql of dangerousQueries) { const input = { sql }; await expect(executeSqlLogic(input)).rejects.toThrow(McpError); await expect(executeSqlLogic(input)).rejects.toThrow( "dangerous patterns", ); } }); it("should allow safe SELECT queries", async () => { const safeQuery = "SELECT COUNT(*) FROM SYSIBM.SYSDUMMY1"; const mockResult = { data: [{ "00001": 1 }], metadata: {}, success: true, is_done: true, has_results: true, update_count: 0, id: "test-query-1", sql_rc: 0, sql_state: "00000", execution_time: 100, }; vi.mocked(IBMiConnectionPool.executeQuery).mockResolvedValue(mockResult); const input = { sql: safeQuery }; const result = await executeSqlLogic(input); expect(result).toBeDefined(); expect(result.data).toEqual([{ "00001": 1 }]); expect(result.rowCount).toBe(1); }); }); describe("SQL Execution", () => { it("should execute valid SQL and return results", async () => { const mockResult = { data: [ { ID: 1, NAME: "Test User 1" }, { ID: 2, NAME: "Test User 2" }, ], metadata: { column_count: 2, columns: [ { display_size: 10, label: "ID", name: "ID", type: "INTEGER", precision: 10, scale: 0, autoIncrement: false, nullable: 0, readOnly: false, writeable: true, }, { display_size: 50, label: "NAME", name: "NAME", type: "VARCHAR", precision: 50, scale: 0, autoIncrement: false, nullable: 1, readOnly: false, writeable: true, }, ], }, success: true, is_done: true, has_results: true, update_count: 0, id: "test-query-2", sql_rc: 0, sql_state: "00000", execution_time: 150, }; vi.mocked(IBMiConnectionPool.executeQuery).mockResolvedValue(mockResult); const input = { sql: "SELECT ID, NAME FROM TEST_TABLE" }; const result = await executeSqlLogic(input); // Validate response schema const validation = ExecuteSqlResponseSchema.safeParse(result); expect(validation.success).toBe(true); // Validate response content expect(result.data).toEqual(mockResult.data); expect(result.rowCount).toBe(2); expect(result.executionTimeMs).toBeTypeOf("number"); expect(result.metadata).toEqual(mockResult.metadata); }); it("should handle empty result sets", async () => { const mockResult = { data: [], metadata: {}, success: true, is_done: true, has_results: false, update_count: 0, id: "test-query-3", sql_rc: 0, sql_state: "00000", execution_time: 50, }; vi.mocked(IBMiConnectionPool.executeQuery).mockResolvedValue(mockResult); const input = { sql: "SELECT * FROM EMPTY_TABLE WHERE 1=0" }; const result = await executeSqlLogic(input); expect(result.data).toEqual([]); expect(result.rowCount).toBe(0); expect(result.executionTimeMs).toBeTypeOf("number"); }); it("should handle database errors", async () => { const mockResult = { data: [], metadata: {}, success: false, is_done: true, has_results: false, update_count: 0, id: "test-query-4", sql_rc: -204, // Table not found sql_state: "42704", execution_time: 25, }; vi.mocked(IBMiConnectionPool.executeQuery).mockResolvedValue(mockResult); const input = { sql: "SELECT * FROM NONEXISTENT_TABLE" }; await expect(executeSqlLogic(input)).rejects.toThrow(McpError); await expect(executeSqlLogic(input)).rejects.toThrow("execution failed"); }); it("should handle connection pool errors", async () => { const connectionError = new Error("Connection pool unavailable"); vi.mocked(IBMiConnectionPool.executeQuery).mockRejectedValue( connectionError, ); const input = { sql: "SELECT * FROM SYSIBM.SYSDUMMY1" }; await expect(executeSqlLogic(input)).rejects.toThrow(McpError); await expect(executeSqlLogic(input)).rejects.toThrow( "Connection pool unavailable", ); }); }); describe("Response Validation", () => { it("should return response matching the schema", async () => { const mockResult = { data: [{ TEST_COL: "test_value" }], metadata: { column_count: 1, job: "123", }, success: true, is_done: true, has_results: true, update_count: 0, id: "test-query-5", sql_rc: 0, sql_state: "00000", execution_time: 75, }; vi.mocked(IBMiConnectionPool.executeQuery).mockResolvedValue(mockResult); const input = { sql: "SELECT 'test_value' AS TEST_COL FROM SYSIBM.SYSDUMMY1", }; const result = await executeSqlLogic(input); // Validate against Zod schema const validation = ExecuteSqlResponseSchema.safeParse(result); expect(validation.success).toBe(true); // Check all required fields are present expect(result).toHaveProperty("data"); expect(result).toHaveProperty("rowCount"); expect(result).toHaveProperty("executionTimeMs"); expect(result).toHaveProperty("metadata"); // Check types expect(Array.isArray(result.data)).toBe(true); expect(typeof result.rowCount).toBe("number"); expect(typeof result.executionTimeMs).toBe("number"); expect(typeof result.metadata).toBe("object"); }); }); describe("Performance Tracking", () => { it("should track execution time", async () => { const mockResult = { data: [{ COUNT: 1 }], metadata: {}, success: true, is_done: true, has_results: true, update_count: 0, id: "test-query-6", sql_rc: 0, sql_state: "00000", execution_time: 110, }; // Add artificial delay to test timing vi.mocked(IBMiConnectionPool.executeQuery).mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve(mockResult), 100)), ); const input = { sql: "SELECT COUNT(*) AS COUNT FROM SYSIBM.SYSDUMMY1" }; const result = await executeSqlLogic(input); expect(result.executionTimeMs).toBeGreaterThan(90); // Should be around 100ms expect(result.executionTimeMs).toBeLessThan(200); // With some tolerance }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IBM/ibmi-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server