Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"
This reverts commit488b6e57c4, reversing changes made tob506b4499a.
This commit is contained in:
1 parent
488b6e57c4
commit
b1ddda66ff
802 files changed
+61370
-76659
No files matched your search
@@ -9,146 +9,16 @@ import type { HousekeepingCapabilityContext } from "./contracts";
|
||||
|
||||
const routeMocks = vi.hoisted(() => {
|
||||
const messages: Record<string, string> = {
|
||||
"preview.badge": "HK::preview-badge",
|
||||
"preview.commandDisabled": "HK::command-disabled",
|
||||
"preview.backToSite": "HK::back-to-site",
|
||||
"navigation.skipToContent": "HK::skip-to-content",
|
||||
"navigation.primary": "HK::primary-navigation",
|
||||
"navigation.contextual": "HK::contextual-navigation",
|
||||
"navigation.operatorRank": "HK::operator-rank",
|
||||
"commandDeck.placeholder": "HK::command-placeholder",
|
||||
"commandDeck.navigation": "HK::command-navigation",
|
||||
"commandDeck.commands": "HK::command-commands",
|
||||
"commandDeck.entities": "HK::command-entities",
|
||||
"commandDeck.loading": "HK::command-loading",
|
||||
"commandDeck.empty": "HK::command-empty",
|
||||
"commandDeck.partial": "HK::command-partial",
|
||||
"commandDeck.close": "HK::command-close",
|
||||
"domains.people.title": "HK::people-title",
|
||||
"domains.people.description": "Localized People description",
|
||||
"domains.economy.title": "Localized Economy",
|
||||
"domains.economy.description": "Localized Economy description",
|
||||
"domains.operations.title": "Localized Operations",
|
||||
"domains.operations.description": "Localized Operations description",
|
||||
"domains.content.title": "Localized Content",
|
||||
"domains.content.description": "Localized Content description",
|
||||
"domains.hotel.title": "Localized Hotel",
|
||||
"domains.hotel.description": "Localized Hotel description",
|
||||
"domains.system.title": "HK::system-title",
|
||||
"domains.system.description": "Localized System description",
|
||||
"routes.content.editorial.articles": "HK::content-editorial-articles",
|
||||
"routes.content.editorial.article-create":
|
||||
"HK::content-editorial-article-create",
|
||||
"routes.content.editorial.article-detail":
|
||||
"HK::content-editorial-article-detail",
|
||||
"routes.content.media.photos": "HK::content-media-photos",
|
||||
"routes.content.media.library": "HK::content-media-library",
|
||||
"routes.content.media.banners": "HK::content-media-banners",
|
||||
"routes.content.media.ads": "HK::content-media-ads",
|
||||
"routes.content.media.ad-create": "HK::content-media-ad-create",
|
||||
"routes.content.media.ad-detail": "HK::content-media-ad-detail",
|
||||
"routes.content.engagement.events": "HK::content-engagement-events",
|
||||
"routes.content.engagement.event-create":
|
||||
"HK::content-engagement-event-create",
|
||||
"routes.content.engagement.event-types":
|
||||
"HK::content-engagement-event-types",
|
||||
"routes.content.engagement.event-detail":
|
||||
"HK::content-engagement-event-detail",
|
||||
"routes.content.engagement.polls": "HK::content-engagement-polls",
|
||||
"routes.content.engagement.poll-create":
|
||||
"HK::content-engagement-poll-create",
|
||||
"routes.content.engagement.poll-detail":
|
||||
"HK::content-engagement-poll-detail",
|
||||
"routes.content.help.questions": "HK::content-help-questions",
|
||||
"routes.content.help.question-create": "HK::content-help-question-create",
|
||||
"routes.content.help.question-detail": "HK::content-help-question-detail",
|
||||
"routes.content.editorial.tags": "HK::content-editorial-tags",
|
||||
"routes.content.engagement.prefixes": "HK::content-engagement-prefixes",
|
||||
"routes.content.editorial.writeable-boxes":
|
||||
"HK::content-editorial-writeable-boxes",
|
||||
"routes.content.help.email-templates": "HK::content-help-email-templates",
|
||||
"routes.content.brand.theme": "HK::content-brand-theme",
|
||||
"routes.content.brand.favicon": "HK::content-brand-favicon",
|
||||
"routes.content.localization.overview": "HK::content-localization-overview",
|
||||
"routes.content.localization.client": "HK::content-localization-client",
|
||||
"routes.content.localization.cms": "HK::content-localization-cms",
|
||||
"routes.content.localization.emulator": "HK::content-localization-emulator",
|
||||
"routes.system.access.permissions": "HK::system-access-permissions",
|
||||
"routes.system.access.permission-detail":
|
||||
"HK::system-access-permission-detail",
|
||||
"routes.system.configuration.settings": "HK::system-configuration-settings",
|
||||
"routes.system.configuration.emulator": "HK::system-configuration-emulator",
|
||||
"routes.system.observability.analytics":
|
||||
"HK::system-observability-analytics",
|
||||
"routes.system.observability.analytics-activity":
|
||||
"HK::system-observability-analytics-activity",
|
||||
"routes.system.observability.analytics-economy":
|
||||
"HK::system-observability-analytics-economy",
|
||||
"routes.system.observability.devops": "HK::system-observability-devops",
|
||||
"routes.system.observability.devops-errors":
|
||||
"HK::system-observability-devops-errors",
|
||||
"routes.system.observability.logs-staff":
|
||||
"HK::system-observability-logs-staff",
|
||||
"routes.system.observability.logs-audit":
|
||||
"HK::system-observability-logs-audit",
|
||||
"routes.system.observability.logs-chat":
|
||||
"HK::system-observability-logs-chat",
|
||||
"routes.system.observability.logs-commands":
|
||||
"HK::system-observability-logs-commands",
|
||||
"routes.system.observability.logs-trades":
|
||||
"HK::system-observability-logs-trades",
|
||||
"routes.system.operations.alerts": "HK::system-operations-alerts",
|
||||
"routes.system.operations.command-center":
|
||||
"HK::system-operations-command-center",
|
||||
"routes.system.operations.maintenance": "HK::system-operations-maintenance",
|
||||
"routes.economy.catalog.overview": "HK::economy-catalog-overview",
|
||||
"routes.economy.catalog.detail": "HK::economy-catalog-detail",
|
||||
"routes.economy.catalog.builder-club-detail":
|
||||
"HK::economy-catalog-builder-club-detail",
|
||||
"routes.economy.catalog.maintenance": "HK::economy-catalog-maintenance",
|
||||
"routes.economy.items.overview": "HK::economy-items-overview",
|
||||
"routes.economy.items.detail": "HK::economy-items-detail",
|
||||
"routes.economy.commerce.shop": "HK::economy-commerce-shop",
|
||||
"routes.economy.commerce.shop-create": "HK::economy-commerce-shop-create",
|
||||
"routes.economy.commerce.shop-detail": "HK::economy-commerce-shop-detail",
|
||||
"routes.economy.commerce.marketplace": "HK::economy-commerce-marketplace",
|
||||
"routes.economy.commerce.vouchers": "HK::economy-commerce-vouchers",
|
||||
"routes.economy.commerce.subscriptions":
|
||||
"HK::economy-commerce-subscriptions",
|
||||
"routes.economy.history.transactions": "HK::economy-history-transactions",
|
||||
"routes.economy.value.rare-values": "HK::economy-value-rare-values",
|
||||
"routes.economy.rewards.badges": "HK::economy-rewards-badges",
|
||||
"routes.economy.rewards.achievements": "HK::economy-rewards-achievements",
|
||||
"routes.economy.rewards.sounds": "HK::economy-rewards-sounds",
|
||||
"routes.economy.rewards.calendar": "HK::economy-rewards-calendar",
|
||||
"routes.economy.rewards.calendar-detail":
|
||||
"HK::economy-rewards-calendar-detail",
|
||||
"routes.hotel.rooms": "HK::hotel-rooms",
|
||||
"routes.hotel.room-detail": "HK::hotel-room-detail",
|
||||
"routes.hotel.room-furni": "HK::hotel-room-furni",
|
||||
"routes.hotel.navigation": "HK::hotel-navigation",
|
||||
"routes.hotel.radio.overview": "HK::hotel-radio-overview",
|
||||
"routes.hotel.radio.settings": "HK::hotel-radio-settings",
|
||||
"routes.hotel.radio.monitoring": "HK::hotel-radio-monitoring",
|
||||
"routes.hotel.radio.moderation": "HK::hotel-radio-moderation",
|
||||
"routes.hotel.radio.autodj": "HK::hotel-radio-autodj",
|
||||
"routes.hotel.radio.history": "HK::hotel-radio-history",
|
||||
"routes.hotel.radio.points": "HK::hotel-radio-points",
|
||||
"routes.hotel.radio.ranks": "HK::hotel-radio-ranks",
|
||||
"routes.hotel.radio.api-keys": "HK::hotel-radio-api-keys",
|
||||
"routes.hotel.radio.banners": "HK::hotel-radio-banners",
|
||||
"routes.hotel.radio.embed": "HK::hotel-radio-embed",
|
||||
"routes.hotel.studio.audit": "HK::hotel-studio-audit",
|
||||
"routes.hotel.studio.badges": "HK::hotel-studio-badges",
|
||||
"routes.hotel.studio.clone": "HK::hotel-studio-clone",
|
||||
"routes.hotel.studio.clothing": "HK::hotel-studio-clothing",
|
||||
"routes.hotel.studio.effects": "HK::hotel-studio-effects",
|
||||
"routes.hotel.studio.furni": "HK::hotel-studio-furni",
|
||||
"routes.hotel.studio.maintenance": "HK::hotel-studio-maintenance",
|
||||
"routes.hotel.studio.pets": "HK::hotel-studio-pets",
|
||||
"routes.hotel.studio.repair-icons": "HK::hotel-studio-repair-icons",
|
||||
"routes.hotel.studio.sync": "HK::hotel-studio-sync",
|
||||
"routes.hotel.studio.upload": "HK::hotel-studio-upload",
|
||||
"routes.operations.workspace": "HK::operations-workspace",
|
||||
"states.empty.title": "Localized empty title",
|
||||
"states.empty.description": "Localized empty description",
|
||||
};
|
||||
@@ -160,6 +30,10 @@ const routeMocks = vi.hoisted(() => {
|
||||
});
|
||||
|
||||
return {
|
||||
env: {
|
||||
NODE_ENV: "test" as "development" | "test" | "production",
|
||||
HOUSEKEEPING_NEXT_PREVIEW_ENABLED: true,
|
||||
},
|
||||
getHousekeepingCapabilityContext: vi.fn(),
|
||||
getTranslations: vi.fn(async (namespace: string) => {
|
||||
if (namespace !== "pages.housekeeping") {
|
||||
@@ -173,17 +47,11 @@ 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,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/env", () => ({ env: routeMocks.env }));
|
||||
vi.mock("next/navigation", () => ({
|
||||
notFound: routeMocks.notFound,
|
||||
redirect: routeMocks.redirect,
|
||||
@@ -195,114 +63,31 @@ vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
||||
getHousekeepingCapabilityContext: routeMocks.getHousekeepingCapabilityContext,
|
||||
}));
|
||||
vi.mock("@/lib/db", () => {
|
||||
throw new Error("route entrypoints must not import the database");
|
||||
throw new Error("preview routes must not import the database");
|
||||
});
|
||||
vi.mock("@/lib/auth", () => {
|
||||
throw new Error("route entrypoints must not call auth directly");
|
||||
throw new Error("preview routes must not call auth directly");
|
||||
});
|
||||
vi.mock("@/lib/permissions", () => {
|
||||
throw new Error("route entrypoints must not reload permissions directly");
|
||||
throw new Error("preview routes must not reload permissions directly");
|
||||
});
|
||||
vi.mock("@/actions", () => {
|
||||
throw new Error("route entrypoints must not import actions");
|
||||
throw new Error("preview routes must not import actions");
|
||||
});
|
||||
vi.mock("@/app/actions", () => {
|
||||
throw new Error("route entrypoints must not import 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"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "people.tool-detail",
|
||||
labelKey: "pages.housekeeping.domains.people.title",
|
||||
href: "/ase/people/tools/:id" as const,
|
||||
capability: {
|
||||
mode: "any" as const,
|
||||
slugs: ["admin.users.view"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "people.literal-tool",
|
||||
labelKey: "pages.housekeeping.domains.people.title",
|
||||
href: "/ase/people/tools/a+b[1]" as const,
|
||||
capability: {
|
||||
mode: "any" as const,
|
||||
slugs: ["admin.users.view"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: manifest,
|
||||
),
|
||||
};
|
||||
});
|
||||
vi.mock("@/features/housekeeping/route-handlers", () => ({
|
||||
HOUSEKEEPING_ROUTE_HANDLERS: [
|
||||
{
|
||||
routeId: "operations.workspace",
|
||||
render: routeMocks.renderHousekeepingRoute,
|
||||
},
|
||||
{ routeId: "people.users", render: routeMocks.renderHousekeepingRoute },
|
||||
{
|
||||
routeId: "people.user-detail",
|
||||
render: routeMocks.renderHousekeepingRoute,
|
||||
},
|
||||
{ routeId: "people.bans", render: routeMocks.renderHousekeepingRoute },
|
||||
{
|
||||
routeId: "people.tool-detail",
|
||||
render: routeMocks.renderHousekeepingRoute,
|
||||
},
|
||||
{
|
||||
routeId: "people.literal-tool",
|
||||
render: routeMocks.renderHousekeepingRoute,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
import HousekeepingDomainPage from "@/app/ase/[domain]/[[...segments]]/page";
|
||||
import HousekeepingDomainLayout from "@/app/ase/[domain]/layout";
|
||||
import HousekeepingLayout from "@/app/ase/layout";
|
||||
import HousekeepingPage from "@/app/ase/page";
|
||||
import AdminNextDomainLayout from "@/app/admin-next/[domain]/layout";
|
||||
import AdminNextDomainPage from "@/app/admin-next/[domain]/page";
|
||||
import AdminNextLayout from "@/app/admin-next/layout";
|
||||
import AdminNextPage from "@/app/admin-next/page";
|
||||
|
||||
const routeFiles = [
|
||||
"src/app/ase/layout.tsx",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/ase/[domain]/layout.tsx",
|
||||
"src/app/ase/[domain]/[[...segments]]/page.tsx",
|
||||
"src/app/admin-next/layout.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
"src/app/admin-next/[domain]/layout.tsx",
|
||||
"src/app/admin-next/[domain]/page.tsx",
|
||||
] as const;
|
||||
|
||||
const forbiddenModuleRoots = [
|
||||
@@ -575,42 +360,66 @@ async function renderRoute(route: ReactNode | Promise<ReactNode>) {
|
||||
return renderToStaticMarkup(await route);
|
||||
}
|
||||
|
||||
describe("/ase canonical layout", () => {
|
||||
it("renders without a runtime preview gate", async () => {
|
||||
const html = await renderRoute(
|
||||
HousekeepingLayout({
|
||||
children: createElement("p", null, "Canonical child"),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(html).toContain("Canonical child");
|
||||
expect(routeMocks.notFound).not.toHaveBeenCalled();
|
||||
describe("/admin-next preview gate", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
routeMocks.env.NODE_ENV = "test";
|
||||
routeMocks.env.HOUSEKEEPING_NEXT_PREVIEW_ENABLED = true;
|
||||
});
|
||||
|
||||
it.each([
|
||||
["production", true],
|
||||
["production", false],
|
||||
["development", false],
|
||||
] as const)("returns 404 for NODE_ENV=%s flag=%s", async (nodeEnv, flag) => {
|
||||
routeMocks.env.NODE_ENV = nodeEnv;
|
||||
routeMocks.env.HOUSEKEEPING_NEXT_PREVIEW_ENABLED = flag;
|
||||
|
||||
await expect(async () =>
|
||||
renderRoute(
|
||||
AdminNextLayout({
|
||||
children: createElement("p", null, "Preview child"),
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("NEXT_NOT_FOUND");
|
||||
expect(routeMocks.notFound).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each(["development", "test"] as const)(
|
||||
"renders children in %s when explicitly enabled",
|
||||
async (nodeEnv) => {
|
||||
routeMocks.env.NODE_ENV = nodeEnv;
|
||||
|
||||
const html = await renderRoute(
|
||||
AdminNextLayout({
|
||||
children: createElement("p", null, "Preview child"),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(html).toContain("Preview child");
|
||||
expect(routeMocks.notFound).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("/ase operational home", () => {
|
||||
describe("/admin-next first visible domain", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders the Operations workspace directly for an administrator", async () => {
|
||||
it("redirects an administrator to Operations in locked registry order", async () => {
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
||||
capabilityContext([PERMS.ADMIN_DASHBOARD, PERMS.USERS_VIEW]),
|
||||
);
|
||||
|
||||
const html = await renderRoute(HousekeepingPage());
|
||||
expect(html).toContain("Rendered operations.workspace");
|
||||
expect(html).toContain("HK::operations-workspace");
|
||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
match: expect.objectContaining({ routeId: "operations.workspace" }),
|
||||
}),
|
||||
await expect(AdminNextPage()).rejects.toThrow(
|
||||
"NEXT_REDIRECT:/admin-next/operations",
|
||||
);
|
||||
expect(routeMocks.redirect).not.toHaveBeenCalled();
|
||||
expect(routeMocks.redirect).toHaveBeenCalledWith("/admin-next/operations");
|
||||
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
|
||||
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("redirects a moderator with only an approved mod view capability to People", async () => {
|
||||
@@ -618,10 +427,10 @@ describe("/ase operational home", () => {
|
||||
capabilityContext([PERMS.MOD_CFH_VIEW]),
|
||||
);
|
||||
|
||||
await expect(HousekeepingPage()).rejects.toThrow(
|
||||
"NEXT_REDIRECT:/ase/people/users",
|
||||
await expect(AdminNextPage()).rejects.toThrow(
|
||||
"NEXT_REDIRECT:/admin-next/people",
|
||||
);
|
||||
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase/people/users");
|
||||
expect(routeMocks.redirect).toHaveBeenCalledWith("/admin-next/people");
|
||||
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
@@ -632,7 +441,7 @@ describe("/ase operational home", () => {
|
||||
capabilityContext([]),
|
||||
);
|
||||
|
||||
await expect(HousekeepingPage()).rejects.toThrow("NEXT_NOT_FOUND");
|
||||
await expect(AdminNextPage()).rejects.toThrow("NEXT_NOT_FOUND");
|
||||
expect(routeMocks.notFound).toHaveBeenCalledTimes(1);
|
||||
expect(routeMocks.redirect).not.toHaveBeenCalled();
|
||||
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
|
||||
@@ -641,14 +450,14 @@ describe("/ase operational home", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("/ase/[domain] layout", () => {
|
||||
describe("/admin-next/[domain] layout", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("rejects an unknown domain before loading capability context", async () => {
|
||||
await expect(
|
||||
HousekeepingDomainLayout({
|
||||
AdminNextDomainLayout({
|
||||
children: createElement("p", null, "Unknown body"),
|
||||
params: Promise.resolve({ domain: "unknown" }),
|
||||
}),
|
||||
@@ -663,7 +472,7 @@ describe("/ase/[domain] layout", () => {
|
||||
);
|
||||
|
||||
await expect(
|
||||
HousekeepingDomainLayout({
|
||||
AdminNextDomainLayout({
|
||||
children: createElement("p", null, "Economy body"),
|
||||
params: Promise.resolve({ domain: "economy" }),
|
||||
}),
|
||||
@@ -680,7 +489,7 @@ describe("/ase/[domain] layout", () => {
|
||||
);
|
||||
|
||||
const html = await renderRoute(
|
||||
HousekeepingDomainLayout({
|
||||
AdminNextDomainLayout({
|
||||
children: createElement("p", null, "People body"),
|
||||
params: Promise.resolve({ domain: "people" }),
|
||||
}),
|
||||
@@ -691,7 +500,7 @@ describe("/ase/[domain] layout", () => {
|
||||
expect(html).toContain("HK::primary-navigation");
|
||||
expect(html).toContain("HK::contextual-navigation");
|
||||
expect(html).toContain("HK::command-disabled");
|
||||
expect(html).not.toContain("HK::preview-badge");
|
||||
expect(html).toContain("HK::preview-badge");
|
||||
expect(html).toContain("HK::back-to-site");
|
||||
expect(html).toContain("HK::people-title");
|
||||
expect(html).toContain("People body");
|
||||
@@ -704,153 +513,36 @@ describe("/ase/[domain] layout", () => {
|
||||
);
|
||||
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("builds and translates the complete real System navigation", async () => {
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue({
|
||||
...capabilityContext([]),
|
||||
isSuperAdmin: true,
|
||||
});
|
||||
|
||||
const html = await renderRoute(
|
||||
HousekeepingDomainLayout({
|
||||
children: createElement("p", null, "System body"),
|
||||
params: Promise.resolve({ domain: "system" }),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const label of [
|
||||
"HK::system-access-permissions",
|
||||
"HK::system-access-permission-detail",
|
||||
"HK::system-configuration-settings",
|
||||
"HK::system-configuration-emulator",
|
||||
"HK::system-observability-analytics",
|
||||
"HK::system-observability-analytics-activity",
|
||||
"HK::system-observability-analytics-economy",
|
||||
"HK::system-observability-devops",
|
||||
"HK::system-observability-devops-errors",
|
||||
"HK::system-observability-logs-staff",
|
||||
"HK::system-observability-logs-audit",
|
||||
"HK::system-observability-logs-chat",
|
||||
"HK::system-observability-logs-commands",
|
||||
"HK::system-observability-logs-trades",
|
||||
"HK::system-operations-alerts",
|
||||
"HK::system-operations-command-center",
|
||||
"HK::system-operations-maintenance",
|
||||
]) {
|
||||
expect(html).toContain(label);
|
||||
}
|
||||
expect(html).toContain("HK::system-title");
|
||||
expect(html).toContain("System body");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/ase/[domain]/[[...segments]] page", () => {
|
||||
describe("/admin-next/[domain] page", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("maps a permitted canonical path and renders its handler with one context", async () => {
|
||||
const context = capabilityContext([PERMS.MOD_CFH_VIEW]);
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(context);
|
||||
|
||||
it("renders the real localized manifest and empty state without reloading access", async () => {
|
||||
const html = await renderRoute(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({ domain: "people", segments: ["users"] }),
|
||||
AdminNextDomainPage({
|
||||
params: Promise.resolve({ domain: "people" }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(html).toContain("Rendered people.users for refreshed-moderator");
|
||||
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledTimes(1);
|
||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
||||
context,
|
||||
translate: expect.any(Function),
|
||||
match: {
|
||||
routeId: "people.users",
|
||||
domain: "people",
|
||||
params: {},
|
||||
canonicalHref: "/ase/people/users",
|
||||
},
|
||||
});
|
||||
expect(routeMocks.getTranslations).toHaveBeenCalledOnce();
|
||||
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);
|
||||
});
|
||||
|
||||
it("maps nested canonical segments to canonical dynamic parameters", async () => {
|
||||
const context = capabilityContext([PERMS.USERS_VIEW]);
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(context);
|
||||
|
||||
await renderRoute(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({
|
||||
domain: "people",
|
||||
segments: ["users", "42"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
||||
context,
|
||||
translate: expect.any(Function),
|
||||
match: {
|
||||
routeId: "people.user-detail",
|
||||
domain: "people",
|
||||
params: { id: "42" },
|
||||
canonicalHref: "/ase/people/users/42",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps catch-all encoding from changing the literal route handler", async () => {
|
||||
const context = capabilityContext([PERMS.USERS_VIEW]);
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(context);
|
||||
|
||||
await renderRoute(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({
|
||||
domain: "people",
|
||||
segments: ["tools", "a+b[1]"],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
||||
context,
|
||||
translate: expect.any(Function),
|
||||
match: {
|
||||
routeId: "people.literal-tool",
|
||||
domain: "people",
|
||||
params: {},
|
||||
canonicalHref: "/ase/people/tools/a%2Bb%5B1%5D",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 404 for an inaccessible matched route without invoking it", async () => {
|
||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
||||
capabilityContext([PERMS.MOD_CFH_VIEW]),
|
||||
);
|
||||
|
||||
it("rejects an unknown domain before translating", async () => {
|
||||
await expect(
|
||||
HousekeepingDomainPage({
|
||||
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(
|
||||
HousekeepingDomainPage({
|
||||
params: Promise.resolve({ domain: "people", segments: ["unknown"] }),
|
||||
AdminNextDomainPage({
|
||||
params: Promise.resolve({ domain: "unknown" }),
|
||||
}),
|
||||
).rejects.toThrow("NEXT_NOT_FOUND");
|
||||
expect(routeMocks.getHousekeepingCapabilityContext).not.toHaveBeenCalled();
|
||||
expect(routeMocks.renderHousekeepingRoute).not.toHaveBeenCalled();
|
||||
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -869,247 +561,247 @@ describe("preview route import boundary", () => {
|
||||
it.each([
|
||||
[
|
||||
"relative database import with resolver extension",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../../lib/db.js";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"aliased database import with resolver extension",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "@/lib/db.js";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"action root import with resolver extension",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import actions from "../../actions.mjs";',
|
||||
"src/actions",
|
||||
],
|
||||
[
|
||||
"legacy mod root import with resolver extension",
|
||||
"src/app/ase/layout.tsx",
|
||||
"src/app/admin-next/layout.tsx",
|
||||
'import mod from "../mod.cjs";',
|
||||
"src/app/mod",
|
||||
],
|
||||
[
|
||||
"database import with query suffix",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../../lib/db?server-only";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"action import with hash suffix",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import("../../actions/users#server")',
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"Windows-style relative database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
String.raw`import db from "..\\..\\lib\\db";`,
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"Windows-style aliased database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
String.raw`import db from "@\\lib\\db";`,
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"percent-encoded database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../../lib/%64%62";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"optional CommonJS database require",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'require?.("../../lib/db")',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"module database require",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'module.require("../../lib/db")',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"require.resolve database access",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'require.resolve("../../lib/db")',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"optional module database require",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'module.require?.("../../lib/db")',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"optional require.resolve database access",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'require.resolve?.("../../lib/db")',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"TypeScript import-equals database access",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db = require("../../lib/db");',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"U+2028 line-continuation database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../\\' + "\u2028" + '../lib/db";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"U+2029 line-continuation database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../\\' + "\u2029" + '../lib/db";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"parenthesized dynamic action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import(("../../actions/users"))',
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"regex-brace template-expression action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
`const x = \`${interpolationOpen}/}/.test(value) ? import("../../actions/users") : null}\`;`,
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"CommonJS database require",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'require("../../lib/db")',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"template-literal dynamic action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
"import(`../../actions/users`)",
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"TypeScript-asserted dynamic action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import(("../../actions/users" as string))',
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"template-expression dynamic action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
`const x = \`${interpolationOpen}import("../../actions/users")}\`;`,
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"nested template-expression dynamic action import",
|
||||
"src/app/ase/[domain]/[[...segments]]/page.tsx",
|
||||
`const x = \`${interpolationOpen}ready ? \`${interpolationOpen}import("../../../../actions/nested")}\` : ""}\`;`,
|
||||
"src/app/admin-next/[domain]/page.tsx",
|
||||
`const x = \`${interpolationOpen}ready ? \`${interpolationOpen}import("../../../actions/nested")}\` : ""}\`;`,
|
||||
"src/actions/nested",
|
||||
],
|
||||
[
|
||||
"unicode escaped dynamic app-action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import("\\u002e\\u002e/actions/users")',
|
||||
"src/app/actions/users",
|
||||
],
|
||||
[
|
||||
"code-point escaped dynamic app-action import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import("\\u{2e}\\u{2e}/actions/users")',
|
||||
"src/app/actions/users",
|
||||
],
|
||||
[
|
||||
"hex escaped export-from auth import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'export * from "\\x2e\\x2e/\\x2e\\x2e/lib/auth";',
|
||||
"src/lib/auth",
|
||||
],
|
||||
[
|
||||
"escaped-slash permissions import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import permissions from "..\\/..\\/lib\\/permissions";',
|
||||
"src/lib/permissions",
|
||||
],
|
||||
[
|
||||
"unknown escape database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../../\\lib/db";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"line-continuation database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import db from "../\\' + "\n" + '../lib/db";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"aliased database descendant import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import { query } from "@/lib/db/query";',
|
||||
"src/lib/db/query",
|
||||
],
|
||||
[
|
||||
"root relative database import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import { db } from "../../lib/db";',
|
||||
"src/lib/db",
|
||||
],
|
||||
[
|
||||
"domain relative auth side-effect import",
|
||||
"src/app/ase/[domain]/layout.tsx",
|
||||
"src/app/admin-next/[domain]/layout.tsx",
|
||||
'import "../../../lib/auth";',
|
||||
"src/lib/auth",
|
||||
],
|
||||
[
|
||||
"domain relative permissions export",
|
||||
"src/app/ase/[domain]/[[...segments]]/page.tsx",
|
||||
'export { getAdminContext } from "../../../../lib/permissions";',
|
||||
"src/app/admin-next/[domain]/page.tsx",
|
||||
'export { getAdminContext } from "../../../lib/permissions";',
|
||||
"src/lib/permissions",
|
||||
],
|
||||
[
|
||||
"root relative action dynamic import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import("../../actions/users")',
|
||||
"src/actions/users",
|
||||
],
|
||||
[
|
||||
"root relative app action export",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'export * from "../actions";',
|
||||
"src/app/actions",
|
||||
],
|
||||
[
|
||||
"domain relative legacy admin import",
|
||||
"src/app/ase/[domain]/layout.tsx",
|
||||
"src/app/admin-next/[domain]/layout.tsx",
|
||||
'import page from "../../admin/users/page";',
|
||||
"src/app/admin/users/page",
|
||||
],
|
||||
[
|
||||
"root relative legacy mod dynamic import",
|
||||
"src/app/ase/layout.tsx",
|
||||
"src/app/admin-next/layout.tsx",
|
||||
'import("../mod/users/page")',
|
||||
"src/app/mod/users/page",
|
||||
],
|
||||
[
|
||||
"Prisma TypeScript import type",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'type PrismaClient = import("@prisma/client").PrismaClient;',
|
||||
"@prisma/client",
|
||||
],
|
||||
[
|
||||
"Drizzle package import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import { sql } from "drizzle-orm";',
|
||||
"drizzle-orm",
|
||||
],
|
||||
[
|
||||
"mysql2 package import",
|
||||
"src/app/ase/page.tsx",
|
||||
"src/app/admin-next/page.tsx",
|
||||
'import type { Pool } from "mysql2";',
|
||||
"mysql2",
|
||||
],
|
||||
@@ -1142,7 +834,7 @@ describe("preview route import boundary", () => {
|
||||
],
|
||||
] as const)("fails closed for non-literal %s", (_name, source, violation) => {
|
||||
expect(
|
||||
findRouteImportBoundaryViolations(source, "src/app/ase/page.tsx"),
|
||||
findRouteImportBoundaryViolations(source, "src/app/admin-next/page.tsx"),
|
||||
).toContain(violation);
|
||||
});
|
||||
|
||||
@@ -1151,7 +843,7 @@ describe("preview route import boundary", () => {
|
||||
'import database from "@/lib/database.js?raw";',
|
||||
'import dbTools from "../../lib/db-tools.ts";',
|
||||
'import auth from "../../lib/authentication";',
|
||||
'import preview from "../ase-shared";',
|
||||
'import preview from "../admin-next-shared";',
|
||||
'import prismaTools from "@prisma/client-tools";',
|
||||
'import drizzleTools from "drizzle-orm-kit";',
|
||||
'import mysqlTools from "mysql2-wrapper";',
|
||||
@@ -1161,7 +853,7 @@ describe("preview route import boundary", () => {
|
||||
].join("\n");
|
||||
|
||||
expect(
|
||||
findRouteImportBoundaryViolations(source, "src/app/ase/page.tsx"),
|
||||
findRouteImportBoundaryViolations(source, "src/app/admin-next/page.tsx"),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user