test(housekeeping): isolate request capability contexts

This commit is contained in:
Simo committed 2026-08-26 21:28:26 +02:00
1 parent edc165ba8d
commit bdb8f0b02b
1 file changed
+111 -46
@@ -1,62 +1,135 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
const { auth, getAdminContext, resetRequestCache, staleActor, wrapCache } = const {
vi.hoisted(() => { auth,
const resetters: (() => void)[] = []; getAdminContext,
getTranslations,
notFound,
redirect,
startLogicalRequest,
staleActor,
wrapCache,
} = vi.hoisted(() => {
let requestCache = new Map<symbol, Promise<unknown>>();
return { return {
auth: vi.fn(), auth: vi.fn(),
getAdminContext: vi.fn(), getAdminContext: vi.fn(),
resetRequestCache: () => { getTranslations: vi.fn(),
resetters.forEach((reset) => { notFound: vi.fn((): never => {
reset(); throw new Error("NEXT_NOT_FOUND");
}); }),
}, redirect: vi.fn((href: string): never => {
staleActor: { id: 9, username: "stale-session", rank: 1 }, throw new Error(`NEXT_REDIRECT:${href}`);
wrapCache: (callback: () => Promise<unknown>) => { }),
let value: Promise<unknown> | undefined; startLogicalRequest: () => {
resetters.push(() => { requestCache = new Map();
value = undefined; },
}); staleActor: { id: 9, username: "stale-session", rank: 1 },
return () => (value ??= callback()); wrapCache: (callback: () => Promise<unknown>) => {
}, const cacheKey = Symbol("housekeeping-capability-context");
}; return () => {
}); let value = requestCache.get(cacheKey);
if (!value) {
value = callback();
requestCache.set(cacheKey, value);
}
return value;
};
},
};
});
vi.mock("react", () => ({ cache: wrapCache })); vi.mock("react", () => ({ cache: wrapCache }));
vi.mock("next/navigation", () => ({ notFound, redirect }));
vi.mock("next-intl/server", () => ({ getTranslations }));
vi.mock("@/lib/auth", () => ({ auth })); vi.mock("@/lib/auth", () => ({ auth }));
vi.mock("@/lib/db", () => { vi.mock("@/lib/db", () => {
throw new Error("server capability context must not access the database"); throw new Error("server capability context must not access the database");
}); });
vi.mock("@/lib/permissions", () => ({ getAdminContext })); vi.mock("@/lib/permissions", () => ({ getAdminContext }));
import AdminNextDomainLayout from "@/app/ase-next/[domain]/layout";
import AdminNextPage from "@/app/ase-next/page";
import { requireHousekeepingCapability } from "@/lib/admin/guard"; import { requireHousekeepingCapability } from "@/lib/admin/guard";
import { auth as mockedAuth } from "@/lib/auth"; import { auth as mockedAuth } from "@/lib/auth";
import { anyCapability } from "./contracts"; import { anyCapability } from "./contracts";
import { getHousekeepingCapabilityContext } from "./server-capability-context"; import { getHousekeepingCapabilityContext } from "./server-capability-context";
function adminContext(
actor: { id: number; username: string; rank: number },
granted: readonly string[],
) {
const permissions = new Set(granted);
return {
session: { user: actor },
permissions: {
isSuperAdmin: false,
has: (slug: string) => permissions.has(slug),
hasAny: (...slugs: string[]) =>
slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs: string[]) =>
slugs.every((slug) => permissions.has(slug)),
},
};
}
async function invokeRequestConsumers() {
const shell = await AdminNextDomainLayout({
children: null,
params: Promise.resolve({ domain: "operations" }),
});
await expect(AdminNextPage()).rejects.toThrow(
"NEXT_REDIRECT:/ase-next/operations",
);
const commandContext = await requireHousekeepingCapability(
anyCapability("admin.dashboard"),
);
return { commandContext, shell };
}
describe("getHousekeepingCapabilityContext", () => { describe("getHousekeepingCapabilityContext", () => {
beforeEach(() => { beforeEach(() => {
resetRequestCache(); startLogicalRequest();
vi.clearAllMocks(); vi.clearAllMocks();
auth.mockResolvedValue({ user: staleActor }); auth.mockResolvedValue({ user: staleActor });
getAdminContext.mockResolvedValue({ getTranslations.mockResolvedValue((key: string) => key);
session: { getAdminContext.mockResolvedValue(
user: { adminContext({ id: 42, username: "operator", rank: 7 }, [
id: 42, "admin.dashboard",
username: "operator", "admin.users.view",
rank: 7, ]),
}, );
}, });
permissions: {
isSuperAdmin: false, it("isolates real shell, page, and command preflight consumers between logical requests", async () => {
has: (slug: string) => slug === "admin.users.view", const first = await invokeRequestConsumers();
hasAny: (...slugs: string[]) => slugs.includes("admin.users.view"),
hasAll: (...slugs: string[]) => expect(first.shell).toBeDefined();
slugs.every((slug) => slug === "admin.users.view"), expect(first.commandContext.actor).toEqual({
}, id: 42,
username: "operator",
rank: 7,
}); });
expect(getAdminContext).toHaveBeenCalledTimes(1);
startLogicalRequest();
getAdminContext.mockResolvedValue(
adminContext({ id: 84, username: "second-operator", rank: 2 }, [
"admin.dashboard",
]),
);
const second = await invokeRequestConsumers();
expect(second.shell).toBeDefined();
expect(second.commandContext.actor).toEqual({
id: 84,
username: "second-operator",
rank: 2,
});
expect(second.commandContext.has("admin.users.view")).toBe(false);
expect(getAdminContext).toHaveBeenCalledTimes(2);
}); });
it("uses the refreshed administrator actor instead of the stale auth actor", async () => { it("uses the refreshed administrator actor instead of the stale auth actor", async () => {
@@ -71,12 +144,4 @@ describe("getHousekeepingCapabilityContext", () => {
expect(getAdminContext).toHaveBeenCalledTimes(1); expect(getAdminContext).toHaveBeenCalledTimes(1);
expect(mockedAuth).not.toHaveBeenCalled(); expect(mockedAuth).not.toHaveBeenCalled();
}); });
it("loads the capability context once for shell, page, and command preflight", async () => {
await getHousekeepingCapabilityContext();
await getHousekeepingCapabilityContext();
await requireHousekeepingCapability(anyCapability("admin.users.view"));
expect(getAdminContext).toHaveBeenCalledTimes(1);
});
}); });