test: add widget proxy tests (batch 2)

This commit is contained in:
shamoon
2026-02-03 13:42:42 -08:00
parent e99adf200b
commit 5bf187816a
10 changed files with 847 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
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("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
vi.mock("widgets/widgets", () => ({
default: {
booklore: {
api: "{url}/{endpoint}",
},
},
}));
import bookloreProxyHandler from "./proxy";
describe("widgets/booklore/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("returns 400 when Booklore credentials are missing", async () => {
getServiceWidget.mockResolvedValue({ type: "booklore", url: "http://booklore" });
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await bookloreProxyHandler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "Missing Booklore credentials" });
});
it("logs in and summarizes libraries and book statuses", async () => {
getServiceWidget.mockResolvedValue({
type: "booklore",
url: "http://booklore",
username: "u",
password: "p",
});
const books = [{ readStatus: "reading" }, { readStatus: "read" }, { readStatus: "READ" }, { readStatus: "other" }];
httpProxy
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ accessToken: "tok" }))])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify([{ id: 1 }, { id: 2 }]))])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify(books))]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await bookloreProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
libraries: 2,
books: 4,
reading: 1,
finished: 2,
});
});
});

View File

@@ -0,0 +1,95 @@
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 calendarProxyHandler from "./proxy";
describe("widgets/calendar/proxy", () => {
const envVersion = process.env.NEXT_PUBLIC_VERSION;
beforeEach(() => {
vi.clearAllMocks();
process.env.NEXT_PUBLIC_VERSION = envVersion;
});
it("returns 400 when integration is missing", async () => {
getServiceWidget.mockResolvedValue({ integrations: [] });
const req = { query: { group: "g", service: "svc", endpoint: "foo", index: "0" } };
const res = createMockRes();
await calendarProxyHandler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "Invalid integration" });
});
it("returns 403 when integration has no URL", async () => {
getServiceWidget.mockResolvedValue({ integrations: [{ name: "foo", url: "" }] });
const req = { query: { group: "g", service: "svc", endpoint: "foo", index: "0" } };
const res = createMockRes();
await calendarProxyHandler(req, res);
expect(res.statusCode).toBe(403);
expect(res.body).toEqual({ error: "No integration URL specified" });
});
it("adds a User-Agent for Outlook integrations and returns string data", async () => {
process.env.NEXT_PUBLIC_VERSION = "1.2.3";
getServiceWidget.mockResolvedValue({
integrations: [{ name: "outlook", url: "https://example.com/outlook.ics" }],
});
httpProxy.mockResolvedValueOnce([200, "text/calendar", Buffer.from("CAL")]);
const req = { query: { group: "g", service: "svc", endpoint: "outlook", index: "0" } };
const res = createMockRes();
await calendarProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledWith("https://example.com/outlook.ics", {
headers: { "User-Agent": "gethomepage/1.2.3" },
});
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "text/calendar");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ data: "CAL" });
});
it("passes through non-200 status codes from integrations", async () => {
getServiceWidget.mockResolvedValue({
integrations: [{ name: "foo", url: "https://example.com/foo.ics" }],
});
httpProxy.mockResolvedValueOnce([503, "text/plain", Buffer.from("nope")]);
const req = { query: { group: "g", service: "svc", endpoint: "foo", index: "0" } };
const res = createMockRes();
await calendarProxyHandler(req, res);
expect(res.statusCode).toBe(503);
expect(res.body).toEqual(Buffer.from("nope"));
});
});

View File

@@ -0,0 +1,63 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { sendJsonRpcRequest, getServiceWidget, logger } = vi.hoisted(() => ({
sendJsonRpcRequest: 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/handlers/jsonrpc", () => ({
sendJsonRpcRequest,
}));
vi.mock("widgets/widgets", () => ({
default: {
deluge: {
api: "{url}",
},
},
}));
import delugeProxyHandler from "./proxy";
describe("widgets/deluge/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("logs in and retries the update call after an auth error", async () => {
getServiceWidget.mockResolvedValue({ type: "deluge", url: "http://deluge", password: "pw" });
sendJsonRpcRequest
// update_ui -> error code 1 => 403
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ error: { code: 1 } }))])
// auth.login -> ok
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ result: true }))])
// update_ui retry -> ok
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ result: { torrents: {} } }))]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await delugeProxyHandler(req, res);
expect(sendJsonRpcRequest).toHaveBeenCalledTimes(3);
expect(sendJsonRpcRequest.mock.calls[0][1]).toBe("web.update_ui");
expect(sendJsonRpcRequest.mock.calls[1][1]).toBe("auth.login");
expect(sendJsonRpcRequest.mock.calls[2][1]).toBe("web.update_ui");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from(JSON.stringify({ result: { torrents: {} } })));
});
});

View File

@@ -0,0 +1,108 @@
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("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
vi.mock("widgets/widgets", () => ({
default: {
dispatcharr: {
api: "{url}/{endpoint}",
mappings: {
token: { endpoint: "auth/token" },
},
},
},
}));
import dispatcharrProxyHandler from "./proxy";
describe("widgets/dispatcharr/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("logs in when token is missing and uses Bearer token for requests", async () => {
getServiceWidget.mockResolvedValue({
type: "dispatcharr",
url: "http://dispatcharr",
username: "u",
password: "p",
});
httpProxy
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ access: "t1" }))])
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
const req = { query: { group: "g", service: "svc", endpoint: "items", index: "0" } };
const res = createMockRes();
await dispatcharrProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(2);
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://dispatcharr/auth/token");
expect(httpProxy.mock.calls[1][1].headers.Authorization).toBe("Bearer t1");
expect(res.body).toEqual(Buffer.from("data"));
});
it("retries after a bad response by clearing cache and logging in again", async () => {
cache.put("dispatcharrProxyHandler__token.svc", "old");
getServiceWidget.mockResolvedValue({
type: "dispatcharr",
url: "http://dispatcharr",
username: "u",
password: "p",
});
httpProxy
.mockResolvedValueOnce([400, "application/json", Buffer.from(JSON.stringify({ items: [] }))])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ access: "new" }))])
.mockResolvedValueOnce([200, "application/json", Buffer.from("ok")]);
const req = { query: { group: "g", service: "svc", endpoint: "items", index: "0" } };
const res = createMockRes();
await dispatcharrProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[1][0].toString()).toBe("http://dispatcharr/auth/token");
expect(httpProxy.mock.calls[2][1].headers.Authorization).toBe("Bearer new");
expect(res.body).toEqual(Buffer.from("ok"));
});
});

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, 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: {
filebrowser: {
api: "{url}/{endpoint}",
},
},
}));
import filebrowserProxyHandler from "./proxy";
describe("widgets/filebrowser/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("logs in and uses X-AUTH token for subsequent requests", async () => {
getServiceWidget.mockResolvedValue({
type: "filebrowser",
url: "http://fb",
username: "u",
password: "p",
authHeader: "X-User",
});
httpProxy
.mockResolvedValueOnce([200, "text/plain", "token123"])
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
const req = { query: { group: "g", service: "svc", endpoint: "api/raw", index: "0" } };
const res = createMockRes();
await filebrowserProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(2);
expect(httpProxy.mock.calls[0][0]).toBe("http://fb/login");
expect(httpProxy.mock.calls[0][1].headers).toEqual({ "X-User": "u" });
expect(httpProxy.mock.calls[1][1].headers["X-AUTH"]).toBe("token123");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data"));
});
it("returns 500 when login fails", async () => {
getServiceWidget.mockResolvedValue({ type: "filebrowser", url: "http://fb", username: "u", password: "p" });
httpProxy.mockResolvedValueOnce([401, "text/plain", "nope"]);
const req = { query: { group: "g", service: "svc", endpoint: "api/raw", index: "0" } };
const res = createMockRes();
await filebrowserProxyHandler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: "Failed to authenticate with Filebrowser" });
});
});

View File

@@ -0,0 +1,97 @@
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("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
vi.mock("widgets/widgets", () => ({
default: {
kavita: {
api: "{url}/{endpoint}",
},
},
}));
import kavitaProxyHandler from "./proxy";
describe("widgets/kavita/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("logs in and returns server stats", async () => {
getServiceWidget.mockResolvedValue({ type: "kavita", url: "http://kv", username: "u", password: "p" });
httpProxy
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ token: "tok" }))])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ seriesCount: 5, totalFiles: 100 })),
]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await kavitaProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(2);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ seriesCount: 5, totalFiles: 100 });
});
it("retries after a 401 by obtaining a new session token", async () => {
cache.put("kavitaProxyHandler__sessionToken.svc", "old");
getServiceWidget.mockResolvedValue({ type: "kavita", url: "http://kv", username: "u", password: "p" });
httpProxy
.mockResolvedValueOnce([401, "application/json", Buffer.from("{}")])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ token: "newtok" }))])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ seriesCount: 1, totalFiles: 2 }))]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await kavitaProxyHandler(req, res);
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("Account/login"));
expect(loginCalls).toHaveLength(1);
expect(res.body).toEqual({ seriesCount: 1, totalFiles: 2 });
});
});

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, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
logger: {
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: {
komga: {
api: "{url}/{endpoint}",
mappings: {
series: { endpoint: "series" },
books: { endpoint: "books" },
seriesv2: { endpoint: "series/v2" },
booksv2: { endpoint: "books/v2" },
},
},
},
}));
import komgaProxyHandler from "./proxy";
describe("widgets/komga/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("fetches libraries, series, and books and returns aggregated data", async () => {
getServiceWidget.mockResolvedValue({ type: "komga", url: "http://kg", key: "k" });
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(
JSON.stringify([
{ id: 1, unavailable: false },
{ id: 2, unavailable: true },
]),
),
])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify([{ id: "s1" }]))])
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify([{ id: "b1" }]))]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await komgaProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[0][1].headers["X-API-Key"]).toBe("k");
expect(res.body).toEqual({
libraries: [{ id: 1, unavailable: false }],
series: [{ id: "s1" }],
books: [{ id: "b1" }],
});
});
});

View File

@@ -0,0 +1,67 @@
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,
}));
import qbittorrentProxyHandler from "./proxy";
describe("widgets/qbittorrent/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("logs in and retries after a 403 response", async () => {
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
httpProxy
.mockResolvedValueOnce([403, "application/json", Buffer.from("nope")])
.mockResolvedValueOnce([200, "text/plain", Buffer.from("Ok.")])
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } };
const res = createMockRes();
await qbittorrentProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[1][0]).toBe("http://qb/api/v2/auth/login");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data"));
});
it("returns 401 when login succeeds but response body is not Ok.", async () => {
getServiceWidget.mockResolvedValue({ url: "http://qb", username: "u", password: "p" });
httpProxy
.mockResolvedValueOnce([403, "application/json", Buffer.from("nope")])
.mockResolvedValueOnce([200, "text/plain", Buffer.from("Denied")]);
const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } };
const res = createMockRes();
await qbittorrentProxyHandler(req, res);
expect(res.statusCode).toBe(401);
expect(res.body).toEqual(Buffer.from("Denied"));
});
});

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, 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("utils/logger", () => ({
default: () => logger,
}));
vi.mock("utils/config/service-helpers", () => ({
default: getServiceWidget,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
vi.mock("widgets/widgets", () => ({
default: {
transmission: {
rpcUrl: "/transmission/",
},
},
}));
import transmissionProxyHandler from "./proxy";
describe("widgets/transmission/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("retries after a 409 response by caching the CSRF header", async () => {
getServiceWidget.mockResolvedValue({
type: "transmission",
url: "http://tr",
username: "u",
password: "p",
});
httpProxy
.mockResolvedValueOnce([409, "application/json", Buffer.from("nope"), { "x-transmission-session-id": "csrf" }])
.mockResolvedValueOnce([200, "application/json", Buffer.from("ok"), {}]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await transmissionProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(2);
expect(httpProxy.mock.calls[1][1].headers["x-transmission-session-id"]).toBe("csrf");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("ok"));
});
});

View File

@@ -0,0 +1,85 @@
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,
}));
import unraidProxyHandler from "./proxy";
describe("widgets/unraid/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("calls the Unraid GraphQL endpoint and returns a flattened response", async () => {
getServiceWidget.mockResolvedValue({ url: "http://unraid", key: "k" });
httpProxy.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(
JSON.stringify({
data: {
metrics: { memory: { active: 10, available: 90, percentTotal: 10 }, cpu: { percentTotal: 5 } },
notifications: { overview: { unread: { total: 2 } } },
array: {
state: "STARTED",
capacity: { kilobytes: { free: 10, used: 20, total: 40 } },
caches: [{ name: "cache", fsType: "btrfs", fsSize: 100, fsFree: 25, fsUsed: 75 }],
},
},
}),
),
]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await unraidProxyHandler(req, res);
expect(httpProxy).toHaveBeenCalledTimes(1);
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://unraid/graphql");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(
expect.objectContaining({
memoryUsedPercent: 10,
cpuPercent: 5,
unreadNotifications: 2,
arrayState: "STARTED",
}),
);
expect(res.body.caches.cache.fsUsedPercent).toBe(75);
});
it("returns 500 when the response cannot be processed", async () => {
getServiceWidget.mockResolvedValue({ url: "http://unraid", key: "k" });
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from("not-json")]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await unraidProxyHandler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual(expect.objectContaining({ error: expect.any(String) }));
});
});