test: add widget proxy tests (batch 1)

This commit is contained in:
shamoon
2026-02-03 13:37:18 -08:00
parent 27b3e50227
commit e99adf200b
11 changed files with 1006 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { vi } from "vitest";
export default function createMockRes() {
const res = {
statusCode: null,
body: null,
headers: {},
};
res.status = vi.fn((code) => {
res.statusCode = code;
return res;
});
res.json = vi.fn((body) => {
res.body = body;
return res;
});
res.send = vi.fn((body) => {
res.body = body;
return res;
});
res.end = vi.fn((body) => {
res.body = body;
return res;
});
res.setHeader = vi.fn((key, value) => {
res.headers[key] = value;
return res;
});
return res;
}

View 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] })));
});
});

View 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"));
});
});

View 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 });
});
});

View 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();
});
});

View 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 });
});
});

View 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"));
});
});

View 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 });
});
});

View 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" });
});
});

View 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");
});
});

View 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);
});
});