mcpField
S
stripe

stripe

github.com/vercel-labs测试与质量2.0k273 次安装1 天前更新
代码仓库

关于 stripe

Stripe API 模拟器,为使用 Stripe SDK 的开发者提供有状态的本地测试环境。它在内存中保存支付相关实体,支持 Webhook 和托管结账页面,且不会处理真实付款。

主要功能

  1. 01模拟 Stripe API 调用与响应
  2. 02在内存中保存客户和支付实体
  3. 03状态变化时触发 Webhook
  4. 04提供托管结账页面
  5. 05支持种子数据配置

适用场景

  1. 01本地测试 Stripe SDK 集成
  2. 02验证结账与支付意图流程
  3. 03测试 Webhook 处理逻辑
  4. 04为测试预置确定性实体

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vercel-labs/emulate --skill stripe

安装后在 Claude Code 中输入/stripe确认已加载。

详细介绍

Stripe API 模拟器

完全有状态的 Stripe API 模拟。客户、产品、价格、结账会话、支付意图、扣款和支付方式都会保存在内存中。状态变更时会触发 Webhook。托管结账界面可让你在浏览器中完成支付。

不会处理真实支付。每次 Stripe SDK 调用都会发送到模拟器,并返回逼真的响应。

启动

本地监听器默认绑定到 127.0.0.1。若要从容器或其他机器访问,请向 CLI 传入 --host 0.0.0.0,或向 createEmulator 传入 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外 URL。

# Stripe only
npx emulate --service stripe

# Default port (when run alone)
# http://localhost:4000

也可以通过编程方式启动:

import { createEmulator } from 'emulate'

const stripe = await createEmulator({ service: 'stripe', port: 4000 })
// stripe.url === 'http://localhost:4000'

将应用指向模拟器

Stripe SDK

Stripe Node.js SDK 不会读取用于设置基础 URL 的环境变量。你必须在构造客户端时传入该 URL:

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
  host: 'localhost',
  port: 4000,
  protocol: 'http',
})
在 Next.js 中嵌入(adapter-next)

使用 @emulators/adapter-next 时,模拟器会在 Next.js 应用内的 /emulate/stripe 路径运行。SDK 需要指向 localhost,并通过代理路由将 /v1/* 请求转发到 /emulate/stripe/v1/*:

// next.config.ts
import { withEmulate } from '@emulators/adapter-next'

export default withEmulate({
  env: {
    STRIPE_SECRET_KEY: 'sk_test_emulated',
  },
})
// lib/stripe.ts
import Stripe from 'stripe'

const port = process.env.PORT ?? '3000'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
  host: 'localhost',
  port: parseInt(port, 10),
  protocol: 'http',
})
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as stripe from '@emulators/stripe'

export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
  services: {
    stripe: {
      emulator: stripe,
      seed: {
        products: [
          { id: 'prod_widget', name: 'Widget', description: 'A useful widget' },
        ],
        prices: [
          { id: 'price_widget', product_name: 'Widget', currency: 'usd', unit_amount: 1000 },
        ],
        webhooks: [
          {
            url: `http://localhost:${process.env.PORT ?? '3000'}/api/webhooks/stripe`,
            events: ['*'],
          },
        ],
      },
    },
  },
})
// app/v1/[...path]/route.ts  (proxy for Stripe SDK)
const STRIPE_URL = `http://localhost:${process.env.PORT ?? '3000'}/emulate/stripe`

async function handler(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
  const { path } = await ctx.params
  const url = new URL(req.url)
  const target = `${STRIPE_URL}/v1/${path.join('/')}${url.search}`

  const res = await fetch(target, {
    method: req.method,
    headers: req.headers,
    body: req.body,
    duplex: 'half',
  } as any)

  return new Response(res.body, {
    status: res.status,
    statusText: res.statusText,
    headers: res.headers,
  })
}

export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
直接 fetch
curl http://localhost:4000/v1/customers \
  -H "Authorization: Bearer sk_test_emulated"

种子配置

种子数据为可选项。所有实体都支持可选的 id 字段,以便使用可在服务重启后保持不变的确定性 ID。

stripe:
  customers:
    - id: cus_demo
      email: [email protected]
      name: Demo User
  products:
    - id: prod_tshirt
      name: T-Shirt
      description: A comfortable tee
  prices:
    - id: price_tshirt
      product_name: T-Shirt
      currency: usd
      unit_amount: 2500
  webhooks:
    - url: http://localhost:3000/api/webhooks/stripe
      events: ['*']
      secret: whsec_test

价格中的 product_name 字段会根据名称关联到产品。使用 events: ['*'] 接收所有 Webhook 事件,或指定单个事件类型。

API 端点

客户
# Create customer
curl -X POST http://localhost:4000/v1/customers \
  -d "[email protected]" -d "name=Jane Doe"

# Retrieve customer
curl http://localhost:4000/v1/customers/cus_xxx

# Update customer
curl -X POST http://localhost:4000/v1/customers/cus_xxx \
  -d "name=Updated Name"

# Delete customer
curl -X DELETE http://localhost:4000/v1/customers/cus_xxx

# List customers
curl http://localhost:4000/v1/customers
产品
# Create product
curl -X POST http://localhost:4000/v1/products \
  -d "name=Widget" -d "description=A useful widget"

# Retrieve product
curl http://localhost:4000/v1/products/prod_xxx

# List products
curl "http://localhost:4000/v1/products?active=true"
价格
# Create price
curl -X POST http://localhost:4000/v1/prices \
  -d "product=prod_xxx" -d "currency=usd" -d "unit_amount=1000"

# Retrieve price
curl http://localhost:4000/v1/prices/price_xxx

# List prices
curl "http://localhost:4000/v1/prices?active=true"
结账会话
# Create checkout session
curl -X POST http://localhost:4000/v1/checkout/sessions \
  -d "mode=payment" \
  -d "line_items[0][price]=price_xxx" \
  -d "line_items[0][quantity]=1" \
  -d "success_url=http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}" \
  -d "cancel_url=http://localhost:3000/cart"

# Retrieve session
curl http://localhost:4000/v1/checkout/sessions/cs_xxx

# List sessions
curl http://localhost:4000/v1/checkout/sessions

# Expire a session
curl -X POST http://localhost:4000/v1/checkout/sessions/cs_xxx/expire

会话的 url 字段指向 /checkout/cs_xxx 上的托管结账页面。在该页面点击“Pay”即可完成会话、触发 checkout.session.completed Webhook,并重定向到 success_url。success_url 中的 {CHECKOUT_SESSION_ID} 模板会替换为实际会话 ID。

支付意图
# Create payment intent
curl -X POST http://localhost:4000/v1/payment_intents \
  -d "amount=2000" -d "currency=usd"

# Retrieve
curl http://localhost:4000/v1/payment_intents/pi_xxx

# Update
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx \
  -d "amount=3000"

# Confirm (triggers payment_intent.succeeded + charge.succeeded webhooks)
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx/confirm

# Cancel
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx/cancel

# List
curl http://localhost:4000/v1/payment_intents
扣款
# Retrieve charge
curl http://localhost:4000/v1/charges/ch_xxx

# List charges
curl http://localhost:4000/v1/charges

确认支付意图时会自动创建扣款。

客户会话
# Create customer session
curl -X POST http://localhost:4000/v1/customer_sessions \
  -d "customer=cus_xxx"
支付方式
# List payment methods
curl http://localhost:4000/v1/payment_methods

Webhook

状态变更时,模拟器会分发 Webhook 事件。可通过种子配置或编程方式注册 Webhook。 配置了 secret 的 Webhook 会包含 Stripe-Signature: t=<timestamp>,v1=<signature>。签名是对 <timestamp>.<raw body> 计算的 HMAC SHA-256。

分发的事件
事件触发条件
customer.created创建客户
customer.updated更新客户
customer.deleted删除客户
product.created创建产品
price.created创建价格
payment_intent.created创建支付意图
payment_intent.succeeded确认支付意图
payment_intent.canceled取消支付意图
charge.succeeded确认支付意图(自动创建扣款)
checkout.session.completed通过托管页面完成结账
checkout.session.expired结账会话过期
Webhook 处理程序示例
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const body = await request.json()
  const event = body.type as string
  const obj = body.data?.object

  switch (event) {
    case 'customer.created':
      console.log('Customer created:', obj.id, obj.email)
      break
    case 'checkout.session.completed':
      console.log('Checkout completed:', obj.id)
      break
    case 'payment_intent.succeeded':
      console.log('Payment succeeded:', obj.id)
      break
    case 'charge.succeeded':
      console.log('Charge succeeded:', obj.id)
      break
  }

  return NextResponse.json({ received: true })
}

常见模式

结账流程(嵌入式 Next.js)
// Server action
const customer = await stripe.customers.create({
  email: '[email protected]',
  name: 'Demo Shopper',
})

const session = await stripe.checkout.sessions.create({
  mode: 'payment',
  customer: customer.id,
  line_items: [{ price: 'price_widget', quantity: 2 }],
  success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${origin}/cart`,
})

redirect(session.url!)
在成功页面获取会话
const session = await stripe.checkout.sessions.retrieve(session_id)
const customer = await stripe.customers.retrieve(session.customer as string)

console.log(session.payment_status) // 'paid'
console.log(customer.name)          // 'Demo Shopper'
支付意图流程(无结账界面)
const pi = await stripe.paymentIntents.create({
  amount: 5000,
  currency: 'usd',
  customer: 'cus_xxx',
})

// Confirm triggers payment_intent.succeeded + charge.succeeded webhooks
const confirmed = await stripe.paymentIntents.confirm(pi.id)
console.log(confirmed.status) // 'succeeded'

与内置模拟器并用的自定义模拟器

使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。在 emulate.config.ts 中使用 emulate 提供的 defineConfig 注册它,同时也可注册 { emulator: "stripe" } 等内置条目。运行 npx emulate start --watch 可重新加载导入项,并在打印出的 /_emulate URL 查看自定义状态。成功重新加载后,本次运行会重置为种子数据。现有的扁平种子配置仍可使用;--config 用于指定文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。

评论

支持 Markdown