Files
2026-10-27 21:32:51 +00:00

176 lines
6.6 KiB
JavaScript

import assert from "node:assert/strict";
import * as lib from "@clusterio/lib";
import "../setup_logging.js";
describe("lib/npm_registry", function() {
describe("fetchLatestVersions()", function() {
const _fetch = global.fetch;
let calls;
let cache;
beforeEach(function() {
calls = [];
cache = new Map();
global.fetch = async function(url, options) {
calls.push(url);
assert.ok(options.signal, "lookups have a timeout");
if (url.includes("broken")) {
throw new Error("network down");
}
if (url.includes("missing")) {
return { ok: false, status: 404 };
}
if (url.includes("limited")) {
return { ok: false, status: 429 };
}
return { ok: true, json: async () => ({ version: "1.2.3" }) };
};
});
after(function() {
global.fetch = _fetch;
});
it("returns the latest version of found packages", async function() {
const versions = await lib.fetchLatestVersions(
["latest-a", "@scope/latest-b", "missing-a", "broken-a"], cache,
);
assert.deepEqual(versions, { "latest-a": "1.2.3", "@scope/latest-b": "1.2.3" });
assert.ok(calls.includes("https://registry.npmjs.com/@scope/latest-b/latest"));
});
it("caches lookups", async function() {
await lib.fetchLatestVersions(["cached-a", "missing-b"], cache);
calls = [];
assert.deepEqual(await lib.fetchLatestVersions(["cached-a", "missing-b"], cache), { "cached-a": "1.2.3" });
assert.deepEqual(calls, []);
});
it("retries packages the registry couldn't be reached for or refused", async function() {
await lib.fetchLatestVersions(["broken-b", "limited-a"], cache);
calls = [];
await lib.fetchLatestVersions(["broken-b", "limited-a"], cache);
assert.equal(calls.length, 2);
});
it("drops expired lookups and keeps the cache bounded", async function() {
cache.set("expired", { version: "0.0.1", fetchedAt: Date.now() - 10 * 60e3 });
await lib.fetchLatestVersions(["fresh"], cache);
assert.equal(cache.has("expired"), false);
for (let i = 0; i < 1000; i++) {
cache.set(`filler-${i}`, { version: "1.0.0", fetchedAt: Date.now() });
}
await lib.fetchLatestVersions(["newest"], cache);
assert.equal(cache.size, 1000);
assert.equal(cache.get("newest").version, "1.2.3");
});
it("limits concurrent lookups", async function() {
let active = 0;
let maxActive = 0;
global.fetch = async function() {
active += 1;
maxActive = Math.max(maxActive, active);
await new Promise(resolve => setImmediate(resolve));
active -= 1;
return { ok: true, json: async () => ({ version: "1.0.0" }) };
};
const names = Array.from({ length: 50 }, (_, i) => `package-${i}`);
assert.equal(Object.keys(await lib.fetchLatestVersions(names, cache)).length, 50);
assert.ok(maxActive <= 8, `${maxActive} concurrent lookups`);
});
it("rejects invalid names", async function() {
await assert.rejects(lib.fetchLatestVersions(["foo@1.0.0"], cache), /Invalid plugin name/);
await assert.rejects(lib.fetchLatestVersions(["foo bar"], cache), /Invalid plugin name/);
});
});
describe("searchPlugins()", function() {
const _fetch = global.fetch;
const registryResponse = {
total: 1,
objects: [{
package: {
name: "@clusterio/plugin-foo",
version: "1.0.0",
description: "Foo",
date: "2026-01-01T00:00:00.000Z",
publisher: { username: "bar" },
links: {
npm: "https://www.npmjs.com/package/@clusterio/plugin-foo",
homepage: "https://example.com",
repository: "https://github.com/example/foo",
},
},
}],
};
after(function() {
global.fetch = _fetch;
});
it("queries the registry with the plugin keyword", async function() {
let calledWith;
global.fetch = function(url) {
calledWith = new URL(url);
return { ok: true, json: async () => registryResponse };
};
const response = await lib.searchPlugins("foo", 2, 10);
assert.equal(calledWith.origin + calledWith.pathname, "https://registry.npmjs.com/-/v1/search");
assert.equal(calledWith.searchParams.get("text"), "keywords:clusterio-plugin foo");
assert.equal(calledWith.searchParams.get("size"), "10");
assert.equal(calledWith.searchParams.get("from"), "10");
assert.deepEqual(response, new lib.PluginSearchRequest.Response(1, [
new lib.PluginSearchResult(
"@clusterio/plugin-foo", "1.0.0", "Foo", "bar", "2026-01-01T00:00:00.000Z",
"https://example.com", "https://github.com/example/foo",
"https://www.npmjs.com/package/@clusterio/plugin-foo",
),
]));
});
it("omits the query when empty", async function() {
let calledWith;
global.fetch = function(url) {
calledWith = new URL(url);
return { ok: true, json: async () => ({ total: 0, objects: [] }) };
};
await lib.searchPlugins("", 1, 20);
assert.equal(calledWith.searchParams.get("text"), "keywords:clusterio-plugin");
assert.equal(calledWith.searchParams.get("from"), "0");
});
it("drops optional fields the registry sends as null", async function() {
global.fetch = function() {
return { ok: true, json: async () => ({ total: 1, objects: [{ package: {
name: "foo", version: "1.0.0", description: null, date: null,
publisher: { username: null }, links: { npm: null, homepage: null, repository: null },
} }] }) };
};
const response = await lib.searchPlugins("", 1, 20);
assert.deepEqual(response, new lib.PluginSearchRequest.Response(1, [
new lib.PluginSearchResult("foo", "1.0.0"),
]));
const validate = lib.compile(lib.PluginSearchRequest.Response.jsonSchema);
assert.ok(validate(JSON.parse(JSON.stringify(response))), JSON.stringify(validate.errors));
});
it("handles missing optional fields", async function() {
global.fetch = function() {
return { ok: true, json: async () => ({ total: 1, objects: [{
package: { name: "foo", version: "1.0.0" },
}]}) };
};
const response = await lib.searchPlugins("", 1, 20);
assert.deepEqual(response.results, [new lib.PluginSearchResult("foo", "1.0.0")]);
});
it("rejects invalid paging", async function() {
await assert.rejects(lib.searchPlugins("", 0, 20), /Invalid page or page size/);
await assert.rejects(lib.searchPlugins("", 1, 0), /Invalid page or page size/);
await assert.rejects(lib.searchPlugins("", 1, 251), /Invalid page or page size/);
});
it("rejects overly long queries", async function() {
await assert.rejects(lib.searchPlugins("a".repeat(215), 1, 20), /Search query too long/);
});
it("rejects when the registry errors", async function() {
global.fetch = function() {
return { ok: false, status: 503, statusText: "Service Unavailable" };
};
await assert.rejects(lib.searchPlugins("", 1, 20), /npm registry search failed: 503/);
});
});
});