mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
176 lines
6.6 KiB
JavaScript
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/);
|
|
});
|
|
});
|
|
});
|