mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
69 lines
2.8 KiB
TypeScript
69 lines
2.8 KiB
TypeScript
/**
|
|||
|
|
* 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();
|
||
|
|
});
|
||
|
|
});
|