feat(housekeeping): unify complete rebuild in gated preview
This commit is contained in:
1 parent
4ad007d241
commit
43470ebf82
402 files changed
+78313
-1663
No files matched your search
@@ -1,95 +1,154 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import { anyCapability, type HousekeepingDomainManifest } from "../contracts";
|
||||
import {
|
||||
anyCapability,
|
||||
type CanonicalHousekeepingHref,
|
||||
type HousekeepingDomainManifest,
|
||||
} from "../contracts";
|
||||
import { createHousekeepingRegistry } from "../registry";
|
||||
import { matchHousekeepingRoute } from "./match-route";
|
||||
|
||||
const peopleManifest = {
|
||||
id: "people",
|
||||
labelKey: "pages.housekeeping.domains.people.title",
|
||||
descriptionKey: "pages.housekeeping.domains.people.description",
|
||||
iconId: "users",
|
||||
previewHref: "/ase-next/people",
|
||||
capability: anyCapability(PERMS.USERS_VIEW),
|
||||
landingRouteId: "people.users",
|
||||
routes: [
|
||||
{
|
||||
id: "people.users",
|
||||
labelKey: "pages.housekeeping.people.users.title",
|
||||
href: "/ase-next/people/users",
|
||||
capability: anyCapability(PERMS.USERS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "people.user-settings",
|
||||
labelKey: "pages.housekeeping.people.userSettings.title",
|
||||
href: "/ase-next/people/users/settings",
|
||||
capability: anyCapability(PERMS.USERS_VIEW),
|
||||
},
|
||||
{
|
||||
id: "people.user-detail",
|
||||
labelKey: "pages.housekeeping.people.userDetail.title",
|
||||
href: "/ase-next/people/users/:id",
|
||||
capability: anyCapability(PERMS.USERS_VIEW),
|
||||
},
|
||||
],
|
||||
searchProviders: [],
|
||||
inboxSources: [],
|
||||
widgets: [],
|
||||
} satisfies HousekeepingDomainManifest;
|
||||
const requirement = anyCapability(PERMS.USERS_VIEW);
|
||||
|
||||
const registry = createHousekeepingRegistry([peopleManifest]);
|
||||
function peopleManifest(
|
||||
routes: HousekeepingDomainManifest["routes"],
|
||||
): HousekeepingDomainManifest {
|
||||
return {
|
||||
id: "people",
|
||||
labelKey: "pages.housekeeping.domains.people.title",
|
||||
descriptionKey: "pages.housekeeping.domains.people.description",
|
||||
iconId: "users",
|
||||
canonicalHref: "/ase-next/people",
|
||||
capability: requirement,
|
||||
routes,
|
||||
searchProviders: [],
|
||||
inboxSources: [],
|
||||
widgets: [],
|
||||
};
|
||||
}
|
||||
|
||||
function route(
|
||||
id: string,
|
||||
href: CanonicalHousekeepingHref,
|
||||
): HousekeepingDomainManifest["routes"][number] {
|
||||
return {
|
||||
id,
|
||||
labelKey: `pages.housekeeping.routes.${id}`,
|
||||
href,
|
||||
capability: requirement,
|
||||
};
|
||||
}
|
||||
|
||||
const registry = createHousekeepingRegistry([
|
||||
peopleManifest([
|
||||
route("people.user-detail", "/ase-next/people/users/:id"),
|
||||
route("people.user-create", "/ase-next/people/users/new"),
|
||||
route("people.users", "/ase-next/people/users"),
|
||||
route("people.ticket-note", "/ase-next/people/tickets/:ticketId/notes/:noteId"),
|
||||
route("people.literal-tool", "/ase-next/people/tools/a+b[1]"),
|
||||
]),
|
||||
]);
|
||||
|
||||
describe("matchHousekeepingRoute", () => {
|
||||
it("matches concrete and dynamic preview routes", () => {
|
||||
it("matches a static route without parameters", () => {
|
||||
expect(matchHousekeepingRoute(registry, "/ase-next/people/users")).toEqual({
|
||||
routeId: "people.users",
|
||||
domain: "people",
|
||||
params: {},
|
||||
canonicalHref: "/ase-next/people/users",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers a static route over a dynamic route with the same depth", () => {
|
||||
expect(matchHousekeepingRoute(registry, "/ase-next/people/users/new")).toEqual({
|
||||
routeId: "people.user-create",
|
||||
domain: "people",
|
||||
params: {},
|
||||
canonicalHref: "/ase-next/people/users/new",
|
||||
});
|
||||
});
|
||||
|
||||
it("matches a dynamic segment and decodes its parameter", () => {
|
||||
expect(
|
||||
matchHousekeepingRoute(registry, "/ase-next/people/users/42"),
|
||||
matchHousekeepingRoute(registry, "/ase-next/people/users/Ada%20Lovelace"),
|
||||
).toEqual({
|
||||
routeId: "people.user-detail",
|
||||
domain: "people",
|
||||
params: { id: "42" },
|
||||
canonicalHref: "/ase-next/people/users/42",
|
||||
params: { id: "Ada Lovelace" },
|
||||
canonicalHref: "/ase-next/people/users/Ada%20Lovelace",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers a literal route over a dynamic route", () => {
|
||||
it("matches nested dynamic segments independently", () => {
|
||||
expect(
|
||||
matchHousekeepingRoute(registry, "/ase-next/people/users/settings"),
|
||||
).toMatchObject({
|
||||
routeId: "people.user-settings",
|
||||
params: {},
|
||||
matchHousekeepingRoute(
|
||||
registry,
|
||||
"/ase-next/people/tickets/42/notes/follow-up",
|
||||
),
|
||||
).toEqual({
|
||||
routeId: "people.ticket-note",
|
||||
domain: "people",
|
||||
params: { ticketId: "42", noteId: "follow-up" },
|
||||
canonicalHref: "/ase-next/people/tickets/42/notes/follow-up",
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes a parameter exactly once", () => {
|
||||
it("treats regular-expression metacharacters as literal segment text", () => {
|
||||
expect(
|
||||
matchHousekeepingRoute(registry, "/ase-next/people/users/%34%32"),
|
||||
).toMatchObject({
|
||||
routeId: "people.user-detail",
|
||||
params: { id: "42" },
|
||||
canonicalHref: "/ase-next/people/users/42",
|
||||
});
|
||||
matchHousekeepingRoute(registry, "/ase-next/people/tools/a+b[1]"),
|
||||
)?.toMatchObject({ routeId: "people.literal-tool" });
|
||||
expect(
|
||||
matchHousekeepingRoute(registry, "/ase-next/people/tools/ab1"),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"/ase-next/people",
|
||||
{
|
||||
routes: [
|
||||
route("people.kind-settings", "/ase-next/people/:kind/settings"),
|
||||
route("people.user-detail", "/ase-next/people/users/:id"),
|
||||
],
|
||||
},
|
||||
{
|
||||
routes: [
|
||||
route("people.user-detail", "/ase-next/people/users/:id"),
|
||||
route("people.kind-settings", "/ase-next/people/:kind/settings"),
|
||||
],
|
||||
},
|
||||
])(
|
||||
"prefers the earliest static segment regardless of registration order",
|
||||
({ routes }) => {
|
||||
const orderedRegistry = createHousekeepingRegistry([
|
||||
peopleManifest(routes),
|
||||
]);
|
||||
|
||||
expect(
|
||||
matchHousekeepingRoute(orderedRegistry, "/ase-next/people/users/settings"),
|
||||
)?.toMatchObject({
|
||||
routeId: "people.user-detail",
|
||||
params: { id: "settings" },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"/ase-next/people/unknown",
|
||||
"/ase-next/economy/users",
|
||||
"/ase-next/system/users/Ada",
|
||||
])("returns null for unknown or cross-domain path %s", (path) => {
|
||||
expect(matchHousekeepingRoute(registry, path)).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
"ase/people/users",
|
||||
"/ase-next//people/users",
|
||||
"/ase-next/people/users/",
|
||||
"/ase-next/people/users?rank=7",
|
||||
"/ase-next/people/users#active",
|
||||
"/ase-next/people\\users",
|
||||
"/ase-next/people//users",
|
||||
"/ase-next/people/./users",
|
||||
"/ase-next/people/users?tab=active",
|
||||
"/ase-next/people/users/%",
|
||||
"/ase-next/people/users/%2Fsystem",
|
||||
"/ase-next/people/users/..",
|
||||
"/ase-next/people/users/%2F",
|
||||
"/ase-next/people/users/%252F",
|
||||
"/ase-next/people/users/%00",
|
||||
])("rejects an unregistered canonical path: %s", (pathname) => {
|
||||
expect(matchHousekeepingRoute(registry, pathname)).toBeNull();
|
||||
"/ase-next\\people\\users",
|
||||
])("fails closed for malformed canonical path %s", (path) => {
|
||||
expect(matchHousekeepingRoute(registry, path)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user