test(housekeeping): isolate request capability contexts
This commit is contained in:
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user