feat(housekeeping): dispatch canonical domain routes
This commit is contained in:
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",
|
||||
],
|
||||
[
|
||||
|
||||
Reference in new issue
Block a user