mcpField
P
polar-testing

polar-testing

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

polar-testing について

polar-testingは、Polar連携をテストする開発者向けのスキルです。隔離されたsandbox環境、テストカード、CLIの手順を使い、決済とWebhookのテストを扱います。

主な機能

  1. 01隔離されたsandbox環境
  2. 02決済成功・拒否・3DSのテスト
  3. 03Webhookのローカル転送とイベント送信
  4. 04Webhookペイロードのフィクスチャ生成
  5. 05SDKモックとCIテストの設定

活用シーン

  1. 01checkoutとサブスクリプションの流れを検証
  2. 02Webhookハンドラーをローカルでデバッグ
  3. 03決済拒否とライセンスキー検証をテスト

インストール

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

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

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

詳細

Polar のテストガイド

サンドボックス環境を使用して、Polar の統合を安全にテストできます。本番データに影響を与えたり、実際の支払いを処理したりすることなく試せる、完全に分離されたサーバーです。

サンドボックス環境

アクセス

サンドボックスは本番環境から完全に分離されています。次のものを個別に用意する必要があります。

  • ユーザーアカウント
  • 組織
  • アクセストークン
  • Webhook エンドポイント
セットアップ手順
  1. https://sandbox.polar.sh/start にアクセスします
  2. 新しいアカウントを作成します(または、組織切り替えメニューから「サンドボックスに移動」を選択します)
  3. テスト用の組織を作成します
  4. 設定 → 開発者からアクセストークンを生成します
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 4242Visa3 桁なら任意将来の日付なら任意
5555 5555 5555 4444Mastercard3 桁なら任意将来の日付なら任意
3782 822463 10005Amex4 桁なら任意将来の日付なら任意
6011 1111 1111 1117Discover3 桁なら任意将来の日付なら任意
支払い拒否
カード番号拒否理由
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 セキュアのテスト
カード番号動作
4000 0027 6000 31843DS 認証が必要です
4000 0000 0000 32203DS 認証が必要です

ローカルでの 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 専用のサンドボックス認証情報を作成します。

  1. CI 専用のサンドボックス組織を作成する
  2. CI 専用のアクセストークンを生成する
  3. Webhook フィクスチャをコミットする。ハンドラーのテストではテスト用シークレットを使って署名するため、Webhook エンドポイントは不要です
  4. 認証情報を GitHub Secrets に保存する

デバッグのヒント

Webhook 配信の確認

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

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

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

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

コメント