关于 polar-testing
polar-testing 是一份指导 Polar 集成测试的技能,面向开发者。它借助隔离的 sandbox 环境、测试卡和 CLI 流程覆盖支付与 Webhook 测试。
主要功能
- 01隔离的 sandbox 测试环境
- 02支付成功、拒付与 3DS 测试
- 03本地 Webhook 转发与事件触发
- 04Webhook 负载夹具生成
- 05SDK 模拟与 CI 测试配置
适用场景
- 01验证 checkout 与订阅生命周期
- 02在本地调试 Webhook 处理器
- 03测试拒付和许可证密钥验证
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ polarsource/ skills --skill polar-testing安装后在 Claude Code 中输入/polar-testing确认已加载。
详细介绍
Polar 测试指南
使用沙盒环境安全地测试 Polar 集成——这是一个完全隔离的服务,你可以在其中进行试验,而不会影响生产数据或处理真实付款。
沙盒环境
访问
沙盒与生产环境完全隔离。你需要分别准备:
- 用户账户
- 组织
- 访问令牌
- Webhook 端点
设置步骤
- 前往 https://sandbox.polar.sh/start
- 创建新账户(或在组织切换器中选择“Go to sandbox”)
- 创建测试组织
- 在 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 4242 | Visa | 任意 3 位数字 | 任意未来日期 |
| 5555 5555 5555 4444 | Mastercard | 任意 3 位数字 | 任意未来日期 |
| 3782 822463 10005 | Amex | 任意 4 位数字 | 任意未来日期 |
| 6011 1111 1111 1117 | Discover | 任意 3 位数字 | 任意未来日期 |
付款被拒
| 卡号 | 拒绝原因 |
|---|---|
| 4000 0000 0000 0002 | 常规拒绝 |
| 4000 0000 0000 9995 | 余额不足 |
| 4000 0000 0000 9987 | 卡片已挂失 |
| 4000 0000 0000 9979 | 卡片已被盗 |
| 4000 0000 0000 0069 | 卡片已过期 |
| 4000 0000 0000 0127 | CVC 不正确 |
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 创建沙盒凭据:
- 为 CI 创建专用沙盒组织
- 生成 CI 专用访问令牌
- 将 webhook 测试样例提交到代码仓库;处理器测试会使用测试密钥为其签名,无需 webhook 端点
- 将凭据存储在 GitHub Secrets 中
调试提示
检查 Webhook 投递情况
在本地,polar listen 会记录每个转发事件及处理器的状态;如果处理器失败,polar trigger 会打印其响应正文。
对于已部署的端点:
- 前往 sandbox.polar.sh → Settings → Webhooks
- 点击你的端点
- 查看投递历史记录和载荷
- 检查响应代码和错误
常见问题
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
- 订阅生命周期正常(创建、取消、撤销)
- 已正确授予和撤销权益
- 许可证密钥验证正常
- 已处理付款被拒的情况
- 可访问客户门户
- 已测试退款流程