// REQ-PLUG-2 REQ-HARN-2: the searxng search-source module loads through the real loader and // answers from a fake SearXNG. No test touches the network. import test from "node:test"; import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { createLoader } from "../src/loader.mjs"; import { WEB_REFUSAL, WebRefusal } from "../../discord/src/web.mjs"; const fakeNet = (status, body) => { const requests = []; return { requests, lookup: async () => [{ address: "93.184.216.34", family: 4 }], httpsRequest: (opts) => { const req = new EventEmitter(); req.destroy = () => {}; req.end = () => { requests.push(`${opts.hostname}${opts.path}`); const res = new EventEmitter(); res.statusCode = status; res.headers = { "content-type": "application/json" }; res.destroy = () => {}; queueMicrotask(() => { req.emit("response", res); res.emit("data", Buffer.from(body)); res.emit("end"); }); }; return req; }, }; }; test("REQ-PLUG-2 the real tree's searxng-url module switches on as the search-source and answers", async () => { const logs = []; const loader = createLoader({ log: (e) => logs.push(e) }); const { loaded, refused } = await loader.load({ "search-source": "searxng-url" }); assert.deepEqual(refused, []); assert.deepEqual(loaded["search-source"], ["searxng-url"]); const [mod] = loader.modules("search-source"); const net = fakeNet(200, JSON.stringify({ results: [{ title: " A ", url: "https://example.org/a", content: "about a" }, { url: "javascript:x" }] })); const found = await mod.search({ searxng: "https://search.example.org" }, "mosaic", net); assert.deepEqual(found, { query: "mosaic", total: 2, results: [{ title: "A", url: "https://example.org/a", snippet: "about a" }] }); assert.match(net.requests[0], /^search\.example\.org\/search\?q=mosaic&format=json$/); }); test("REQ-PLUG-2 a SearXNG that refuses JSON (403) is search-down, a bad settings url throws", async () => { const loader = createLoader({ log: () => {} }); await loader.load({ "search-source": "searxng-url" }); const [mod] = loader.modules("search-source"); await assert.rejects(mod.search({ searxng: "https://search.example.org" }, "x", fakeNet(403, "no")), (e) => e instanceof WebRefusal && e.reason === WEB_REFUSAL.SEARCH_DOWN); assert.throws(() => mod.search({ searxng: "http://search.example.org" }, "x"), /https/); }); test("REQ-PLUG-2 search-source takes one name, not a list", async () => { const loader = createLoader({ log: () => {} }); const { refused } = await loader.load({ "search-source": ["searxng-url"] }); assert.equal(refused[0].code, "bad-setting"); assert.deepEqual(loader.loaded()["search-source"], []); }); test("REQ-PLUG-2 searxng-bundled queries the fixed loopback address and ignores web.searxng", async () => { const loader = createLoader({ log: () => {} }); const { loaded } = await loader.load({ "search-source": "searxng-bundled" }); assert.deepEqual(loaded["search-source"], ["searxng-bundled"]); const [mod] = loader.modules("search-source"); const body = JSON.stringify({ results: [{ title: "B", url: "https://example.org/b", content: "b" }] }); const net = fakeNet(200, body); net.httpRequest = net.httpsRequest; net.lookup = async () => [{ address: "127.0.0.1", family: 4 }]; const found = await mod.search({ searxng: "https://elsewhere.example" }, "q", net); assert.equal(found.results.length, 1); assert.match(net.requests[0], /^127\.0\.0\.1\/search\?q=q&format=json$/); }); test("REQ-PLUG-2 harness-builtin switches on, serves no broker search, and refuses if called", async () => { const loader = createLoader({ log: () => {} }); const { loaded, refused } = await loader.load({ "search-source": "harness-builtin" }); assert.deepEqual(refused, []); assert.deepEqual(loaded["search-source"], ["harness-builtin"]); const [mod] = loader.modules("search-source"); assert.throws(() => mod.search({}, "q"), (e) => e instanceof WebRefusal && e.reason === WEB_REFUSAL.SEARCH_DOWN); }); test("the searxng modules end the request when the broker's AbortSignal fires", async () => { for (const name of ["searxng-url", "searxng-bundled"]) { const loader = createLoader({ log: () => {} }); await loader.load({ "search-source": name }); const [mod] = loader.modules("search-source"); let destroyed = 0; const net = { lookup: async () => [{ address: "127.0.0.1", family: 4 }], httpsRequest: () => { const req = new EventEmitter(); req.destroy = () => { destroyed += 1; }; req.end = () => {}; return req; }, }; net.httpRequest = net.httpsRequest; const controller = new AbortController(); const pending = mod.search({ searxng: "https://search.example.org" }, "q", net, { signal: controller.signal }); await new Promise((r) => setTimeout(r, 20)); controller.abort(); // webSearch reports any failed request as "search is unavailable"; the abort is what ended it. await assert.rejects(pending, (e) => e instanceof WebRefusal && e.reason === WEB_REFUSAL.SEARCH_DOWN, name); assert.ok(destroyed >= 1, name); } }); test("hasModule: true for a shipped module, false for a missing one, an unknown kind or a bad name", async () => { const { hasModule } = await import("../src/loader.mjs"); for (const name of ["harness-builtin", "searxng-bundled", "searxng-url"]) assert.equal(hasModule("search-source", name), true, name); for (const [kind, name] of [["search-source", "ghost"], ["made-up", "searxng-url"], ["search-source", "../x"], ["search-source", "Searxng"], ["usage-source", "searxng-url"]]) assert.equal(hasModule(kind, name), false, `${kind}/${name}`); });