mirror of
https://github.com/gethomepage/homepage.git
synced 2026-02-08 17:00:51 +08:00
test: add widget proxy tests (batch 1)
This commit is contained in:
117
src/widgets/beszel/proxy.test.js
Normal file
117
src/widgets/beszel/proxy.test.js
Normal file
@@ -0,0 +1,117 @@
|
||||
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: {
|
||||
beszel: {
|
||||
api: "{url}/{endpoint}",
|
||||
mappings: {
|
||||
authv1: { endpoint: "api/auth" },
|
||||
authv2: { endpoint: "api/auth/v2" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import beszelProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/beszel/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cache._reset();
|
||||
});
|
||||
|
||||
it("logs in when token is missing and uses Bearer token for requests", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "beszel",
|
||||
url: "http://beszel",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ token: "t1" }))])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ items: [1] }))]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "items", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await beszelProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(2);
|
||||
expect(httpProxy.mock.calls[0][0]).toBe("http://beszel/api/auth");
|
||||
expect(httpProxy.mock.calls[1][0].toString()).toBe("http://beszel/items");
|
||||
expect(httpProxy.mock.calls[1][1]).toEqual({
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer t1",
|
||||
},
|
||||
});
|
||||
expect(res.send).toHaveBeenCalledWith(Buffer.from(JSON.stringify({ items: [1] })));
|
||||
});
|
||||
|
||||
it("retries after receiving an empty list by clearing cache and logging in again", async () => {
|
||||
cache.put("beszelProxyHandler__token.svc", "old");
|
||||
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "beszel",
|
||||
url: "http://beszel",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ items: [] }))])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ token: "new" }))])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ items: [1] }))]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "items", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await beszelProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(3);
|
||||
expect(httpProxy.mock.calls[0][1].headers.Authorization).toBe("Bearer old");
|
||||
expect(httpProxy.mock.calls[1][0]).toBe("http://beszel/api/auth");
|
||||
expect(httpProxy.mock.calls[2][1].headers.Authorization).toBe("Bearer new");
|
||||
expect(res.send).toHaveBeenCalledWith(Buffer.from(JSON.stringify({ items: [1] })));
|
||||
});
|
||||
});
|
||||
69
src/widgets/flood/proxy.test.js
Normal file
69
src/widgets/flood/proxy.test.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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 floodProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/flood/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("logs in and retries after a 401 response", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://flood" });
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([401, "application/json", Buffer.from("nope")])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from("ok")])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "stats", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await floodProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(3);
|
||||
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://flood/api/stats");
|
||||
expect(httpProxy.mock.calls[1][0]).toBe("http://flood/api/auth/authenticate");
|
||||
expect(httpProxy.mock.calls[1][1].body).toBeNull();
|
||||
expect(httpProxy.mock.calls[2][0].toString()).toBe("http://flood/api/stats");
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual(Buffer.from("data"));
|
||||
});
|
||||
|
||||
it("returns the login error status when authentication fails", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://flood", username: "u", password: "p" });
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([401, "application/json", Buffer.from("nope")])
|
||||
.mockResolvedValueOnce([500, "application/json", Buffer.from("bad")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "stats", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await floodProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy.mock.calls[1][1].body).toBe(JSON.stringify({ username: "u", password: "p" }));
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.end).toHaveBeenCalledWith(Buffer.from("bad"));
|
||||
});
|
||||
});
|
||||
112
src/widgets/freshrss/proxy.test.js
Normal file
112
src/widgets/freshrss/proxy.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
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: {
|
||||
freshrss: {
|
||||
api: "{url}/{endpoint}",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import freshrssProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/freshrss/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cache._reset();
|
||||
});
|
||||
|
||||
it("logs in, caches token, and returns subscription + unread counts", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "freshrss",
|
||||
url: "http://fresh",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([200, "text/plain", Buffer.from("SID=1\nAuth=token123\n")])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ subscriptions: [1, 2, 3] }))])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ max: 7 }))]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await freshrssProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(3);
|
||||
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://fresh/accounts/ClientLogin");
|
||||
expect(httpProxy.mock.calls[1][1].headers.Authorization).toBe("GoogleLogin auth=token123");
|
||||
expect(httpProxy.mock.calls[2][1].headers.Authorization).toBe("GoogleLogin auth=token123");
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith({ subscriptions: 3, unread: 7 });
|
||||
});
|
||||
|
||||
it("retries API calls after a 401 by obtaining a new session token", async () => {
|
||||
cache.put("freshrssProxyHandler__sessionToken.svc", "old");
|
||||
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "freshrss",
|
||||
url: "http://fresh",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([401, "application/json", Buffer.from("{}")])
|
||||
.mockResolvedValueOnce([200, "text/plain", Buffer.from("SID=1\nAuth=newtok\n")])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ subscriptions: [1] }))])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ max: 2 }))]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await freshrssProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(4);
|
||||
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("accounts/ClientLogin"));
|
||||
expect(loginCalls).toHaveLength(1);
|
||||
const listCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("subscription/list"));
|
||||
expect(listCalls).toHaveLength(2);
|
||||
expect(res.body).toEqual({ subscriptions: 1, unread: 2 });
|
||||
});
|
||||
});
|
||||
102
src/widgets/jellyfin/proxy.test.js
Normal file
102
src/widgets/jellyfin/proxy.test.js
Normal file
@@ -0,0 +1,102 @@
|
||||
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: {
|
||||
jellyfin: {
|
||||
api: "{url}/{endpoint}",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import jellyfinProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/jellyfin/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
validateWidgetData.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("adds MediaBrowser auth header and applies an optional mapping function", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "jellyfin",
|
||||
url: "http://jf",
|
||||
key: "abc",
|
||||
service_group: "mygroup",
|
||||
service_name: "myservice",
|
||||
});
|
||||
|
||||
httpProxy.mockResolvedValueOnce([200, "application/json", { items: [1] }]);
|
||||
|
||||
const req = { method: "GET", query: { group: "g", service: "svc", endpoint: "Users", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await jellyfinProxyHandler(req, res, () => ({ mapped: true }));
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(1);
|
||||
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://jf/Users");
|
||||
expect(httpProxy.mock.calls[0][1].headers.Authorization).toBe(
|
||||
'MediaBrowser Token="abc", Client="Homepage", Device="Homepage", DeviceId="mygroup-myservice", Version="1.0.0"',
|
||||
);
|
||||
expect(validateWidgetData).toHaveBeenCalledWith(expect.objectContaining({ type: "jellyfin" }), "Users", {
|
||||
items: [1],
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual({ mapped: true });
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "application/json");
|
||||
});
|
||||
|
||||
it("returns 500 when data validation fails", async () => {
|
||||
validateWidgetData.mockReturnValue(false);
|
||||
getServiceWidget.mockResolvedValue({ type: "jellyfin", url: "http://jf", key: "abc" });
|
||||
httpProxy.mockResolvedValueOnce([200, "application/json", { nope: true }]);
|
||||
|
||||
const req = { method: "GET", query: { group: "g", service: "svc", endpoint: "Users", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await jellyfinProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.body.error.message).toBe("Invalid data");
|
||||
});
|
||||
|
||||
it("ends the response for 204 responses", async () => {
|
||||
getServiceWidget.mockResolvedValue({ type: "jellyfin", url: "http://jf", key: "abc" });
|
||||
httpProxy.mockResolvedValueOnce([204, "application/json", {}]);
|
||||
|
||||
const req = { method: "GET", query: { group: "g", service: "svc", endpoint: "Users", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await jellyfinProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(204);
|
||||
expect(res.end).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
66
src/widgets/minecraft/proxy.test.js
Normal file
66
src/widgets/minecraft/proxy.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import createMockRes from "test-utils/create-mock-res";
|
||||
|
||||
const { mc, getServiceWidget, logger } = vi.hoisted(() => ({
|
||||
mc: { lookup: vi.fn() },
|
||||
getServiceWidget: vi.fn(),
|
||||
logger: { error: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("minecraftstatuspinger", () => ({
|
||||
default: mc,
|
||||
...mc,
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/service-helpers", () => ({
|
||||
default: getServiceWidget,
|
||||
}));
|
||||
|
||||
vi.mock("utils/logger", () => ({
|
||||
default: () => logger,
|
||||
}));
|
||||
|
||||
import minecraftProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/minecraft/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("returns online=true with version and player data when lookup succeeds", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://example.com:25565" });
|
||||
mc.lookup.mockResolvedValue({
|
||||
status: {
|
||||
version: { name: "1.20" },
|
||||
players: { online: 1, max: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await minecraftProxyHandler(req, res);
|
||||
|
||||
expect(mc.lookup).toHaveBeenCalledWith({ host: "example.com", port: "25565" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual({
|
||||
version: "1.20",
|
||||
online: true,
|
||||
players: { online: 1, max: 10 },
|
||||
});
|
||||
});
|
||||
|
||||
it("returns online=false when lookup fails", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://example.com:25565" });
|
||||
mc.lookup.mockRejectedValue(new Error("nope"));
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await minecraftProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual({ version: undefined, online: false, players: undefined });
|
||||
});
|
||||
});
|
||||
114
src/widgets/npm/proxy.test.js
Normal file
114
src/widgets/npm/proxy.test.js
Normal file
@@ -0,0 +1,114 @@
|
||||
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: {
|
||||
npm: {
|
||||
api: "{url}/{endpoint}",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import npmProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/npm/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cache._reset();
|
||||
});
|
||||
|
||||
it("logs in when token is missing and uses Bearer token for requests", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "npm",
|
||||
url: "http://npm",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
Buffer.from(JSON.stringify({ token: "t1", expires: new Date(Date.now() + 60_000).toISOString() })),
|
||||
])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from("data")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "api/v1/stats", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await npmProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(2);
|
||||
expect(httpProxy.mock.calls[0][0]).toBe("http://npm/api/tokens");
|
||||
expect(httpProxy.mock.calls[1][1].headers.Authorization).toBe("Bearer t1");
|
||||
expect(res.body).toEqual(Buffer.from("data"));
|
||||
});
|
||||
|
||||
it("retries after a 403 response by clearing cache and logging in again", async () => {
|
||||
cache.put("npmProxyHandler__token.svc", "old");
|
||||
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "npm",
|
||||
url: "http://npm",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([403, "application/json", Buffer.from("nope")])
|
||||
.mockResolvedValueOnce([
|
||||
200,
|
||||
"application/json",
|
||||
Buffer.from(JSON.stringify({ token: "new", expires: new Date(Date.now() + 60_000).toISOString() })),
|
||||
])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from("ok")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "api/v1/stats", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await npmProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(3);
|
||||
expect(httpProxy.mock.calls[0][1].headers.Authorization).toBe("Bearer old");
|
||||
expect(httpProxy.mock.calls[1][0]).toBe("http://npm/api/tokens");
|
||||
expect(httpProxy.mock.calls[2][1].headers.Authorization).toBe("Bearer new");
|
||||
expect(res.body).toEqual(Buffer.from("ok"));
|
||||
});
|
||||
});
|
||||
105
src/widgets/pyload/proxy.test.js
Normal file
105
src/widgets/pyload/proxy.test.js
Normal file
@@ -0,0 +1,105 @@
|
||||
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: {
|
||||
error: vi.fn(),
|
||||
info: 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: {
|
||||
pyload: {
|
||||
api: "{url}/api/{endpoint}",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import pyloadProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/pyload/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cache._reset();
|
||||
});
|
||||
|
||||
it("uses Basic auth when credentials work and returns data", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "pyload",
|
||||
url: "http://pyload",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ ok: true })), {}]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "status", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await pyloadProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(1);
|
||||
expect(httpProxy.mock.calls[0][1].headers.Authorization).toMatch(/^Basic /);
|
||||
expect(cache.put).toHaveBeenCalledWith("pyloadProxyHandler__isNg.svc", true);
|
||||
expect(res.body).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("retries after 403 by clearing session and logging in again", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "pyload",
|
||||
url: "http://pyload",
|
||||
username: "u",
|
||||
password: "",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
// login -> sessionId
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify("sid1")), {}])
|
||||
// fetch -> unauthorized
|
||||
.mockResolvedValueOnce([403, "application/json", Buffer.from(JSON.stringify({ error: "bad" })), {}])
|
||||
// relogin -> sessionId
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify("sid2")), {}])
|
||||
// retry fetch -> ok
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ ok: true })), {}]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "status", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await pyloadProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(4);
|
||||
expect(cache.del).toHaveBeenCalledWith("pyloadProxyHandler__sessionId.svc");
|
||||
expect(res.body).toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
120
src/widgets/urbackup/proxy.test.js
Normal file
120
src/widgets/urbackup/proxy.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import createMockRes from "test-utils/create-mock-res";
|
||||
|
||||
const { UrbackupServer, state, getServiceWidget } = vi.hoisted(() => {
|
||||
const state = { instances: [] };
|
||||
|
||||
const UrbackupServer = vi.fn((opts) => {
|
||||
const instance = {
|
||||
opts,
|
||||
getStatus: vi.fn(),
|
||||
getUsage: vi.fn(),
|
||||
};
|
||||
state.instances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
return {
|
||||
UrbackupServer,
|
||||
state,
|
||||
getServiceWidget: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("urbackup-server-api", () => ({
|
||||
UrbackupServer,
|
||||
}));
|
||||
|
||||
vi.mock("utils/config/service-helpers", () => ({
|
||||
default: getServiceWidget,
|
||||
}));
|
||||
|
||||
import urbackupProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/urbackup/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
state.instances.length = 0;
|
||||
});
|
||||
|
||||
it("returns client statuses and maxDays without disk usage by default", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
url: "http://ur",
|
||||
username: "u",
|
||||
password: "p",
|
||||
maxDays: 5,
|
||||
});
|
||||
|
||||
UrbackupServer.mockImplementationOnce((opts) => {
|
||||
const instance = {
|
||||
opts,
|
||||
getStatus: vi.fn().mockResolvedValue([{ id: 1 }]),
|
||||
getUsage: vi.fn(),
|
||||
};
|
||||
state.instances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await urbackupProxyHandler(req, res);
|
||||
|
||||
expect(UrbackupServer).toHaveBeenCalledWith({ url: "http://ur", username: "u", password: "p" });
|
||||
expect(state.instances[0].getUsage).not.toHaveBeenCalled();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual({ clientStatuses: [{ id: 1 }], diskUsage: false, maxDays: 5 });
|
||||
});
|
||||
|
||||
it("fetches disk usage when requested via fields", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
url: "http://ur",
|
||||
username: "u",
|
||||
password: "p",
|
||||
maxDays: 1,
|
||||
fields: ["totalUsed"],
|
||||
});
|
||||
|
||||
UrbackupServer.mockImplementationOnce((opts) => {
|
||||
const instance = {
|
||||
opts,
|
||||
getStatus: vi.fn().mockResolvedValue([{ id: 1 }]),
|
||||
getUsage: vi.fn().mockResolvedValue({ totalUsed: 123 }),
|
||||
};
|
||||
state.instances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await urbackupProxyHandler(req, res);
|
||||
|
||||
expect(state.instances[0].getUsage).toHaveBeenCalled();
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body.diskUsage).toEqual({ totalUsed: 123 });
|
||||
});
|
||||
|
||||
it("returns 500 on server errors", async () => {
|
||||
getServiceWidget.mockResolvedValue({ url: "http://ur", username: "u", password: "p" });
|
||||
|
||||
UrbackupServer.mockImplementationOnce((opts) => {
|
||||
const instance = {
|
||||
opts,
|
||||
getStatus: vi.fn().mockRejectedValue(new Error("nope")),
|
||||
getUsage: vi.fn(),
|
||||
};
|
||||
state.instances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await urbackupProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(res.body).toEqual({ error: "Error communicating with UrBackup server" });
|
||||
});
|
||||
});
|
||||
64
src/widgets/watchtower/proxy.test.js
Normal file
64
src/widgets/watchtower/proxy.test.js
Normal file
@@ -0,0 +1,64 @@
|
||||
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: {
|
||||
watchtower: {
|
||||
api: "{url}/{endpoint}",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import watchtowerProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/watchtower/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("parses watchtower metrics and returns a key/value object", async () => {
|
||||
getServiceWidget.mockResolvedValue({ type: "watchtower", url: "http://watch", key: "k" });
|
||||
httpProxy.mockResolvedValueOnce([
|
||||
200,
|
||||
"text/plain",
|
||||
Buffer.from("watchtower_running 1\nfoo 2\nwatchtower_status 3\n"),
|
||||
]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", endpoint: "metrics", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await watchtowerProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(1);
|
||||
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://watch/metrics");
|
||||
expect(httpProxy.mock.calls[0][1]).toEqual({
|
||||
method: "GET",
|
||||
headers: { Authorization: "Bearer k" },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual({ watchtower_running: "1", watchtower_status: "3" });
|
||||
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "text/plain");
|
||||
});
|
||||
});
|
||||
101
src/widgets/xteve/proxy.test.js
Normal file
101
src/widgets/xteve/proxy.test.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
}));
|
||||
|
||||
vi.mock("widgets/widgets", () => ({
|
||||
default: {
|
||||
xteve: {
|
||||
api: "{url}/{endpoint}",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import xteveProxyHandler from "./proxy";
|
||||
|
||||
describe("widgets/xteve/proxy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("logs in when credentials are provided and includes token in subsequent status request", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "xteve",
|
||||
url: "http://xteve",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ status: true, token: "tok" }))])
|
||||
.mockResolvedValueOnce([200, "application/json", Buffer.from("status-data")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await xteveProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(2);
|
||||
expect(httpProxy.mock.calls[0][0]).toBe("http://xteve/api/");
|
||||
expect(JSON.parse(httpProxy.mock.calls[0][1].body)).toEqual({
|
||||
cmd: "login",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
expect(JSON.parse(httpProxy.mock.calls[1][1].body)).toEqual({ cmd: "status", token: "tok" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toEqual(Buffer.from("status-data"));
|
||||
});
|
||||
|
||||
it("returns 401 when authentication fails", async () => {
|
||||
getServiceWidget.mockResolvedValue({
|
||||
type: "xteve",
|
||||
url: "http://xteve",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
|
||||
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ status: false }))]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await xteveProxyHandler(req, res);
|
||||
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(res.body.error.message).toBe("Authentication failed");
|
||||
});
|
||||
|
||||
it("skips login when credentials are not provided", async () => {
|
||||
getServiceWidget.mockResolvedValue({ type: "xteve", url: "http://xteve" });
|
||||
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from("status-data")]);
|
||||
|
||||
const req = { query: { group: "g", service: "svc", index: "0" } };
|
||||
const res = createMockRes();
|
||||
|
||||
await xteveProxyHandler(req, res);
|
||||
|
||||
expect(httpProxy).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(httpProxy.mock.calls[0][1].body)).toEqual({ cmd: "status" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user