Files

112 lines
4.1 KiB
JavaScript

/**
* @file Tests for server/lib/ai-tool-icons.js — which domain a tool's icon is
* fetched from, the raster-only magic-byte sniff (no SVG), the size cap, the
* off-site redirect refusal, and the hit/miss cache.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, beforeEach, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ai-icons-"));
const cat = path.join(root, "cat");
const empty = path.join(root, "empty");
const data = path.join(root, "data");
for (const d of [cat, empty, data]) fs.mkdirSync(d);
const origData = process.env.DASHBOARD_DATA_DIR;
process.env.DASHBOARD_DATA_DIR = data;
const aiTools = require("../lib/ai-tools");
const { iconFor, iconDomain } = require("../lib/ai-tool-icons");
const tool = (id, extra) => ({ id, label: id, detect: [id], prompt: { mode: "argv" }, ...extra });
const write = (doc) => fs.writeFileSync(path.join(cat, `${doc.id}.json`), JSON.stringify(doc));
let calls;
const PNG = Buffer.from("89504e470d0a1a0a", "hex");
function serve({ body = Buffer.concat([PNG, Buffer.alloc(2)]), url, status = 200 } = {}) {
calls = [];
globalThis.fetch = async (u) => {
calls.push(u);
return {
ok: status === 200,
status,
url: url || u,
headers: { get: () => "image/png" }, // ignored: the bytes decide
arrayBuffer: async () => body,
};
};
}
let origFetch;
before(() => {
origFetch = globalThis.fetch;
write(tool("fav", { faviconDomain: "example.com" }));
write(tool("home", { homepageUrl: "https://docs.example.org/cli" }));
write(tool("ip", { faviconDomain: "127.0.0.1" }));
write(tool("port", { homepageUrl: "https://example.com:8443/x" }));
write(tool("none"));
aiTools.__setCatalogDirs([cat, empty]);
});
beforeEach(() => fs.rmSync(path.join(data, "runtime"), { recursive: true, force: true }));
after(() => {
globalThis.fetch = origFetch;
aiTools.__setCatalogDirs(null);
if (origData === undefined) delete process.env.DASHBOARD_DATA_DIR;
else process.env.DASHBOARD_DATA_DIR = origData;
fs.rmSync(root, { recursive: true, force: true });
});
describe("iconDomain", () => {
it("uses faviconDomain, else the homepage host, and refuses IPs, ports and nothing", () => {
assert.equal(iconDomain(aiTools.get("fav")), "example.com");
assert.equal(iconDomain(aiTools.get("home")), "docs.example.org");
assert.equal(iconDomain(aiTools.get("ip")), null);
assert.equal(iconDomain(aiTools.get("port")), null);
assert.equal(iconDomain(aiTools.get("none")), null);
});
});
describe("iconFor", () => {
it("fetches once and serves the cache after", async () => {
serve();
const a = await iconFor("fav");
assert.equal(a.type, "image/png");
assert.equal(a.body.length, 10);
assert.deepEqual(calls, ["https://example.com/favicon.ico"]);
await iconFor("fav");
assert.equal(calls.length, 1);
});
it("recognises an ico with no content type at all", async () => {
serve({ body: Buffer.from("0000010001002020", "hex") });
assert.equal((await iconFor("fav")).type, "image/x-icon");
});
it("refuses SVG even when labelled image/png, and remembers the miss", async () => {
serve({ body: Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"><script/></svg>') });
assert.equal(await iconFor("fav"), null);
serve();
assert.equal(await iconFor("fav"), null);
assert.equal(calls.length, 0);
});
it("refuses an oversized body and an off-site redirect", async () => {
serve({ body: Buffer.concat([PNG, Buffer.alloc(256 * 1024)]) });
assert.equal(await iconFor("fav"), null);
fs.rmSync(path.join(data, "runtime"), { recursive: true, force: true });
serve({ url: "https://evil.example.net/favicon.ico" });
assert.equal(await iconFor("fav"), null);
});
it("never fetches for a tool without an allowed domain", async () => {
serve();
assert.equal(await iconFor("ip"), null);
assert.equal(await iconFor("nope"), null);
assert.equal(calls.length, 0);
});
});