ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SurfSense 第三方服务 Mock 实战:用 Playwright 拦截 OAuth、支付、邮件与短信验证

SurfSense 第三方服务 Mock 实战:用 Playwright 拦截 OAuth、支付、邮件与短信验证 SurfSense 第三方服务 Mock 实战用 Playwright 拦截 OAuth、支付、邮件与短信验证【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense导读SurfSense 的 Web 端 E2E 测试套件surfsense_web/playwright.config.ts运行在真实 Next.js 前端之上但绝不依赖真实第三方服务Google/GitHub OAuth、Stripe/PayPal 支付、邮件与短信验证、埋点统计全部通过page.route与addInitScript在浏览器层拦截。本指南以.cursor/skills/playwright-testing/advanced/third-party.md为骨架结合仓库内真实配置、auth 辅助工具与后端 E2E fake 模块讲解如何在 SurfSense 中写出稳定、快速、可重复的第三方服务 Mock 测试并规避真实凭据、真实扣款与真实外发邮件带来的 flaky 风险。一、为什么必须在浏览器层 Mock 第三方服务第三方服务接入点是 E2E 测试中最容易 flaky 的三类依赖依赖类型真实调用的代价OAuth/SSO需要真实应用凭据、浏览器弹出外部授权页、回跳延迟不可控支付网关真实扣款、需要沙箱环境与密钥、国际网络延迟邮件/SMS外发延迟大、收件箱轮询不稳定、验证码易过期埋点统计污染生产/测试数据分析结果、请求拖慢测试SurfSense 的架构天然适合前端 Mock 后端 Fake双层策略前端层page.route()拦截所有跨域请求page.addInitScript()在页面加载前注入第三方 SDK 的桩实现后端层surfsense_backend/tests/e2e/run_backend.py在启动测试后端时把composio、clickup等 SDK 替换为同目录fakes/下的 drop-in 实现见 surfsense_backend/tests/e2e/README.md使 connector 流程不需要真实三方账号。这种分层的结果是浏览器永远不离开应用源站测试天然具备离线、可重复、并行安全三个特性。二、OAuth / SSO Mocking2.1 用page.routeMock Google OAuth 回调最直接的 OAuth Mock 思路是拦截授权回调 URL直接以 302 携带 token 跳回应用再 Mock token 校验接口test(Google OAuth login, async ({ page }) { // Mock the OAuth callback await page.route(**/auth/google/callback**, (route) { const url new URL(route.request().url()); // Simulate successful OAuth by redirecting with token route.fulfill({ status: 302, headers: { Location: /dashboard?tokenmock-jwt-token, }, }); }); // Mock the token verification endpoint await page.route(**/api/auth/verify, (route) route.fulfill({ json: { valid: true, user: { id: 123, email: testgmail.com, name: Test User, }, }, }), ); await page.goto(/login); await page.getByRole(button, { name: Sign in with Google }).click(); await expect(page.getByText(Welcome, Test User)).toBeVisible(); });要点解析**是 Playwright 的 glob 通配可匹配任意路径段**/auth/google/callback**对 URL 中任意层级、带 query 的回调都生效route.fulfill直接伪造响应浏览器永远不会发起真实网络请求断言使用 Playwright 自动等待的expect(...).toBeVisible()无需手工waitForTimeout。2.2 用自定义 Fixture 封装 OAuth Mock把「Mock 回调 Mock 会话 Mock 用户信息」封装成 fixture可在多个测试中复用并支持任意 provider// fixtures/oauth.fixture.ts type OAuthProvider google | github | microsoft; type OAuthUser { id: string; email: string; name: string; avatar?: string; }; type OAuthFixtures { mockOAuth: (provider: OAuthProvider, user: OAuthUser) Promisevoid; }; export const test base.extendOAuthFixtures({ mockOAuth: async ({ page }, use) { await use(async (provider, user) { // Mock callback redirect await page.route(**/auth/${provider}/callback**, (route) route.fulfill({ status: 302, headers: { Location: /auth/success?provider${provider} }, }), ); // Mock session/user endpoint await page.route(**/api/auth/session, (route) route.fulfill({ json: { user, provider, authenticated: true }, }), ); // Mock user info endpoint await page.route(**/api/me, (route) route.fulfill({ json: user })); }); }, }); // Usage test(login with GitHub, async ({ page, mockOAuth }) { await mockOAuth(github, { id: gh-123, email: devgithub.com, name: GitHub User, }); await page.goto(/login); await page.getByRole(button, { name: Sign in with GitHub }).click(); await expect(page.getByText(Welcome, GitHub User)).toBeVisible(); });2.3 与 SurfSense 真实 OAuth 处理的对照SurfSense 没有在测试里走浏览器 OAuth 弹窗而是采用更彻底的后端直连策略surfsense_web/tests/helpers/api/auth.ts 提供mintTestToken()直接请求测试专用端点/__e2e__/auth/token由 surfsense_backend/tests/e2e/run_backend.py 挂载绕过桌面登录的限流端点未挂载时回退到/auth/desktop/loginsurfsense_web/tests/auth.setup.ts 在setupproject 中把 JWT 写入surfsense_sessioncookie 并持久化到playwright/.auth/user.json之后所有 chromium 用例通过storageState直接以已登录状态启动前端侧同样保留了路由 Mock 的兜底能力surfsense_web/tests/helpers/mocks/composio-oauth.ts 的mockComposioOAuthRedirect()用page.route(/composio\.dev/)拦截外部授权域名并 302 改写回应用回调专门为将来「篡改 auth_url、验证前端不盲从跨域跳转」的负面测试预留。后端侧surfsense_backend/tests/e2e/fakes/composio_module.py是composioPython SDK 的严格 drop-in 替代通过sys.modules[composio]注入connected_accounts.initiate()返回同源auth_url浏览器自然落在本应用回调上全程不会导航到 composio.dev。这印证了文档的结论OAuth Mock 的终极形态不是模拟外部页面而是让外部页面根本不存在。2.4 Mock SAML SSO企业 SSO 走 SAML 时需同时伪造 Assertion Consumer ServiceACS端点与 Cookietest(SAML SSO login, async ({ page }) { // Mock SAML assertion consumer service await page.route(**/saml/acs, async (route) { route.fulfill({ status: 302, headers: { Location: /dashboard, Set-Cookie: sessionmock-saml-session; Path/; HttpOnly, }, }); }); // Mock session validation await page.route(**/api/session, (route) route.fulfill({ json: { user: { email: usercompany.com, name: SSO User }, provider: saml, }, }), ); await page.goto(/login); await page.getByRole(button, { name: SSO Login }).click(); await expect(page).toHaveURL(/dashboard); });注意Set-Cookie需要带Path/否则 Cookie 可能不会被正确写入应用域。三、Payment Gateway Mocking3.1 Mock Stripe注入 SDK 桩 拦截后端端点Stripe 的支付表单由stripe.js在 iframe 中渲染真实测试既慢又依赖沙箱密钥。正确姿势是用addInitScript在页面加载前替换window.Stripetest(Stripe checkout, async ({ page }) { // Mock Stripe.js await page.addInitScript(() { (window as any).Stripe () ({ elements: () ({ create: () ({ mount: () {}, on: () {}, destroy: () {}, }), }), confirmCardPayment: async () ({ paymentIntent: { status: succeeded, id: pi_mock_123 }, }), createPaymentMethod: async () ({ paymentMethod: { id: pm_mock_123 }, }), }); }); // Mock backend payment endpoint await page.route(**/api/create-payment-intent, (route) route.fulfill({ json: { clientSecret: pi_mock_123_secret_mock }, }), ); await page.route(**/api/confirm-payment, (route) route.fulfill({ json: { success: true, orderId: order-123 }, }), ); await page.goto(/checkout); await page.getByRole(button, { name: Pay $99.99 }).click(); await expect(page.getByText(Payment successful)).toBeVisible(); });为什么必须用addInitScript它会在任何页面脚本执行之前运行保证应用初始化时读到的是桩对象而不是等stripe.js真实加载后再覆盖——后者会造成竞态。SDK 桩只需实现应用实际调用的那几个方法mount、on、confirmCardPayment、createPaymentMethod不必完整实现整个 SDK。3.2 Mock PayPalPayPal 智能按钮同样通过addInitScript注入并用page.evaluate模拟用户点击按钮后的 approve 回调test(PayPal checkout, async ({ page }) { // Mock PayPal SDK await page.addInitScript(() { (window as any).paypal { Buttons: () ({ render: () Promise.resolve(), isEligible: () true, }), FUNDING: { PAYPAL: paypal, CARD: card }, }; }); // Mock PayPal order creation await page.route(**/api/paypal/create-order, (route) route.fulfill({ json: { orderId: PAYPAL-ORDER-123 }, }), ); // Mock PayPal capture await page.route(**/api/paypal/capture, (route) route.fulfill({ json: { success: true, transactionId: TXN-123 }, }), ); await page.goto(/checkout); // Simulate PayPal approval callback await page.evaluate(() { (window as any).onPayPalApprove?.({ orderID: PAYPAL-ORDER-123 }); }); await expect(page.getByText(Order confirmed)).toBeVisible(); });page.evaluate在这里相当于「代替用户在 PayPal 弹窗里点击 Approve」是把外部弹窗交互折叠成一次内存调用的关键技巧。3.3 支持失败场景的 Payment Fixture支付测试最大的价值在于验证失败分支。把桩做成可配置的 fixture即可一键切换成功/失败// fixtures/payment.fixture.ts type PaymentFixtures { mockStripe: (options?: { failPayment?: boolean }) Promisevoid; }; export const test base.extendPaymentFixtures({ mockStripe: async ({ page }, use) { await use(async (options {}) { await page.addInitScript( ([shouldFail]) { (window as any).Stripe () ({ elements: () ({ create: () ({ mount: () {}, on: (event: string, handler: Function) { if (event ready) setTimeout(handler, 100); }, destroy: () {}, }), }), confirmCardPayment: async () { if (shouldFail) { return { error: { message: Card declined } }; } return { paymentIntent: { status: succeeded } }; }, }); }, [options.failPayment], ); }); }, }); // Usage test(handles declined card, async ({ page, mockStripe }) { await mockStripe({ failPayment: true }); await page.goto(/checkout); await page.getByRole(button, { name: Pay }).click(); await expect(page.getByText(Card declined)).toBeVisible(); });这段代码展示了两个高阶细节addInitScript的第二个参数是可序列化参数经[options.failPayment]传入在浏览器上下文里以shouldFail接收——这是向addInitScript传参的标准姿势用setTimeout(handler, 100)模拟 stripe iframeready事件保留 loading 时序使「支付中 → 结果」的 UI 流转真实可测。四、Email Verification4.1 Mock 邮件 API捕获 token 并闭环验证真实邮件验证流程需要等待外部邮件送达极不可靠。Mock 思路是拦截发送接口、在测试内存中保存验证 token、再模拟用户点击邮件里的验证链接test(email verification flow, async ({ page, request }) { let verificationToken: string; // Capture the verification email await page.route(**/api/send-verification, async (route) { const body route.request().postDataJSON(); verificationToken mock-token-${Date.now()}; // Dont actually send email, just store token route.fulfill({ json: { sent: true, messageId: msg-123 }, }); }); // Mock token verification await page.route(**/api/verify-email**, (route) { const url new URL(route.request().url()); const token url.searchParams.get(token); if (token verificationToken) { route.fulfill({ json: { verified: true } }); } else { route.fulfill({ status: 400, json: { error: Invalid token } }); } }); await page.goto(/signup); await page.getByLabel(Email).fill(testexample.com); await page.getByRole(button, { name: Sign Up }).click(); await expect(page.getByText(Check your email)).toBeVisible(); // Simulate clicking email link await page.goto(/verify?token${verificationToken}); await expect(page.getByText(Email verified)).toBeVisible(); });关键点route.request().postDataJSON()可读取被拦截 POST 请求的 JSON 体用于提取请求参数校验接口按 token 是否匹配返回成功或 400顺带测出了「无效 token 被拒绝」的负面分支用page.goto模拟「点击邮件里的验证链接」把真实邮件链路压缩成一次导航。4.2 需要真实收件箱时Mailinator/Temp Mail 轮询某些场景如验证平台实际发送行为、链接点击统计必须收真实邮件时可用临时邮箱 API。将「获取验证邮件并提取链接」封装为 fixture// fixtures/email.fixture.ts type EmailFixtures { getVerificationEmail: (inbox: string) Promise{ link: string }; }; export const test base.extendEmailFixtures({ getVerificationEmail: async ({ request }, use) { await use(async (inbox) { // Poll Mailinator API for new email const response await request.get( https://api.mailinator.com/v2/domains/public/inboxes/${inbox}, { headers: { Authorization: Bearer ${process.env.MAILINATOR_API_KEY}, }, }, ); const messages await response.json(); const latest messages.msgs[0]; // Get full message const msgResponse await request.get( https://api.mailinator.com/v2/domains/public/inboxes/${inbox}/messages/${latest.id}, { headers: { Authorization: Bearer ${process.env.MAILINATOR_API_KEY}, }, }, ); const message await msgResponse.json(); // Extract verification link from HTML const linkMatch message.parts[0].body.match( /href([^]*verify[^]*)/, ); return { link: linkMatch?.[1] || }; }); }, });这类方案虽然保留了真实发送链路但引入了外部 API 依赖与轮询延迟应仅在必要时使用日常 E2E 优先走 4.1 的纯 Mock 方案。五、SMS VerificationSMS 的 Mock 思路与邮件完全同构拦截发送接口生成 6 位随机码再拦截校验接口按码匹配test(SMS verification, async ({ page }) { let smsCode: string; // Capture SMS send await page.route(**/api/send-sms, (route) { smsCode Math.random().toString().slice(2, 8); // 6-digit code route.fulfill({ json: { sent: true, messageId: sms-123 }, }); }); // Mock code verification await page.route(**/api/verify-sms, (route) { const body route.request().postDataJSON(); if (body.code smsCode) { route.fulfill({ json: { verified: true } }); } else { route.fulfill({ status: 400, json: { error: Invalid code } }); } }); await page.goto(/verify-phone); await page.getByLabel(Phone).fill(1234567890); await page.getByRole(button, { name: Send Code }).click(); // Enter the code await page.getByLabel(Verification Code).fill(smsCode); await page.getByRole(button, { name: Verify }).click(); await expect(page.getByText(Phone verified)).toBeVisible(); });一个实用技巧Math.random().toString().slice(2, 8)生成 6 位纯数字验证码配合fill直接输入彻底免去真实短信网关。与邮件 Mock 一样这里把「验证码错误 → 400」的负面路径也一并覆盖了。六、Analytics Tracking6.1 测试中屏蔽埋点防止数据污染第三方埋点脚本会拖慢测试并污染分析数据用test.beforeEach统一拦截test.beforeEach(async ({ page }) { // Block all analytics/tracking await page.route( /google-analytics|googletagmanager|facebook|hotjar|segment|mixpanel|amplitude/, (route) route.abort(), ); });这里直接使用了正则表达式作为page.route的匹配模式Playwright 支持 glob、正则、URL 函数三种匹配器一次拦截所有主流埋点域名route.abort()使请求直接失败且不产生响应。SurfSense 的 E2E 套件同样在 surfsense_web/playwright.config.ts 中为所有请求附加了x-playwright-test: true请求头并在auth.setup.ts中预置了surfsense_announcements_state与surfsense-tour-userId的 localStorage目的相同让测试流量与用户行为完全隔离且不被新用户引导浮层干扰点击。6.2 捕获埋点事件用于断言有时需要反过来验证「事件确实上报了」——此时把埋点接口变成测试的内存记录器test(tracks purchase event, async ({ page }) { const analyticsEvents: any[] []; // Capture analytics calls await page.route(**/api/analytics/**, (route) { analyticsEvents.push(route.request().postDataJSON()); route.fulfill({ status: 200 }); }); // Mock analytics SDK await page.addInitScript(() { (window as any).analytics { track: (event: string, props: any) { fetch(/api/analytics/track, { method: POST, body: JSON.stringify({ event, props }), }); }, }; }); await page.goto(/checkout); await page.getByRole(button, { name: Complete Purchase }).click(); // Verify analytics event was sent expect(analyticsEvents).toContainEqual( expect.objectContaining({ event: Purchase Completed, props: expect.objectContaining({ amount: expect.any(Number) }), }), ); });这里有三层配合addInitScript注入的 SDK 桩把track转成对本地**/api/analytics/**的 fetch路由拦截器把请求体 push 进测试侧数组最后用expect.objectContaining做部分匹配断言避免因props里多了字段而脆断。这种「桩 SDK → 本地端点 → 内存数组」的三段式结构是验证埋点/统计类行为最稳的范式。七、Anti-Patterns必须避开的反模式Anti-Pattern问题解决方案在测试中使用真实 OAuth慢、需要凭据、易 flakyMock OAuth 端点真实支付处理产生真实扣款、慢使用测试模式或 Mock等待真实邮件非常慢、不可靠Mock 邮件 API不 Mock 埋点污染埋点数据拦截或 Mock 埋点这四条反模式的本质是同一个原则的四个侧面E2E 测试要验证的是应用代码在既定输入下是否表现出预期行为而不是验证第三方服务的可用性。第三方服务的可用性应由其自身的集成测试与监控负责。与之配套的还有三条从源码实践中提炼的纪律与姊妹篇 .cursor/skills/playwright-testing/advanced/network-advanced.md 中的网络拦截反模式表一致只 Mock 必要范围不要无差别拦截所有请求否则测试验证的是 Mock 自己而不是应用SurfSense 对后端 API 走真实请求auth.setup.ts直连 FastAPI 后端只 Mock 浏览器无法触达的外部三方检查请求方法拦截时注意route.request().method()避免 POST 的 Mock 误伤 GETMock 数据工厂化用 fixture 与类型化参数如OAuthUser、PaymentFixtures取代硬编码响应保持可维护性。八、在 SurfSense 套件中的落地位置把上述模式落到 SurfSense 仓库你可以按以下路径找到对应的真实代码全局配置surfsense_web/playwright.config.ts 定义了setup先跑tests/auth.setup.ts生成playwright/.auth/user.json与chromium两个 project 的依赖关系CI 下retries: 1、本地workers: 1trace 在首次重试时开启认证基建surfsense_web/tests/auth.setup.ts 与 surfsense_web/tests/helpers/api/auth.ts 实现「免限流 token 会话 Cookie 持久化」替代了本文档中的 OAuth UI Mock 作为主路径OAuth 兜底 Mocksurfsense_web/tests/helpers/mocks/composio-oauth.ts 演示了如何用正则 route 拦截composio.dev外部授权域后端 Fake 支撑surfsense_backend/tests/e2e/fakes/composio_module.py 与 surfsense_backend/tests/e2e/fakes/clickup_module.py 是第三方 SDK 的 drop-in 替代前者连connected_accounts.initiate()的 OAuth 码fake-clickup-oauth-code都是假的使 connector 集成测试无需任何真实三方账号。九、相关参考网络拦截进阶请求改写、GraphQL Mock、HAR 录制回放、条件 Mock 见 .cursor/skills/playwright-testing/advanced/network-advanced.md认证模式Basic Auth 与复杂认证流MFA、密码重置见 .cursor/skills/playwright-testing/core/fixtures-hooks.md 与 .cursor/skills/playwright-testing/advanced/authentication-flows.mdMock 与真实服务的选择何时该 Mock、何时该走真实服务的架构决策见 .cursor/skills/playwright-testing/architecture/when-to-mock.mdOAuth 弹窗与多上下文真实 OAuth 弹窗的跨上下文处理见 .cursor/skills/playwright-testing/advanced/multi-context.md。结语第三方服务 Mock 不是绕过测试而是把测试的关注点拉回应用自身用page.route拦截外部端点、用addInitScript预注入 SDK 桩、用 fixture 参数化成功与失败分支再配合 SurfSense 现有的后端 fake 与测试专用 token 端点就能让 OAuth、支付、邮件、短信、埋点这些最易 flaky 的场景变得确定、快速且可重复。唯一需要遵守的铁律是Mock 的是外部世界而不是应用自己的行为——这正是 .cursor/skills/playwright-testing/architecture/when-to-mock.md 反复强调的边界。【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进