polar-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePolar Testing Guide
Polar 测试指南
Test Polar integrations safely using the sandbox environment - a fully isolated server where you can experiment without affecting production data or processing real payments.
使用沙箱环境安全测试Polar集成——这是一个完全隔离的服务器,你可以在其中进行实验,而不会影响生产数据或处理真实支付。
Sandbox Environment
沙箱环境
Access
访问方式
- Dashboard: https://sandbox.polar.sh
- API: https://sandbox-api.polar.sh/v1
The sandbox is completely isolated from production. You need separate:
- User account
- Organization
- Access tokens
- Webhook endpoints
沙箱与生产环境完全隔离。你需要单独的:
- 用户账户
- 组织
- 访问令牌
- Webhook端点
Setup Steps
设置步骤
- Go to https://sandbox.polar.sh/start
- Create a new account (or use "Go to sandbox" from org switcher)
- Create a test organization
- Generate an access token in Settings → Developers
- 访问 https://sandbox.polar.sh/start
- 创建新账户(或从组织切换器中选择"Go to sandbox")
- 创建测试组织
- 在设置→开发者页面生成访问令牌
SDK Configuration
SDK 配置
typescript
// 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
undefinedtypescript
// TypeScript
import { Polar } from "@polar-sh/sdk";
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "sandbox", // 生产环境切换为"production"
});python
undefinedPython
Python
from polar_sdk import Polar
polar = Polar(
access_token=os.environ["POLAR_ACCESS_TOKEN"],
server="sandbox",
)
```go
// Go
s := polargo.New(
polargo.WithServer("sandbox"),
polargo.WithSecurity(os.Getenv("POLAR_ACCESS_TOKEN")),
)from polar_sdk import Polar
polar = Polar(
access_token=os.environ["POLAR_ACCESS_TOKEN"],
server="sandbox",
)
```go
// Go
s := polargo.New(
polargo.WithServer("sandbox"),
polargo.WithSecurity(os.Getenv("POLAR_ACCESS_TOKEN")),
)Sandbox Limitations
沙箱限制
- Subscriptions auto-cancel after 90 days
- No real money is processed
- Data is isolated from production
- 订阅会在90天后自动取消
- 不处理真实资金
- 数据与生产环境隔离
Test Cards
测试卡
Polar uses Stripe for payment processing. Use these test card numbers:
Polar使用Stripe处理支付。使用以下测试卡号:
Successful Payments
支付成功场景
| Card Number | Brand | CVC | Expiry |
|---|---|---|---|
| 4242 4242 4242 4242 | Visa | Any 3 digits | Any future date |
| 5555 5555 5555 4444 | Mastercard | Any 3 digits | Any future date |
| 3782 822463 10005 | Amex | Any 4 digits | Any future date |
| 6011 1111 1111 1117 | Discover | Any 3 digits | Any future date |
| 卡号 | 品牌 | CVC | 有效期 |
|---|---|---|---|
| 4242 4242 4242 4242 | Visa | 任意3位数字 | 任意未来日期 |
| 5555 5555 5555 4444 | Mastercard | 任意3位数字 | 任意未来日期 |
| 3782 822463 10005 | Amex | 任意4位数字 | 任意未来日期 |
| 6011 1111 1111 1117 | Discover | 任意3位数字 | 任意未来日期 |
Declined Payments
支付拒绝场景
| Card Number | Decline Reason |
|---|---|
| 4000 0000 0000 0002 | Generic decline |
| 4000 0000 0000 9995 | Insufficient funds |
| 4000 0000 0000 9987 | Lost card |
| 4000 0000 0000 9979 | Stolen card |
| 4000 0000 0000 0069 | Expired card |
| 4000 0000 0000 0127 | Incorrect CVC |
| 卡号 | 拒绝原因 |
|---|---|
| 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 Testing
3D Secure 测试
| Card Number | Behavior |
|---|---|
| 4000 0027 6000 3184 | Requires 3DS authentication |
| 4000 0000 0000 3220 | Requires 3DS authentication |
| 卡号 | 行为 |
|---|---|
| 4000 0027 6000 3184 | 需要3DS认证 |
| 4000 0000 0000 3220 | 需要3DS认证 |
Local Webhook Testing
本地Webhook测试
Using ngrok
使用ngrok
bash
undefinedbash
undefinedStart your app
启动你的应用
npm run dev
npm run dev
In another terminal, start ngrok
在另一个终端启动ngrok
ngrok http 3000
Copy the ngrok URL (e.g., `https://abc123.ngrok.io`) and configure it in Polar:
1. Go to sandbox.polar.sh → Settings → Webhooks
2. Add endpoint: `https://abc123.ngrok.io/api/webhooks/polar`
3. Select events to receive
4. Copy the webhook secretngrok http 3000
复制ngrok URL(例如 `https://abc123.ngrok.io`)并在Polar中配置:
1. 访问 sandbox.polar.sh → 设置 → Webhooks
2. 添加端点:`https://abc123.ngrok.io/api/webhooks/polar`
3. 选择要接收的事件
4. 复制Webhook密钥Environment Variables
环境变量
bash
undefinedbash
undefined.env.local
.env.local
POLAR_ACCESS_TOKEN=pat_sandbox_xxx
POLAR_WEBHOOK_SECRET=whsec_sandbox_xxx
POLAR_SERVER=sandbox
undefinedPOLAR_ACCESS_TOKEN=pat_sandbox_xxx
POLAR_WEBHOOK_SECRET=whsec_sandbox_xxx
POLAR_SERVER=sandbox
undefinedIntegration Testing
集成测试
Test Checkout Flow
测试结账流程
typescript
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: "test@example.com",
});
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);
});
});typescript
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 () => {
// 创建测试产品
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: "test@example.com",
});
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);
});
});Test Webhook Handler
测试Webhook处理器
typescript
import { describe, it, expect } from "vitest";
import { createHmac } from "crypto";
describe("Webhook Handler", () => {
const webhookSecret = "whsec_test_secret";
function signPayload(payload: string, timestamp: number): string {
const signedPayload = `${timestamp}.${payload}`;
return createHmac("sha256", webhookSecret)
.update(signedPayload)
.digest("hex");
}
it("should verify valid webhook signature", async () => {
const payload = JSON.stringify({
type: "order.paid",
data: { id: "order_123" },
});
const timestamp = Math.floor(Date.now() / 1000);
const signature = signPayload(payload, timestamp);
const response = await fetch("/api/webhooks/polar", {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": "evt_123",
"webhook-timestamp": timestamp.toString(),
"webhook-signature": `v1,${signature}`,
},
body: payload,
});
expect(response.status).toBe(200);
});
it("should reject invalid signature", async () => {
const response = await fetch("/api/webhooks/polar", {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": "evt_123",
"webhook-timestamp": "1234567890",
"webhook-signature": "v1,invalid",
},
body: JSON.stringify({ type: "order.paid" }),
});
expect(response.status).toBe(400);
});
});typescript
import { describe, it, expect } from "vitest";
import { createHmac } from "crypto";
describe("Webhook Handler", () => {
const webhookSecret = "whsec_test_secret";
function signPayload(payload: string, timestamp: number): string {
const signedPayload = `${timestamp}.${payload}`;
return createHmac("sha256", webhookSecret)
.update(signedPayload)
.digest("hex");
}
it("should verify valid webhook signature", async () => {
const payload = JSON.stringify({
type: "order.paid",
data: { id: "order_123" },
});
const timestamp = Math.floor(Date.now() / 1000);
const signature = signPayload(payload, timestamp);
const response = await fetch("/api/webhooks/polar", {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": "evt_123",
"webhook-timestamp": timestamp.toString(),
"webhook-signature": `v1,${signature}`,
},
body: payload,
});
expect(response.status).toBe(200);
});
it("should reject invalid signature", async () => {
const response = await fetch("/api/webhooks/polar", {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": "evt_123",
"webhook-timestamp": "1234567890",
"webhook-signature": "v1,invalid",
},
body: JSON.stringify({ type: "order.paid" }),
});
expect(response.status).toBe(400);
});
});Test License Key Validation
测试许可证密钥验证
typescript
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);
});
});typescript
describe("License Keys", () => {
it("should validate license key", async () => {
// 首先创建带有许可证密钥权益的客户
// 然后验证密钥
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);
});
});Mocking Polar in Unit Tests
单元测试中Mock Polar
Mock SDK
Mock SDK
typescript
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({}),
},
})),
}));typescript
import { vi } from "vitest";
// Mock整个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({}),
},
})),
}));Mock Webhook Payloads
Mock Webhook 负载
typescript
export const mockWebhookPayloads = {
orderPaid: {
type: "order.paid",
data: {
id: "order_123",
status: "paid",
customer_id: "cust_123",
product_id: "prod_123",
total_amount: 2900,
currency: "usd",
},
},
subscriptionCreated: {
type: "subscription.created",
data: {
id: "sub_123",
status: "active",
customer_id: "cust_123",
product_id: "prod_123",
current_period_end: "2025-02-15T00:00:00Z",
},
},
subscriptionCanceled: {
type: "subscription.canceled",
data: {
id: "sub_123",
status: "active",
cancel_at_period_end: true,
ends_at: "2025-02-15T00:00:00Z",
},
},
benefitGrantCreated: {
type: "benefit_grant.created",
data: {
id: "grant_123",
customer_id: "cust_123",
benefit_id: "benefit_123",
is_granted: true,
properties: {
license_key: "TEST-XXXX-XXXX-XXXX",
},
},
},
};typescript
export const mockWebhookPayloads = {
orderPaid: {
type: "order.paid",
data: {
id: "order_123",
status: "paid",
customer_id: "cust_123",
product_id: "prod_123",
total_amount: 2900,
currency: "usd",
},
},
subscriptionCreated: {
type: "subscription.created",
data: {
id: "sub_123",
status: "active",
customer_id: "cust_123",
product_id: "prod_123",
current_period_end: "2025-02-15T00:00:00Z",
},
},
subscriptionCanceled: {
type: "subscription.canceled",
data: {
id: "sub_123",
status: "active",
cancel_at_period_end: true,
ends_at: "2025-02-15T00:00:00Z",
},
},
benefitGrantCreated: {
type: "benefit_grant.created",
data: {
id: "grant_123",
customer_id: "cust_123",
benefit_id: "benefit_123",
is_granted: true,
properties: {
license_key: "TEST-XXXX-XXXX-XXXX",
},
},
},
};CI/CD Integration
CI/CD 集成
GitHub Actions
GitHub Actions
yaml
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:integrationyaml
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:integrationEnvironment Setup
环境设置
Create sandbox credentials specifically for CI:
- Create a dedicated sandbox organization for CI
- Generate a CI-specific access token
- Set up webhook endpoint (or skip webhook tests in CI)
- Store credentials in GitHub Secrets
为CI创建专门的沙箱凭据:
- 为CI创建专用的沙箱组织
- 生成CI专用的访问令牌
- 设置Webhook端点(或在CI中跳过Webhook测试)
- 将凭据存储在GitHub Secrets中
Debugging Tips
调试技巧
Check Webhook Delivery
检查Webhook交付情况
- Go to sandbox.polar.sh → Settings → Webhooks
- Click on your endpoint
- View delivery history and payloads
- Check response codes and errors
- 访问 sandbox.polar.sh → 设置 → Webhooks
- 点击你的端点
- 查看交付历史和负载
- 检查响应码和错误
Common Issues
常见问题
Webhook signature mismatch
- Ensure you're using the sandbox webhook secret
- Check that the raw body is being passed (not parsed JSON)
- Verify timestamp is within tolerance (5 minutes)
Checkout not completing
- Use test cards, not real cards
- Check browser console for errors
- Verify successUrl is correct
API returns 401
- Verify you're using sandbox token with sandbox API
- Check token hasn't expired
- Ensure token has required scopes
Webhook签名不匹配
- 确保你使用的是沙箱Webhook密钥
- 检查是否传递了原始请求体(而非解析后的JSON)
- 验证时间戳是否在容忍范围内(5分钟)
结账流程未完成
- 使用测试卡,而非真实卡片
- 检查浏览器控制台是否有错误
- 验证successUrl是否正确
API返回401
- 确认你在沙箱API中使用的是沙箱令牌
- 检查令牌是否过期
- 确保令牌具有所需的权限
Enable Debug Logging
启用调试日志
typescript
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);typescript
const polar = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "sandbox",
// 如果可用,启用调试模式
});
// 记录所有请求
polar.checkouts.create({...}).then(console.log).catch(console.error);Test Checklist
测试清单
Before going to production:
- Checkout flow completes successfully
- Webhooks received and processed
- Subscription lifecycle works (create, cancel, revoke)
- Benefits granted and revoked correctly
- License key validation works
- Error handling for declined payments
- Customer portal accessible
- Refund flow tested
上线前需完成:
- 结账流程成功完成
- Webhook已接收并处理
- 订阅生命周期正常(创建、取消、撤销)
- 权益正确授予和撤销
- 许可证密钥验证正常
- 支付拒绝场景的错误处理正常
- 客户门户可访问
- 退款流程已测试