Unkey Dashboard 认证测试 Mock 体系实战:基于 Vitest 的 WorkOS、Radar 与本地认证测试指南
Unkey Dashboard 认证测试 Mock 体系实战基于 Vitest 的 WorkOS、Radar 与本地认证测试指南【免费下载链接】unkeyThe Developer Platform for Modern APIs项目地址: https://gitcode.com/GitHub_Trending/un/unkey本篇技术指南以 Auth Test Mocks 说明文档 为骨架系统讲解 Unkey DashboardNext.js 前端中认证模块的可复用测试 Mock 体系包括环境配置 Mockenv.ts、WorkOS SDK Mockworkos.ts、测试基础设施工具setup.ts以及它们在实际认证测试中的标准组装方式。读完本文你将掌握如何在 Vitest 测试中隔离 WorkOS、Radar 等外部认证依赖为WorkOSAuthProvider、本地认证、Cookie 与会话等场景编写稳定、可复用的测试。一、__mocks__目录结构与设计思想认证测试的最大难点在于被测代码会触发大量外部副作用——读取环境变量、初始化 WorkOS 客户端、访问全局fetch、操作 Cookie 等。如果不在测试中提前拦截轻则测试无法运行如缺少真实 API Key重则产生真实的网络请求与用户数据写入。Unkey Dashboard 在 web/apps/dashboard/lib/auth/mocks/ 下集中放置了四类可复用 Mock 资产__mocks__/ ├── env.ts # 环境配置 MockWorkOS / 本地认证两套环境 ├── workos.ts # WorkOS SDK Mock 实例工厂 ├── setup.ts # 测试基础设施与辅助工具fetch、Radar、认证依赖 └── README.md # 本文档使用说明目录划分遵循按职责分层的原则环境配置与外部 SDK各自独立成文件通用测试工具单独放在setup.ts中。这样的好处是 Mock 之间相互独立、可按需组合避免把多个 Mock 耦合在同一个文件里导致难以复用。该目录的消费方是同级 tests/ 目录下的认证测试如 workos-auth.test.ts、local.test.ts、cookies.test.ts它们通过../__mocks__/env、../__mocks__/workos、../__mocks__/setup的相对路径引用这些 Mock。二、环境配置 Mockenv.tsenv.ts 提供两个返回模拟环境变量对象的纯函数用于在vi.mock(/lib/env)中替换真实的env()调用。2.1 WorkOS 环境mockWorkOSEnvexport const mockWorkOSEnv () ({ AUTH_PROVIDER: workos as const, WORKOS_COOKIE_PASSWORD: test-cookie-password-32-chars-long!!, WORKOS_API_KEY: test-api-key, WORKOS_CLIENT_ID: test-client-id, });注意WORKOS_COOKIE_PASSWORD的值刻意超过了 32 个字符。这并非随意为之在真实实现 web/apps/dashboard/lib/auth/workos.ts 中WorkOSAuthProvider的构造函数会校验WORKOS_COOKIE_PASSWORD是否存在且长度至少为 32否则直接抛出WORKOS_COOKIE_PASSWORD must be at least 32 characters...错误。该校验存在的根本原因是底层的 iron-webcrypto 在会话 Cookie 过短时会报出晦涩的Password string too short提前校验可以把问题定位在更早、信息更明确的阶段。Mock 中维持 32 字符可以保证被测代码通过构造校验让失败点落在测试逻辑本身。2.2 本地认证环境mockLocalEnvexport const mockLocalEnv () ({ AUTH_PROVIDER: local as const, });本地认证Local Auth模式不依赖 WorkOS因此只需要AUTH_PROVIDER: local。该模式对应 web/apps/dashboard/lib/auth/local.ts 中的LocalAuthProvider其会话使用固定的本地用户常量LOCAL_USER_ID、LOCAL_ORG_ID、LOCAL_ORG_ROLE定义在 types.ts无需任何外部服务即可在开发环境中跑通认证流程。2.3 自定义扩展mockWorkOSEnv()返回的是普通对象可直接展开并覆盖任意字段vi.mock(/lib/env, () ({ env: vi.fn(() ({ ...mockWorkOSEnv(), CUSTOM_VAR: custom-value, })), }));这一模式让开发者可以在不改变 Mock 默认值的前提下针对单个测试定制额外的环境变量。三、WorkOS SDK Mockworkos.tsworkos.ts 导出一个工厂函数createMockWorkOSInstance(viFn)它返回一个覆盖 WorkOS Node SDK 常用 API 的全量 Mock 实例所有方法均为viFn.fn()即vi.fn()生成的空 Mock 函数可在测试中按需定制返回值与断言调用。export const createMockWorkOSInstance (viFn: typeof import(vitest).vi) ({ userManagement: { createUser: viFn.fn(), createMagicAuth: viFn.fn(), listUsers: viFn.fn(), getUser: viFn.fn(), authenticateWithMagicAuth: viFn.fn(), authenticateWithCode: viFn.fn(), authenticateWithEmailVerification: viFn.fn(), authenticateWithOrganizationSelection: viFn.fn(), authenticateWithTotp: viFn.fn(), authenticateWithRadarEmailChallenge: viFn.fn(), sendRadarSmsChallenge: viFn.fn(), authenticateWithRadarSmsChallenge: viFn.fn(), loadSealedSession: viFn.fn(), createOrganizationMembership: viFn.fn(), listOrganizationMemberships: viFn.fn(), getOrganizationMembership: viFn.fn(), updateOrganizationMembership: viFn.fn(), deleteOrganizationMembership: viFn.fn(), deactivateOrganizationMembership: viFn.fn(), sendInvitation: viFn.fn(), listInvitations: viFn.fn(), findInvitationByToken: viFn.fn(), getInvitation: viFn.fn(), revokeInvitation: viFn.fn(), acceptInvitation: viFn.fn(), getAuthorizationUrl: viFn.fn(), }, multiFactorAuth: { createUserAuthFactor: viFn.fn(), listUserAuthFactors: viFn.fn(), challengeFactor: viFn.fn(), verifyChallenge: viFn.fn(), deleteFactor: viFn.fn(), }, organizations: { createOrganization: viFn.fn(), getOrganization: viFn.fn(), updateOrganization: viFn.fn(), }, });3.1 与真实使用场景的映射对照 web/apps/dashboard/lib/auth/workos.ts 中的WorkOSAuthProvider实现可以清晰地看到 Mock 方法覆盖了提供者的全部调用面Mock 方法分组对应真实业务场景提供者中的调用点示例userManagement.createUser/createMagicAuth邮箱注册与魔法链接发码signUpViaEmailuserManagement.authenticateWithMagicAuth验证一次性验证码verifyAuthCodeuserManagement.authenticateWithCodeOAuth 回调换会话completeOAuthSignInuserManagement.authenticateWithTotp/authenticateWithRadarEmailChallenge/authenticateWithRadarSmsChallenge完成 MFA / Radar 挑战completeMfaChallenge等userManagement.loadSealedSession会话校验与刷新validateSession/refreshSessionuserManagement.getAuthorizationUrl发起 OAuth 授权跳转signInViaOAuthmultiFactorAuth.*TOTP 因子管理与挑战beginMfaEnrollment/listMfaFactorsorganizations.*组织租户创建与维护createTenant/updateOrguserManagement.*Membership/*Invitation成员与邀请管理updateMembership/inviteMember从源码结构可以推断Mock 的方法清单几乎与WorkOSAuthProvider对 SDK 的全部调用一一对应这意味着任何新增的 SDK 调用点都应同步补充到该 Mock 中否则相关测试会因not a function而失败。3.2 在测试中定制 Mock 行为由于所有方法都是vi.fn()测试内可以像操作普通 Vitest Mock 一样设置返回值const mockWorkOS createMockWorkOSInstance(vi); mockWorkOS.userManagement.createUser.mockResolvedValue({ id: user_123, email: testexample.com, });结合 workos-auth.test.ts 中的实践还可以通过vi.mocked(WorkOS).mock.results拿到被测 Provider 构造时实际使用的 Mock 实例从而在beforeEach中统一准备桩数据function getMockInstance(): MockWorkOS { const results vi.mocked(WorkOS).mock.results; return results[results.length - 1].value as MockWorkOS; }四、测试基础设施与工具setup.tssetup.ts 是三类工具的集合认证依赖整体拦截、全局fetchMock、Radar API 响应模拟。4.1 一键拦截全部认证依赖setupAuthTestMocks该函数集中了导入WorkOSAuthProvider前必须拦截的所有模块避免服务端初始化副作用export const setupAuthTestMocks () { // Mock get-auth 模块防止服务端初始化 vi.mock(../get-auth, () ({ getAuth: vi.fn().mockResolvedValue({ userId: test-user-id }), })); // Mock utils 模块 vi.mock(../../utils, () ({ getBaseUrl: vi.fn().mockReturnValue(http://localhost:3000), })); // Mock cookie 模块 vi.mock(../cookies, () ({ getCookie: vi.fn(), setCookie: vi.fn(), deleteCookie: vi.fn(), getCookieOptionsAsString: vi.fn(), setSessionCookie: vi.fn(), })); // Mock cookie 安全配置 vi.mock(../cookie-security, () ({ getAuthCookieOptions: vi.fn().mockReturnValue({ httpOnly: true, secure: false, sameSite: lax, path: /, }), getDefaultCookieOptions: vi.fn(), shouldUseSecureCookies: vi.fn(), })); };这里有两个值得注意的细节getAuth被 Mock 为返回固定的{ userId: test-user-id }真实实现 get-auth.ts 会调用updateSession去解密并校验会话 Cookie在测试中这属于不必要的重量级初始化因此被替换为恒定的模拟用户。Cookie 选项被固定为secure: false真实实现 cookie-security.ts 中secure标记取决于VERCEL_ENV ! development即生产/预览环境为true、开发环境为false。Mock 固定为false可以保证测试不受运行环境差异影响同时也规避了 Safari 在 HTTP 环境下拒绝secureCookie 的兼容性问题。4.2 全局 fetch MocksetupFetchMock与createMockFetchResponseexport const setupFetchMock () { const fetchMock vi.fn(); // biome-ignore lint/suspicious/noExplicitAny: 需要 Mock 全局 fetch global.fetch fetchMock as any; return fetchMock; }; export const createMockFetchResponse (data: any, ok true, status 200) ({ ok, status, json: async () data, text: async () JSON.stringify(data), headers: new Headers(), });setupFetchMock()用一个vi.fn()替换全局fetch并返回该 Mock测试可以直接配置一次性响应const fetchMock setupFetchMock(); fetchMock.mockResolvedValueOnce( createMockFetchResponse({ data: value }, true, 200) );createMockFetchResponse构造了符合Response接口的最小对象ok、status、json()、text()、headers足以支撑被测代码对响应体的常规消费。4.3 Radar API 响应模拟Radar 是 WorkOS 提供的风控服务会在认证流程中返回allow/block/challenge三种判定。setup.ts提供了三种场景化工具export const mockRadarResponse (action: allow | block | challenge, reason?: string) { const response createMockFetchResponse({ verdict: action, ...(reason { reason }), }); (global.fetch as any).mockResolvedValueOnce(response); return response; }; export const mockRadarFailure (status 500) { const response createMockFetchResponse({}, false, status); (global.fetch as any).mockResolvedValueOnce(response); return response; }; export const mockRadarNetworkError (errorMessage Network error) { (global.fetch as any).mockRejectedValueOnce(new Error(errorMessage)); };三者分别覆盖了风控判定的三种走向mockRadarResponse(action, reason?)模拟判定成功返回verdict为allow/block/challengereason为可选的风控原因描述mockRadarFailure(status 500)模拟 API 返回非 2xx 错误默认 500mockRadarNetworkError(message)模拟网络层异常fetch直接 reject。这三类工具配合真实提供者中的错误映射逻辑见下文第六节可以完整覆盖允许、拒绝、服务异常、网络异常四条测试路径。五、标准测试脚手架Basic Setup对于大多数认证测试README 推荐在测试文件顶层一次性装配全部标准 Mockimport { beforeEach, describe, expect, it, vi } from vitest; import { mockWorkOSEnv } from ../__mocks__/env; import { createMockWorkOSInstance } from ../__mocks__/workos; // Mock the env module BEFORE importing anything else vi.mock(/lib/env, () ({ env: vi.fn(() mockWorkOSEnv()), })); // Mock the get-auth module to prevent server initialization vi.mock(../get-auth, () ({ getAuth: vi.fn().mockResolvedValue({ userId: test-user-id }), })); // Mock the utils module vi.mock(../../utils, () ({ getBaseUrl: vi.fn().mockReturnValue(http://localhost:3000), })); // Mock the cookie modules vi.mock(../cookies, () ({ getCookie: vi.fn(), setCookie: vi.fn(), deleteCookie: vi.fn(), getCookieOptionsAsString: vi.fn(), setSessionCookie: vi.fn(), })); vi.mock(../cookie-security, () ({ getAuthCookieOptions: vi.fn().mockReturnValue({ httpOnly: true, secure: false, sameSite: lax, path: /, }), getDefaultCookieOptions: vi.fn(), shouldUseSecureCookies: vi.fn(), })); // Mock the WorkOS SDK vi.mock(workos-inc/node, () ({ WorkOS: vi.fn().mockImplementation((apiKey: string) createMockWorkOSInstance(apiKey, vi)), })); // Mock fetch globally global.fetch vi.fn(); // Now import after mocks are set up import { WorkOSAuthProvider } from ../workos; describe(Your test suite, () { // Your tests here });关键约束所有vi.mock()调用必须位于测试文件顶层、且早于任何可能使用这些模块的 import 语句。这是 Vitest 对 Mock 提升hoisting的硬性要求——Vitest 会把vi.mock提升到文件顶部执行但依赖该机制正确性的前提是 Mock 声明出现在引用之前否则被测模块会在 Mock 生效前就完成初始化。5.1 保留真实异常类的进阶模式在真实测试 workos-auth.test.ts 中Mock WorkOS SDK 时使用了importOriginal模式把 SDK 的真实异常类保留下来vi.mock(workos-inc/node, async (importOriginal) { const actual await importOriginaltypeof import(workos-inc/node)(); return { ...actual, WorkOS: vi.fn().mockImplementation(() createMockWorkOSInstance(vi)), }; });这一做法的原因在于WorkOSAuthProvider内部大量使用instanceof AuthenticationException、instanceof OauthException等类型判断例如 workos.ts 的mapAuthenticationException与 workos.ts 的isRadarBlock。如果连异常类一起被 Mock 掉instanceof判断会全部失效错误映射逻辑将无法被正确测试。测试中通过new AuthenticationException(403, rawData, req_test)直接构造带指定错误码的异常来驱动分支覆盖。六、实战示例6.1 测试 Radar 集成block 判定Radar 的block判定对应注册/登录被拒绝的交互路径。README 给出的测试如下import { mockRadarResponse } from ../__mocks__/setup; it(should block signup when Radar returns block action, async () { mockRadarResponse(block, Suspicious activity detected); const result await provider.signUpViaEmail({ email: testexample.com, firstName: Test, lastName: User, }); expect(result.success).toBe(false); });结合源码可以更深入理解这条链路的内部逻辑。在真实提供者 workos.ts 的signUpViaEmail中createUser抛出的错误会先经过isRadarBlock判定该方法会排除AuthenticationException与RateLimitExceededException这两类错误由其他分支处理然后识别 403 状态的OauthException/GenericServerException或匹配错误体中包含radar、blocked、impossible_travel、blocklist、bot_detection等关键词的提示命中后统一返回AUTHENTICATION_BLOCKED错误码与固定的 contact support 文案见 radarBlockedResponse。在 workos-auth.test.ts 中对应的测试是用new OauthException(403, req_test, access_denied, Country is blocked, {})来模拟这一场景并断言result.code为AuthErrorCode.AUTHENTICATION_BLOCKED。6.2 测试自定义 WorkOS 响应当需要完全掌控 Provider 内部持有的 WorkOS 实例时README 展示了直接替换 provider 属性的做法it(should create user successfully, async () { mockRadarResponse(allow); const mockProvider { userManagement: { createUser: vi.fn().mockResolvedValue({ id: user_123 }), createMagicAuth: vi.fn().mockResolvedValue({}), }, key: test-api-key, }; (provider as any).provider mockProvider; const result await provider.signUpViaEmail({ email: testexample.com, firstName: Test, lastName: User, }); expect(result.success).toBe(true); expect(mockProvider.userManagement.createUser).toHaveBeenCalled(); });更推荐的做法是沿用createMockWorkOSInstance并在beforeEach中通过getMockInstance()拿到实例后统一配置桩数据见 workos-auth.test.ts这样既保留了类型完备的 Mock 结构又能在每个测试内精细控制返回值。七、Mock 与认证错误映射的深度对照要写出高质量的认证测试理解Mock 返回值如何驱动真实错误映射逻辑是关键。下面把 README 涉及的 Mock 能力与 web/apps/dashboard/lib/auth/workos.ts 的核心分支对应起来1. 认证中断pending类分支。WorkOSAuthProvider的mapAuthenticationException会把携带pendingAuthenticationToken的AuthenticationException映射为待续挑战响应organization_selection_required组织选择、mfa_challengeTOTP 挑战需要先用multiFactorAuth.challengeFactor发起挑战、mfa_enrollmentMFA 注册、radar_email_challenge/radar_sms_challengeRadar 邮件/短信挑战。这些分支在 workos-auth.test.ts 中均有对应测试通过构造不同code的AuthenticationException逐一验证。2. 错误码与 Cookie 联动。每个 pending 响应都会携带写入PENDING_SESSION_COOKIEsess-temp与AUTH_CHALLENGE_COOKIEauth-challengeJSON 编码的AuthChallengeCookieData的 Cookie 指令并遵循PENDING_AUTH_COOKIE_MAX_AGE 600秒的有效期见 workos.ts。测试中通过断言result.cookies里的 Cookie 名称与 JSON 内容来验证这一行为。3. 会话校验与刷新。validateSession只在 access token 过期INVALID_JWT时才返回shouldRefresh: true对无法解密的 Cookieinvalid_session_cookie直接返回shouldRefresh: false以省去注定失败的刷新请求对 SDK 抛出的瞬时异常如 JWKS 拉取失败、网络抖动则保守地返回shouldRefresh: true见 workos.ts。这三条分支在 workos-auth.test.ts 中通过loadSealedSession的 Mock 返回值逐一覆盖。4. 超大会话 Cookie 分片。cookies.test.ts 展示了另一类不依赖 WorkOS SDK 的测试WorkOS 的密封会话可能远超单浏览器 Cookie 的 4KB 上限因此 Cookie 工具会把会话分片为unkey-session.0、unkey-session.1等并确保每片小于 4096 字节超过 4 片约 14KB时则拒绝写入并上报 SentryWorkOS session exceeds Vercel cookie capacity。这类测试通过setCookiesOnResponse与getCookie的往返验证分片与还原逻辑。5. 本地认证回归。local.test.ts 不依赖任何 Mock 环境直接实例化LocalAuthProvider验证本地会话返回固定的管理员角色以及 OAuth 重定向目标会被sanitizeRedirectPath清洗https://evil.com、//evil.com等不安全值一律回退为/apis。八、扩展新 Mock 的规范当认证功能演进、需要新增 Mock 时README 明确了三条放置约定env.ts新增环境配置如新的认证提供方所需的变量workos.ts扩展 WorkOS SDK 的 Mock 方法如新增的 SDK API 调用点setup.ts新增面向常见测试场景的辅助工具函数。核心原则是保持 Mock 聚焦focused且可组合composable每个 Mock 只负责一个明确的职责边界多个 Mock 通过顶层vi.mock()自由组装从而在最大程度上提升复用性。新增 Mock 时还应同步在 tests/ 目录补充对应测试确保 Mock 本身的行为可被验证。结语Unkey Dashboard 的认证测试 Mock 体系把环境配置、外部 SDK、全局基础设施三类依赖彻底隔离env.ts提供与真实构造校验兼容的模拟环境workos.ts提供覆盖全部 SDK 调用面的可定制 Mock 实例setup.ts则统一处理getAuth、Cookie、fetch与 Radar 场景。配合vi.mock顶层提升、importOriginal保留真实异常类等 Vitest 高级用法这套体系能够让认证逻辑邮箱注册、验证码校验、OAuth 回调、MFA/Radar 挑战、会话刷新、组织与成员管理在完全不触碰外部服务的前提下获得完整、稳定、可断言的测试覆盖。对任何集成 WorkOS 与 Radar 的 Next.js 项目而言这套 Mock 组织方式都具备直接的参考价值。【免费下载链接】unkeyThe Developer Platform for Modern APIs项目地址: https://gitcode.com/GitHub_Trending/un/unkey创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考