import { describe, expect, it } from "vitest"; import { dedupeFeeds } from "./dedupe.js"; import { discoverFeeds } from "./discovery.js"; import { parseFeedDocument } from "./feed-parsing.js"; import { renderDiscoveryOutput } from "./output/index.js"; import { WebCandidateFeedProbeProvider } from "./providers/web-feed-probe.js"; import { extractAlternateFeedLinks } from "./probe-site.js"; import { scoreFeed } from "./scoring.js"; import type { DiscoveredFeed, FeedDiscoveryProvider } from "./types.js"; import { assertSafeHttpUrl } from "./url-safety.js"; const baseFeed: DiscoveredFeed = { title: "MicroSaaS Ideas", description: "Ideas for indie SaaS founders", homepageUrl: "https://example.com", feedUrl: "https://example.com/feed.xml", kind: "blog", provider: "manual-curated", score: 0, confidence: 0.5, tags: ["microsaas", "saas"] }; describe("feed parsing", () => { it("parses RSS feeds with sample items", () => { const result = parseFeedDocument("https://example.com/rss.xml", `MicroSaaS Ideashttps://example.comLaunch tiny productshttps://example.com/postTue, 09 Jun 2026 00:00:00 GMT`); expect(result.ok).toBe(true); expect(result.title).toBe("MicroSaaS Ideas"); expect(result.sampleItems[0]).toMatchObject({ title: "Launch tiny products", url: "https://example.com/post" }); }); it("detects podcast RSS feeds from item enclosures", () => { const result = parseFeedDocument( "https://example.com/podcast.xml", `Audio Showhttps://example.comEpisode 1` ); expect(result.ok).toBe(true); expect(result.kind).toBe("podcast"); }); it("parses Atom and JSON Feed documents", () => { const atom = parseFeedDocument("https://example.com/atom.xml", `Atom FeedEntry2026-06-09T00:00:00Z`); const json = parseFeedDocument("https://example.com/feed.json", JSON.stringify({ version: "https://jsonfeed.org/version/1.1", title: "JSON Feed", items: [{ title: "Item", url: "https://example.com/item" }] }), "application/feed+json"); expect(atom.ok).toBe(true); expect(json.ok).toBe(true); }); }); describe("site probing helpers", () => { it("extracts alternate feed links", () => { const links = extractAlternateFeedLinks( ``, "https://example.com/blog" ); expect(links).toEqual(["https://example.com/rss.xml"]); }); it("blocks private SSRF targets", async () => { await expect(assertSafeHttpUrl("http://0.0.0.0/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://127.0.0.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://192.0.2.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://198.18.0.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://198.51.100.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://203.0.113.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://224.0.0.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://240.0.0.1/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://[::]/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://[2001:db8::1]/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://[ff02::1]/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://[::ffff:192.168.1.10]/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("http://[::192.168.1.10]/feed")).rejects.toThrow(/Blocked internal/); await expect(assertSafeHttpUrl("file:///etc/passwd")).rejects.toThrow(/Unsupported URL protocol/); }); it("ignores malformed configured candidate URLs", async () => { const provider = new WebCandidateFeedProbeProvider({ cacheTtlSeconds: 60, maxProviders: 1, maxProbes: 1, requestTimeoutMs: 10, maxBodyBytes: 1024, userAgent: "test", opmlPaths: [], candidateUrls: ["not-a-url|micro"], podcastIndexApiKey: undefined, podcastIndexApiSecret: undefined }); await expect(provider.search({ q: "micro" })).resolves.toEqual([]); }); }); describe("scoring, dedupe, and output", () => { it("scores relevant fresh feeds and dedupes canonical URLs", () => { const scored = scoreFeed({ ...baseFeed, lastPublishedAt: new Date().toISOString() }, { q: "microsaas" }); const duplicate = { ...scored, feedUrl: "https://example.com/feed.xml?utm_source=test", score: scored.score - 0.1 }; expect(scored.score).toBeGreaterThan(0.5); expect(dedupeFeeds([duplicate, scored])).toHaveLength(1); }); it("renders JSON, OPML, and RSS outputs", () => { const response = { query: "microsaas", normalizedQuery: "microsaas", count: 1, providerErrors: [], results: [scoreFeed(baseFeed, { q: "microsaas" })] }; expect(JSON.parse(renderDiscoveryOutput(response, "json"))).toMatchObject({ count: 1 }); expect(renderDiscoveryOutput(response, "opml")).toContain(" { it("isolates provider failures", async () => { const okProvider: FeedDiscoveryProvider = { id: "ok-provider", name: "OK", enabledByDefault: true, requiresApiKey: false, async search() { return [baseFeed]; } }; const failingProvider: FeedDiscoveryProvider = { id: "broken-provider", name: "Broken", enabledByDefault: true, requiresApiKey: false, async search() { throw new Error("provider unavailable"); } }; const response = await discoverFeeds({ q: "microsaas", includeUnvalidated: true }, { providers: [failingProvider, okProvider] }); expect(response.providerErrors).toEqual([{ provider: "broken-provider", error: "provider unavailable" }]); expect(response.results).toHaveLength(1); }); it("enforces the requested freshness window", async () => { const provider: FeedDiscoveryProvider = { id: "freshness-provider", name: "Freshness", enabledByDefault: true, requiresApiKey: false, async search() { return [ { ...baseFeed, feedUrl: "https://example.com/fresh.xml", lastPublishedAt: new Date(Date.now() - 5 * 86_400_000).toISOString() }, { ...baseFeed, feedUrl: "https://example.com/stale.xml", lastPublishedAt: new Date(Date.now() - 60 * 86_400_000).toISOString() }, { ...baseFeed, feedUrl: "https://example.com/unknown.xml", lastPublishedAt: undefined }, { ...baseFeed, feedUrl: "https://example.com/invalid.xml", lastPublishedAt: "not-a-date" }, { ...baseFeed, feedUrl: "https://example.com/future.xml", lastPublishedAt: new Date(Date.now() + 86_400_000).toISOString() } ]; } }; const response = await discoverFeeds( { q: "microsaas", freshnessDays: 30, includeUnvalidated: true }, { providers: [provider] } ); expect(response.results.map((feed) => feed.feedUrl)).toEqual(["https://example.com/fresh.xml"]); }); });