Files
SnapOtter/tests/unit/api/tool-access-url.test.ts
T

69 lines
2.8 KiB
TypeScript
Raw Normal View History

/**
* Route resolution for the tool access gate (issue #645).
*
* The gate protects whatever tool this resolves, so getting it wrong either
* opens a tool up or 403s something that was never a tool run. It reads the
* matched route pattern rather than the raw URL on purpose: find-my-way
* decodes before matching, so a second parse of the raw string disagrees with
* the router and fails open on `/api/v1/tools/image/%66avicon`.
*/
import { apiToolPath, TOOLS, toolSection } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
import { toolIdFromRoute } from "../../../apps/api/src/plugins/tool-access.js";
const BATCH_PATTERN = "/api/v1/tools/:section/:toolId/batch";
describe("toolIdFromRoute", () => {
it("resolves every tool in the catalog from its own registered path", () => {
const unresolved = TOOLS.filter(
(tool) => toolIdFromRoute(apiToolPath(tool.id)) !== tool.id,
).map((tool) => tool.id);
expect(unresolved, `tools whose own path does not resolve: ${unresolved.join(", ")}`).toEqual(
[],
);
});
it.each(["batch", "info", "preview", "analyze", "inspect", "generate", "effects"])(
"resolves the /%s sub-path to its parent tool",
(suffix) => {
expect(toolIdFromRoute(`/api/v1/tools/image/resize/${suffix}`)).toBe("resize");
},
);
it("resolves the parametric batch route from the router's decoded params", () => {
expect(toolIdFromRoute(BATCH_PATTERN, { section: "image", toolId: "resize" })).toBe("resize");
});
it("returns null when the parametric route names an unknown tool", () => {
expect(toolIdFromRoute(BATCH_PATTERN, { section: "image", toolId: "nope" })).toBeNull();
});
it("returns null when the parametric route's section does not match the tool", () => {
expect(toolIdFromRoute(BATCH_PATTERN, { section: "video", toolId: "resize" })).toBeNull();
});
it("returns null when the parametric route has no params", () => {
expect(toolIdFromRoute(BATCH_PATTERN)).toBeNull();
});
it.each([
["no matched route", undefined],
["the catalog root", "/api/v1/tools/"],
["a single-segment listing", "/api/v1/tools/popular"],
["an unrelated route", "/api/v1/jobs/:jobId/progress"],
["a path that merely looks similar", "/api/v1/toolsomething/image/resize"],
])("returns null for %s", (_label, routeUrl) => {
expect(toolIdFromRoute(routeUrl)).toBeNull();
});
it("returns null for an unknown tool id so the route can still 404", () => {
expect(toolIdFromRoute("/api/v1/tools/image/not-a-real-tool")).toBeNull();
});
it("returns null when a real tool is registered under the wrong section", () => {
const image = TOOLS.find((t) => toolSection(t) === "image");
if (!image) throw new Error("no image-section tool in the catalog");
expect(toolIdFromRoute(`/api/v1/tools/video/${image.id}`)).toBeNull();
});
});