fix: route authenticated SSR through Go
This commit is contained in:
226
tests/auth-current-user-go-bridge.test.ts
Normal file
226
tests/auth-current-user-go-bridge.test.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
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<string, string>(),
|
||||
}));
|
||||
|
||||
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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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<typeof fetch>().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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user