polar-testing について
polar-testingは、Polar連携をテストする開発者向けのスキルです。隔離されたsandbox環境、テストカード、CLIの手順を使い、決済とWebhookのテストを扱います。
主な機能
- 01隔離されたsandbox環境
- 02決済成功・拒否・3DSのテスト
- 03Webhookのローカル転送とイベント送信
- 04Webhookペイロードのフィクスチャ生成
- 05SDKモックとCIテストの設定
活用シーン
- 01checkoutとサブスクリプションの流れを検証
- 02Webhookハンドラーをローカルでデバッグ
- 03決済拒否とライセンスキー検証をテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ polarsource/ skills --skill polar-testingインストール後、Claude Code で/polar-testingを入力して読み込まれたか確認します。
詳細
Polar のテストガイド
サンドボックス環境を使用して、Polar の統合を安全にテストできます。本番データに影響を与えたり、実際の支払いを処理したりすることなく試せる、完全に分離されたサーバーです。
サンドボックス環境
アクセス
- ダッシュボード: https://sandbox.polar.sh
- API: https://sandbox-api.polar.sh/v1
サンドボックスは本番環境から完全に分離されています。次のものを個別に用意する必要があります。
- ユーザーアカウント
- 組織
- アクセストークン
- Webhook エンドポイント
セットアップ手順
- https://sandbox.polar.sh/start にアクセスします
- 新しいアカウントを作成します(または、組織切り替えメニューから「サンドボックスに移動」を選択します)
- テスト用の組織を作成します
- 設定 → 開発者からアクセストークンを生成します
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 セキュアのテスト
| カード番号 | 動作 |
|---|---|
| 4000 0027 6000 3184 | 3DS 認証が必要です |
| 4000 0000 0000 3220 | 3DS 認証が必要です |
ローカルでの Webhook テスト
Polar CLI を使用します。トンネルや公開 URL、ダッシュボード上の Webhook エンドポイントを使わずに、組織の 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、リモートマシン、クラウドエージェント)では、組織のアクセストークンを 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 のレシピ 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 が受信され、処理される
- サブスクリプションのライフサイクルが機能する(作成、キャンセル、取り消し)
- 特典が正しく付与および取り消される
- ライセンスキーの検証が機能する
- 支払いが拒否された場合のエラー処理
- 顧客ポータルにアクセスできる
- 返金フローをテスト済み