关于 polar-testing
面向开发者的 Polar 集成测试指南,介绍如何使用隔离的 sandbox 测试结账、Webhook 和许可证密钥。还涵盖本地事件转发、测试夹具、SDK 模拟及 CI 配置。
主要功能
- 01使用 sandbox 隔离测试数据
- 02通过 Polar CLI 转发及触发 Webhook
- 03生成 Webhook 测试夹具
- 04模拟 SDK 和 Webhook 负载
- 05配置集成测试与 CI
适用场景
- 01在不处理真实付款的情况下测试结账
- 02本地调试 Webhook 验证与处理
- 03为 Webhook 处理程序编写夹具测试
- 04在 CI 中运行 Polar 集成测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ polarsource/ agent-toolkit --skill polar-testing安装后在 Claude Code 中输入/polar-testing确认已加载。
详细介绍
Polar 测试指南
使用沙盒环境安全地测试 Polar 集成——这是一个完全隔离的服务,你可以在其中进行试验,而不会影响生产数据或处理真实付款。
沙盒环境
访问
沙盒与生产环境完全隔离。你需要分别准备:
- 用户账户
- 组织
- 访问令牌
- Webhook 端点
设置步骤
- 前往 https://sandbox.polar.sh/start
- 创建新账户(或在组织切换器中选择“Go to sandbox”)
- 创建测试组织
- 在 Settings → Developers 中生成访问令牌
SDK 配置
按照 polar-integration(“Setup”)中的说明配置客户端,并传入 environment: "sandbox":
import { createPolar } from "@polar-sh/sdk/2026-10";
const polar = createPolar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
environment: "sandbox",
});
import os
from polar.v2026_10 import Polar
polar = Polar(os.environ["POLAR_ACCESS_TOKEN"], environment="sandbox")
沙盒限制
- 不会处理真实资金,数据也与生产环境隔离
- 面向客户的邮件(收据、续订提醒)仅会发送给组织成员;可以使用
[email protected]别名 - API 每分钟允许 100 次请求,而生产环境为 500 次,这会影响大型集成测试套件
测试卡
Polar 使用 Stripe 处理付款,因此 Stripe 的测试卡均可使用。请使用未来的到期日期和任意 CVC:
| 卡号 | 行为 |
|---|---|
| 4242 4242 4242 4242 | 付款成功 |
| 4000 0000 0000 0002 | 通用拒绝 |
| 4000 0000 0000 9995 | 余额不足 |
| 4000 0027 6000 3184 | 需要 3D Secure 身份验证 |
本地 Webhook 测试
使用 Polar CLI。它会将组织的 Webhook 事件直接转发到你的本地服务:无需隧道、公开 URL,也无需在控制面板中配置 Webhook 端点。
安装并登录
npm install -g @polar-sh/cli
polar auth login --sandbox
如需了解其他安装方式,请参阅 CLI 文档。
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
如果 Shell 中已导出 POLAR_ACCESS_TOKEN(例如来自应用的 .env),CLI 会使用它,而不是登录凭据。
将事件转发到应用
polar webhook listen 3000/api/polar/webhook
listen 接受端口、port/path 或完整 URL。它会持续运行,直到停止,因此请在单独的终端或后台进程中启动,并在测试期间保持运行。它会转发组织的所有 Webhook 事件,包括真实沙盒操作产生的事件,例如完成测试结账。如果账户有多个组织,请使用 --org <id> 或 polar auth org 选择一个组织。
设置 Webhook 密钥
listen 使用本地密钥对转发的事件签名,而不是控制面板端点的密钥。请将密钥写入 .env,不要打印出来:
secret=$(polar webhook listen --print-secret) && echo "POLAR_WEBHOOK_SECRET=$secret" >> .env
如果 .env 中已有 POLAR_WEBHOOK_SECRET 行,请替换它,而不是追加,然后重启开发服务器。已部署的环境使用其控制面板 Webhook 端点的密钥。
发送测试事件
polar webhook trigger order.paid
trigger 会通过正在运行的 listen 发送示例事件。如果你的处理程序未接受该事件(响应非 2xx、连接错误或重定向),它会打印处理程序的响应正文,并以状态码 1 退出。查看输出、修复处理程序,然后重复运行同一命令,直到以状态码 0 退出。
重定向通常意味着身份验证中间件拦截了 Webhook 路由。请将该路由排除在中间件之外:Polar 投递 Webhook 时绝不会跟随重定向。
polar webhook trigger --list --json # every event you can send
polar webhook trigger order.paid --override [email protected]
polar webhook trigger order.paid --seed 7 # the same IDs on every run
--override 接受 path=value,并且可以重复使用。API 会拒绝在负载中不存在的路径。
集成测试
测试结账流程
使用具有 products:write 和 checkouts:write 作用域的组织访问令牌。组织令牌已隐含组织信息,因此无需传递 organization_id。
import { describe, it, expect, beforeAll } from "vitest";
import { createPolar } from "@polar-sh/sdk/2026-10";
const polar = createPolar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
environment: "sandbox",
});
describe("Polar Checkout", () => {
let testProductId: string;
beforeAll(async () => {
const product = await polar.products.create({
name: "Test Product",
prices: [{ amount_type: "fixed", price_amount: 1000, price_currency: "usd" }],
});
testProductId = product.id;
});
it("creates a checkout session", async () => {
const checkout = await polar.checkouts.create({
products: [testProductId],
success_url: "http://localhost:3000/success",
customer_email: "[email protected]",
});
expect(checkout.status).toBe("open");
expect(checkout.url).toContain("sandbox");
});
it("retrieves a checkout", async () => {
const checkout = await polar.checkouts.create({
products: [testProductId],
success_url: "http://localhost:3000/success",
});
const retrieved = await polar.checkouts.get(checkout.id);
expect(retrieved.id).toBe(checkout.id);
});
});
测试 Webhook 处理程序
请根据真实负载生成测试数据,而不是手动编写。--dry-run 会打印负载而不发送,因此无需运行 listen:
mkdir -p test/fixtures
polar webhook trigger order.paid --dry-run --json --seed 1 > test/fixtures/order.paid.json
polar webhook trigger subscription.created --dry-run --json --seed 1 > test/fixtures/subscription.created.json
按照 polar webhook listen 的方式对其签名(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(202);
});
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);
});
});
测试许可证密钥验证
验证需要具有 license_keys:write 作用域的令牌。有效密钥会返回 status: "granted";未知密钥会抛出 ResourceNotFound:
import { describe, it, expect } from "vitest";
import { createPolar, errors } from "@polar-sh/sdk/2026-10";
const polar = createPolar({
accessToken: process.env.POLAR_ACCESS_TOKEN!,
environment: "sandbox",
});
const organization_id = process.env.POLAR_ORG_ID!;
describe("License keys", () => {
it("validates a granted key", async () => {
const result = await polar.licenseKeys.validate({ key: process.env.TEST_LICENSE_KEY!, organization_id });
expect(result.status).toBe("granted");
});
it("rejects an unknown key", async () => {
await expect(
polar.licenseKeys.validate({ key: "INVALID-KEY", organization_id }),
).rejects.toBeInstanceOf(errors.ResourceNotFound);
});
});
在单元测试中模拟 Polar
模拟 SDK
模拟 createPolar,并保留真实的 webhooks 和 errors 导出。响应使用与 API 相同的 snake_case 字段:
import { vi } from "vitest";
vi.mock("@polar-sh/sdk/2026-10", async (importOriginal) => ({
...(await importOriginal<typeof import("@polar-sh/sdk/2026-10")>()),
createPolar: () => ({
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: {
getStateExternal: vi.fn().mockResolvedValue({
active_subscriptions: [{ id: "sub_mock", status: "active", product_id: "prod_mock" }],
granted_benefits: [],
active_meters: [],
}),
},
subscriptions: {
revoke: vi.fn(),
update: vi.fn(),
},
}),
}));
模拟 Webhook 负载
加载通过 polar webhook trigger <event> --dry-run --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_ORG_ID: ${{ secrets.POLAR_SANDBOX_ORG_ID }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
如果 CI 也运行 Polar CLI,请添加 POLAR_ENVIRONMENT: sandbox。
环境设置
专门为 CI 创建沙盒凭据:
- 为 CI 创建专用的沙盒组织
- 为其创建组织访问令牌,且只授予测试所需的作用域(
polar auth token --scope products:write --scope checkouts:write会打开已预选这些作用域的令牌页面) - 提交 webhook 测试数据;处理程序测试会使用测试密钥对其签名,无需 webhook 端点
- 将令牌和组织 ID 存储在 GitHub Secrets 中
调试提示
检查 Webhook 投递情况
在本地,polar webhook listen 会记录每个转发事件及处理程序的状态;如果处理程序失败,polar webhook trigger 会打印其响应正文。
对于已部署的端点:
- 前往 sandbox.polar.sh → Settings → Webhooks
- 点击你的端点
- 查看投递历史记录和负载
- 检查响应代码和错误
常见问题
Webhook 签名不匹配
- 确保使用的是沙盒 webhook 密钥
- 在本地,使用
polar webhook listen --print-secret输出的密钥;更改.env后重启开发服务器 - 检查传入的是原始正文(而非解析后的 JSON)
- 确认时间戳在容差范围内(5 分钟)
结账未完成
- 使用测试卡,而非真实银行卡
- 检查浏览器控制台是否有错误
- 确认
success_url正确
API 返回 401
- 确认使用的是沙盒令牌,并调用沙盒 API
- 检查令牌是否已过期或被撤销
API 返回 403
- 令牌缺少端点所需的作用域
检查 API 错误
SDK 会抛出带有 HTTP 状态和响应正文的类型化错误:
import { PolarClientError } from "@polar-sh/sdk";
try {
await polar.checkouts.create({ products: [productId], success_url: "http://localhost:3000/success" });
} catch (error) {
if (error instanceof PolarClientError) console.error(error.statusCode, error.error);
throw error;
}
测试清单
上线前:
- 结账流程成功完成
- 收到并处理 Webhook
- 订阅生命周期正常(创建、取消、撤销)
- 权益已正确授予和撤销
- 许可证密钥验证正常
- 已处理付款被拒的情况
- 可访问客户门户
- 已测试退款流程