diff --git a/src/widgets/crowdsec/proxy.test.js b/src/widgets/crowdsec/proxy.test.js new file mode 100644 index 000000000..7555cf16a --- /dev/null +++ b/src/widgets/crowdsec/proxy.test.js @@ -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" }); + }); +}); diff --git a/src/widgets/frigate/proxy.js b/src/widgets/frigate/proxy.js index b73f5533f..276f9d7c1 100644 --- a/src/widgets/frigate/proxy.js +++ b/src/widgets/frigate/proxy.js @@ -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, diff --git a/src/widgets/frigate/proxy.test.js b/src/widgets/frigate/proxy.test.js new file mode 100644 index 000000000..af6ad442b --- /dev/null +++ b/src/widgets/frigate/proxy.test.js @@ -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" }); + }); +}); diff --git a/src/widgets/fritzbox/proxy.test.js b/src/widgets/fritzbox/proxy.test.js new file mode 100644 index 000000000..c390dea45 --- /dev/null +++ b/src/widgets/fritzbox/proxy.test.js @@ -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", + }), + ); + }); +}); diff --git a/src/widgets/gamedig/proxy.test.js b/src/widgets/gamedig/proxy.test.js new file mode 100644 index 000000000..2b9fe5627 --- /dev/null +++ b/src/widgets/gamedig/proxy.test.js @@ -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 }); + }); +}); diff --git a/src/widgets/homeassistant/proxy.test.js b/src/widgets/homeassistant/proxy.test.js new file mode 100644 index 000000000..7c118ea65 --- /dev/null +++ b/src/widgets/homeassistant/proxy.test.js @@ -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" }, + ]); + }); +}); diff --git a/src/widgets/jdownloader/proxy.test.js b/src/widgets/jdownloader/proxy.test.js new file mode 100644 index 000000000..4312f2508 --- /dev/null +++ b/src/widgets/jdownloader/proxy.test.js @@ -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, + }); + }); +}); diff --git a/src/widgets/komodo/proxy.test.js b/src/widgets/komodo/proxy.test.js new file mode 100644 index 000000000..5364702ef --- /dev/null +++ b/src/widgets/komodo/proxy.test.js @@ -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"); + }); +}); diff --git a/src/widgets/qnap/proxy.test.js b/src/widgets/qnap/proxy.test.js new file mode 100644 index 000000000..1de805528 --- /dev/null +++ b/src/widgets/qnap/proxy.test.js @@ -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" } })); + }); +}); diff --git a/src/widgets/suwayomi/proxy.test.js b/src/widgets/suwayomi/proxy.test.js new file mode 100644 index 000000000..ebbdcce86 --- /dev/null +++ b/src/widgets/suwayomi/proxy.test.js @@ -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"); + }); +}); diff --git a/src/widgets/tdarr/proxy.test.js b/src/widgets/tdarr/proxy.test.js new file mode 100644 index 000000000..03eb5583d --- /dev/null +++ b/src/widgets/tdarr/proxy.test.js @@ -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")); + }); +});