Skip to main content
Glama
toolFactory.test.ts20.6 kB
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, } from "vitest"; import { ToolFactory } from "../../src/services/toolFactory"; import { buildSchema, type GraphQLSchema, type GraphQLField, type GraphQLObjectType, } from "graphql"; import { createSimpleSchema, createComprehensiveSchema, } from "../fixtures/testSchema"; describe("ToolFactory unit tests", () => { let toolFactory: ToolFactory; let simpleSchema: GraphQLSchema; let comprehensiveSchema: GraphQLSchema; let mockFetch: ReturnType<typeof vi.fn>; beforeAll(() => { simpleSchema = createSimpleSchema(); comprehensiveSchema = createComprehensiveSchema(); }); beforeEach(() => { mockFetch = vi.fn(); global.fetch = mockFetch; toolFactory = new ToolFactory( "http://localhost:4000/graphql", "test-token", ); }); afterAll(() => { vi.restoreAllMocks(); }); describe("constructor", () => { it("should initialize with API URL and token", () => { const factory = new ToolFactory("http://example.com", "my-token"); expect(factory).toBeInstanceOf(ToolFactory); }); it("should initialize with API URL only", () => { const factory = new ToolFactory("http://example.com"); expect(factory).toBeInstanceOf(ToolFactory); }); }); describe("createToolInfo", () => { it("should create tool info for simple query field", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); expect(toolInfo).toEqual({ name: "getUser", description: "Executes the getUser query.", inputSchema: { type: "object", properties: { id: { type: "string" }, }, required: ["id"], }, handler: expect.any(Function), }); }); it("should create tool info for simple mutation field", async () => { const mutationType = simpleSchema.getMutationType() as GraphQLObjectType; const createUserField = mutationType.getFields()["createUser"]; const toolInfo = await toolFactory.createToolInfo( "createUser", createUserField, "mutation", ); expect(toolInfo).toEqual({ name: "createUser", description: "Executes the createUser mutation.", inputSchema: { type: "object", properties: { name: { type: "string" }, }, required: ["name"], }, handler: expect.any(Function), }); }); it("should handle field without description", async () => { const customSchema = buildSchema(` type Query { testField(arg: String!): String } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const testField = queryType.getFields()["testField"]; const toolInfo = await toolFactory.createToolInfo( "testField", testField, "query", ); expect(toolInfo.description).toBe("Executes the testField query."); }); it("should handle multiple arguments", async () => { const customSchema = buildSchema(` type Query { searchUsers(name: String!, age: Int, active: Boolean): [User] } type User { id: ID! name: String! } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const searchField = queryType.getFields()["searchUsers"]; const toolInfo = await toolFactory.createToolInfo( "searchUsers", searchField, "query", ); expect(toolInfo.inputSchema).toEqual({ type: "object", properties: { name: { type: "string" }, age: { type: "number" }, active: { type: "boolean" }, }, required: ["name", "age", "active"], }); }); it("should handle array arguments", async () => { const customSchema = buildSchema(` type Query { getUsers(ids: [ID!]!): [User] } type User { id: ID! name: String! } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const getUsersField = queryType.getFields()["getUsers"]; const toolInfo = await toolFactory.createToolInfo( "getUsers", getUsersField, "query", ); expect(toolInfo.inputSchema.properties.ids).toEqual({ type: "array", items: { type: "string" }, }); }); it("should handle no arguments", async () => { const customSchema = buildSchema(` type Query { getAllUsers: [User] } type User { id: ID! name: String! } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const getAllUsersField = queryType.getFields()["getAllUsers"]; const toolInfo = await toolFactory.createToolInfo( "getAllUsers", getAllUsersField, "query", ); expect(toolInfo.inputSchema).toEqual({ type: "object", properties: {}, required: undefined, }); }); }); describe("handler execution", () => { it("should execute successful query with arguments", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUser: { id: "123", name: "John Doe" }, }, }), }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); const result = await toolInfo.handler({ id: "123" }); expect(mockFetch).toHaveBeenCalledWith("http://localhost:4000/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer test-token", }, body: expect.stringContaining("query($id: ID!)"), }); const callArgs = mockFetch.mock.calls[0][1]; const body = JSON.parse(callArgs.body); expect(body.query).toContain("query($id: ID!)"); expect(body.query).toContain("getUser(id: $id)"); expect(body.variables).toEqual({ id: "123" }); expect(result).toEqual({ content: [ { type: "text", text: JSON.stringify({ id: "123", name: "John Doe" }, null, 2), }, ], }); }); it("should execute successful mutation with arguments", async () => { const mutationType = simpleSchema.getMutationType() as GraphQLObjectType; const createUserField = mutationType.getFields()["createUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { createUser: { id: "456", name: "Jane Doe" }, }, }), }); const toolInfo = await toolFactory.createToolInfo( "createUser", createUserField, "mutation", ); const result = await toolInfo.handler({ name: "Jane Doe" }); expect(mockFetch).toHaveBeenCalledWith("http://localhost:4000/graphql", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer test-token", }, body: expect.stringContaining("mutation($name: String!)"), }); const callArgs = mockFetch.mock.calls[0][1]; const body = JSON.parse(callArgs.body); expect(body.query).toContain("mutation($name: String!)"); expect(body.query).toContain("createUser(name: $name)"); expect(body.variables).toEqual({ name: "Jane Doe" }); expect(result).toEqual({ content: [ { type: "text", text: JSON.stringify({ id: "456", name: "Jane Doe" }, null, 2), }, ], }); }); it("should execute without authorization header when no token provided", async () => { const factoryWithoutToken = new ToolFactory( "http://localhost:4000/graphql", ); const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUser: { id: "123", name: "John Doe" }, }, }), }); const toolInfo = await factoryWithoutToken.createToolInfo( "getUser", getUserField, "query", ); await toolInfo.handler({ id: "123" }); expect(mockFetch).toHaveBeenCalledWith("http://localhost:4000/graphql", { method: "POST", headers: { "Content-Type": "application/json", }, body: expect.any(String), }); }); it("should handle GraphQL errors", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ errors: [{ message: "User not found" }], }), }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); await expect(toolInfo.handler({ id: "999" })).rejects.toThrow( "API Error: User not found", ); }); it("should handle network errors", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockRejectedValueOnce(new Error("Network failure")); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); await expect(toolInfo.handler({ id: "123" })).rejects.toThrow( "Network failure", ); }); it("should handle multiple GraphQL errors", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ errors: [{ message: "First error" }, { message: "Second error" }], }), }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); await expect(toolInfo.handler({ id: "999" })).rejects.toThrow( "API Error: First error", ); }); it("should handle no arguments correctly", async () => { const customSchema = buildSchema(` type Query { getAllUsers: [User] } type User { id: ID! name: String! } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const getAllUsersField = queryType.getFields()["getAllUsers"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getAllUsers: [{ id: "1", name: "User 1" }], }, }), }); const toolInfo = await toolFactory.createToolInfo( "getAllUsers", getAllUsersField, "query", ); const result = await toolInfo.handler({}); const callArgs = mockFetch.mock.calls[0][1]; const body = JSON.parse(callArgs.body); expect(body.query).toContain("query {"); expect(body.query).toContain("getAllUsers"); expect(body.variables).toEqual({}); }); }); describe("selection set building", () => { it("should handle scalar return types", async () => { const customSchema = buildSchema(` type Query { getUserCount: Int } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const countField = queryType.getFields()["getUserCount"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUserCount: 42 }, }), }); const toolInfo = await toolFactory.createToolInfo( "getUserCount", countField, "query", ); await toolInfo.handler({}); const callArgs = mockFetch.mock.calls[0][1]; const body = JSON.parse(callArgs.body); // Should not include selection set for scalar types expect(body.query).toContain("getUserCount"); expect(body.query).not.toContain("getUserCount {"); }); it("should handle object return types with selection set", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUser: { id: "123", name: "John" }, }, }), }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); await toolInfo.handler({ id: "123" }); const callArgs = mockFetch.mock.calls[0][1]; const body = JSON.parse(callArgs.body); expect(body.query).toContain("getUser(id: $id) {"); expect(body.query).toContain("id"); expect(body.query).toContain("name"); expect(body.query).toContain("__typename"); }); it("should handle list return types", async () => { const customSchema = buildSchema(` type Query { getUsers: [User] } type User { id: ID! name: String! } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const getUsersField = queryType.getFields()["getUsers"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUsers: [{ id: "1", name: "User 1" }], }, }), }); const toolInfo = await toolFactory.createToolInfo( "getUsers", getUsersField, "query", ); await toolInfo.handler({}); const callArgs = mockFetch.mock.calls[0][1]; const body = JSON.parse(callArgs.body); expect(body.query).toContain("getUsers {"); expect(body.query).toContain("id"); expect(body.query).toContain("name"); }); }); describe("GraphQL type mapping", () => { it("should map GraphQL types to Zod correctly", async () => { const customSchema = buildSchema(` type Query { complexQuery( stringArg: String! intArg: Int! floatArg: Float! boolArg: Boolean! idArg: ID! optionalString: String stringList: [String!]! ): String } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const complexField = queryType.getFields()["complexQuery"]; const toolInfo = await toolFactory.createToolInfo( "complexQuery", complexField, "query", ); expect(toolInfo.inputSchema).toEqual({ type: "object", properties: { stringArg: { type: "string" }, intArg: { type: "number" }, floatArg: { type: "number" }, boolArg: { type: "boolean" }, idArg: { type: "string" }, optionalString: { type: "string" }, stringList: { type: "array", items: { type: "string" }, }, }, required: [ "stringArg", "intArg", "floatArg", "boolArg", "idArg", "optionalString", "stringList", ], }); }); it("should handle custom scalar types", async () => { const customSchema = buildSchema(` scalar DateTime type Query { getByDate(date: DateTime!): String } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const dateField = queryType.getFields()["getByDate"]; const toolInfo = await toolFactory.createToolInfo( "getByDate", dateField, "query", ); expect(toolInfo.inputSchema.properties.date).toEqual({ type: "string", }); }); it("should handle enum types", async () => { const customSchema = buildSchema(` enum Status { ACTIVE INACTIVE } type Query { getByStatus(status: Status!): String } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const statusField = queryType.getFields()["getByStatus"]; const toolInfo = await toolFactory.createToolInfo( "getByStatus", statusField, "query", ); expect(toolInfo.inputSchema.properties.status).toEqual({ type: "string", }); }); it("should handle input object types", async () => { const customSchema = buildSchema(` input UserInput { name: String! email: String! } type Query { searchUser(input: UserInput!): String } `); const queryType = customSchema.getQueryType() as GraphQLObjectType; const searchField = queryType.getFields()["searchUser"]; const toolInfo = await toolFactory.createToolInfo( "searchUser", searchField, "query", ); expect(toolInfo.inputSchema.properties.input).toEqual({ type: "string", }); }); }); describe("error edge cases", () => { it("should handle malformed JSON responses", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => { throw new Error("Invalid JSON"); }, }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); await expect(toolInfo.handler({ id: "123" })).rejects.toThrow( "Invalid JSON", ); }); it("should handle responses with both data and errors", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUser: { id: "123" } }, errors: [{ message: "Warning: deprecated field" }], }), }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); await expect(toolInfo.handler({ id: "123" })).rejects.toThrow( "API Error: Warning: deprecated field", ); }); it("should handle empty response data", async () => { const queryType = simpleSchema.getQueryType() as GraphQLObjectType; const getUserField = queryType.getFields()["getUser"]; mockFetch.mockResolvedValueOnce({ json: async () => ({ data: { getUser: null }, }), }); const toolInfo = await toolFactory.createToolInfo( "getUser", getUserField, "query", ); const result = await toolInfo.handler({ id: "999" }); expect(result).toEqual({ content: [ { type: "text", text: "null", }, ], }); }); }); describe("comprehensive schema integration", () => { it("should handle complex types from comprehensive schema", async () => { const queryType = comprehensiveSchema.getQueryType() as GraphQLObjectType; const searchField = queryType.getFields()["search"]; const toolInfo = await toolFactory.createToolInfo( "search", searchField, "query", ); expect(toolInfo.name).toBe("search"); expect(toolInfo.inputSchema.properties.query).toEqual({ type: "string", }); expect(toolInfo.inputSchema.required).toContain("query"); }); it("should handle subscription type fields", async () => { const subscriptionType = comprehensiveSchema.getSubscriptionType() as GraphQLObjectType; const userCreatedField = subscriptionType.getFields()["userCreated"]; const toolInfo = await toolFactory.createToolInfo( "userCreated", userCreatedField, "query", ); expect(toolInfo.name).toBe("userCreated"); expect(toolInfo.inputSchema.properties).toEqual({}); }); }); });

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/nfishel48/conduit'

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