mcpField
P
polar-testing

polar-testing

github.com/polarsourceTesting & quality5172 installsUpdated 2w ago
Repository

About polar-testing

polar-testing is a skill for developers testing Polar integrations. It uses an isolated sandbox, test cards, and CLI workflows to cover payment and webhook testing.

Key features

  1. 01Isolated sandbox environment
  2. 02Successful, declined, and 3DS payment tests
  3. 03Local webhook forwarding and event triggers
  4. 04Webhook payload fixture generation
  5. 05SDK mocking and CI test setup

Use cases

  1. 01Verify checkout and subscription lifecycles
  2. 02Debug webhook handlers locally
  3. 03Test declined payments and license key validation

Install

Copy the command and run it in a terminal.

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

After installing, type/polar-testingin Claude Code to confirm it loaded.

Details

From the project README

Polar Testing Guide

Test Polar integrations safely using the sandbox environment - a fully isolated server where you can experiment without affecting production data or processing real payments.

Sandbox Environment

Access

The sandbox is completely isolated from production. You need separate:

  • User account
  • Organization
  • Access tokens
  • Webhook endpoints
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
SDK Configuration
// 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")),
)
Sandbox Limitations
  • Subscriptions auto-cancel after 90 days
  • No real money is processed
  • Data is isolated from production

Test Cards

Polar uses Stripe for payment processing. Use these test card numbers:

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
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
3D Secure Testing
Card NumberBehavior
4000 0027 6000 3184Requires 3DS authentication
4000 0000 0000 3220Requires 3DS authentication

Local Webhook Testing

Use the Polar CLI. It forwards your organization's webhook events straight to your local server: no tunnel, no public URL, and no webhook endpoint in the dashboard.

Install and sign in
# macOS, Linux, WSL
curl -fsSL https://polar.sh/install.sh | bash

polar auth login --sandbox

polar auth login opens a browser, so ask the user to run it. Check the result with polar auth whoami --json: environments lists the signed-in environments and organization is the active one.

Where no browser is available (CI, remote machines, cloud agents), an organization access token replaces auth login:

export POLAR_ACCESS_TOKEN=polar_oat_...   # scopes: webhooks:read, webhooks:write, organizations:read
export POLAR_ENVIRONMENT=sandbox
Forward events to your app
polar listen 3000/api/polar/webhook

listen takes a port, a port/path, or a full URL. It runs until stopped, so start it in its own terminal or as a background process and keep it running while you test. It forwards every webhook event for the organization, including the ones real sandbox activity produces, such as completing a test checkout.

Set the webhook secret

listen signs forwarded events with a local secret, not a dashboard endpoint's secret. Write it to .env without printing it:

secret=$(polar listen --print-secret) && echo "POLAR_WEBHOOK_SECRET=$secret" >> .env

If .env already has a POLAR_WEBHOOK_SECRET line, replace it instead of appending, then restart the dev server. Deployed environments use the secret of their dashboard webhook endpoint.

Send test events
polar trigger order.paid

trigger sends a sample event through the running listen. When your handler doesn't accept it (a non-2xx response, a connection error, or a redirect), it prints the handler's response body and exits with status 1. Read that output, fix the handler, and run the same command again until it exits with 0.

A redirect usually means auth middleware is intercepting the webhook route. Exclude the route from it: Polar never follows redirects when delivering webhooks.

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 takes path=value and can be repeated. The API rejects paths that don't exist in the payload.

Integration Testing

Test Checkout Flow
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);
  });
});
Test Webhook Handler

Generate fixtures from real payloads instead of writing them by hand. --json prints the payload without sending it, so listen doesn't need to be running:

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

Sign them the way Polar does (Standard Webhooks): an HMAC-SHA256 of id.timestamp.body, base64-encoded and keyed with the secret string. This test calls the webhook route from polar-integration Recipe 3 directly:

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);
  });
});
Test License Key Validation
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);
  });
});

Mocking Polar in Unit Tests

Mock 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({}),
    },
  })),
}));
Mock Webhook Payloads

Load the fixtures generated with polar trigger <event> --json (see Test Webhook Handler). Hand-written payloads drift from the real shape, and validateEvent rejects them.

import orderPaid from "./fixtures/order.paid.json";
import subscriptionCreated from "./fixtures/subscription.created.json";

CI/CD Integration

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
Environment Setup

Create sandbox credentials specifically for CI:

  1. Create a dedicated sandbox organization for CI
  2. Generate a CI-specific access token
  3. Commit the webhook fixtures; handler tests sign them with a test secret and need no webhook endpoint
  4. Store credentials in GitHub Secrets

Debugging Tips

Check Webhook Delivery

Locally, polar listen logs every forwarded event with your handler's status, and polar trigger prints the handler's response body when it fails.

For a deployed endpoint:

  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
Common Issues

Webhook signature mismatch

  • Ensure you're using the sandbox webhook secret
  • Locally, use the secret from polar listen --print-secret and restart the dev server after changing .env
  • 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
Enable Debug Logging
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);

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

Comments

Markdown supported