polar-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Polar 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

访问方式

The sandbox is completely isolated from production. You need separate:
  • User account
  • Organization
  • Access tokens
  • Webhook endpoints
沙箱与生产环境完全隔离。你需要单独的:
  • 用户账户
  • 组织
  • 访问令牌
  • Webhook端点

Setup Steps

设置步骤

  1. Go to https://sandbox.polar.sh/start
  2. Create a new account (or use "Go to sandbox" from org switcher)
  3. Create a test organization
  4. Generate an access token in Settings → Developers
  1. 访问 https://sandbox.polar.sh/start
  2. 创建新账户(或从组织切换器中选择"Go to sandbox")
  3. 创建测试组织
  4. 在设置→开发者页面生成访问令牌

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
undefined
typescript
// TypeScript
import { Polar } from "@polar-sh/sdk";

const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "sandbox", // 生产环境切换为"production"
});
python
undefined

Python

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 NumberBrandCVCExpiry
4242 4242 4242 4242VisaAny 3 digitsAny future date
5555 5555 5555 4444MastercardAny 3 digitsAny future date
3782 822463 10005AmexAny 4 digitsAny future date
6011 1111 1111 1117DiscoverAny 3 digitsAny future date
卡号品牌CVC有效期
4242 4242 4242 4242Visa任意3位数字任意未来日期
5555 5555 5555 4444Mastercard任意3位数字任意未来日期
3782 822463 10005Amex任意4位数字任意未来日期
6011 1111 1111 1117Discover任意3位数字任意未来日期

Declined Payments

支付拒绝场景

Card NumberDecline Reason
4000 0000 0000 0002Generic decline
4000 0000 0000 9995Insufficient funds
4000 0000 0000 9987Lost card
4000 0000 0000 9979Stolen card
4000 0000 0000 0069Expired card
4000 0000 0000 0127Incorrect CVC
卡号拒绝原因
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 Testing

3D Secure 测试

Card NumberBehavior
4000 0027 6000 3184Requires 3DS authentication
4000 0000 0000 3220Requires 3DS authentication
卡号行为
4000 0027 6000 3184需要3DS认证
4000 0000 0000 3220需要3DS认证

Local Webhook Testing

本地Webhook测试

Using ngrok

使用ngrok

bash
undefined
bash
undefined

Start 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 secret
ngrok 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
undefined
bash
undefined

.env.local

.env.local

POLAR_ACCESS_TOKEN=pat_sandbox_xxx POLAR_WEBHOOK_SECRET=whsec_sandbox_xxx POLAR_SERVER=sandbox
undefined
POLAR_ACCESS_TOKEN=pat_sandbox_xxx POLAR_WEBHOOK_SECRET=whsec_sandbox_xxx POLAR_SERVER=sandbox
undefined

Integration 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:integration
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:integration

Environment Setup

环境设置

Create sandbox credentials specifically for CI:
  1. Create a dedicated sandbox organization for CI
  2. Generate a CI-specific access token
  3. Set up webhook endpoint (or skip webhook tests in CI)
  4. Store credentials in GitHub Secrets
为CI创建专门的沙箱凭据:
  1. 为CI创建专用的沙箱组织
  2. 生成CI专用的访问令牌
  3. 设置Webhook端点(或在CI中跳过Webhook测试)
  4. 将凭据存储在GitHub Secrets中

Debugging Tips

调试技巧

Check Webhook Delivery

检查Webhook交付情况

  1. Go to sandbox.polar.sh → Settings → Webhooks
  2. Click on your endpoint
  3. View delivery history and payloads
  4. Check response codes and errors
  1. 访问 sandbox.polar.sh → 设置 → Webhooks
  2. 点击你的端点
  3. 查看交付历史和负载
  4. 检查响应码和错误

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已接收并处理
  • 订阅生命周期正常(创建、取消、撤销)
  • 权益正确授予和撤销
  • 许可证密钥验证正常
  • 支付拒绝场景的错误处理正常
  • 客户门户可访问
  • 退款流程已测试