mcpField
P
polar-testing

polar-testing

github.com/polarsourceテスト・品質5178 回インストール2 週間前に更新
リポジトリ

polar-testing について

Polar 連携を開発する人向けのテストガイドです。本番から隔離された sandbox でのチェックアウト、Webhook、ライセンスキー検証に加え、ローカル転送、フィクスチャ、SDK モック、CI 設定を説明します。

主な機能

  1. 01本番と分離された sandbox でのテスト
  2. 02Polar CLI による Webhook の転送と送信
  3. 03Webhook テストフィクスチャの生成
  4. 04SDK 応答と Webhook ペイロードのモック
  5. 05統合テストと CI の設定

活用シーン

  1. 01実際の決済なしでのチェックアウトテスト
  2. 02Webhook の検証と処理のローカルデバッグ
  3. 03フィクスチャを使った Webhook ハンドラーテスト
  4. 04CI での Polar 統合テスト実行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/polarsource/agent-toolkit --skill polar-testing

インストール後、Claude Code で/polar-testingを入力して読み込まれたか確認します。

詳細

Polar のテストガイド

サンドボックス環境を使って、Polar との連携を安全にテストできます。本番データや実際の決済に影響を与えることなく実験できる、完全に隔離されたサーバーです。

サンドボックス環境

アクセス

サンドボックスは本番環境から完全に隔離されています。以下はそれぞれ別途必要です。

  • ユーザーアカウント
  • 組織
  • アクセストークン
  • Webhook エンドポイント
セットアップ手順
  1. https://sandbox.polar.sh/start にアクセスします
  2. 新しいアカウントを作成します(または、組織切り替えメニューから「Go to sandbox」を選択します)
  3. テスト用の組織を作成します
  4. 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 31843D 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 専用のサンドボックス認証情報を作成します。

  1. CI 専用のサンドボックス組織を作成します
  2. テストに必要なスコープのみを持つ組織アクセストークンを作成します(polar auth token --scope products:write --scope checkouts:write を実行すると、これらのスコープがあらかじめ選択されたトークンページが開きます)
  3. Webhook フィクスチャをコミットします。ハンドラーテストはテスト用シークレットで署名するため、Webhook エンドポイントは不要です
  4. トークンと組織 ID を GitHub Secrets に保存します

デバッグのヒント

Webhook 配信の確認

ローカルでは、polar webhook listen が転送した各イベントをハンドラーのステータスとともに記録します。また、polar webhook trigger はハンドラーが失敗したときにレスポンス本文を出力します。

デプロイ済みのエンドポイントの場合:

  1. sandbox.polar.sh → Settings → Webhooks に移動します
  2. エンドポイントをクリックします
  3. 配信履歴とペイロードを確認します
  4. レスポンスコードとエラーを確認します
よくある問題

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 を受信して処理できる
  • サブスクリプションのライフサイクル(作成、キャンセル、取り消し)が機能する
  • 特典が正しく付与、取り消しされる
  • ライセンスキーの検証が機能する
  • 決済が拒否された場合のエラー処理
  • カスタマーポータルにアクセスできる
  • 返金フローをテスト済み

コメント