Add GeoIP lookup support and update device fingerprints to base58
- Add maxmind library for GeoIP database lookups when not behind Cloudflare - Extract client IP from multiple header sources (CF, X-Real-IP, X-Forwarded-For, etc.) - Change device fingerprints from UUID to base58 with device_ prefix - Add isValidDeviceFingerprint() that accepts both new and legacy formats - Colocate unit tests with source files, remove __tests__/unit directory - Add test coverage reporting to test script Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
222
apps/api-server/src/utils/geo.test.ts
Normal file
222
apps/api-server/src/utils/geo.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import {
|
||||
_resetForTesting,
|
||||
_setReaderForTesting,
|
||||
extractClientIP,
|
||||
getGeoInfo,
|
||||
getUserAgent,
|
||||
lookupGeoFromIP,
|
||||
} from "./geo.js";
|
||||
|
||||
const createHeaders = (entries: Record<string, string>): Headers => {
|
||||
return new Headers(entries);
|
||||
};
|
||||
|
||||
describe("extractClientIP", () => {
|
||||
test("extracts from CF-Connecting-IP", () => {
|
||||
const headers = createHeaders({ "CF-Connecting-IP": "1.2.3.4" });
|
||||
expect(extractClientIP(headers)).toBe("1.2.3.4");
|
||||
});
|
||||
|
||||
test("extracts from True-Client-IP", () => {
|
||||
const headers = createHeaders({ "True-Client-IP": "5.6.7.8" });
|
||||
expect(extractClientIP(headers)).toBe("5.6.7.8");
|
||||
});
|
||||
|
||||
test("extracts from X-Real-IP", () => {
|
||||
const headers = createHeaders({ "X-Real-IP": "10.20.30.40" });
|
||||
expect(extractClientIP(headers)).toBe("10.20.30.40");
|
||||
});
|
||||
|
||||
test("extracts first IP from X-Forwarded-For", () => {
|
||||
const headers = createHeaders({
|
||||
"X-Forwarded-For": "100.1.2.3, 200.4.5.6, 10.0.0.1",
|
||||
});
|
||||
expect(extractClientIP(headers)).toBe("100.1.2.3");
|
||||
});
|
||||
|
||||
test("extracts from X-Client-IP", () => {
|
||||
const headers = createHeaders({ "X-Client-IP": "192.168.1.1" });
|
||||
expect(extractClientIP(headers)).toBe("192.168.1.1");
|
||||
});
|
||||
|
||||
test("prioritizes CF-Connecting-IP over others", () => {
|
||||
const headers = createHeaders({
|
||||
"CF-Connecting-IP": "1.1.1.1",
|
||||
"True-Client-IP": "2.2.2.2",
|
||||
"X-Real-IP": "3.3.3.3",
|
||||
"X-Forwarded-For": "4.4.4.4",
|
||||
"X-Client-IP": "5.5.5.5",
|
||||
});
|
||||
expect(extractClientIP(headers)).toBe("1.1.1.1");
|
||||
});
|
||||
|
||||
test("returns null when no IP headers present", () => {
|
||||
expect(extractClientIP(createHeaders({}))).toBeNull();
|
||||
});
|
||||
|
||||
test("trims whitespace", () => {
|
||||
const headers = createHeaders({ "CF-Connecting-IP": " 1.2.3.4 " });
|
||||
expect(extractClientIP(headers)).toBe("1.2.3.4");
|
||||
});
|
||||
|
||||
test("handles IPv6", () => {
|
||||
const headers = createHeaders({
|
||||
"CF-Connecting-IP": "2001:0db8:85a3::8a2e:0370:7334",
|
||||
});
|
||||
expect(extractClientIP(headers)).toBe("2001:0db8:85a3::8a2e:0370:7334");
|
||||
});
|
||||
});
|
||||
|
||||
describe("lookupGeoFromIP", () => {
|
||||
beforeEach(() => {
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
test("returns nulls when reader not initialized", () => {
|
||||
expect(lookupGeoFromIP("8.8.8.8")).toEqual({
|
||||
city: null,
|
||||
region: null,
|
||||
country: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns geo data from reader", () => {
|
||||
_setReaderForTesting({
|
||||
get: (ip: string) =>
|
||||
ip === "8.8.8.8"
|
||||
? {
|
||||
city: { names: { en: "Mountain View" } },
|
||||
subdivisions: [{ names: { en: "California" } }],
|
||||
country: { iso_code: "US" },
|
||||
}
|
||||
: null,
|
||||
} as never);
|
||||
|
||||
expect(lookupGeoFromIP("8.8.8.8")).toEqual({
|
||||
city: "Mountain View",
|
||||
region: "California",
|
||||
country: "US",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns nulls for unknown IP", () => {
|
||||
_setReaderForTesting({ get: () => null } as never);
|
||||
expect(lookupGeoFromIP("0.0.0.0")).toEqual({
|
||||
city: null,
|
||||
region: null,
|
||||
country: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("handles partial geo data", () => {
|
||||
_setReaderForTesting({
|
||||
get: () => ({ country: { iso_code: "DE" } }),
|
||||
} as never);
|
||||
|
||||
expect(lookupGeoFromIP("1.2.3.4")).toEqual({
|
||||
city: null,
|
||||
region: null,
|
||||
country: "DE",
|
||||
});
|
||||
});
|
||||
|
||||
test("handles reader errors gracefully", () => {
|
||||
_setReaderForTesting({
|
||||
get: () => {
|
||||
throw new Error("Lookup failed");
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(lookupGeoFromIP("invalid")).toEqual({
|
||||
city: null,
|
||||
region: null,
|
||||
country: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getGeoInfo", () => {
|
||||
beforeEach(() => {
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
test("uses Cloudflare geo headers when present", () => {
|
||||
const headers = createHeaders({
|
||||
"CF-Connecting-IP": "1.2.3.4",
|
||||
"CF-IPCountry": "US",
|
||||
"CF-IPCity": "San Francisco",
|
||||
"CF-Region": "California",
|
||||
});
|
||||
|
||||
expect(getGeoInfo(headers)).toEqual({
|
||||
ip: "1.2.3.4",
|
||||
city: "San Francisco",
|
||||
region: "California",
|
||||
country: "US",
|
||||
});
|
||||
});
|
||||
|
||||
test("prefers CF headers over GeoIP lookup", () => {
|
||||
_setReaderForTesting({
|
||||
get: () => ({
|
||||
city: { names: { en: "Wrong" } },
|
||||
country: { iso_code: "XX" },
|
||||
}),
|
||||
} as never);
|
||||
|
||||
const headers = createHeaders({
|
||||
"CF-Connecting-IP": "1.2.3.4",
|
||||
"CF-IPCountry": "US",
|
||||
});
|
||||
|
||||
expect(getGeoInfo(headers).country).toBe("US");
|
||||
});
|
||||
|
||||
test("falls back to GeoIP lookup when no CF geo headers", () => {
|
||||
_setReaderForTesting({
|
||||
get: () => ({
|
||||
city: { names: { en: "Berlin" } },
|
||||
subdivisions: [{ names: { en: "Berlin" } }],
|
||||
country: { iso_code: "DE" },
|
||||
}),
|
||||
} as never);
|
||||
|
||||
const headers = createHeaders({ "X-Real-IP": "1.2.3.4" });
|
||||
|
||||
expect(getGeoInfo(headers)).toEqual({
|
||||
ip: "1.2.3.4",
|
||||
city: "Berlin",
|
||||
region: "Berlin",
|
||||
country: "DE",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns IP even without geo data", () => {
|
||||
const headers = createHeaders({ "X-Forwarded-For": "203.0.113.50" });
|
||||
const result = getGeoInfo(headers);
|
||||
|
||||
expect(result.ip).toBe("203.0.113.50");
|
||||
expect(result.city).toBeNull();
|
||||
});
|
||||
|
||||
test("returns all nulls when no headers", () => {
|
||||
expect(getGeoInfo(createHeaders({}))).toEqual({
|
||||
ip: null,
|
||||
city: null,
|
||||
region: null,
|
||||
country: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserAgent", () => {
|
||||
test("returns User-Agent header", () => {
|
||||
const headers = createHeaders({ "User-Agent": "Mozilla/5.0 Chrome/120" });
|
||||
expect(getUserAgent(headers)).toBe("Mozilla/5.0 Chrome/120");
|
||||
});
|
||||
|
||||
test('returns "Unknown" when missing', () => {
|
||||
expect(getUserAgent(createHeaders({}))).toBe("Unknown");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user