mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
* Fix geoip resolution timeout * fix: keep GeoIP timeout inside resolution path
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { COUNTRY_LOCALE_MAP, maybeResolveGeoip, resolveProxyGeo, resolveProxyIp } from "../src/geoip.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
delete process.env.CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS;
|
|
delete process.env.CLOAKBROWSER_CACHE_DIR;
|
|
for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("resolveProxyIp", () => {
|
|
it("returns literal IPv4 from proxy URL", async () => {
|
|
expect(await resolveProxyIp("http://10.50.96.5:8888")).toBe("10.50.96.5");
|
|
});
|
|
|
|
it("handles proxy URL with credentials", async () => {
|
|
expect(await resolveProxyIp("http://user:pass@10.50.96.5:8888")).toBe(
|
|
"10.50.96.5"
|
|
);
|
|
});
|
|
|
|
it("resolves localhost", async () => {
|
|
const ip = await resolveProxyIp("http://localhost:8888");
|
|
expect(ip).toBeTruthy();
|
|
expect(["127.0.0.1", "::1"]).toContain(ip);
|
|
});
|
|
|
|
it("returns null for invalid URL", async () => {
|
|
expect(await resolveProxyIp("not-a-url")).toBeNull();
|
|
});
|
|
|
|
it("returns null for empty string", async () => {
|
|
expect(await resolveProxyIp("")).toBeNull();
|
|
});
|
|
|
|
it("returns null for schemeless proxy (shows why normalization is needed)", async () => {
|
|
// no scheme — new URL() gives empty hostname for both bare formats
|
|
expect(await resolveProxyIp("user:pass@10.50.96.5:8888")).toBeNull();
|
|
expect(await resolveProxyIp("10.50.96.5:8888")).toBeNull();
|
|
});
|
|
|
|
it("extracts IP after normalization (http:// prepended by maybeResolveGeoip)", async () => {
|
|
expect(await resolveProxyIp("http://user:pass@10.50.96.5:8888")).toBe("10.50.96.5");
|
|
expect(await resolveProxyIp("http://10.50.96.5:8888")).toBe("10.50.96.5");
|
|
});
|
|
});
|
|
|
|
describe("maybeResolveGeoip", () => {
|
|
it("does not apply the GeoIP resolution timeout to first-use database download", async () => {
|
|
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cloak-geoip-download-"));
|
|
tempDirs.push(cacheDir);
|
|
process.env.CLOAKBROWSER_CACHE_DIR = cacheDir;
|
|
process.env.CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS = "0.001";
|
|
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
|
ok: true,
|
|
body: new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new Uint8Array([1, 2, 3]));
|
|
controller.close();
|
|
},
|
|
}),
|
|
} as Response);
|
|
|
|
const result = await resolveProxyGeo("http://203.0.113.10:8080");
|
|
|
|
expect(result).toEqual({ timezone: null, locale: null, exitIp: null });
|
|
expect(fetchSpy).toHaveBeenCalledOnce();
|
|
expect(fetchSpy.mock.calls[0][1]).toEqual({ redirect: "follow" });
|
|
});
|
|
|
|
it("returns quickly when GeoIP resolution times out", async () => {
|
|
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cloak-geoip-timeout-"));
|
|
tempDirs.push(cacheDir);
|
|
process.env.CLOAKBROWSER_CACHE_DIR = cacheDir;
|
|
process.env.CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS = "0.025";
|
|
|
|
const start = performance.now();
|
|
const result = await maybeResolveGeoip({
|
|
geoip: true,
|
|
proxy: "http://203.0.113.10:8080",
|
|
timezone: "Europe/Paris",
|
|
locale: "fr-FR",
|
|
});
|
|
const elapsed = performance.now() - start;
|
|
|
|
expect(result).toEqual({ timezone: "Europe/Paris", locale: "fr-FR", exitIp: undefined });
|
|
expect(elapsed).toBeLessThan(500);
|
|
});
|
|
});
|
|
|
|
|
|
describe("COUNTRY_LOCALE_MAP", () => {
|
|
it("contains common countries", () => {
|
|
for (const code of ["US", "GB", "DE", "FR", "JP", "BR", "IL", "RU"]) {
|
|
expect(COUNTRY_LOCALE_MAP[code]).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("values are BCP 47 language-REGION format", () => {
|
|
for (const [code, locale] of Object.entries(COUNTRY_LOCALE_MAP)) {
|
|
const parts = locale.split("-");
|
|
expect(parts).toHaveLength(2);
|
|
expect(parts[0]).toMatch(/^[a-z]{2,3}$/);
|
|
expect(parts[1]).toMatch(/^[A-Z]{2}$/);
|
|
}
|
|
});
|
|
});
|