From 8e9b3e217a5bdc61eda111db51b9c07bfc76a566 Mon Sep 17 00:00:00 2001 From: Mcdowell Terence Date: Sun, 31 May 2026 14:43:11 +0200 Subject: [PATCH] fix(deps): remove proxifly package dependency --- package-lock.json | 227 +----------------------- package.json | 1 - src/lib/freeProxyProviders/proxifly.ts | 130 ++++++++++---- tests/unit/free-proxy-providers.test.ts | 56 +++++- 4 files changed, 151 insertions(+), 263 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7ba5d616a..bc576f81da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,6 @@ "pino-abstract-transport": "^3.0.0", "pino-pretty": "^13.1.3", "playwright": "1.60.0", - "proxifly": "^3.0.1", "react": "19.2.6", "react-dom": "19.2.6", "react-is": "^19.2.6", @@ -7727,6 +7726,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -7955,6 +7955,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -8768,6 +8769,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/concurrently": { @@ -11449,22 +11451,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/fs-jetpack": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-4.3.1.tgz", - "integrity": "sha512-dbeOK84F6BiQzk2yqqCVwCPWTxAvVGJ3fMQc6E2wuEohS28mR6yHngbrKuVCK1KHRx/ccByDylqu4H5PCP2urQ==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.2", - "rimraf": "^2.6.3" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -12635,17 +12621,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -13767,58 +13742,6 @@ "node": ">= 0.4" } }, - "node_modules/itwcw-package-analytics": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/itwcw-package-analytics/-/itwcw-package-analytics-1.0.8.tgz", - "integrity": "sha512-Uvu8uj2iDlAAKkugGSJ0WD1dSZW7wec8Vso8qvcfKfmtHz/QGqITdu8TPsle5FuNnIm9fr/H3vjM1YfzLj37jA==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "fs-jetpack": "^4.3.1", - "uuid": "^9.0.1", - "wonderful-fetch": "^1.3.4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/itwcw-package-analytics/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/itwcw-package-analytics/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/itwcw-package-analytics/node_modules/wonderful-fetch": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/wonderful-fetch/-/wonderful-fetch-1.3.4.tgz", - "integrity": "sha512-vyo+dH0e7kqFBd6hHuU+3VO8+N6iQ87gLrg6cwdj63VaXyXBsZKiSO6CoWeKzK4eRvdlUMB4hnsfNXK+sYdGgQ==", - "license": "MIT", - "dependencies": { - "fs-jetpack": "^4.3.1", - "itwcw-package-analytics": "^1.0.6", - "json5": "^2.2.1", - "mime-types": "^2.1.35", - "node-fetch": "^2.7.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -14025,6 +13948,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -16063,6 +15987,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -16422,48 +16347,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-loader": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz", @@ -17022,15 +16905,6 @@ "node": ">=14.0.0" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -17487,19 +17361,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxifly": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/proxifly/-/proxifly-3.0.1.tgz", - "integrity": "sha512-tb3fnZLF/H4d2jQr6sO9NVRqTXmJgNZtb3cEuR7bTEqiGDx6weaFZqXSxaxZQYA+KsE4u2OW+nPchvZ3rK9FVg==", - "license": "MIT", - "dependencies": { - "itwcw-package-analytics": "^1.0.8", - "wonderful-fetch": "^2.0.4" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -18344,40 +18205,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -21275,50 +21102,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wonderful-fetch": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/wonderful-fetch/-/wonderful-fetch-2.0.5.tgz", - "integrity": "sha512-V8uDL2A+ZDxVA5s11QsOGBaMgr4i9v7kM4yacyThyrKvnj0VpWqMPxxZLZ3/UkH+5/jq9XyK0oMXyZpRT1E8LQ==", - "license": "MIT", - "dependencies": { - "fs-jetpack": "^5.1.0", - "itwcw-package-analytics": "^1.0.8", - "mime-types": "^3.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/wonderful-fetch/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/wonderful-fetch/node_modules/fs-jetpack": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-5.1.0.tgz", - "integrity": "sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==", - "license": "MIT", - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/wonderful-fetch/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 5ed04610bc..aa7363f81b 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,6 @@ "pino-abstract-transport": "^3.0.0", "pino-pretty": "^13.1.3", "playwright": "1.60.0", - "proxifly": "^3.0.1", "react": "19.2.6", "react-dom": "19.2.6", "react-is": "^19.2.6", diff --git a/src/lib/freeProxyProviders/proxifly.ts b/src/lib/freeProxyProviders/proxifly.ts index 6895952118..3d1a9d3026 100644 --- a/src/lib/freeProxyProviders/proxifly.ts +++ b/src/lib/freeProxyProviders/proxifly.ts @@ -3,16 +3,53 @@ import { isPrivateHost } from "@/shared/network/outboundUrlGuard"; const DEFAULT_QUANTITY = 100; const DEFAULT_ANONYMITY = "elite"; +const DEFAULT_API_URL = "https://api.proxifly.dev/proxy"; +const DEFAULT_PROTOCOL = "http"; +const DEFAULT_TIMEOUT_MS = 15_000; +const MAX_BATCH_QUANTITY = 20; type ProxiflyProxy = { - ip: string; - port: number; - protocol: string; - country: string; - anonymity: string; - speed: number; + ip?: string; + port?: number | string; + protocol?: string; + country?: string; + anonymity?: string; + speed?: number; + score?: number; + quality_score?: number; + geolocation?: { + country?: string | null; + } | null; }; +function parsePositiveInt(value: string | undefined, fallback: number): number { + const parsed = parseInt(value || "", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function normalizeProxyResponse(value: unknown): ProxiflyProxy[] { + if (Array.isArray(value)) return value as ProxiflyProxy[]; + if (value && typeof value === "object") { + const maybeWrapped = value as { proxies?: unknown }; + if (Array.isArray(maybeWrapped.proxies)) return maybeWrapped.proxies as ProxiflyProxy[]; + return [value as ProxiflyProxy]; + } + return []; +} + +function normalizeProxyType(protocol: string | undefined): FreeProxyItem["type"] { + const normalized = protocol?.toLowerCase(); + return normalized === "https" || normalized === "socks4" || normalized === "socks5" + ? normalized + : "http"; +} + +function normalizeQualityScore(proxy: ProxiflyProxy): number | null { + const raw = proxy.speed ?? proxy.quality_score ?? proxy.score; + if (raw == null || !Number.isFinite(raw)) return null; + return Math.min(100, Math.max(0, Math.round(raw))); +} + export class ProxiflyProvider implements FreeProxyProvider { readonly id = "proxifly" as const; readonly name = "Proxifly"; @@ -27,50 +64,65 @@ export class ProxiflyProvider implements FreeProxyProvider { } const { upsertFreeProxy } = await import("../db/freeProxies"); - const quantity = - parseInt(process.env.FREE_PROXY_PROXIFLY_QUANTITY || "", 10) || DEFAULT_QUANTITY; + const quantity = parsePositiveInt(process.env.FREE_PROXY_PROXIFLY_QUANTITY, DEFAULT_QUANTITY); const anonymity = process.env.FREE_PROXY_PROXIFLY_ANONYMITY || DEFAULT_ANONYMITY; const errors: string[] = []; let added = 0; let updated = 0; let fetched = 0; + let requested = 0; try { - const proxiflyModule = await import("proxifly"); - const proxifly = proxiflyModule.default ?? proxiflyModule; - const result = await (proxifly as { getProxy: (opts: unknown) => Promise }).getProxy( - { - protocol: "http", - anonymity: anonymity as "elite" | "anonymous" | "transparent", - speed: "fast", - quantity, - } - ); + while (requested < quantity) { + const batchQuantity = Math.min(MAX_BATCH_QUANTITY, quantity - requested); + const url = new URL(DEFAULT_API_URL); + url.searchParams.set("format", "json"); + url.searchParams.set("quantity", String(batchQuantity)); + url.searchParams.set("protocol", DEFAULT_PROTOCOL); + url.searchParams.set("anonymity", anonymity); - const proxies: ProxiflyProxy[] = Array.isArray(result) ? result : [result as ProxiflyProxy]; + const res = await fetch(url, { + signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS), + }); - for (const p of proxies) { - if (!p.ip || !p.port) continue; - if (isPrivateHost(p.ip)) { - errors.push(`Proxifly: skipped private/loopback host ${p.ip}`); - continue; + if (!res.ok) { + const text = await res.text().catch(() => ""); + errors.push(`HTTP ${res.status}: ${text.slice(0, 100)}`); + break; } - const item: FreeProxyItem = { - source: "proxifly", - host: p.ip, - port: Number(p.port), - type: (p.protocol || "http").toLowerCase() as FreeProxyItem["type"], - countryCode: p.country?.slice(0, 2).toUpperCase() || null, - qualityScore: p.speed != null ? Math.min(100, Math.max(0, Math.round(p.speed))) : null, - latencyMs: null, - anonymity: p.anonymity || null, - lastValidated: new Date().toISOString(), - }; - const r = await upsertFreeProxy(item); - if (r.action === "created") added++; - else updated++; - fetched++; + + const proxies = normalizeProxyResponse(await res.json()); + if (proxies.length === 0) break; + requested += proxies.length; + + for (const p of proxies) { + if (!p.ip || !p.port) continue; + if (isPrivateHost(p.ip)) { + errors.push(`Proxifly: skipped private/loopback host ${p.ip}`); + continue; + } + const item: FreeProxyItem = { + source: "proxifly", + host: p.ip, + port: Number(p.port), + type: normalizeProxyType(p.protocol), + countryCode: + p.geolocation?.country?.slice(0, 2).toUpperCase() || + p.country?.slice(0, 2).toUpperCase() || + null, + qualityScore: normalizeQualityScore(p), + latencyMs: null, + anonymity: p.anonymity || null, + lastValidated: new Date().toISOString(), + }; + const r = await upsertFreeProxy(item); + if (r.action === "created") added++; + else updated++; + fetched++; + } + + if (proxies.length < batchQuantity) break; } } catch (err) { errors.push(err instanceof Error ? err.message : String(err)); diff --git a/tests/unit/free-proxy-providers.test.ts b/tests/unit/free-proxy-providers.test.ts index 2c075ec69b..ac566d289e 100644 --- a/tests/unit/free-proxy-providers.test.ts +++ b/tests/unit/free-proxy-providers.test.ts @@ -45,7 +45,7 @@ test("getProvider returns the correct provider by id", () => { }); test("getProvider returns undefined for unknown id", () => { - const p = getProvider("unknown" as any); + const p = getProvider("unknown" as Parameters[0]); assert.equal(p, undefined); }); @@ -154,6 +154,60 @@ test("ProxiflyProvider.sync returns disabled error when not enabled", async () = 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" }, + })); + + 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, 25); + assert.equal(result.added, 25); + assert.equal(result.updated, 0); + assert.deepEqual(result.errors, []); + + const items = await p.list({ limit: 30 }); + assert.equal(items.length, 25); + 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", () => {