Skip to main content
Glama

EVE Online OSINT MCP Server

server.test.ts21.2 kB
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; /** * Tests for EVE Online OSINT utility functions */ // Set test environment process.env.NODE_ENV = "test"; // Import functions from server for testing import { fetchWithRetry, resolveNamesToIds, resolveIdsToNames, getESICharacterInfo, } from "./server.js"; const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)); // Helper function to create mock responses const createMockResponse = (data: any, status = 200, ok = true): Response => ({ ok, status, statusText: ok ? "OK" : "Error", json: async () => data, } as Response); // Mock fetch for testing const mockFetch = async (input: Request | string | URL) => { const url = typeof input === "string" ? input : input.toString(); if (url.includes("/universe/ids/")) { // Mock ESI response for name resolution return createMockResponse({ alliances: [{ id: 555666777, name: "Test Alliance" }], characters: [{ id: 123456789, name: "Test Character" }], corporations: [{ id: 987654321, name: "Test Corporation" }], }); } if (url.includes("/universe/names/")) { // Mock ESI response for ID to name resolution return createMockResponse([ { category: "character", id: 123456789, name: "Test Character" }, { category: "corporation", id: 987654321, name: "Test Corporation" }, { category: "alliance", id: 555666777, name: "Test Alliance" }, ]); } if (url.includes("/characters/123456789/portrait")) { // Mock ESI character portrait response return createMockResponse({ px128x128: "https://images.evetech.net/characters/123456789/portrait?size=128", px256x256: "https://images.evetech.net/characters/123456789/portrait?size=256", px512x512: "https://images.evetech.net/characters/123456789/portrait?size=512", px64x64: "https://images.evetech.net/characters/123456789/portrait?size=64", }); } if (url.includes("/characters/123456789/corporationhistory")) { // Mock ESI corporation history response return createMockResponse([ { corporation_id: 987654321, is_deleted: false, record_id: 1, start_date: "2023-01-01", }, { corporation_id: 111222333, is_deleted: false, record_id: 2, start_date: "2022-06-01", }, ]); } if ( url.includes("/characters/123456789/") && !url.includes("portrait") && !url.includes("corporationhistory") ) { // Mock ESI character info response return createMockResponse({ alliance_id: 555666777, birthday: "2010-01-01T00:00:00Z", bloodline_id: 4, corporation_id: 987654321, description: "Test character description", gender: "Male", name: "Test Character", race_id: 1, security_status: -2.5, title: "Test Title", }); } if (url.includes("/characters/affiliation/")) { // Mock ESI character affiliation response return createMockResponse([ { alliance_id: 555666777, character_id: 123456789, corporation_id: 987654321, }, ]); } if (url.includes("/corporations/987654321/")) { // Mock ESI corporation info response return createMockResponse({ alliance_id: 555666777, ceo_id: 123456789, creator_id: 123456789, date_founded: "2020-01-01", description: "Test corporation description", member_count: 150, name: "Test Corporation", tax_rate: 0.1, ticker: "TEST", url: "https://example.com", war_eligible: true, }); } if (url.includes("/alliances/555666777/")) { // Mock ESI alliance info response return createMockResponse({ creator_corporation_id: 987654321, creator_id: 123456789, date_founded: "2019-01-01", executor_corporation_id: 987654321, name: "Test Alliance", ticker: "TESTA", }); } if (url.includes("/api/character/123456789")) { // Mock EveWho character response for valid ID return createMockResponse({ character_id: 123456789, corporation: { corporation_id: 987654321, name: "Test Corporation", }, history: [ { corporation: { name: "Previous Corp" }, end_date: "2023-06-01", start_date: "2023-01-01", }, ], name: "Test Character", security_status: -2.5, }); } if (url.includes("zkillboard.com/api/characterID/123456789")) { // Mock zKillboard killmails response return createMockResponse([ { attackers: [ { character_id: 987654321, corporation_id: 123456789, damage_done: 1500, final_blow: true, security_status: 0.5, ship_type_id: 587, }, ], killmail_id: 123456789, killmail_time: "2024-01-15T12:30:00Z", solar_system_id: 30000142, victim: { character_id: 123456789, corporation_id: 987654321, damage_taken: 1500, ship_type_id: 588, }, zkb: { destroyedValue: 50000000, droppedValue: 25000000, fittedValue: 75000000, hash: "abc123def456", href: "https://zkillboard.com/kill/123456789/", locationID: 40000001, npc: false, points: 1, solo: true, totalValue: 75000000, }, }, ]); } if (url.includes("zkillboard.com/api/stats/characterID/123456789")) { // Mock zKillboard stats response return createMockResponse({ allTimeSum: 150, groups: { "25": { isk: 5000000000, kills: 50 }, "26": { isk: 3000000000, kills: 30 }, }, id: 123456789, months: { "202312": { isk: 2000000000, kills: 20 }, "202401": { isk: 2500000000, kills: 25 }, }, topAllTime: [ { id: 587, isk: 1000000000, kills: 10, type: "shipTypeID", }, ], topIsk: [ { id: 588, isk: 2000000000, kills: 5, type: "shipTypeID", }, ], type: "characterID", }); } // Default to error response for invalid URLs return createMockResponse({}, 404, false); }; // Set the mock fetch as global global.fetch = mockFetch; describe("Retry Logic", () => { beforeEach(() => { vi.useFakeTimers(); vi.clearAllMocks(); }); afterEach(() => { vi.useRealTimers(); global.fetch = mockFetch; // Reset to default mock }); it("should retry on 5xx server errors", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; if (callCount < 3) { return { ok: false, status: 500, statusText: "Internal Server Error", } as Response; } return { ok: true, status: 200, json: async () => ({ success: true }), } as Response; }); const promise = fetchWithRetry("https://test.com", {}, 5); // Fast-forward through the delays await vi.runAllTimersAsync(); const response = await promise; expect(response.ok).toBe(true); expect(callCount).toBe(3); }); it("should retry on 429 rate limiting", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; if (callCount < 2) { return { ok: false, status: 429, statusText: "Too Many Requests", } as Response; } return { ok: true, status: 200, json: async () => ({ success: true }), } as Response; }); const promise = fetchWithRetry("https://test.com", {}, 5); // Fast-forward through the delays await vi.runAllTimersAsync(); const response = await promise; expect(response.ok).toBe(true); expect(callCount).toBe(2); }); it("should retry on 408 timeout", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; if (callCount < 2) { return { ok: false, status: 408, statusText: "Request Timeout", } as Response; } return { ok: true, status: 200, json: async () => ({ success: true }), } as Response; }); const promise = fetchWithRetry("https://test.com", {}, 5); // Fast-forward through the delays await vi.runAllTimersAsync(); const response = await promise; expect(response.ok).toBe(true); expect(callCount).toBe(2); }); it("should retry on network errors", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; if (callCount < 3) { throw new Error("Network error"); } return { ok: true, status: 200, json: async () => ({ success: true }), } as Response; }); const fetchPromise = fetchWithRetry("https://test.com", {}, 5); // Fast-forward through all timers await vi.runAllTimersAsync(); const response = await fetchPromise; expect(response.ok).toBe(true); expect(callCount).toBe(3); }); it("should not retry on 4xx client errors (except 408 and 429)", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; return { ok: false, status: 404, statusText: "Not Found", } as Response; }); const response = await fetchWithRetry("https://test.com", {}, 5); expect(response.ok).toBe(false); expect(response.status).toBe(404); expect(callCount).toBe(1); // Should not retry }); it("should return last response after max retries for retryable errors", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; return { ok: false, status: 500, statusText: "Internal Server Error", } as Response; }); const promise = fetchWithRetry("https://test.com", {}, 3); // Fast-forward through the delays await vi.runAllTimersAsync(); const response = await promise; expect(response.ok).toBe(false); expect(response.status).toBe(500); expect(callCount).toBe(3); // Should try 3 times }); it("should throw error after max retries for network errors", async () => { let callCount = 0; global.fetch = vi.fn().mockImplementation(async () => { callCount++; throw new Error("Network error"); }); // Use a more controlled approach to handle the promise const fetchPromise = fetchWithRetry("https://test.com", {}, 3); // Fast-forward through all timers await vi.runAllTimersAsync(); // Wait for the promise to settle and expect it to reject await expect(fetchPromise).rejects.toThrow("Network error"); expect(callCount).toBe(3); // Should try 3 times }); it("should use exponential backoff with jitter", async () => { let callCount = 0; const delays: number[] = []; // Mock Math.random to make jitter predictable const originalRandom = Math.random; Math.random = vi.fn().mockReturnValue(0.5); // Fixed jitter of 500ms // Mock sleep function to capture delays const mockSleep = vi.fn().mockImplementation(async (ms: number) => { delays.push(ms); return Promise.resolve(); }); global.fetch = vi.fn().mockImplementation(async () => { callCount++; if (callCount < 4) { return { ok: false, status: 500, statusText: "Internal Server Error", } as Response; } return { ok: true, status: 200, json: async () => ({ success: true }), } as Response; }); // Create a custom fetchWithRetry that uses our mocked sleep const testFetchWithRetry = async ( url: string, options: RequestInit, maxRetries = 5, ): Promise<Response> => { let lastError: Error | undefined; for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url, options); const shouldRetry = response.status >= 500 || response.status === 429 || response.status === 408; if (shouldRetry) { if (i === maxRetries - 1) return response; const delay = Math.pow(2, i) * 1000 + Math.random() * 1000; await mockSleep(delay); continue; } return response; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (i === maxRetries - 1) throw lastError; const delay = Math.pow(2, i) * 1000 + Math.random() * 1000; await mockSleep(delay); } } throw lastError || new Error(`Request failed after ${maxRetries} retries.`); }; await testFetchWithRetry("https://test.com", {}, 5); // Restore mocks Math.random = originalRandom; // Check that delays are increasing (exponential backoff) expect(delays.length).toBe(3); // 3 retries = 3 delays expect(delays[0]).toBe(1500); // First delay: 2^0 * 1000 + 500 jitter = 1500 expect(delays[1]).toBe(2500); // Second delay: 2^1 * 1000 + 500 jitter = 2500 expect(delays[2]).toBe(4500); // Third delay: 2^2 * 1000 + 500 jitter = 4500 }); }); describe("Server Functions", () => { beforeEach(() => { global.fetch = mockFetch; }); describe("resolveNamesToIds", () => { it("should resolve character names to IDs", async () => { const result = await resolveNamesToIds(["Test Character"]); expect(result.characters).toBeDefined(); expect(result.characters![0].name).toBe("Test Character"); expect(result.characters![0].id).toBe(123456789); }); it("should resolve corporation names to IDs", async () => { const result = await resolveNamesToIds(["Test Corporation"]); expect(result.corporations).toBeDefined(); expect(result.corporations![0].name).toBe("Test Corporation"); expect(result.corporations![0].id).toBe(987654321); }); it("should resolve alliance names to IDs", async () => { const result = await resolveNamesToIds(["Test Alliance"]); expect(result.alliances).toBeDefined(); expect(result.alliances![0].name).toBe("Test Alliance"); expect(result.alliances![0].id).toBe(555666777); }); it("should handle API errors", async () => { // Mock a 404 error (non-retryable) to avoid timeout global.fetch = vi.fn().mockResolvedValue(createMockResponse({}, 404, false)); await expect(resolveNamesToIds(["Invalid"])).rejects.toThrow("ESI API error"); }); }); describe("resolveIdsToNames", () => { it("should resolve IDs to names", async () => { const result = await resolveIdsToNames([123456789, 987654321, 555666777]); expect(result).toHaveLength(3); expect(result[0].name).toBe("Test Character"); expect(result[1].name).toBe("Test Corporation"); expect(result[2].name).toBe("Test Alliance"); }); it("should handle API errors", async () => { // Mock a 404 error (non-retryable) to avoid timeout global.fetch = vi.fn().mockResolvedValue(createMockResponse({}, 404, false)); await expect(resolveIdsToNames([123456789])).rejects.toThrow("ESI API error"); }); }); describe("getESICharacterInfo", () => { it("should fetch character information", async () => { const result = await getESICharacterInfo(123456789); expect(result.name).toBe("Test Character"); expect(result.corporation_id).toBe(987654321); expect(result.alliance_id).toBe(555666777); expect(result.security_status).toBe(-2.5); }); it("should handle API errors", async () => { global.fetch = vi.fn().mockResolvedValue(createMockResponse({}, 404, false)); await expect(getESICharacterInfo(999999999)).rejects.toThrow("ESI API error"); }); }); }); describe("EVE Online OSINT Server - Mock API Tests", () => { beforeEach(() => { global.fetch = mockFetch; }); it("should resolve character names to IDs", async () => { const response = await fetch( "https://esi.evetech.net/latest/universe/ids/", { body: JSON.stringify(["Test Character"]), method: "POST", }, ); expect(response.ok).toBe(true); const data = (await response.json()) as { characters: Array<{ id: number; name: string }>; }; expect(data.characters).toBeDefined(); expect(data.characters[0].name).toBe("Test Character"); }); it("should fetch character information from EveWho", async () => { const response = await fetch("https://evewho.com/api/character/123456789"); expect(response.ok).toBe(true); const data = (await response.json()) as { character_id: number; name: string; }; expect(data.character_id).toBe(123456789); expect(data.name).toBe("Test Character"); }); it("should fetch zKillboard killmails", async () => { const response = await fetch( "https://zkillboard.com/api/characterID/123456789/", ); expect(response.ok).toBe(true); const data = (await response.json()) as Array<{ killmail_id: number; zkb: { totalValue: number }; }>; expect(Array.isArray(data)).toBe(true); expect(data[0].killmail_id).toBe(123456789); expect(data[0].zkb.totalValue).toBe(75000000); }); it("should fetch zKillboard statistics", async () => { const response = await fetch( "https://zkillboard.com/api/stats/characterID/123456789/", ); expect(response.ok).toBe(true); const data = (await response.json()) as { allTimeSum: number; id: number; type: string; }; expect(data.id).toBe(123456789); expect(data.type).toBe("characterID"); expect(data.allTimeSum).toBe(150); }); it("should fetch ESI character information", async () => { const response = await fetch( "https://esi.evetech.net/latest/characters/123456789/", ); expect(response.ok).toBe(true); const data = (await response.json()) as { alliance_id: number; corporation_id: number; name: string; security_status: number; }; expect(data.name).toBe("Test Character"); expect(data.corporation_id).toBe(987654321); expect(data.alliance_id).toBe(555666777); expect(data.security_status).toBe(-2.5); }); it("should fetch ESI character corporation history", async () => { const response = await fetch( "https://esi.evetech.net/latest/characters/123456789/corporationhistory/", ); expect(response.ok).toBe(true); const data = (await response.json()) as Array<{ corporation_id: number; record_id: number; start_date: string; }>; expect(Array.isArray(data)).toBe(true); expect(data[0].corporation_id).toBe(987654321); expect(data[0].start_date).toBe("2023-01-01"); }); it("should fetch ESI corporation information", async () => { const response = await fetch( "https://esi.evetech.net/latest/corporations/987654321/", ); expect(response.ok).toBe(true); const data = (await response.json()) as { member_count: number; name: string; tax_rate: number; ticker: string; }; expect(data.name).toBe("Test Corporation"); expect(data.ticker).toBe("TEST"); expect(data.member_count).toBe(150); expect(data.tax_rate).toBe(0.1); }); it("should fetch ESI alliance information", async () => { const response = await fetch( "https://esi.evetech.net/latest/alliances/555666777/", ); expect(response.ok).toBe(true); const data = (await response.json()) as { date_founded: string; name: string; ticker: string; }; expect(data.name).toBe("Test Alliance"); expect(data.ticker).toBe("TESTA"); expect(data.date_founded).toBe("2019-01-01"); }); it("should resolve IDs to names", async () => { const response = await fetch( "https://esi.evetech.net/latest/universe/names/", { body: JSON.stringify([123456789, 987654321, 555666777]), method: "POST", }, ); expect(response.ok).toBe(true); const data = (await response.json()) as Array<{ category: string; id: number; name: string; }>; expect(Array.isArray(data)).toBe(true); expect(data[0].name).toBe("Test Character"); expect(data[1].name).toBe("Test Corporation"); expect(data[2].name).toBe("Test Alliance"); }); it("should handle API errors gracefully", async () => { const response = await fetch("https://evewho.com/api/character/invalid"); expect(response.ok).toBe(false); expect(response.status).toBe(404); }); });

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/kongyo2/EVE-Online-OSINT-MCP'

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