TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import { createServer, type Server } from "node:http";2import { afterAll, beforeAll, describe, expect, it } from "vitest";3import { DecodoProvider, DirectProvider, OxylabsProvider, ProviderError, ProviderRegistry, SoaxProvider, type ProviderRequest, type ProxyProvider } from "../src/index";45const env = {6 OXYLABS_USERNAME: "customer-acme",7 OXYLABS_PASSWORD: "pw1",8 DECODO_USERNAME: "acme",9 DECODO_PASSWORD: "pw2",10 SOAX_USERNAME: "package-123",11 SOAX_PASSWORD: "pw3",12 FETCHA_DIRECT_EGRESS: "1",13} as NodeJS.ProcessEnv;1415const baseReq = (url: string, extra: Partial<ProviderRequest> = {}): ProviderRequest => ({16 requestId: "req_test",17 attemptId: "att_test",18 url,19 method: "GET",20 headers: {},21 timeoutMs: 5000,22 network: "residential",23 geo: { country: null, region: null, city: null },24 followRedirects: true,25 maxRedirects: 5,26 maxResponseBytes: 1_000_000,27 ...extra,28});2930/** Contract every adapter must satisfy. */31function contract(name: string, make: () => ProxyProvider) {32 describe(`${name} contract`, () => {33 const p = make();34 it("is configured when credentials are present", () => expect(p.isConfigured()).toBe(true));35 it("declares at least one network", () => expect(p.networks.length).toBeGreaterThan(0));36 it("prices bandwidth", () => {37 expect(p.pricePerGb(p.networks[0]!)).toBeGreaterThan(0);38 expect(p.estimateCost(p.networks[0]!, 1_073_741_824)).toBeCloseTo(p.pricePerGb(p.networks[0]!));39 expect(p.estimateCost(p.networks[0]!, 0)).toBe(0);40 });41 it("supports worldwide geo", () => expect(p.supportsGeo({ country: "CA", region: null, city: null })).toBe(p.id !== "direct"));42 });43}4445contract("Oxylabs", () => new OxylabsProvider(env));46contract("Decodo", () => new DecodoProvider(env));47contract("SOAX", () => new SoaxProvider(env));48contract("Direct", () => new DirectProvider(env));4950describe("username grammar (geo + sessions)", () => {51 it("oxylabs", () => {52 const p = new OxylabsProvider(env);53 expect(p.endpoint({ geo: { country: "CA", region: "quebec", city: "montreal" }, sessionKey: "abc-123", sessionMinutes: 10 })).toEqual({54 host: "pr.oxylabs.io",55 port: 7777,56 username: "customer-acme-cc-CA-st-ca_quebec-city-montreal-sessid-abc123-sesstime-10",57 password: "pw1",58 });59 expect(p.endpoint({ geo: { country: null, region: null, city: null } }).username).toBe("customer-acme");60 });61 it("decodo", () => {62 const p = new DecodoProvider(env);63 expect(p.endpoint({ geo: { country: "US", region: "new_york", city: null }, sessionKey: "s1", sessionMinutes: 5 })).toEqual({64 host: "gate.decodo.com",65 port: 7000,66 username: "user-acme-country-us-state-us_new_york-session-s1-sessionduration-5",67 password: "pw2",68 });69 });70 it("soax", () => {71 const p = new SoaxProvider(env);72 expect(p.endpoint({ geo: { country: "GB", region: null, city: "london" }, sessionKey: "k", sessionMinutes: 2 }).username).toBe("package-123-country-gb-city-london-sessionid-k-sessionlength-120");73 });74});7576describe("registry", () => {77 it("excludes unconfigured and disabled providers", () => {78 const r = new ProviderRegistry({ env: { OXYLABS_USERNAME: "a", OXYLABS_PASSWORD: "b" } as NodeJS.ProcessEnv });79 expect(r.available().map((p) => p.id)).toEqual(["oxylabs"]);80 expect(r.available("mobile")).toHaveLength(0);81 r.setDisabled("oxylabs", true);82 expect(r.available()).toHaveLength(0);83 });84});8586describe("direct HTTP execution (local server)", () => {87 let server: Server;88 let base = "";89 beforeAll(async () => {90 server = createServer((req, res) => {91 if (req.url === "/redirect") {92 res.writeHead(302, { location: "/final" });93 return res.end();94 }95 if (req.url === "/final") {96 res.writeHead(200, { "content-type": "text/html", "set-cookie": "a=1; Path=/" });97 return res.end("<html><body>final</body></html>");98 }99 if (req.url === "/big") {100 res.writeHead(200, { "content-type": "application/octet-stream" });101 return res.end(Buffer.alloc(50_000, 1));102 }103 if (req.url === "/slow") {104 return setTimeout(() => {105 res.writeHead(200);106 res.end("late");107 }, 2000);108 }109 if (req.url === "/echo") {110 let body = "";111 req.on("data", (c) => (body += c));112 return req.on("end", () => {113 res.writeHead(200, { "content-type": "application/json" });114 res.end(JSON.stringify({ method: req.method, body, ua: req.headers["user-agent"], x: req.headers["x-test"] }));115 });116 }117 res.writeHead(404);118 res.end("nope");119 });120 await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));121 const addr = server.address();122 base = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`;123 });124 afterAll(() => new Promise<void>((r) => server.close(() => r())));125126 const p = new DirectProvider(env);127128 it("follows redirects and validates each hop via onRedirect", async () => {129 const hops: string[] = [];130 const res = await p.fetch(baseReq(`${base}/redirect`, { onRedirect: async (u) => void hops.push(u) }));131 expect(res.status).toBe(200);132 expect(res.redirects).toBe(1);133 expect(hops[0]).toBe(`${base}/final`);134 expect(res.finalUrl).toBe(`${base}/final`);135 expect(res.headers["set-cookie"]).toContain("a=1");136 expect(res.bytesIn).toBeGreaterThan(30);137 });138139 it("aborts redirect chains when the callback throws", async () => {140 await expect(p.fetch(baseReq(`${base}/redirect`, { onRedirect: async () => { throw new Error("blocked"); } }))).rejects.toThrow("blocked");141 });142143 it("enforces the response size cap", async () => {144 await expect(p.fetch(baseReq(`${base}/big`, { maxResponseBytes: 10_000 }))).rejects.toMatchObject({ kind: "too_large" });145 });146147 it("times out", async () => {148 await expect(p.fetch(baseReq(`${base}/slow`, { timeoutMs: 300 }))).rejects.toBeInstanceOf(ProviderError);149 });150151 it("performs POST with headers and body", async () => {152 const res = await p.fetch(baseReq(`${base}/echo`, { method: "POST", body: '{"a":1}', headers: { "x-test": "yes", host: "evil" } }));153 const j = JSON.parse(res.body.toString()) as { method: string; body: string; x: string };154 expect(j.method).toBe("POST");155 expect(j.body).toBe('{"a":1}');156 expect(j.x).toBe("yes");157 });158159 it("records timing", async () => {160 const res = await p.fetch(baseReq(`${base}/final`));161 expect(res.timing.total_ms).toBeGreaterThanOrEqual(0);162 expect(res.timing.origin_ms).toBeGreaterThanOrEqual(0);163 });164});165