From bdb8f0b02b4cebfc2def5a7266da846ebe0a5990 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 26 Aug 2026 21:28:26 +0200 Subject: [PATCH] test(housekeeping): isolate request capability contexts --- .../server-capability-context.test.ts | 157 +++++++++++++----- 1 file changed, 111 insertions(+), 46 deletions(-) diff --git a/src/features/housekeeping/foundation/server-capability-context.test.ts b/src/features/housekeeping/foundation/server-capability-context.test.ts index d83a7671..b3a93a68 100644 --- a/src/features/housekeeping/foundation/server-capability-context.test.ts +++ b/src/features/housekeeping/foundation/server-capability-context.test.ts @@ -1,62 +1,135 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -const { auth, getAdminContext, resetRequestCache, staleActor, wrapCache } = - vi.hoisted(() => { - const resetters: (() => void)[] = []; +const { + auth, + getAdminContext, + getTranslations, + notFound, + redirect, + startLogicalRequest, + staleActor, + wrapCache, +} = vi.hoisted(() => { + let requestCache = new Map>(); - return { - auth: vi.fn(), - getAdminContext: vi.fn(), - resetRequestCache: () => { - resetters.forEach((reset) => { - reset(); - }); - }, - staleActor: { id: 9, username: "stale-session", rank: 1 }, - wrapCache: (callback: () => Promise) => { - let value: Promise | undefined; - resetters.push(() => { - value = undefined; - }); - return () => (value ??= callback()); - }, - }; - }); + return { + auth: vi.fn(), + getAdminContext: vi.fn(), + getTranslations: vi.fn(), + notFound: vi.fn((): never => { + throw new Error("NEXT_NOT_FOUND"); + }), + redirect: vi.fn((href: string): never => { + throw new Error(`NEXT_REDIRECT:${href}`); + }), + startLogicalRequest: () => { + requestCache = new Map(); + }, + staleActor: { id: 9, username: "stale-session", rank: 1 }, + wrapCache: (callback: () => Promise) => { + 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("next/navigation", () => ({ notFound, redirect })); +vi.mock("next-intl/server", () => ({ getTranslations })); vi.mock("@/lib/auth", () => ({ auth })); vi.mock("@/lib/db", () => { throw new Error("server capability context must not access the database"); }); 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 { auth as mockedAuth } from "@/lib/auth"; import { anyCapability } from "./contracts"; 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", () => { beforeEach(() => { - resetRequestCache(); + startLogicalRequest(); vi.clearAllMocks(); auth.mockResolvedValue({ user: staleActor }); - getAdminContext.mockResolvedValue({ - session: { - user: { - id: 42, - username: "operator", - rank: 7, - }, - }, - permissions: { - isSuperAdmin: false, - has: (slug: string) => slug === "admin.users.view", - hasAny: (...slugs: string[]) => slugs.includes("admin.users.view"), - hasAll: (...slugs: string[]) => - slugs.every((slug) => slug === "admin.users.view"), - }, + getTranslations.mockResolvedValue((key: string) => key); + getAdminContext.mockResolvedValue( + adminContext({ id: 42, username: "operator", rank: 7 }, [ + "admin.dashboard", + "admin.users.view", + ]), + ); + }); + + it("isolates real shell, page, and command preflight consumers between logical requests", async () => { + const first = await invokeRequestConsumers(); + + expect(first.shell).toBeDefined(); + 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 () => { @@ -71,12 +144,4 @@ describe("getHousekeepingCapabilityContext", () => { expect(getAdminContext).toHaveBeenCalledTimes(1); 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); - }); });