Integration

Testing Patterns

Patterns for unit tests, integration tests, and CI/CD pipelines using Ghostbox sandboxes.

Unit test setup

Override the base URL in your test setup to point at Ghostbox. Your application code under test makes real HTTP requests -- but they go to the sandbox instead of the live API.

test/setup.ts (Vitest)typescript
import { beforeAll } from 'vitest';

beforeAll(() => {
  // Point all API clients at Ghostbox
  process.env.STRIPE_API_KEY = process.env.GHOSTBOX_API_KEY;
  process.env.STRIPE_BASE_URL = 'https://ghostbox.dev/sandbox/stripe';
  process.env.RESEND_API_KEY = process.env.GHOSTBOX_API_KEY;
  process.env.RESEND_BASE_URL = 'https://ghostbox.dev/sandbox/resend';
});
test/stripe.test.tstypescript
import { describe, it, expect } from 'vitest';
import { createCustomer, getCustomer } from '../src/stripe-client';

describe('Stripe integration', () => {
  it('creates and retrieves a customer', async () => {
    const customer = await createCustomer({
      email: 'test@example.com',
      name: 'Test User',
    });

    expect(customer.id).toMatch(/^cus_/);
    expect(customer.email).toBe('test@example.com');

    const retrieved = await getCustomer(customer.id);
    expect(retrieved.id).toBe(customer.id);
  });
});

Integration test patterns

Ghostbox sandboxes are stateful, so you can test full CRUD lifecycles and cross-resource relationships.

Full lifecycle testtypescript
import { describe, it, expect } from 'vitest';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.GHOSTBOX_API_KEY!, {
  host: 'ghostbox.dev/sandbox/stripe',
  protocol: 'https',
});

describe('Payment flow', () => {
  it('completes a full payment lifecycle', async () => {
    // 1. Create a customer
    const customer = await stripe.customers.create({
      email: 'buyer@example.com',
    });

    // 2. Create a product and price
    const product = await stripe.products.create({
      name: 'Widget',
    });
    const price = await stripe.prices.create({
      currency: 'usd',
      product: product.id,
      unit_amount: 1500,
    });

    // 3. Create a payment intent
    const pi = await stripe.paymentIntents.create({
      amount: 1500,
      currency: 'usd',
      customer: customer.id,
    });
    expect(pi.status).toBe('requires_payment_method');

    // 4. Confirm the payment
    const confirmed = await stripe.paymentIntents.confirm(pi.id, {
      payment_method: '4242424242424242',
    });
    expect(confirmed.status).toBe('succeeded');
    expect(confirmed.amount_received).toBe(1500);
  });
});

Error path testing

Use error injection headers and magic values to test how your code handles failures. See the Error Simulation page for the full list of options.

Testing error handlingtypescript
import { describe, it, expect } from 'vitest';

describe('Error handling', () => {
  it('handles rate limit errors gracefully', async () => {
    const response = await fetch(
      'https://ghostbox.dev/sandbox/stripe/v1/customers',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
          'X-Sandbox-Error': 'rate_limit',
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: 'email=test@example.com',
      },
    );

    expect(response.status).toBe(429);
    const body = await response.json();
    expect(body.error.code).toBe('rate_limit');
  });

  it('handles declined cards', async () => {
    const response = await fetch(
      'https://ghostbox.dev/sandbox/stripe/v1/charges',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: 'amount=2000&currency=usd&source[number]=4000000000000002',
      },
    );

    expect(response.status).toBe(402);
    const body = await response.json();
    expect(body.error.code).toBe('card_declined');
  });

  it('handles bounced emails', async () => {
    const response = await fetch(
      'https://ghostbox.dev/sandbox/resend/emails',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          from: 'hello@example.com',
          to: 'bounce@resend.dev',
          subject: 'Test bounce',
          html: '<p>test</p>',
        }),
      },
    );

    const { id } = await response.json();

    // Retrieve the email to check the bounce status
    const emailResponse = await fetch(
      `https://ghostbox.dev/sandbox/resend/emails/${id}`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.GHOSTBOX_API_KEY}`,
        },
      },
    );

    const email = await emailResponse.json();
    expect(email.last_event).toBe('bounced');
  });
});

CI/CD configuration

Add your Ghostbox API key as a secret in your CI environment and set the base URL environment variables.

GitHub Actionsyaml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      GHOSTBOX_API_KEY: ${{ secrets.GHOSTBOX_API_KEY }}
      STRIPE_BASE_URL: https://ghostbox.dev/sandbox/stripe
      RESEND_BASE_URL: https://ghostbox.dev/sandbox/resend

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm test
GitLab CIyaml
test:
  image: node:22
  variables:
    GHOSTBOX_API_KEY: $GHOSTBOX_API_KEY
    STRIPE_BASE_URL: https://ghostbox.dev/sandbox/stripe
    RESEND_BASE_URL: https://ghostbox.dev/sandbox/resend
  script:
    - npm ci
    - npm test

State isolation guarantees

Every API key gets its own isolated state per service. This is critical for testing:

  • Parallel test suites -- multiple CI jobs can run simultaneously with different keys without data conflicts.
  • Clean state per run -- create a new key per CI run to guarantee a fresh starting state.
  • No cross-service leakage -- creating a Stripe customer does not affect the Resend sandbox.
  • Deterministic responses -- same input always produces the same output (except for generated IDs and timestamps).