Files
homarr/packages/custom-widgets/src/test/request-manifest.spec.ts
T

85 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { customJsxRequestSchema } from "../core";
import { renderRequestBody, renderRequestTarget, resolveCustomWidgetRequestValues } from "../server";
const request = customJsxRequestSchema.parse({
kind: "action",
method: "POST",
path: "/devices/{option:room}/{param:id}",
query: { verbose: true, level: { $param: "level" } },
body: { enabled: { $param: "enabled" }, room: { $option: "room" } },
});
describe("lean request bindings", () => {
it("infers and resolves option and invocation references", () => {
const values = resolveCustomWidgetRequestValues(
request,
{ room: "office" },
{ id: "lamp/1", level: 5, enabled: true },
);
expect(renderRequestTarget("https://example.com/api/", request, values).toString()).toBe(
"https://example.com/api/devices/office/lamp%2F1?verbose=true&level=5",
);
expect(JSON.parse(renderRequestBody(request, values) ?? "null")).toEqual({ enabled: true, room: "office" });
});
it("rejects missing and extra invocation params", () => {
expect(() => resolveCustomWidgetRequestValues(request, { room: "office" }, { id: "lamp", level: 5 })).toThrow(
"do not match",
);
expect(() =>
resolveCustomWidgetRequestValues(
request,
{ room: "office" },
{ id: "lamp", level: 5, enabled: true, extra: true },
),
).toThrow("do not match");
});
it("rejects missing options and non-primitive URL values", () => {
expect(() => resolveCustomWidgetRequestValues(request, {}, { id: "lamp", level: 5, enabled: true })).toThrow(
"room",
);
const values = resolveCustomWidgetRequestValues(request, { room: [] }, { id: "lamp", level: 5, enabled: true });
expect(() => renderRequestTarget("https://example.com", request, values)).toThrow("Path value");
});
it("supports structured options in JSON bodies", () => {
const bodyRequest = customJsxRequestSchema.parse({
kind: "action",
method: "POST",
path: "/rules",
body: { entities: { $option: "entities" }, rule: { $option: "rule" } },
});
const values = resolveCustomWidgetRequestValues(bodyRequest, {
entities: ["light.office", "light.desk"],
rule: { transition: 2 },
});
expect(JSON.parse(renderRequestBody(bodyRequest, values) ?? "null")).toEqual({
entities: ["light.office", "light.desk"],
rule: { transition: 2 },
});
});
it("keeps a source base path", () => {
const pokemon = customJsxRequestSchema.parse({ path: "/pokemon" });
expect(renderRequestTarget("https://pokeapi.co/api/v2", pokemon, {}).toString()).toBe(
"https://pokeapi.co/api/v2/pokemon",
);
});
it.each(["start", "stop", "restart"])("renders a Portainer %s action without duplicating declarations", (action) => {
const portainer = customJsxRequestSchema.parse({
kind: "action",
method: "POST",
path: `/api/endpoints/{option:endpointId}/docker/containers/{param:id}/${action}`,
invalidates: ["containers"],
});
const values = resolveCustomWidgetRequestValues(portainer, { endpointId: 2 }, { id: "abc/123" });
expect(renderRequestTarget("https://portainer.example", portainer, values).pathname).toBe(
`/api/endpoints/2/docker/containers/abc%2F123/${action}`,
);
});
});