import { describe, expect, it } from "vitest"; import { HttpClient, RateLimiter, TokenBucket, assertPublicUrl, extractTables, htmlFingerprints, isPrivateAddress, parseDelimited, parseFeed, parseNumber, redactObject, redactUrl, schemaFingerprint } from "./index.js"; import { fakeFetch } from "./testing.js"; describe("rate limiter", () => { it("token bucket paces requests", async () => { const b = new TokenBucket(50, 2); const t0 = Date.now(); for (let i = 0; i < 6; i++) await b.acquire(); expect(Date.now() - t0).toBeGreaterThanOrEqual(60); // 4 extra tokens at 50/s ≈ 80 ms }); it("limiter keys by host", () => { const l = new RateLimiter({ ratePerSec: 1 }); l.configure("a.example", 10); expect(l.bucket("A.EXAMPLE").ratePerSec).toBe(10); expect(l.bucket("b.example").ratePerSec).toBe(1); }); }); describe("http client", () => { it("uses ETag for conditional requests and returns cached body on 304", async () => { let calls = 0; const fetchImpl = (async (_url: string | URL | Request, init?: RequestInit) => { calls++; const h = (init?.headers ?? {}) as Record; if (h["if-none-match"] === '"v1"') return new Response(null, { status: 304 }); return new Response('{"a":1}', { status: 200, headers: { etag: '"v1"' } }); }) as typeof fetch; const c = new HttpClient({ userAgent: "t", limiter: new RateLimiter({ ratePerSec: 1000 }), fetchImpl }); const r1 = await c.getJson<{ a: number }>("https://x.example/data"); const r2 = await c.getJson<{ a: number }>("https://x.example/data"); expect(r1.data.a).toBe(1); expect(r2.response.notModified).toBe(true); expect(r2.data.a).toBe(1); expect(calls).toBe(2); }); it("retries 5xx then throws HttpError with redacted URL", async () => { const c = new HttpClient({ userAgent: "t", limiter: new RateLimiter({ ratePerSec: 1000 }), fetchImpl: fakeFetch({ boom: { status: 503, body: "x" } }) }); await expect(c.getText("https://x.example/boom?api_key=SECRET", { retries: 1 })).rejects.toThrow(/HTTP 503.*api_key=\[redacted\]/); }); }); describe("ssrf guard", () => { it("blocks private and internal targets", async () => { await expect(assertPublicUrl("http://127.0.0.1/")).rejects.toThrow(); await expect(assertPublicUrl("http://10.0.0.5/x")).rejects.toThrow(); await expect(assertPublicUrl("http://localhost:8080/")).rejects.toThrow(); await expect(assertPublicUrl("ftp://example.com/")).rejects.toThrow(); await expect(assertPublicUrl("http://169.254.169.254/latest/meta-data")).rejects.toThrow(); await expect(assertPublicUrl("https://m3u96a.maclustr.io/")).rejects.toThrow(); expect(isPrivateAddress("192.168.2.1")).toBe(true); expect(isPrivateAddress("8.8.8.8")).toBe(false); expect(isPrivateAddress("fd00::1")).toBe(true); }); }); describe("parsers", () => { it("schema fingerprint ignores values but not shape", () => { const a = schemaFingerprint({ sym: "AAPL", lp: "1.0", v: 1, ts: 2 }); const b = schemaFingerprint({ sym: "MSFT", lp: "2.0", v: 9, ts: 3 }); const c = schemaFingerprint({ ticker: "AAPL", lastPrice: 1, eventTime: 2 }); expect(a).toBe(b); expect(a).not.toBe(c); }); it("parses numbers in several locales", () => { expect(parseNumber("1,234.5")).toBe(1234.5); expect(parseNumber("1.234,5")).toBe(1234.5); expect(parseNumber("(12.3)")).toBe(-12.3); expect(parseNumber("12.3%")).toBe(12.3); expect(parseNumber("N/A")).toBeNull(); expect(parseNumber("$1,000")).toBe(1000); }); it("parses pipe-delimited and quoted csv", () => { const rows = parseDelimited('a|b\n1|"x|y"\n2|z', "|"); expect(rows).toEqual([{ a: "1", b: "x|y" }, { a: "2", b: "z" }]); }); it("parses RSS and Atom into items", () => { const rss = parseFeed('TAhttp://ag1Fri, 11 Sep 2026 04:00:00 GMT'); expect(rss.items[0]).toMatchObject({ title: "A", link: "http://a", id: "g1" }); const atom = parseFeed('FEi12026-09-11T00:00:00Z'); expect(atom.items[0]).toMatchObject({ title: "E", link: "http://e", id: "i1" }); expect(atom.items[0]!.categories[0]!.term).toBe("8-K"); }); it("extracts tables and fingerprints html sections", () => { const html = "

Title

ab
12

Other

x

"; expect(extractTables(html)).toEqual([[["a", "b"], ["1", "2"]]]); const fp1 = htmlFingerprints(html); const fp2 = htmlFingerprints(html.replace("

x

", "

y

")); expect(fp1.document).not.toBe(fp2.document); expect(fp1.sections["Title"]).toBe(fp2.sections["Title"]); expect(fp1.sections["Other"]).not.toBe(fp2.sections["Other"]); }); it("redacts secrets", () => { expect(redactUrl("https://x/y?token=abc&z=1")).toBe("https://x/y?token=[redacted]&z=1"); expect(redactObject({ headers: { Authorization: "Bearer xyz", accept: "*/*" }, cookie: "a=b", price: 1 })).toEqual({ headers: { Authorization: "[redacted]", accept: "*/*" }, cookie: "[redacted]", price: 1 }); }); });