mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 08:50:52 +08:00
test: add widget proxy tests (batch 2)
This commit is contained in:
98
src/widgets/booklore/proxy.test.js
Normal file
98
src/widgets/booklore/proxy.test.js
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
95
src/widgets/calendar/proxy.test.js
Normal file
95
src/widgets/calendar/proxy.test.js
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
63
src/widgets/deluge/proxy.test.js
Normal file
63
src/widgets/deluge/proxy.test.js
Normal 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: {} } })));
|
||||||
|
});
|
||||||
|
});
|
||||||
108
src/widgets/dispatcharr/proxy.test.js
Normal file
108
src/widgets/dispatcharr/proxy.test.js
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
79
src/widgets/filebrowser/proxy.test.js
Normal file
79
src/widgets/filebrowser/proxy.test.js
Normal 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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
97
src/widgets/kavita/proxy.test.js
Normal file
97
src/widgets/kavita/proxy.test.js
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
76
src/widgets/komga/proxy.test.js
Normal file
76
src/widgets/komga/proxy.test.js
Normal 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" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
67
src/widgets/qbittorrent/proxy.test.js
Normal file
67
src/widgets/qbittorrent/proxy.test.js
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
79
src/widgets/transmission/proxy.test.js
Normal file
79
src/widgets/transmission/proxy.test.js
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
85
src/widgets/unraid/proxy.test.js
Normal file
85
src/widgets/unraid/proxy.test.js
Normal 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) }));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user