Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/web/__tests__/ddg.test.ts

Raw
import { afterEach, describe, expect, it, vi } from "vitest";
import { DDG_HTML_URL } from "../constants.js";
import * as ddg from "../ddg.js";
import { buildProviders } from "../search.js";
import * as zai from "../zai.js";

afterEach(() => vi.restoreAllMocks());

async function search(signal?: AbortSignal, timeout = 5000) {
	vi.spyOn(zai, "isZaiAvailable").mockResolvedValue(false);
	const providers = await buildProviders({
		query: "Node & Rust + docs",
		count: 5,
		timeout,
		signal,
	});
	const provider = providers.find((entry) => entry.source === "ddg");
	if (!provider) throw new Error("DDG provider missing");
	return provider.search();
}

describe("DDG form POST through the provider HTTP path", () => {
	it("keeps nested result blocks separate and decodes snippet entities", () => {
		const card = (id: number) =>
			`<div class="result results_links"><a class="result__a" href="https://example.com/${id}">Docs ${id}</a><div class="result__extras">URL</div><a class="result__snippet">don&#39;t &amp; &#x1f980; &#999999999; &#xD800;</a></div>`;
		const result = ddg.parseDdgResults(card(1) + card(2));
		expect(result.results).toHaveLength(2);
		expect(result.results.map(({ snippet }) => snippet)).toEqual([
			"don't & 🦀 � �",
			"don't & 🦀 � �",
		]);
		expect(result.diagnostics.rawBlockCount).toBe(2);
	});

	it("posts the no-JS form with origin headers and preserves full snippets", async () => {
		vi.spyOn(ddg, "throttleWait").mockReturnValue(0);
		const snippet = "full excerpt ".repeat(3000).trim();
		// Reduced live HTML: nested result__extras must not terminate the outer result block.
		const html = `<div class="result results_links"><div class="links_main result__body"><h2 class="result__title"><a class="result__a" href="https://example.com/docs">Docs</a></h2><div class="result__extras"><div class="result__extras__url">example.com</div></div><a class="result__snippet">${snippet}</a></div></div>`;
		const network = vi
			.spyOn(globalThis, "fetch")
			.mockResolvedValue(new Response(html));
		expect(await search()).toEqual([
			{ title: "Docs", url: "https://example.com/docs", snippet },
		]);
		const [url, init] = network.mock.calls[0];
		expect(url).toBe(DDG_HTML_URL);
		expect(init?.method).toBe("POST");
		expect(init?.redirect).toBe("manual");
		expect(init?.headers).toMatchObject({
			"Content-Type": "application/x-www-form-urlencoded",
			Origin: "https://html.duckduckgo.com",
			Referer: "https://html.duckduckgo.com/",
		});
		expect(Object.fromEntries(new URLSearchParams(String(init?.body)))).toEqual(
			{ q: "Node & Rust + docs", b: "", kl: "wt-wt" },
		);
	});

	it("still reports CAPTCHA as failure rather than empty results", async () => {
		vi.spyOn(ddg, "throttleWait").mockReturnValue(0);
		vi.spyOn(globalThis, "fetch").mockResolvedValue(
			new Response('<div class="anomaly-modal">Challenge</div>', {
				status: 202,
			}),
		);
		await expect(search()).rejects.toThrow(/captcha/i);
	});

	it("refuses redirects rather than forwarding the search form", async () => {
		vi.spyOn(ddg, "throttleWait").mockReturnValue(0);
		const network = vi.spyOn(globalThis, "fetch").mockResolvedValue(
			new Response(null, {
				status: 302,
				headers: { Location: "http://127.0.0.1/" },
			}),
		);
		await expect(search()).rejects.toThrow(/302/);
		expect(network).toHaveBeenCalledTimes(1);
		expect(network.mock.calls[0][1]?.redirect).toBe("manual");
	});

	it("honors cancellation before I/O", async () => {
		const network = vi.spyOn(globalThis, "fetch");
		await expect(
			search(AbortSignal.abort(new Error("cancelled"))),
		).rejects.toThrow("cancelled");
		expect(network).not.toHaveBeenCalled();
	});

	it("bounds the throttle wait by the request deadline", async () => {
		vi.spyOn(ddg, "throttleWait").mockReturnValue(5000);
		const network = vi.spyOn(globalThis, "fetch");
		await expect(search(new AbortController().signal, 10)).rejects.toThrow(
			/abort/i,
		);
		expect(network).not.toHaveBeenCalled();
	});
});