Skip to main content
Glama

QuickFile MCP Server

by marcusquinn
client.test.ts12.2 kB
/** * Unit tests for API client module */ import { QuickFileApiError } from "../../src/api/client"; // Mock the auth module to prevent file system access jest.mock("../../src/api/auth", () => ({ loadCredentials: jest.fn().mockReturnValue({ accountNumber: "12345678", apiKey: "TEST-API-KEY-1234", applicationId: "12345678-1234-1234-1234-123456789012", }), createAuthHeader: jest.fn().mockReturnValue({ MessageType: "Request", SubmissionNumber: "test-submission", Authentication: { AccNumber: "12345678", MD5Value: "abc123def456", ApplicationID: "12345678-1234-1234-1234-123456789012", }, }), })); describe("QuickFileApiError", () => { describe("constructor", () => { it("should create error with message and code", () => { const error = new QuickFileApiError("Not found", "NOT_FOUND"); expect(error.message).toBe("Not found"); expect(error.code).toBe("NOT_FOUND"); expect(error.name).toBe("QuickFileApiError"); }); it("should be instanceof Error", () => { const error = new QuickFileApiError("Test error", "TEST"); expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(QuickFileApiError); }); it("should preserve stack trace", () => { const error = new QuickFileApiError("Test", "CODE"); expect(error.stack).toBeDefined(); expect(error.stack).toContain("QuickFileApiError"); }); it("should handle empty message", () => { const error = new QuickFileApiError("", "EMPTY"); expect(error.message).toBe(""); expect(error.code).toBe("EMPTY"); }); it("should handle special characters in message", () => { const message = 'Error: "Something" went wrong & failed < test >'; const error = new QuickFileApiError(message, "SPECIAL"); expect(error.message).toBe(message); }); it("should handle HTTP status codes", () => { const error = new QuickFileApiError("HTTP 401: Unauthorized", "401"); expect(error.code).toBe("401"); expect(error.message).toBe("HTTP 401: Unauthorized"); }); it("should handle network error codes", () => { const error = new QuickFileApiError( "Connection refused", "NETWORK_ERROR", ); expect(error.code).toBe("NETWORK_ERROR"); }); it("should handle timeout errors", () => { const error = new QuickFileApiError( "Request timeout after 30000ms", "TIMEOUT", ); expect(error.code).toBe("TIMEOUT"); }); }); describe("error code patterns", () => { it("should work with common QuickFile error codes", () => { const errorCodes = [ "INVALID_AUTH", "CLIENT_404", "INVOICE_NOT_FOUND", "PARSE_ERROR", "NETWORK_ERROR", "TIMEOUT", "UNKNOWN", "VALIDATION_ERROR", ]; errorCodes.forEach((code) => { const error = new QuickFileApiError(`Error with code ${code}`, code); expect(error.code).toBe(code); }); }); }); }); describe("QuickFileApiClient", () => { let originalFetch: typeof globalThis.fetch; let mockFetch: jest.Mock; beforeEach(() => { originalFetch = globalThis.fetch; mockFetch = jest.fn(); globalThis.fetch = mockFetch; }); afterEach(() => { globalThis.fetch = originalFetch; jest.clearAllMocks(); }); // Import after mocking const getClient = async () => { const { QuickFileApiClient } = await import("../../src/api/client"); return new QuickFileApiClient(); }; describe("constructor", () => { it("should create client with default options", async () => { const client = await getClient(); expect(client.isTestMode()).toBe(false); }); it("should create client with test mode enabled", async () => { const { QuickFileApiClient } = await import("../../src/api/client"); const client = new QuickFileApiClient({ testMode: true }); expect(client.isTestMode()).toBe(true); }); it("should get account number", async () => { const client = await getClient(); expect(client.getAccountNumber()).toBe("12345678"); }); }); describe("setTestMode", () => { it("should toggle test mode on", async () => { const client = await getClient(); client.setTestMode(true); expect(client.isTestMode()).toBe(true); }); it("should toggle test mode off", async () => { const { QuickFileApiClient } = await import("../../src/api/client"); const client = new QuickFileApiClient({ testMode: true }); client.setTestMode(false); expect(client.isTestMode()).toBe(false); }); }); describe("request", () => { it("should make successful API request", async () => { const mockResponse = { Client_Search: { Header: { MessageType: "Response", SubmissionNumber: "test-123" }, Body: { Clients: [{ ClientID: 1, CompanyName: "Test Corp" }] }, }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); const result = await client.request("Client_Search", { CompanyName: "Test", }); expect(result).toEqual({ Clients: [{ ClientID: 1, CompanyName: "Test Corp" }], }); expect(mockFetch).toHaveBeenCalledTimes(1); }); it("should throw on HTTP error response", async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Internal Server Error", }); const client = await getClient(); await expect(client.request("Client_Search", {})).rejects.toThrow( QuickFileApiError, ); // Need to mock again for the second assertion mockFetch.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Internal Server Error", }); await expect(client.request("Client_Search", {})).rejects.toThrow( "HTTP 500", ); }); it("should throw on API error response", async () => { const mockResponse = { Errors: [ { ErrorCode: "AUTH_FAILED", ErrorMessage: "Invalid credentials" }, ], }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); await expect(client.request("Client_Search", {})).rejects.toThrow( "Invalid credentials", ); }); it("should combine multiple API errors", async () => { const mockResponse = { Errors: [ { ErrorCode: "ERROR_1", ErrorMessage: "First error" }, { ErrorCode: "ERROR_2", ErrorMessage: "Second error" }, ], }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); await expect(client.request("Client_Search", {})).rejects.toThrow( "First error; Second error", ); }); it("should throw on timeout", async () => { // Create a client with a very short timeout const { QuickFileApiClient } = await import("../../src/api/client"); const client = new QuickFileApiClient({ timeout: 1 }); // Mock a slow response - use AbortController signal handling mockFetch.mockImplementationOnce( (_, options: { signal?: AbortSignal }) => new Promise((resolve, reject) => { const timeoutId = setTimeout(resolve, 100); if (options?.signal) { options.signal.addEventListener("abort", () => { clearTimeout(timeoutId); const abortError = new Error("Aborted"); abortError.name = "AbortError"; reject(abortError); }); } }), ); await expect(client.request("Client_Search", {})).rejects.toMatchObject({ code: "TIMEOUT", }); }); it("should throw on network error", async () => { mockFetch.mockRejectedValueOnce(new Error("Network failure")); const client = await getClient(); await expect(client.request("Client_Search", {})).rejects.toMatchObject({ code: "NETWORK_ERROR", }); }); it("should throw on invalid response structure", async () => { const mockResponse = { // Missing method key and Body SomeOtherKey: [], }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); await expect(client.request("Client_Search", {})).rejects.toMatchObject({ code: "PARSE_ERROR", }); }); it("should build correct URL for simple method names", async () => { const mockResponse = { Client_Search: { Header: { MessageType: "Response", SubmissionNumber: "test" }, Body: { Clients: [] }, }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); await client.request("Client_Search", {}); expect(mockFetch).toHaveBeenCalledWith( "https://api.quickfile.co.uk/1_2/client/search", expect.any(Object), ); }); it("should build correct URL for compound method names", async () => { const mockResponse = { System_GetAccountDetails: { Header: { MessageType: "Response", SubmissionNumber: "test" }, Body: {}, }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); await client.request("System_GetAccountDetails", {}); expect(mockFetch).toHaveBeenCalledWith( "https://api.quickfile.co.uk/1_2/system/getaccountdetails", expect.any(Object), ); }); it("should send correct headers", async () => { const mockResponse = { Client_Search: { Header: { MessageType: "Response", SubmissionNumber: "test" }, Body: {}, }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); await client.request("Client_Search", {}); expect(mockFetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, }), ); }); it("should handle response with alternative key structure", async () => { // Some responses might have a different structure const mockResponse = { AnotherMethod_Response: { Header: { MessageType: "Response", SubmissionNumber: "test" }, Body: { data: "test" }, }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = await getClient(); const result = await client.request("SomeMethod_Call", {}); expect(result).toEqual({ data: "test" }); }); }); }); describe("getApiClient", () => { beforeEach(() => { // Reset module to clear singleton jest.resetModules(); }); it("should return same instance on multiple calls", async () => { const { getApiClient } = await import("../../src/api/client"); const client1 = getApiClient(); const client2 = getApiClient(); expect(client1).toBe(client2); }); it("should create new instance when options provided", async () => { const { getApiClient } = await import("../../src/api/client"); // First call creates default instance getApiClient(); // Passing options creates new instance with those options const clientWithOptions = getApiClient({ testMode: true }); // New options should create a new instance expect(clientWithOptions.isTestMode()).toBe(true); }); });

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/marcusquinn/quickfile-mcp'

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