Jason-approved merge, rebased: git diff 813671596..f4d7b91a9 from
work/vikunja-19-305, stacked on 295. Business reviewer verdict 57d7f0b7
approve on f4d7b91a9; Jason resolved merge decision 0eebf83f via the CLI.
Conflict resolution only. packages/bus/src/broker.mjs conflicted with
row 123 (89fd2d6fe): both sides extend the limits branch of request().
Kept row 123's organization.set role path and ORGANIZATION_KEYS, and
added 305's plugins and plugins.set to the verb list and its keys lines.
plugins and plugins.set take the human-only path as in 305: role is true
only for organization.set, the actor is the human, and the verb reaches
the organization writer unchanged. organization.set is unchanged.
packages/plugins/tests/searxng.test.mjs conflicted with 307: both add a
test at the end of the file. Kept 307's AbortSignal test, then 305's
hasModule test. Three other paths that refactor also changed merged
without conflict, with the source commit's +/- lines; the other six
paths are the source blobs. Filbert reviews the rebased diff.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
112 lines
5.7 KiB
JavaScript
112 lines
5.7 KiB
JavaScript
// 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}`);
|
|
});
|