mcpField
P
polar-testing

polar-testing

github.com/polarsource测试与质量5172 次安装2 周前更新
代码仓库

关于 polar-testing

polar-testing 是一份指导 Polar 集成测试的技能,面向开发者。它借助隔离的 sandbox 环境、测试卡和 CLI 流程覆盖支付与 Webhook 测试。

主要功能

  1. 01隔离的 sandbox 测试环境
  2. 02支付成功、拒付与 3DS 测试
  3. 03本地 Webhook 转发与事件触发
  4. 04Webhook 负载夹具生成
  5. 05SDK 模拟与 CI 测试配置

适用场景

  1. 01验证 checkout 与订阅生命周期
  2. 02在本地调试 Webhook 处理器
  3. 03测试拒付和许可证密钥验证

安装

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

$npx skills add https://github.com/polarsource/skills --skill polar-testing

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

详细介绍

Polar 测试指南

使用沙盒环境安全地测试 Polar 集成——这是一个完全隔离的服务,你可以在其中进行试验,而不会影响生产数据或处理真实付款。

沙盒环境

访问

沙盒与生产环境完全隔离。你需要分别准备:

  • 用户账户
  • 组织
  • 访问令牌
  • Webhook 端点
设置步骤
  1. 前往 https://sandbox.polar.sh/start
  2. 创建新账户(或在组织切换器中选择“Go to sandbox”)
  3. 创建测试组织
  4. 在 Settings → Developers 中生成访问令牌
SDK 配置
// TypeScript
import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "sandbox", // Switch to "production" for live
});
# Python
from polar_sdk import Polar

polar = Polar(
    access_token=os.environ["POLAR_ACCESS_TOKEN"],
    server="sandbox",
)
// Go
s := polargo.New(
    polargo.WithServer("sandbox"),
    polargo.WithSecurity(os.Getenv("POLAR_ACCESS_TOKEN")),
)
沙盒限制
  • 订阅会在 90 天后自动取消
  • 不会处理真实货币
  • 数据与生产环境隔离

测试卡

Polar 使用 Stripe 处理付款。请使用以下测试卡号:

付款成功
卡号品牌CVC有效期
4242 4242 4242 4242Visa任意 3 位数字任意未来日期
5555 5555 5555 4444Mastercard任意 3 位数字任意未来日期
3782 822463 10005Amex任意 4 位数字任意未来日期
6011 1111 1111 1117Discover任意 3 位数字任意未来日期
付款被拒
卡号拒绝原因
4000 0000 0000 0002常规拒绝
4000 0000 0000 9995余额不足
4000 0000 0000 9987卡片已挂失
4000 0000 0000 9979卡片已被盗
4000 0000 0000 0069卡片已过期
4000 0000 0000 0127CVC 不正确
3D Secure 测试
卡号行为
4000 0027 6000 3184需要进行 3DS 身份验证
4000 0000 0000 3220需要进行 3DS 身份验证

本地 Webhook 测试

使用 Polar CLI。它会将组织的 Webhook 事件直接转发到你的本地服务:无需隧道、无需公共 URL,也无需在控制面板中配置 Webhook 端点。

安装并登录
# macOS, Linux, WSL
curl -fsSL https://polar.sh/install.sh | bash

polar auth login --sandbox

polar auth login 会打开浏览器,因此请让用户运行此命令。使用 polar auth whoami --json 检查结果:environments 列出已登录的环境,organization 则是当前活动组织。

在无法使用浏览器的环境中(CI、远程计算机、云端 Agent),组织访问令牌可替代 auth login:

export POLAR_ACCESS_TOKEN=polar_oat_...   # scopes: webhooks:read, webhooks:write, organizations:read
export POLAR_ENVIRONMENT=sandbox
将事件转发到你的应用
polar listen 3000/api/polar/webhook

listen 接受端口、port/path 或完整 URL。它会持续运行,直到停止为止,因此请在单独的终端或后台进程中启动它,并在测试期间保持运行。它会转发组织的所有 Webhook 事件,包括真实沙盒活动产生的事件,例如完成测试结账。

设置 Webhook 密钥

listen 使用本地密钥为转发的事件签名,而不是使用控制面板端点的密钥。将密钥写入 .env,不要打印出来:

secret=$(polar listen --print-secret) && echo "POLAR_WEBHOOK_SECRET=$secret" >> .env

如果 .env 中已有 POLAR_WEBHOOK_SECRET 行,请替换该行而不是追加,然后重启开发服务。已部署环境应使用其控制面板 Webhook 端点的密钥。

发送测试事件
polar trigger order.paid

trigger 会通过正在运行的 listen 发送示例事件。当你的处理程序未接受该事件时(响应非 2xx、发生连接错误或出现重定向),它会打印处理程序的响应正文,并以状态码 1 退出。阅读输出内容,修复处理程序,然后再次运行同一命令,直到以状态码 0 退出。

重定向通常表示身份验证中间件拦截了 Webhook 路由。请将该路由排除在中间件之外:Polar 发送 Webhook 时绝不会跟随重定向。

polar trigger --list --json                     # every event you can send
polar trigger order.paid --override [email protected]
polar trigger order.paid --seed 7               # the same IDs on every run

--override 接受 path=value,并且可以重复使用。API 会拒绝载荷中不存在的路径。

集成测试

测试结账流程
import { describe, it, expect, beforeAll } from "vitest";
import { Polar } from "@polar-sh/sdk";

describe("Polar Checkout", () => {
  const polar = new Polar({
    accessToken: process.env.POLAR_SANDBOX_TOKEN!,
    server: "sandbox",
  });

  let testProductId: string;

  beforeAll(async () => {
    // Create test product
    const product = await polar.products.create({
      name: "Test Product",
      organizationId: process.env.POLAR_ORG_ID!,
      prices: [{
        type: "one_time",
        amountType: "fixed",
        priceAmount: 1000,
        priceCurrency: "usd",
      }],
    });
    testProductId = product.id;
  });

  it("should create checkout session", async () => {
    const checkout = await polar.checkouts.create({
      products: [testProductId],
      successUrl: "http://localhost:3000/success",
      customerEmail: "[email protected]",
    });

    expect(checkout.status).toBe("open");
    expect(checkout.url).toBeDefined();
    expect(checkout.url).toContain("sandbox");
  });

  it("should retrieve checkout", async () => {
    const checkout = await polar.checkouts.create({
      products: [testProductId],
      successUrl: "http://localhost:3000/success",
    });

    const retrieved = await polar.checkouts.get({ id: checkout.id });
    expect(retrieved.id).toBe(checkout.id);
  });
});
测试 Webhook 处理器

根据真实载荷生成测试样例,而不是手动编写。--json 会打印载荷但不发送,因此无需运行 listen:

mkdir -p test/fixtures
polar trigger order.paid --json --seed 1 > test/fixtures/order.paid.json
polar trigger subscription.created --json --seed 1 > test/fixtures/subscription.created.json

按照 Polar 的方式为其签名(Standard Webhooks):对 id.timestamp.body 计算 HMAC-SHA256,以密钥字符串作为密钥,并进行 Base64 编码。此测试会直接调用 polar-integration Recipe 3 中的 webhook 路由:

import { createHmac } from "node:crypto";
import { readFile } from "node:fs/promises";
import { beforeAll, describe, expect, it, vi } from "vitest";

const secret = "test_webhook_secret";
let POST: (request: Request) => Promise<Response>;

beforeAll(async () => {
  vi.stubEnv("POLAR_WEBHOOK_SECRET", secret);
  ({ POST } = await import("../app/api/polar/webhook/route"));
});

const sign = (id: string, timestamp: number, body: string) =>
  `v1,${createHmac("sha256", secret).update(`${id}.${timestamp}.${body}`).digest("base64")}`;

const webhookRequest = (body: string, signature?: string) => {
  const id = "msg_test";
  const timestamp = Math.floor(Date.now() / 1000);
  return new Request("http://localhost/api/polar/webhook", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "webhook-id": id,
      "webhook-timestamp": String(timestamp),
      "webhook-signature": signature ?? sign(id, timestamp, body),
    },
    body,
  });
};

describe("Polar webhook handler", () => {
  it("accepts a signed order.paid event", async () => {
    const body = await readFile("test/fixtures/order.paid.json", "utf8");
    const response = await POST(webhookRequest(body));
    expect(response.status).toBe(200);
  });

  it("rejects an invalid signature", async () => {
    const body = await readFile("test/fixtures/order.paid.json", "utf8");
    const response = await POST(webhookRequest(body, "v1,invalid"));
    expect(response.status).toBe(403);
  });
});
测试许可证密钥验证
describe("License Keys", () => {
  it("should validate license key", async () => {
    // First create a customer with a license key benefit
    // Then validate the key
    const result = await polar.licenseKeys.validate({
      key: "TEST-XXXX-XXXX-XXXX",
      organizationId: process.env.POLAR_ORG_ID!,
    });

    expect(result.valid).toBe(true);
    expect(result.customer).toBeDefined();
  });

  it("should reject invalid license key", async () => {
    const result = await polar.licenseKeys.validate({
      key: "INVALID-KEY",
      organizationId: process.env.POLAR_ORG_ID!,
    });

    expect(result.valid).toBe(false);
  });
});

在单元测试中模拟 Polar

模拟 SDK
import { vi } from "vitest";

// Mock the entire SDK
vi.mock("@polar-sh/sdk", () => ({
  Polar: vi.fn().mockImplementation(() => ({
    checkouts: {
      create: vi.fn().mockResolvedValue({
        id: "checkout_mock",
        url: "https://sandbox.polar.sh/checkout/mock",
        status: "open",
      }),
      get: vi.fn().mockResolvedValue({
        id: "checkout_mock",
        status: "succeeded",
      }),
    },
    customers: {
      getState: vi.fn().mockResolvedValue({
        activeSubscriptions: [
          { id: "sub_mock", status: "active", productId: "prod_mock" },
        ],
        grantedBenefits: [],
      }),
    },
    subscriptions: {
      list: vi.fn().mockResolvedValue([]),
      cancel: vi.fn().mockResolvedValue({}),
    },
  })),
}));
模拟 Webhook 载荷

加载使用 polar trigger <event> --json 生成的测试样例(参见“测试 Webhook 处理器”)。手写的载荷容易与真实格式不符,导致 validateEvent 拒绝它们。

import orderPaid from "./fixtures/order.paid.json";
import subscriptionCreated from "./fixtures/subscription.created.json";

CI/CD 集成

GitHub Actions
name: Test Polar Integration

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    env:
      POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_SANDBOX_TOKEN }}
      POLAR_WEBHOOK_SECRET: ${{ secrets.POLAR_SANDBOX_WEBHOOK_SECRET }}
      POLAR_ORG_ID: ${{ secrets.POLAR_SANDBOX_ORG_ID }}
      POLAR_SERVER: sandbox

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci

      - name: Run unit tests
        run: npm run test:unit

      - name: Run integration tests
        run: npm run test:integration
环境设置

专门为 CI 创建沙盒凭据:

  1. 为 CI 创建专用沙盒组织
  2. 生成 CI 专用访问令牌
  3. 将 webhook 测试样例提交到代码仓库;处理器测试会使用测试密钥为其签名,无需 webhook 端点
  4. 将凭据存储在 GitHub Secrets 中

调试提示

检查 Webhook 投递情况

在本地,polar listen 会记录每个转发事件及处理器的状态;如果处理器失败,polar trigger 会打印其响应正文。

对于已部署的端点:

  1. 前往 sandbox.polar.sh → Settings → Webhooks
  2. 点击你的端点
  3. 查看投递历史记录和载荷
  4. 检查响应代码和错误
常见问题

Webhook 签名不匹配

  • 确保使用的是沙盒 webhook 密钥
  • 在本地,使用 polar listen --print-secret 显示的密钥;更改 .env 后重启开发服务器
  • 检查是否传入了原始正文(而非已解析的 JSON)
  • 确认时间戳在容差范围内(5 分钟)

结账未完成

  • 使用测试卡,而不是真实银行卡
  • 检查浏览器控制台中的错误
  • 确认 successUrl 正确

API 返回 401

  • 确认使用的是沙盒 API 对应的沙盒令牌
  • 检查令牌是否已过期
  • 确保令牌具有所需的作用域
启用调试日志
const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "sandbox",
  // Enable debug mode if available
});

// Log all requests
polar.checkouts.create({...}).then(console.log).catch(console.error);

测试清单

上线前:

  • 结账流程成功完成
  • 已接收并处理 Webhook
  • 订阅生命周期正常(创建、取消、撤销)
  • 已正确授予和撤销权益
  • 许可证密钥验证正常
  • 已处理付款被拒的情况
  • 可访问客户门户
  • 已测试退款流程

评论

支持 Markdown