polar-testing について
Polar 連携を開発する人向けのテストガイドです。本番から隔離された sandbox でのチェックアウト、Webhook、ライセンスキー検証に加え、ローカル転送、フィクスチャ、SDK モック、CI 設定を説明します。
主な機能
- 01本番と分離された sandbox でのテスト
- 02Polar CLI による Webhook の転送と送信
- 03Webhook テストフィクスチャの生成
- 04SDK 応答と Webhook ペイロードのモック
- 05統合テストと CI の設定
活用シーン
- 01実際の決済なしでのチェックアウトテスト
- 02Webhook の検証と処理のローカルデバッグ
- 03フィクスチャを使った Webhook ハンドラーテスト
- 04CI での 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 のリクエスト上限は本番環境の 500 件に対して毎分 100 件です。大規模な連携テストスイートではこの制限に注意してください
テストカード
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 を使用します。トンネルも公開 URL もダッシュボード上の Webhook エンドポイントも必要なく、組織の Webhook イベントをローカルサーバーに直接転送できます。
インストールとサインイン
npm install -g @polar-sh/cli
polar auth login --sandbox
その他のインストール方法については、CLI のドキュメントを参照してください。
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
アプリの .env などから POLAR_ACCESS_TOKEN がすでにシェルでエクスポートされている場合、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 を受信して処理できる
- サブスクリプションのライフサイクル(作成、キャンセル、取り消し)が機能する
- 特典が正しく付与、取り消しされる
- ライセンスキーの検証が機能する
- 決済が拒否された場合のエラー処理
- カスタマーポータルにアクセスできる
- 返金フローをテスト済み