test: add widget proxy tests (batch 4)

This commit is contained in:
shamoon
2026-02-03 13:54:54 -08:00
parent 02c1435b5e
commit 5a06c22e8e
11 changed files with 718 additions and 1 deletions

View File

@@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => store.get(k)),
put: vi.fn((k, v) => store.set(k, v)),
del: vi.fn((k) => store.delete(k)),
_reset: () => store.clear(),
},
logger: {
debug: vi.fn(),
error: vi.fn(),
},
};
});
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("widgets/widgets", () => ({
default: {
crowdsec: {
api: "{url}/{endpoint}",
loginURL: "{url}/login",
},
},
}));
import crowdsecProxyHandler from "./proxy";
describe("widgets/crowdsec/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("logs in, caches a token, and uses it for requests", async () => {
getServiceWidget.mockResolvedValue({
type: "crowdsec",
url: "http://cs",
username: "machine",
password: "pw",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ token: "tok", expire: new Date(Date.now() + 60_000).toISOString() }),
])
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } };
const res = createMockRes();
await crowdsecProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(2);
expect(httpProxy.mock.calls[1][1].headers.Authorization).toBe("Bearer tok");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data"));
});
it("returns 500 if token cannot be obtained", async () => {
getServiceWidget.mockResolvedValue({ type: "crowdsec", url: "http://cs", username: "machine", password: "pw" });
httpProxy.mockResolvedValueOnce([200, "application/json", JSON.stringify({ expire: "2099-01-01T00:00:00Z" })]);
const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } };
const res = createMockRes();
await crowdsecProxyHandler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" });
});
});

View File

@@ -69,7 +69,7 @@ export default async function frigateProxyHandler(req, res, map) {
data = asJson(data);
if (endpoint == "stats") {
res.status(status).send({
return res.status(status).send({
num_cameras: data?.cameras !== undefined ? Object.keys(data?.cameras).length : 0,
uptime: data?.service?.uptime,
version: data?.service.version,

View File

@@ -0,0 +1,71 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, cookieJar, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cookieJar: {
addCookieToJar: vi.fn(),
},
logger: {
debug: vi.fn(),
error: vi.fn(),
},
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/proxy/cookie-jar", () => cookieJar);
vi.mock("widgets/widgets", () => ({
default: {
frigate: {
api: "{url}/api/{endpoint}",
},
},
}));
import frigateProxyHandler from "./proxy";
describe("widgets/frigate/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("logs in after a 401 and returns derived stats", async () => {
getServiceWidget.mockResolvedValue({
type: "frigate",
url: "http://frigate",
username: "u",
password: "p",
});
httpProxy
// initial request
.mockResolvedValueOnce([401, "application/json", Buffer.from("nope")])
// login
.mockResolvedValueOnce([200, "application/json", Buffer.from("{}"), { "set-cookie": ["sid=1"] }])
// retry stats
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ cameras: { a: {}, b: {} }, service: { uptime: 123, version: "1.0" } })),
]);
const req = { query: { group: "g", service: "svc", endpoint: "stats", index: "0" } };
const res = createMockRes();
await frigateProxyHandler(req, res);
expect(cookieJar.addCookieToJar).toHaveBeenCalled();
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ num_cameras: 2, uptime: 123, version: "1.0" });
});
});

View File

@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, xml2json, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
xml2json: vi.fn((xml) => {
const xmlString = Buffer.isBuffer(xml) ? xml.toString() : xml;
if (xmlString === "GetStatusInfo") {
return JSON.stringify({
elements: [
{
elements: [
{
elements: [
{
elements: [
{ name: "NewConnectionStatus", elements: [{ text: "Connected" }] },
{ name: "NewUptime", elements: [{ text: "42" }] },
],
},
],
},
],
},
],
});
}
return JSON.stringify({ elements: [] });
}),
logger: { debug: vi.fn() },
}));
vi.mock("xml-js", () => ({
xml2json,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
import fritzboxProxyHandler from "./proxy";
describe("widgets/fritzbox/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("queries the configured fields and returns derived data", async () => {
getServiceWidget.mockResolvedValue({
url: "http://fritz.box",
fields: ["connectionStatus", "uptime"],
});
httpProxy.mockResolvedValueOnce([200, "text/xml", Buffer.from("GetStatusInfo")]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await fritzboxProxyHandler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
connectionStatus: "Connected",
uptime: "42",
}),
);
});
});

View File

@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { GameDig, getServiceWidget, logger } = vi.hoisted(() => ({
GameDig: { query: vi.fn() },
getServiceWidget: vi.fn(),
logger: { error: vi.fn() },
}));
vi.mock("gamedig", () => ({
GameDig,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import gamedigProxyHandler from "./proxy";
describe("widgets/gamedig/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns online=true with server details when query succeeds", async () => {
getServiceWidget.mockResolvedValue({ url: "http://example.com:1234", serverType: "csgo" });
GameDig.query.mockResolvedValue({
name: "Server",
map: "de_dust2",
numplayers: 3,
maxplayers: 10,
bots: [],
ping: 42,
});
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await gamedigProxyHandler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
online: true,
name: "Server",
players: 3,
maxplayers: 10,
}),
);
});
it("returns online=false when query fails", async () => {
getServiceWidget.mockResolvedValue({ url: "http://example.com:1234", serverType: "csgo" });
GameDig.query.mockRejectedValue(new Error("nope"));
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await gamedigProxyHandler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ online: false });
});
});

View File

@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
logger: { debug: vi.fn() },
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
import homeassistantProxyHandler from "./proxy";
describe("widgets/homeassistant/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when custom JSON cannot be parsed", async () => {
getServiceWidget.mockResolvedValue({ url: "http://hass", key: "k", custom: "{not-json" });
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await homeassistantProxyHandler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "Error parsing widget custom label" });
});
it("runs default template queries and returns label/value pairs", async () => {
getServiceWidget.mockResolvedValue({ url: "http://hass", key: "k" });
httpProxy
.mockResolvedValueOnce([200, "text/plain", Buffer.from("1 / 2")])
.mockResolvedValueOnce([200, "text/plain", Buffer.from("3 / 4")])
.mockResolvedValueOnce([200, "text/plain", Buffer.from("5 / 6")]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await homeassistantProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual([
{ label: "homeassistant.people_home", value: "1 / 2" },
{ label: "homeassistant.lights_on", value: "3 / 4" },
{ label: "homeassistant.switches_on", value: "5 / 6" },
]);
});
});

View File

@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, tools, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
tools: {
uniqueRid: vi.fn(() => 123),
sha256: vi.fn(() => "secret"),
validateRid: vi.fn(() => true),
createEncryptionToken: vi.fn(() => "enc-token"),
decrypt: vi.fn((cipherText) => {
if (cipherText === "connect") {
return JSON.stringify({ rid: 123, sessiontoken: "sess" });
}
if (cipherText === "devices") {
return JSON.stringify({ list: [{ name: "myclient", id: "dev1" }] });
}
if (cipherText === "packages") {
return JSON.stringify({
data: [
{ bytesLoaded: 40, bytesTotal: 100, finished: false, speed: 10 },
{ bytesLoaded: 100, bytesTotal: 100, finished: true, speed: 0 },
],
});
}
return JSON.stringify({});
}),
encrypt: vi.fn(() => "encrypted-body"),
},
logger: { debug: vi.fn(), error: vi.fn() },
}));
vi.mock("./tools", () => tools);
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
import jdownloaderProxyHandler from "./proxy";
describe("widgets/jdownloader/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("aggregates package stats from the JDownloader API", async () => {
getServiceWidget.mockResolvedValue({
url: "http://ignored",
username: "user@example.com",
password: "pw",
client: "myclient",
});
httpProxy
.mockResolvedValueOnce([200, "application/json", Buffer.from("connect")])
.mockResolvedValueOnce([200, "application/json", Buffer.from("devices")])
.mockResolvedValueOnce([200, "application/json", Buffer.from("packages")]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await jdownloaderProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(res.body).toEqual({
downloadCount: 2,
bytesRemaining: 60,
totalBytes: 200,
totalSpeed: 10,
});
});
});

View File

@@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, validateWidgetData, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
validateWidgetData: vi.fn(() => true),
logger: { debug: vi.fn(), error: vi.fn() },
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/proxy/validate-widget-data", () => ({
default: validateWidgetData,
}));
vi.mock("widgets/widgets", () => ({
default: {
komodo: {
api: "{url}/{endpoint}",
mappings: {
stats: { body: { hello: "world" } },
},
},
},
}));
import komodoProxyHandler from "./proxy";
describe("widgets/komodo/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
validateWidgetData.mockReturnValue(true);
});
it("POSTs to the unified read endpoint with API key/secret", async () => {
getServiceWidget.mockResolvedValue({ type: "komodo", url: "http://komodo", key: "k", secret: "s" });
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from("ok")]);
const req = { query: { group: "g", service: "svc", endpoint: "stats", index: "0" } };
const res = createMockRes();
await komodoProxyHandler(req, res);
expect(httpProxy.mock.calls[0][0]).toBe("http://komodo/read");
expect(httpProxy.mock.calls[0][1].headers["X-API-Key"]).toBe("k");
expect(httpProxy.mock.calls[0][1].headers["X-API-Secret"]).toBe("s");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("ok"));
});
it("returns 500 when data validation fails", async () => {
validateWidgetData.mockReturnValue(false);
getServiceWidget.mockResolvedValue({ type: "komodo", url: "http://komodo", key: "k", secret: "s" });
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from("bad")]);
const req = { query: { group: "g", service: "svc", endpoint: "stats", index: "0" } };
const res = createMockRes();
await komodoProxyHandler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body.error.message).toBe("Invalid data");
});
});

View File

@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, cache, xml2json, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => store.get(k)),
put: vi.fn((k, v) => store.set(k, v)),
del: vi.fn((k) => store.delete(k)),
_reset: () => store.clear(),
},
xml2json: vi.fn((xml) => {
if (xml === "login") {
return JSON.stringify({ QDocRoot: { authSid: { _cdata: "sid1" } } });
}
if (xml === "system") {
return JSON.stringify({
QDocRoot: {
authPassed: { _cdata: "1" },
func: { ownContent: { root: { cpu: 1 } } },
},
});
}
if (xml === "volume") {
return JSON.stringify({ QDocRoot: { authPassed: { _cdata: "1" }, volume: { ok: true } } });
}
return JSON.stringify({ QDocRoot: { authPassed: { _cdata: "1" } } });
}),
logger: { debug: vi.fn(), error: vi.fn() },
};
});
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
vi.mock("xml-js", () => ({
xml2json,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
import qnapProxyHandler from "./proxy";
describe("widgets/qnap/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("logs in and returns system + volume data", async () => {
getServiceWidget.mockResolvedValue({ url: "http://qnap", username: "u", password: "p" });
httpProxy
// login
.mockResolvedValueOnce([200, "application/xml", Buffer.from("login")])
// system
.mockResolvedValueOnce([200, "application/xml", Buffer.from("system")])
// volume
.mockResolvedValueOnce([200, "application/xml", Buffer.from("volume")]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await qnapProxyHandler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.system).toEqual({ cpu: 1 });
expect(res.body.volume).toEqual(expect.objectContaining({ authPassed: { _cdata: "1" } }));
});
});

View File

@@ -0,0 +1,68 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
logger: { debug: vi.fn(), error: vi.fn() },
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("widgets/widgets", () => ({
default: {
suwayomi: {
api: "{url}/graphql",
},
},
}));
import suwayomiProxyHandler from "./proxy";
describe("widgets/suwayomi/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns extracted counts from GraphQL response (no category)", async () => {
getServiceWidget.mockResolvedValue({ type: "suwayomi", url: "http://su", fields: ["download", "unread"] });
httpProxy.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ data: { download: { totalCount: 2 }, unread: { totalCount: 5 } } })),
]);
const req = { query: { group: "g", service: "svc", endpoint: "graphql", index: "0" } };
const res = createMockRes();
await suwayomiProxyHandler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual([
{ count: 2, label: "suwayomi.download" },
{ count: 5, label: "suwayomi.unread" },
]);
});
it("returns 401 when credentials are invalid", async () => {
getServiceWidget.mockResolvedValue({ type: "suwayomi", url: "http://su", username: "u", password: "p" });
httpProxy.mockResolvedValueOnce([401, "application/json", Buffer.from("{}")]);
const req = { query: { group: "g", service: "svc", endpoint: "graphql", index: "0" } };
const res = createMockRes();
await suwayomiProxyHandler(req, res);
expect(res.statusCode).toBe(401);
expect(res.body.error.message).toContain("unauthorized");
});
});

View File

@@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
logger: { debug: vi.fn(), error: vi.fn() },
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("widgets/widgets", () => ({
default: {
tdarr: {
api: "{url}/api",
},
},
}));
import tdarrProxyHandler from "./proxy";
describe("widgets/tdarr/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("POSTs the stats request and includes the API key header", async () => {
getServiceWidget.mockResolvedValue({ type: "tdarr", url: "http://td", key: "k" });
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from("ok")]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await tdarrProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(1);
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://td/api");
expect(httpProxy.mock.calls[0][1].headers["x-api-key"]).toBe("k");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("ok"));
});
});