PinStripe.Test.Fixtures (PinStripe v0.3.1)

View Source

Automatically generates and caches Stripe API fixtures for testing.

Fixtures are generated using your Stripe account's API version via the Stripe CLI and cached in test/fixtures/stripe/. The API version is tracked in .api_version.

⚠️ Important: Side Effects

Fixture generation creates real test data in your Stripe account.

  • Resources (customers, products, etc.) are created in test mode
  • Objects are marked with "PinStripe Test Fixture" for identification
  • Only test mode API keys (starting with sk_test_) are allowed
  • Consider committing fixtures to git to avoid regenerating

Requirements

  • Stripe CLI installed: brew install stripe/stripe-cli/stripe
  • Stripe CLI authenticated: stripe login
  • Test mode API key configured (starts with sk_test_)

Quick Start

Configuration

# config/test.exs
config :pin_stripe,
  stripe_api_key: System.get_env("STRIPE_SECRET_KEY")

Basic Usage

# In your test
test "creates a customer" do
  # Load/generate a customer fixture (use atoms for API resources)
  customer = PinStripe.Test.Fixtures.load(:customer)

  # Use with Req.Test
  Req.Test.stub(PinStripe, fn conn ->
    Req.Test.json(conn, customer)
  end)

  {:ok, response} = Client.create(:customers, %{email: "test@example.com"})
  assert response.body["object"] == "customer"
end

Customizing Fixtures

Generate fixtures with specific attributes by passing options:

# Customer with specific email (atoms for API resources)
customer = PinStripe.Test.Fixtures.load(:customer,
  email: "alice@example.com",
  name: "Alice Smith"
)

# Customer with metadata
customer = PinStripe.Test.Fixtures.load(:customer,
  metadata: %{user_id: "123", plan: "premium"}
)

# Webhook event with custom data (strings for webhook events - they have dots)
event = PinStripe.Test.Fixtures.load("customer.created",
  data: %{object: %{email: "custom@example.com"}}
)

Each unique set of options creates a separate cached fixture file. The filename includes a hash of the options for uniqueness:

test/fixtures/stripe/
  customer.json              # Base customer
  customer-a3f2b9c1.json    # Customized customer

API Version Management

Fixtures match your Stripe account's default API version at the time they're first generated. The version is stored in test/fixtures/stripe/.api_version.

When you upgrade your Stripe account's API version, run the sync task to clear old fixtures and regenerate them with the new version:

mix pin_stripe.sync_api_version

This task:

  • Detects your account's current API version
  • Compares it with the cached version
  • Clears all fixtures if the version changed
  • Updates the .api_version file

Supported Fixtures

API Resources (Atoms)

  • :customer - Customer objects
  • :product - Product objects
  • :price - Price objects
  • :subscription - Subscription objects
  • :invoice - Invoice objects
  • :charge - Charge objects (one-time payments)
  • :payment_intent - PaymentIntent objects (modern payment flow)
  • :refund - Refund objects

Webhook Events (Strings - they have dots)

  • "customer.created", "customer.updated", "customer.deleted"
  • "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted"
  • "invoice.paid", "invoice.payment_failed"

Error Responses (Atoms)

  • :error_404 - Not found (resource_missing)
  • :error_400 - Bad request (invalid_request_error)
  • :error_401 - Unauthorized (authentication error)
  • error_429 - Rate limit exceeded

Testing Best Practices

Commit Fixtures to Git

To avoid generating fixtures in CI or on every developer machine:

# Commit fixtures to version control
git add test/fixtures/stripe
git commit -m "Add Stripe test fixtures"

Use Base Fixtures and Modify

Instead of generating many custom fixtures:

# Load base fixture
customer = PinStripe.Test.Fixtures.load(:customer)

# Modify as needed in your test
customer = Map.put(customer, "email", "specific@test.com")

Clean Up Test Data

Objects created during fixture generation accumulate in your Stripe test account. They're marked with pinstripe_fixture: true metadata for easy identification.

To clean up manually:

# List PinStripe test objects
stripe customers list --limit 100 | grep "PinStripe Test Fixture"

# Delete a specific customer
stripe customers delete cus_xxx

Examples

Testing API Responses

test "handles customer creation" do
  fixture = PinStripe.Test.Fixtures.load(:customer)

  Req.Test.stub(PinStripe, fn conn ->
    Req.Test.json(conn, fixture)
  end)

  {:ok, response} = Client.create(:customers, %{email: "test@example.com"})
  assert response.body["id"] == fixture["id"]
end

Testing Webhook Handlers

test "handles customer.created webhook" do
  event = PinStripe.Test.Fixtures.load("customer.created")

  conn = build_webhook_conn(event)
  conn = MyAppWeb.StripeWebhookController.create(conn, event)

  assert conn.status == 200
end

Testing Error Handling

test "handles not found error" do
  error = PinStripe.Test.Fixtures.load(:error_404)

  Req.Test.stub(PinStripe, fn conn ->
    conn
    |> Plug.Conn.put_status(404)
    |> Req.Test.json(error)
  end)

  assert {:error, %{status: 404}} = Client.read("cus_nonexistent")
end

Testing with Multiple Customers

test "handles multiple customers" do
  alice = PinStripe.Test.Fixtures.load(:customer, email: "alice@test.com")
  bob = PinStripe.Test.Fixtures.load(:customer, email: "bob@test.com")

  # Each gets cached separately
  assert alice["email"] == "alice@test.com"
  assert bob["email"] == "bob@test.com"
end

Summary

Functions

Returns the API version used for cached fixtures.

Detects the current Stripe account's API version.

Lists all available (cached) fixtures.

Loads a fixture by name, generating it if not cached.

Functions

api_version()

Returns the API version used for cached fixtures.

Returns nil if no .api_version file exists yet.

Examples

iex> # When no .api_version file exists
iex> Application.put_env(:pin_stripe, :fixtures_dir, "test/fixtures/doctest_no_version")
iex> File.rm_rf!("test/fixtures/doctest_no_version")
iex> PinStripe.Test.Fixtures.api_version()
nil

iex> # After creating a version file
iex> Application.put_env(:pin_stripe, :fixtures_dir, "test/fixtures/doctest_version")
iex> File.rm_rf!("test/fixtures/doctest_version")
iex> File.mkdir_p!("test/fixtures/doctest_version")
iex> File.write!("test/fixtures/doctest_version/.api_version", "2024-06-20")
iex> PinStripe.Test.Fixtures.api_version()
"2024-06-20"
iex> File.rm_rf!("test/fixtures/doctest_version")
iex> Application.delete_env(:pin_stripe, :fixtures_dir)
:ok

detect_account_api_version()

Detects the current Stripe account's API version.

Makes a quick API call to determine the version. Requires a valid Stripe API key and authenticated Stripe CLI.

Examples

# Requires real Stripe setup
PinStripe.Test.Fixtures.detect_account_api_version()
#=> "2024-06-20"

list()

Lists all available (cached) fixtures.

Returns a list of fixture filenames (without .json extension), sorted alphabetically. Returns an empty list if no fixtures directory exists.

Examples

iex> # When no fixtures exist
iex> Application.put_env(:pin_stripe, :fixtures_dir, "test/fixtures/doctest_empty")
iex> File.rm_rf!("test/fixtures/doctest_empty")
iex> PinStripe.Test.Fixtures.list()
[]

iex> # Lists cached API resource fixtures (not error fixtures which use atoms)
iex> # Error fixtures don't create files, so list() returns only cached API resources
iex> Application.put_env(:pin_stripe, :fixtures_dir, "test/fixtures/stripe")
iex> list = PinStripe.Test.Fixtures.list()
iex> "error_400" in list
true
iex> "error_404" in list
true
iex> Application.delete_env(:pin_stripe, :fixtures_dir)
:ok

load(fixture_name, opts \\ [])

Loads a fixture by name, generating it if not cached.

Fixture Types

Error Fixtures (Atoms) - Self-contained, no Stripe CLI required:

  • Use atoms: :error_400, :error_401, :error_402, :error_403, :error_404, :error_409, :error_424, :error_429, :error_500, :error_502, :error_503, :error_504
  • Generated instantly (no API calls)
  • Match actual Stripe error responses
  • Not cached to filesystem

API Resources (Atoms) - Require Stripe CLI and API key:

  • Use atoms: :customer, :product, :price, :subscription, :invoice, :charge, :payment_intent, :refund
  • Created via Stripe API
  • Cached to filesystem
  • Options passed to Stripe CLI during creation

Webhook Events (Strings) - Require Stripe CLI and API key:

  • Use strings with dots: "customer.created", "invoice.paid", etc.
  • Generated via Stripe CLI trigger
  • Cached to filesystem
  • Options used to modify event data after generation

Options

Options are used to customize fixtures. Each unique combination of options creates a separate cached fixture (for API resources/webhooks only).

Examples

Error fixtures use atoms and are self-contained:

iex> error = PinStripe.Test.Fixtures.load(:error_404)
iex> error["error"]["type"]
"invalid_request_error"
iex> error["error"]["code"]
"resource_missing"

iex> error = PinStripe.Test.Fixtures.load(:error_400)
iex> error["error"]["type"]
"invalid_request_error"
iex> error["error"]["code"]
"parameter_invalid_empty"

iex> error = PinStripe.Test.Fixtures.load(:error_401)
iex> error["error"]["type"]
"invalid_request_error"

iex> error = PinStripe.Test.Fixtures.load(:error_429)
iex> error["error"]["type"]
"rate_limit_error"

API resources use atoms and webhook events use strings (both require Stripe CLI):

# API resources use atoms - require real Stripe setup
PinStripe.Test.Fixtures.load(:customer)
#=> %{"id" => "cus_...", "object" => "customer", ...}

PinStripe.Test.Fixtures.load(:customer, email: "test@example.com")
#=> %{"id" => "cus_...", "email" => "test@example.com", ...}

# Webhook events use strings (they have dots)
PinStripe.Test.Fixtures.load("customer.created")
#=> %{"id" => "evt_...", "type" => "customer.created", ...}