feat(housekeeping): dispatch canonical domain routes

This commit is contained in:
Simo committed 2026-08-28 21:35:08 +02:00
1 parent 2970dff563
commit e5c230ba35
13 files changed
+680 -82

No files matched your search

@@ -47,6 +47,13 @@ const routeMocks = vi.hoisted(() => {
redirect: vi.fn((href: string): never => {
throw new Error(`NEXT_REDIRECT:${href}`);
}),
renderHousekeepingRoute: vi.fn(
async (input: {
context: { actor: { username: string } };
match: { routeId: string };
}) =>
`Rendered ${input.match.routeId} for ${input.context.actor.username}`,
),
translate,
};
});
@@ -77,9 +84,62 @@ vi.mock("@/actions", () => {
vi.mock("@/app/actions", () => {
throw new Error("preview routes must not import actions");
});
vi.mock("@/features/housekeeping/manifests", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/features/housekeeping/manifests")>();
return {
HOUSEKEEPING_MANIFESTS: actual.HOUSEKEEPING_MANIFESTS.map((manifest) =>
manifest.id === "people"
? {
...manifest,
routes: [
{
id: "people.users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as const,
capability: {
mode: "any" as const,
slugs: ["admin.users.view", "mod.cfh.view"],
},
},
{
id: "people.user-detail",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users/:id" as const,
capability: {
mode: "any" as const,
slugs: ["admin.users.view", "mod.cfh.view"],
},
},
{
id: "people.bans",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/bans" as const,
capability: {
mode: "any" as const,
slugs: ["admin.bans.view"],
},
},
],
}
: manifest,
),
};
});
vi.mock("@/features/housekeeping/route-handlers", () => ({
HOUSEKEEPING_ROUTE_HANDLERS: [
{ routeId: "people.users", render: routeMocks.renderHousekeepingRoute },
{
routeId: "people.user-detail",
render: routeMocks.renderHousekeepingRoute,
},
{ routeId: "people.bans", render: routeMocks.renderHousekeepingRoute },
],
}));
import AdminNextDomainPage from "@/app/ase-next/[domain]/[[...segments]]/page";
import AdminNextDomainLayout from "@/app/ase-next/[domain]/layout";
import AdminNextDomainPage from "@/app/ase-next/[domain]/page";
import AdminNextLayout from "@/app/ase-next/layout";
import AdminNextPage from "@/app/ase-next/page";
@@ -87,7 +147,7 @@ const routeFiles = [
"src/app/ase-next/layout.tsx",
"src/app/ase-next/page.tsx",
"src/app/ase-next/[domain]/layout.tsx",
"src/app/ase-next/[domain]/page.tsx",
"src/app/ase-next/[domain]/[[...segments]]/page.tsx",
] as const;
const forbiddenModuleRoots = [
@@ -407,15 +467,15 @@ describe("/ase-next first visible domain", () => {
vi.clearAllMocks();
});
it("redirects an administrator away from the Operations preview root", async () => {
it("redirects an administrator to the first permitted registered route", async () => {
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
capabilityContext([PERMS.ADMIN_DASHBOARD, PERMS.USERS_VIEW]),
);
await expect(AdminNextPage()).rejects.toThrow(
"NEXT_REDIRECT:/ase-next/operations",
"NEXT_REDIRECT:/ase-next/people/users",
);
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/operations");
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people/users");
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
1,
);
@@ -428,9 +488,9 @@ describe("/ase-next first visible domain", () => {
);
await expect(AdminNextPage()).rejects.toThrow(
"NEXT_REDIRECT:/ase-next/people",
"NEXT_REDIRECT:/ase-next/people/users",
);
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people");
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people/users");
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
1,
);
@@ -515,34 +575,86 @@ describe("/ase-next/[domain] layout", () => {
});
});
describe("/ase-next/[domain] page", () => {
describe("/ase-next/[domain]/[[...segments]] page", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders the real localized manifest and empty state without reloading access", async () => {
it("maps a permitted preview path and renders its handler with one context", async () => {
const context = capabilityContext([PERMS.MOD_CFH_VIEW]);
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(context);
const html = await renderRoute(
AdminNextDomainPage({
params: Promise.resolve({ domain: "people" }),
params: Promise.resolve({ domain: "people", segments: ["users"] }),
}),
);
expect(html).toContain("HK::people-title");
expect(html).toContain("Localized People description");
expect(html).toContain("Localized empty title");
expect(html).toContain("Localized empty description");
expect(routeMocks.getHousekeepingCapabilityContext).not.toHaveBeenCalled();
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
expect(html).toContain("Rendered people.users for refreshed-moderator");
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
1,
);
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledTimes(1);
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
match: {
routeId: "people.users",
domain: "people",
params: {},
canonicalHref: "/ase/people/users",
},
});
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
});
it("rejects an unknown domain before translating", async () => {
it("maps nested preview segments to canonical dynamic parameters", async () => {
const context = capabilityContext([PERMS.USERS_VIEW]);
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(context);
await renderRoute(
AdminNextDomainPage({
params: Promise.resolve({
domain: "people",
segments: ["users", "42"],
}),
}),
);
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
match: {
routeId: "people.user-detail",
domain: "people",
params: { id: "42" },
canonicalHref: "/ase/people/users/42",
},
});
});
it("returns 404 for an inaccessible matched route without invoking it", async () => {
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
capabilityContext([PERMS.MOD_CFH_VIEW]),
);
await expect(
AdminNextDomainPage({
params: Promise.resolve({ domain: "unknown" }),
params: Promise.resolve({ domain: "people", segments: ["bans"] }),
}),
).rejects.toThrow("NEXT_NOT_FOUND");
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
1,
);
expect(routeMocks.renderHousekeepingRoute).not.toHaveBeenCalled();
});
it("rejects an unknown route before loading capability context", async () => {
await expect(
AdminNextDomainPage({
params: Promise.resolve({ domain: "people", segments: ["unknown"] }),
}),
).rejects.toThrow("NEXT_NOT_FOUND");
expect(routeMocks.getHousekeepingCapabilityContext).not.toHaveBeenCalled();
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
expect(routeMocks.renderHousekeepingRoute).not.toHaveBeenCalled();
});
});
@@ -699,8 +811,8 @@ describe("preview route import boundary", () => {
],
[
"nested template-expression dynamic action import",
"src/app/ase-next/[domain]/page.tsx",
`const x = \`${interpolationOpen}ready ? \`${interpolationOpen}import("../../../actions/nested")}\` : ""}\`;`,
"src/app/ase-next/[domain]/[[...segments]]/page.tsx",
`const x = \`${interpolationOpen}ready ? \`${interpolationOpen}import("../../../../actions/nested")}\` : ""}\`;`,
"src/actions/nested",
],
[
@@ -759,8 +871,8 @@ describe("preview route import boundary", () => {
],
[
"domain relative permissions export",
"src/app/ase-next/[domain]/page.tsx",
'export { getAdminContext } from "../../../lib/permissions";',
"src/app/ase-next/[domain]/[[...segments]]/page.tsx",
'export { getAdminContext } from "../../../../lib/permissions";',
"src/lib/permissions",
],
[