diff --git a/apps/api-server/src/utils/geo.test.ts b/apps/api-server/src/utils/geo.test.ts index f7c9521..2f78ad9 100644 --- a/apps/api-server/src/utils/geo.test.ts +++ b/apps/api-server/src/utils/geo.test.ts @@ -1,10 +1,18 @@ -import { beforeEach, describe, expect, test } from "bun:test"; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + test, +} from "bun:test"; import { _resetForTesting, _setReaderForTesting, extractClientIP, getGeoInfo, getUserAgent, + initGeoReader, lookupGeoFromIP, } from "./geo.js"; @@ -220,3 +228,110 @@ describe("getUserAgent", () => { expect(getUserAgent(createHeaders({}))).toBe("Unknown"); }); }); + +describe("initGeoReader", () => { + beforeEach(() => { + _resetForTesting(); + }); + + test("calling initGeoReader twice does not reinitialize", async () => { + // First call initializes + await initGeoReader(); + + // Second call should return early (covers the early return branch) + await initGeoReader(); + + // If we get here without error, the early return worked + expect(true).toBe(true); + }); + + test("handles missing database file gracefully", async () => { + // Save original env + const originalPath = Bun.env.GEOIP_DATABASE_PATH; + + // Point to non-existent file + Bun.env.GEOIP_DATABASE_PATH = "/nonexistent/path/to/db.mmdb"; + + // Should not throw, just log a warning + await initGeoReader(); + + // Lookups should return nulls since reader failed to initialize + expect(lookupGeoFromIP("8.8.8.8")).toEqual({ + city: null, + region: null, + country: null, + }); + + // Restore original env + if (originalPath) { + Bun.env.GEOIP_DATABASE_PATH = originalPath; + } else { + delete Bun.env.GEOIP_DATABASE_PATH; + } + }); +}); + +// Only run real database tests if GEOIP_DATABASE_PATH is set +const hasGeoDatabase = !!Bun.env.GEOIP_DATABASE_PATH; + +describe.skipIf(!hasGeoDatabase)("real GeoIP database", () => { + beforeAll(async () => { + _resetForTesting(); + await initGeoReader(); + }); + + afterAll(() => { + _resetForTesting(); + }); + + test("looks up Google DNS (8.8.8.8) - US", () => { + const result = lookupGeoFromIP("8.8.8.8"); + expect(result.country).toBe("US"); + }); + + test("looks up Cloudflare DNS (1.1.1.1) - AU", () => { + const result = lookupGeoFromIP("1.1.1.1"); + // Cloudflare's 1.1.1.1 is geolocated to Sydney, Australia + expect(result.country).toBe("AU"); + }); + + test("looks up known German IP", () => { + // Deutsche Telekom IP range + const result = lookupGeoFromIP("80.150.6.143"); + expect(result.country).toBe("DE"); + }); + + test("looks up known UK IP", () => { + // BBC IP range + const result = lookupGeoFromIP("212.58.244.71"); + expect(result.country).toBe("GB"); + }); + + test("returns city data for major IPs", () => { + const result = lookupGeoFromIP("8.8.8.8"); + // DBIP returns "Mountain View" for Google DNS + expect(result.city).toBe("Mountain View"); + expect(result.region).toBe("California"); + }); + + test("getGeoInfo uses real database when no CF headers", () => { + const headers = createHeaders({ "X-Real-IP": "8.8.8.8" }); + const result = getGeoInfo(headers); + + expect(result.ip).toBe("8.8.8.8"); + expect(result.country).toBe("US"); + expect(result.city).toBe("Mountain View"); + }); + + test("returns nulls for private/reserved IPs", () => { + const result = lookupGeoFromIP("192.168.1.1"); + expect(result.city).toBeNull(); + expect(result.country).toBeNull(); + }); + + test("returns nulls for localhost", () => { + const result = lookupGeoFromIP("127.0.0.1"); + expect(result.city).toBeNull(); + expect(result.country).toBeNull(); + }); +}); diff --git a/devenv.nix b/devenv.nix index ffbe666..09c66de 100644 --- a/devenv.nix +++ b/devenv.nix @@ -7,6 +7,7 @@ git dbmate ast-grep + dbip-city-lite ]; dotenv.enable = true; @@ -39,6 +40,7 @@ env = { DATABASE_URL = "postgres://reviq:reviq@localhost/reviq-dashboard?sslmode=disable"; TEST_DATABASE_URL = "postgres://reviq:reviq@localhost/reviq-dashboard_test?sslmode=disable"; + GEOIP_DATABASE_PATH = "${pkgs.dbip-city-lite}/share/dbip/dbip-city-lite.mmdb"; }; scripts = {