如何在Next.js13 App目录的Cypress测试中Mock NextAuth的/api/auth/session会话对象?
针对你遇到的Next.js 13.4.4 App Router + NextAuth v4.22.1的服务端会话无法在Cypress 12.3.0中Mock的问题,下面提供几个实用的解决方案:
方案1:测试环境下修改NextAuth配置,直接返回Mock会话
这个方案无需修改业务代码,只在NextAuth的认证逻辑中加入测试环境判断,绕过真实认证直接返回预设用户会话。
步骤:
配置测试环境变量
在cypress.config.ts中添加Cypress专属环境变量:import { defineConfig } from "cypress"; export default defineConfig({ e2e: { env: { NEXT_PUBLIC_CYPRESS_TEST: "true", }, // 其他Cypress配置 }, });修改NextAuth路由配置
在app/api/auth/[...nextauth]/route.ts中,针对测试环境修改认证和会话回调:import NextAuth from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; const handler = NextAuth({ providers: [ CredentialsProvider({ async authorize(credentials, req) { // 测试环境直接返回Mock用户 if (process.env.NEXT_PUBLIC_CYPRESS_TEST === "true") { return { id: "1", name: "Test User", email: "test@example.com", // 按需添加其他用户字段 }; } // 生产/开发环境的正常凭证认证逻辑 const user = // 你的数据库查询或验证逻辑 return user ?? null; }, }), ], session: { strategy: "jwt" }, // 保持你原本的session策略 callbacks: { async session({ session, token }) { // 测试环境注入Mock会话数据 if (process.env.NEXT_PUBLIC_CYPRESS_TEST === "true") { return { ...session, user: { id: "1", name: "Test User", email: "test@example.com", }, expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), }; } // 正常环境的session回调逻辑 session.user.id = token.sub!; return session; }, }, }); export { handler as GET, handler as POST };在Cypress测试中启用Mock
在cypress/support/e2e.ts中添加全局前置操作,确保测试环境变量生效:beforeEach(() => { cy.visit("/", { onBeforeLoad(win) { win.localStorage.setItem("NEXT_PUBLIC_CYPRESS_TEST", "true"); }, }); });
之后编写测试用例时,直接访问受保护页面即可自动获取Mock会话,无需登录操作。
方案2:直接设置NextAuth的Session Cookie
NextAuth的会话通过HttpOnly Cookie存储,Cypress可以直接设置这个Cookie来模拟已登录状态。
步骤:
添加Cypress自定义命令
在cypress/support/commands.ts中定义设置Mock会话的命令:import jwt from "jsonwebtoken"; Cypress.Commands.add("mockNextAuthSession", () => { const secret = Cypress.env("NEXTAUTH_SECRET"); if (!secret) throw new Error("请在Cypress环境变量中配置NEXTAUTH_SECRET"); // 生成符合NextAuth格式的JWT Token const mockToken = jwt.sign( { sub: "1", name: "Test User", email: "test@example.com", iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 24 * 60 * 60, // 24小时过期 }, secret, { algorithm: "HS256" } ); // 设置Session Cookie,参数根据你的应用配置调整 cy.setCookie("__Secure-next-auth.session-token", mockToken, { domain: "localhost", path: "/", secure: false, // 开发环境HTTP下设为false,HTTPS设为true httpOnly: true, }); });在测试用例中使用
describe("受保护页面测试", () => { it("无需登录即可访问受保护内容", () => { cy.mockNextAuthSession(); cy.visit("/protected"); cy.contains("欢迎,Test User").should("可见"); }); });
方案3:拦截服务端Session请求(针对Server Components)
如果你的受保护页面使用了Server Components,客户端的cy.intercept无法拦截服务端发起的Session请求,此时可以通过Next.js的Middleware注入Mock会话。
步骤:
创建Middleware注入Mock会话头
在项目根目录创建middleware.ts:import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { if (process.env.NEXT_PUBLIC_CYPRESS_TEST === "true") { const mockSession = JSON.stringify({ user: { id: "1", name: "Test User", email: "test@example.com" }, expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), }); const requestHeaders = new Headers(request.headers); requestHeaders.set("x-mock-session", mockSession); return NextResponse.next({ request: { headers: requestHeaders }, }); } return NextResponse.next(); } export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], };修改Server Component中的Session获取逻辑
在Server Component中判断测试环境,优先使用Mock会话:import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { headers } from "next/headers"; export default async function ProtectedPage() { let session; const headersList = headers(); const mockSession = headersList.get("x-mock-session"); if (process.env.NEXT_PUBLIC_CYPRESS_TEST === "true" && mockSession) { session = JSON.parse(mockSession); } else { session = await getServerSession(authOptions); } if (!session) return <div>未授权</div>; return <div>欢迎,{session.user.name}</div>; }
这个方案侵入性稍强,适合必须保留Server Component原生会话获取逻辑的场景。
内容的提问来源于stack exchange,提问作者Kunal Ukey

