import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-free-providers-")); process.env.DATA_DIR = TEST_DATA_DIR; // Providers read process.env at call-time, so we can set flags before import process.env.FREE_PROXY_1PROXY_ENABLED = "true"; process.env.FREE_PROXY_PROXIFLY_ENABLED = "false"; process.env.FREE_PROXY_IPLOCATE_ENABLED = "false"; const core = await import("../../src/lib/db/core.ts"); const { getProvider, getEnabledProviders, getAllProviders } = await import("../../src/lib/freeProxyProviders/index.ts"); async function reset() { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); } test.after(() => { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); // ── Registry ───────────────────────────────────────────────────────────────── test("getAllProviders returns exactly 4 providers", () => { const providers = getAllProviders(); assert.equal(providers.length, 4); const ids = providers.map((p) => p.id); assert.ok(ids.includes("1proxy")); assert.ok(ids.includes("proxifly")); assert.ok(ids.includes("iplocate")); assert.ok(ids.includes("webshare")); }); test("getProvider returns the correct provider by id", () => { const p = getProvider("1proxy"); assert.ok(p); assert.equal(p.id, "1proxy"); }); test("getProvider returns undefined for unknown id", () => { const p = getProvider("unknown" as Parameters[0]); assert.equal(p, undefined); }); test("getEnabledProviders respects env flags", () => { // Only 1proxy is enabled in this test env const enabled = getEnabledProviders(); const ids = enabled.map((p) => p.id); assert.ok(ids.includes("1proxy")); assert.ok(!ids.includes("proxifly")); assert.ok(!ids.includes("iplocate")); }); // ── OneproxyProvider ────────────────────────────────────────────────────────── test("OneproxyProvider.isEnabled returns false when env is 'false'", () => { const original = process.env.FREE_PROXY_1PROXY_ENABLED; process.env.FREE_PROXY_1PROXY_ENABLED = "false"; const p = getProvider("1proxy")!; assert.equal(p.isEnabled(), false); process.env.FREE_PROXY_1PROXY_ENABLED = original; }); test("OneproxyProvider.sync returns disabled error when not enabled", async () => { const original = process.env.FREE_PROXY_1PROXY_ENABLED; process.env.FREE_PROXY_1PROXY_ENABLED = "false"; await reset(); const p = getProvider("1proxy")!; const result = await p.sync(); assert.equal(result.fetched, 0); assert.ok(result.errors.length > 0); assert.ok(result.errors[0].includes("disabled")); process.env.FREE_PROXY_1PROXY_ENABLED = original; }); test("OneproxyProvider.sync handles HTTP error and increments failure count", async () => { await reset(); const original = process.env.FREE_PROXY_1PROXY_API_URL; // Point to a URL that returns non-200 process.env.FREE_PROXY_1PROXY_ENABLED = "true"; process.env.FREE_PROXY_1PROXY_API_URL = "http://127.0.0.1:1/nonexistent"; const p = getProvider("1proxy")!; const result = await p.sync(); assert.equal(result.fetched, 0); assert.ok(result.errors.length > 0); process.env.FREE_PROXY_1PROXY_API_URL = original ?? ""; }); test("OneproxyProvider.list delegates to listFreeProxiesBySource", async () => { await reset(); process.env.FREE_PROXY_1PROXY_ENABLED = "true"; const freeProxiesDb = await import("../../src/lib/db/freeProxies.ts"); await freeProxiesDb.upsertFreeProxy({ source: "1proxy", host: "99.0.0.1", port: 8080, type: "http", countryCode: "US", qualityScore: 70, latencyMs: 100, anonymity: null, lastValidated: null, }); const p = getProvider("1proxy")!; const items = await p.list({ limit: 10 }); assert.ok(Array.isArray(items)); assert.ok(items.length >= 1); assert.equal(items[0].source, "1proxy"); }); // ── ProxiflyProvider ────────────────────────────────────────────────────────── test("ProxiflyProvider.isEnabled returns true when not set (enabled by default)", () => { const original = process.env.FREE_PROXY_PROXIFLY_ENABLED; delete process.env.FREE_PROXY_PROXIFLY_ENABLED; const p = getProvider("proxifly")!; assert.equal(p.isEnabled(), true); if (original !== undefined) process.env.FREE_PROXY_PROXIFLY_ENABLED = original; }); test("ProxiflyProvider.isEnabled returns true when explicitly set", () => { const original = process.env.FREE_PROXY_PROXIFLY_ENABLED; process.env.FREE_PROXY_PROXIFLY_ENABLED = "true"; const p = getProvider("proxifly")!; assert.equal(p.isEnabled(), true); process.env.FREE_PROXY_PROXIFLY_ENABLED = original ?? ""; }); test("ProxiflyProvider.sync returns disabled error when not enabled", async () => { const original = process.env.FREE_PROXY_PROXIFLY_ENABLED; process.env.FREE_PROXY_PROXIFLY_ENABLED = "false"; await reset(); const p = getProvider("proxifly")!; const result = await p.sync(); assert.equal(result.fetched, 0); assert.ok(result.errors.some((e) => e.includes("disabled"))); process.env.FREE_PROXY_PROXIFLY_ENABLED = original ?? ""; }); test("ProxiflyProvider.sync fetches proxies in API-sized batches", async () => { await reset(); const originalEnabled = process.env.FREE_PROXY_PROXIFLY_ENABLED; const originalQuantity = process.env.FREE_PROXY_PROXIFLY_QUANTITY; const originalFetch = globalThis.fetch; const requestedQuantities: string[] = []; process.env.FREE_PROXY_PROXIFLY_ENABLED = "true"; process.env.FREE_PROXY_PROXIFLY_QUANTITY = "25"; globalThis.fetch = (async (input: RequestInfo | URL, _init?: RequestInit) => { const url = new URL(String(input)); requestedQuantities.push(url.searchParams.get("quantity") || ""); assert.equal(url.searchParams.get("format"), "json"); assert.equal(url.searchParams.get("protocol"), "http"); assert.equal(url.searchParams.get("anonymity"), "elite"); const quantity = Number(url.searchParams.get("quantity")); const batchIndex = requestedQuantities.length - 1; const body = Array.from({ length: quantity }, (_, index) => ({ ip: `42.${batchIndex}.${index + 1}.1`, port: 8000 + index, protocol: "http", anonymity: "elite", score: 50 + index, geolocation: { country: "US" }, })); if (batchIndex === 0) { body[0] = null as unknown as (typeof body)[number]; } return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }); }) as typeof fetch; try { const p = getProvider("proxifly")!; const result = await p.sync(); assert.deepEqual(requestedQuantities, ["20", "5"]); assert.equal(result.fetched, 24); assert.equal(result.added, 24); assert.equal(result.updated, 0); assert.deepEqual(result.errors, []); const items = await p.list({ limit: 30 }); assert.equal(items.length, 24); assert.ok(items.every((item) => item.source === "proxifly")); } finally { globalThis.fetch = originalFetch; process.env.FREE_PROXY_PROXIFLY_ENABLED = originalEnabled ?? ""; process.env.FREE_PROXY_PROXIFLY_QUANTITY = originalQuantity ?? ""; } }); // ── IplocateProvider ────────────────────────────────────────────────────────── test("IplocateProvider.isEnabled returns false by default", () => { const original = process.env.FREE_PROXY_IPLOCATE_ENABLED; delete process.env.FREE_PROXY_IPLOCATE_ENABLED; const p = getProvider("iplocate")!; assert.equal(p.isEnabled(), false); if (original !== undefined) process.env.FREE_PROXY_IPLOCATE_ENABLED = original; }); test("IplocateProvider.sync returns disabled error when not enabled", async () => { const original = process.env.FREE_PROXY_IPLOCATE_ENABLED; process.env.FREE_PROXY_IPLOCATE_ENABLED = "false"; await reset(); const p = getProvider("iplocate")!; const result = await p.sync(); assert.equal(result.fetched, 0); assert.ok(result.errors.some((e) => e.includes("disabled"))); process.env.FREE_PROXY_IPLOCATE_ENABLED = original ?? ""; }); test("IplocateProvider.sync parses the plain-text ip:port lists (.txt, not .json) (#5595)", async () => { const original = process.env.FREE_PROXY_IPLOCATE_ENABLED; const originalFetch = globalThis.fetch; process.env.FREE_PROXY_IPLOCATE_ENABLED = "true"; await reset(); const seenUrls: string[] = []; globalThis.fetch = (async (input: RequestInfo | URL) => { seenUrls.push(String(input)); // The iplocate/free-proxy-list repo serves one `ip:port` per line (no JSON). // Include a blank line and a comment to exercise line-skipping. const body = "103.173.141.10:8080\n8.211.49.86:9028\n\n# comment line\n"; return new Response(body, { status: 200, headers: { "content-type": "text/plain" } }); }) as typeof fetch; try { const p = getProvider("iplocate")!; const result = await p.sync(); // RED before the fix: the URL ended in `.json` and `res.json()` threw on the // plain-text payload → every protocol errored and 0 proxies were parsed. assert.ok( seenUrls.length > 0 && seenUrls.every((u) => u.endsWith(".txt")), `expected .txt URLs, got: ${seenUrls.join(", ")}` ); assert.ok(result.fetched > 0, `expected proxies parsed from the txt list, got ${result.fetched}`); const items = await p.list({ limit: 50 }); assert.ok( items.some((i) => i.host === "103.173.141.10" && i.port === 8080), "parsed ip:port must be stored" ); assert.ok(items.every((i) => i.source === "iplocate")); } finally { globalThis.fetch = originalFetch; process.env.FREE_PROXY_IPLOCATE_ENABLED = original ?? ""; } });