import { describe, expect, it } from "bun:test"; import { APP_DESTINATIONS, AppRouteError, resolveAppRoute } from "./app-routes"; describe("resolveAppRoute", () => { it("resolves static destinations to their canonical path", () => { expect(resolveAppRoute({ destination: "timeline" }).route).toBe( "/timeline", ); expect(resolveAppRoute({ destination: "projects" }).route).toBe( "/projects", ); expect(resolveAppRoute({ destination: "sync" }).route).toBe("/sync"); expect(resolveAppRoute({ destination: "settings" }).route).toBe( "/settings", ); expect(resolveAppRoute({ destination: "scan-roots" }).route).toBe( "/scan-roots", ); }); it("returns the normalized destination key", () => { expect(resolveAppRoute({ destination: "timeline" }).destination).toBe( "timeline", ); }); it("resolves a project detail route with a valid id", () => { expect( resolveAppRoute({ destination: "project", projectId: "abc123" }), ).toEqual({ destination: "project", route: "/projects/abc123" }); }); it("rejects a project route with no id", () => { expect(() => resolveAppRoute({ destination: "project" })).toThrow( AppRouteError, ); }); it("rejects a project id containing path separators (no traversal)", () => { expect(() => resolveAppRoute({ destination: "project", projectId: "../settings" }), ).toThrow(AppRouteError); expect(() => resolveAppRoute({ destination: "project", projectId: "a/b" }), ).toThrow(AppRouteError); }); it("rejects an unknown destination (no arbitrary navigation)", () => { expect(() => resolveAppRoute({ destination: "https://evil.example" }), ).toThrow(AppRouteError); expect(() => resolveAppRoute({ destination: "/etc/passwd" })).toThrow( AppRouteError, ); expect(() => resolveAppRoute({ destination: "nope" })).toThrow( AppRouteError, ); }); it("exposes all destinations including project", () => { expect(APP_DESTINATIONS).toContain("timeline"); expect(APP_DESTINATIONS).toContain("project"); expect(APP_DESTINATIONS).toContain("settings"); }); describe("settings sections", () => { it("resolves an allowlisted subsection", () => { expect( resolveAppRoute({ destination: "settings", section: "templates" }) .route, ).toBe("/settings/templates"); }); it("rejects an unknown subsection", () => { expect(() => resolveAppRoute({ destination: "settings", section: "secrets" }), ).toThrow(AppRouteError); }); it("ignores section on non-settings destinations", () => { expect( resolveAppRoute({ destination: "timeline", section: "templates" }) .route, ).toBe("/timeline"); }); }); describe("timeline filters", () => { it("builds a view query", () => { expect( resolveAppRoute({ destination: "timeline", view: "heatmap" }).route, ).toBe("/timeline?view=heatmap"); }); it("rejects an unknown view", () => { expect(() => resolveAppRoute({ destination: "timeline", view: "spiral" }), ).toThrow(AppRouteError); }); it("builds a sources query and rejects unknown sources", () => { expect( resolveAppRoute({ destination: "timeline", sources: ["git", "linear"] }) .route, ).toBe("/timeline?sources=git%2Clinear"); expect(() => resolveAppRoute({ destination: "timeline", sources: ["git", "bogus"] }), ).toThrow(AppRouteError); }); it("ignores an empty sources array", () => { expect( resolveAppRoute({ destination: "timeline", sources: [] }).route, ).toBe("/timeline"); }); it("builds a date-range query and rejects malformed dates", () => { expect( resolveAppRoute({ destination: "timeline", start: "2026-01-01", end: "2026-01-31", }).route, ).toBe("/timeline?start=2026-01-01&end=2026-01-31"); expect(() => resolveAppRoute({ destination: "timeline", start: "01/01/2026" }), ).toThrow(AppRouteError); }); it("combines view, sources and dates in order", () => { expect( resolveAppRoute({ destination: "timeline", view: "activity", sources: ["git"], start: "2026-02-01", }).route, ).toBe("/timeline?view=activity&sources=git&start=2026-02-01"); }); it("does not apply timeline filters to other destinations", () => { expect( resolveAppRoute({ destination: "projects", view: "heatmap" }).route, ).toBe("/projects"); }); }); });