import { afterEach, describe, expect, it, vi } from "vitest"; import { SESSION_COOKIE_NAME } from "@/lib/auth/config"; import { chunkCookieValue, chunkedCookieName, createSignedJsonValue, type AuthSession } from "@/lib/auth/session"; const { cookieValues } = vi.hoisted(() => ({ cookieValues: new Map(), })); vi.mock("next/headers", () => ({ cookies: vi.fn(async () => ({ get: (name: string) => { const value = cookieValues.get(name); return value === undefined ? undefined : { name, value }; }, })), })); import { getShellAuthState } from "@/lib/server/auth/current-user"; const authEnvironmentKeys = [ "NODE_ENV", "ZHINIAN_AUTH_REQUIRED", "ZHINIAN_AUTH_SESSION_SECRET", "ZHINIAN_GO_INTERNAL_BASE_URL", ] as const; const originalEnvironment = new Map(authEnvironmentKeys.map((key) => [key, process.env[key]])); function configureGoBridge() { Reflect.set(process.env, "NODE_ENV", "production"); Reflect.set(process.env, "ZHINIAN_AUTH_REQUIRED", "true"); Reflect.set(process.env, "ZHINIAN_AUTH_SESSION_SECRET", "bridge-test-secret"); Reflect.set(process.env, "ZHINIAN_GO_INTERNAL_BASE_URL", "http://go-api.internal/"); } async function seedPlatformSessionCookie() { const session: AuthSession = { version: 1, authMode: "user", issuedAt: Math.floor(Date.now() / 1000) - 10, expiresAt: Math.floor(Date.now() / 1000) + 3600, sessionVersion: 7, user: { id: "account-1", subject: "account-1", displayName: "旧名称", clientId: "platform", organizationId: "org-old", organizationName: "旧组织", role: "user", authorities: ["ROLE_USER"], scope: [], }, }; const signed = await createSignedJsonValue(session, "bridge-test-secret"); const chunks = chunkCookieValue(signed, 80); chunks.forEach((value, index) => cookieValues.set(chunkedCookieName(SESSION_COOKIE_NAME, index), value)); return chunks; } afterEach(() => { vi.unstubAllGlobals(); cookieValues.clear(); for (const key of authEnvironmentKeys) { const original = originalEnvironment.get(key); if (original === undefined) Reflect.deleteProperty(process.env, key); else Reflect.set(process.env, key, original); } }); describe("authenticated shell state through the Go identity boundary", () => { it("refreshes the signed platform session and forwards only its cookie chunks", async () => { configureGoBridge(); const chunks = await seedPlatformSessionCookie(); cookieValues.set("theme", "dark"); cookieValues.set("analytics_id", "do-not-forward"); const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ authenticated: true, authRequired: true, authConfigured: true, authMode: "admin", user: { id: "account-1", subject: "account-1", username: "13800138001", phone: "13800138001", displayName: "刷新名称", clientId: "platform", organizationId: "org-1", organizationName: "刷新组织", role: "organization_admin", status: "active", authorities: ["ROLE_ORGANIZATION_ADMIN", "ORGANIZATION_ADMIN"], scope: [], }, }), { status: 200, headers: { "content-type": "application/json" } })); vi.stubGlobal("fetch", fetchMock); await expect(getShellAuthState()).resolves.toEqual({ user: expect.objectContaining({ id: "account-1", displayName: "刷新名称", organizationId: "org-1", role: "organization_admin", }), authRequired: true, authConfigured: true, isAdmin: true, isSuperAdmin: false, }); expect(fetchMock).toHaveBeenCalledOnce(); expect(fetchMock).toHaveBeenCalledWith("http://go-api.internal/api/auth/me", { cache: "no-store", headers: { cookie: chunks .map((value, index) => `${chunkedCookieName(SESSION_COOKIE_NAME, index)}=${value}`) .join("; "), }, }); }); it("treats a Go-rejected session as anonymous", async () => { configureGoBridge(); await seedPlatformSessionCookie(); vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ authenticated: false, authRequired: true, authConfigured: true, authMode: null, user: null, }), { status: 200, headers: { "content-type": "application/json" } }))); await expect(getShellAuthState()).resolves.toEqual({ user: null, authRequired: true, authConfigured: true, isAdmin: false, isSuperAdmin: false, }); }); it.each([ ["an upstream error", new Response(null, { status: 503 }), "status 503"], [ "an invalid authenticated response", new Response(JSON.stringify({ authenticated: true, authRequired: true, authConfigured: true, authMode: "admin", user: null, }), { status: 200, headers: { "content-type": "application/json" } }), "invalid authenticated response", ], ])("fails closed for %s", async (_caseName, response, expectedMessage) => { configureGoBridge(); await seedPlatformSessionCookie(); vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response)); await expect(getShellAuthState()).rejects.toThrow(expectedMessage); }); const validOrganizationAdmin = { id: "account-1", subject: "account-1", username: "13800138001", phone: "13800138001", displayName: "刷新名称", clientId: "platform", organizationId: "org-1", organizationName: "刷新组织", role: "organization_admin", status: "active", authorities: ["ROLE_ORGANIZATION_ADMIN", "ORGANIZATION_ADMIN"], scope: [], }; const { role: _role, status: _status, ...userWithoutRoleOrStatus } = validOrganizationAdmin; const { organizationId: _organizationId, organizationName: _organizationName, ...userWithoutOrganization } = validOrganizationAdmin; it.each([ ["missing platform role and status", { authMode: "admin", authConfigured: true, user: { ...userWithoutRoleOrStatus, authorities: ["SUPER_ADMIN"] }, }], ["a disabled account", { authMode: "admin", authConfigured: true, user: { ...validOrganizationAdmin, status: "disabled" }, }], ["a mismatched subject", { authMode: "admin", authConfigured: true, user: { ...validOrganizationAdmin, subject: "other-account" }, }], ["a role/authMode mismatch", { authMode: "user", authConfigured: true, user: validOrganizationAdmin, }], ["an unconfigured authenticated response", { authMode: "admin", authConfigured: false, user: validOrganizationAdmin, }], ["an organization-bound role without an organization", { authMode: "admin", authConfigured: true, user: userWithoutOrganization, }], ])("rejects %s", async (_caseName, invalid) => { configureGoBridge(); await seedPlatformSessionCookie(); vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ authenticated: true, authRequired: true, authMode: invalid.authMode, authConfigured: invalid.authConfigured, user: invalid.user, }), { status: 200, headers: { "content-type": "application/json" } }))); await expect(getShellAuthState()).rejects.toThrow("invalid authenticated response"); }); });