Add a basic endpoint

This commit is contained in:
shamoon
2026-09-22 19:05:23 -07:00
parent cf1964d652
commit 19497d9908
2 changed files with 139 additions and 0 deletions
@@ -0,0 +1,95 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getPrivateWidgetOptions, httpProxy, logger } = vi.hoisted(() => ({
getPrivateWidgetOptions: vi.fn(),
httpProxy: vi.fn(),
logger: { debug: vi.fn() },
}));
vi.mock("utils/config/widget-helpers", () => ({
getPrivateWidgetOptions,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "pages/api/widgets/customapi";
describe("pages/api/widgets/customapi", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns 400 when the widget URL is missing", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({});
const res = createMockRes();
await handler({ query: { index: "0" } }, res);
expect(getPrivateWidgetOptions).toHaveBeenCalledWith("customapi", "0");
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe("Missing Custom API URL");
});
it("returns 400 when the widget URL is invalid", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "not a url" });
const res = createMockRes();
await handler({ query: { index: "0" } }, res);
expect(res.statusCode).toBe(400);
expect(httpProxy).not.toHaveBeenCalled();
});
it("proxies with headers, basic auth, method and JSON body", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({
url: "http://api.local/data",
username: "u",
password: "p",
method: "POST",
headers: { "X-Test": "1" },
requestBody: { foo: "bar" },
});
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from('{"a":1}')]);
const res = createMockRes();
await handler({ query: { index: "0" } }, res);
expect(httpProxy).toHaveBeenCalledWith(new URL("http://api.local/data"), {
method: "POST",
headers: { "X-Test": "1", Authorization: `Basic ${Buffer.from("u:p").toString("base64")}` },
body: '{"foo":"bar"}',
});
expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "application/json");
expect(res.statusCode).toBe(200);
expect(res.body.toString()).toBe('{"a":1}');
});
it("defaults to GET and passes string bodies through", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://api.local", requestBody: "raw" });
httpProxy.mockResolvedValueOnce([200, null, Buffer.from("{}")]);
const res = createMockRes();
await handler({ query: { index: "0" } }, res);
expect(httpProxy).toHaveBeenCalledWith(expect.any(URL), { method: "GET", headers: {}, body: "raw" });
});
it("returns a sanitized error without upstream data on HTTP errors", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://api.local/secret?token=abc" });
httpProxy.mockResolvedValueOnce([500, "text/plain", Buffer.from("boom")]);
const res = createMockRes();
await handler({ query: { index: "0" } }, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: { message: "HTTP Error", url: "api.local (see logs for details)" } });
});
});
+44
View File
@@ -0,0 +1,44 @@
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
import createLogger from "utils/logger";
import { sanitizeErrorURL } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
const logger = createLogger("customapi");
export default async function handler(req, res) {
const { index } = req.query;
const options = await getPrivateWidgetOptions("customapi", index);
if (!options?.url) {
return res.status(400).json({ error: "Missing Custom API URL" });
}
let url;
try {
url = new URL(options.url);
} catch {
return res.status(400).json({ error: "Invalid Custom API URL" });
}
const headers = { ...(options.headers ?? {}) };
if (options.username && options.password) {
headers.Authorization = `Basic ${Buffer.from(`${options.username}:${options.password}`).toString("base64")}`;
}
const params = { method: options.method ?? "GET", headers };
if (options.requestBody) {
params.body = typeof options.requestBody === "object" ? JSON.stringify(options.requestBody) : options.requestBody;
}
const [status, contentType, data] = await httpProxy(url, params);
if (status >= 400) {
logger.debug("HTTP Error %d calling %s//%s%s", status, url.protocol, url.host, url.pathname);
return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url) } });
}
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}