Skip to main content
VaultSaaS SDK provides comprehensive testing utilities to help you test payment integrations without hitting real payment providers.

Testing Utilities Overview

The SDK’s testing utilities are exported from @vaultsaas/core/testing and include:
  • MockAdapter: Configurable mock payment adapter with scenario support
  • Compliance Harness: Validates adapter implementations against SDK contracts
  • Webhook Helpers: Generate signed webhook payloads for testing
All testing utilities are included in the main SDK package and available at /home/daytona/workspace/source/src/testing/index.ts:1.

Mock Adapter

The MockAdapter class allows you to simulate payment provider behavior in tests without making real API calls.

Basic Usage

import { VaultClient, MockAdapter } from '@vaultsaas/core';
import type { ChargeRequest, PaymentResult } from '@vaultsaas/core';

const mockAdapter = new MockAdapter({
  handlers: {
    async charge(request: ChargeRequest): Promise<PaymentResult> {
      return {
        id: 'txn_mock_123',
        status: 'completed',
        provider: 'mock',
        providerId: 'mock_pi_123',
        amount: request.amount,
        currency: request.currency,
        paymentMethod: { type: request.paymentMethod.type },
        metadata: request.metadata ?? {},
        routing: { source: 'local', reason: 'mock adapter' },
        createdAt: new Date().toISOString(),
        providerMetadata: {},
      };
    },
  },
});

const vault = new VaultClient({
  providers: {
    mock: {
      adapter: MockAdapter,
      config: {},
    },
  },
  routing: {
    rules: [{ match: { default: true }, provider: 'mock' }],
  },
});

const result = await vault.charge({
  amount: 1000,
  currency: 'USD',
  paymentMethod: {
    type: 'card',
    number: '4242424242424242',
    expMonth: 12,
    expYear: 2030,
    cvc: '123',
  },
});

console.log(result.status); // 'completed'

Scenario-Based Testing

Use scenarios to test different payment outcomes:
import { MockAdapter } from '@vaultsaas/core/testing';

const mockAdapter = new MockAdapter({
  scenarios: {
    charge: [
      // First call succeeds
      {
        id: 'txn_1',
        status: 'completed',
        provider: 'mock',
        providerId: 'pi_1',
        amount: 1000,
        currency: 'USD',
        paymentMethod: { type: 'card' },
        metadata: {},
        routing: { source: 'local', reason: 'test' },
        createdAt: new Date().toISOString(),
        providerMetadata: {},
      },
      // Second call fails
      new Error('Insufficient funds'),
      // Third call requires action
      {
        id: 'txn_3',
        status: 'requires_action',
        provider: 'mock',
        providerId: 'pi_3',
        amount: 1000,
        currency: 'USD',
        paymentMethod: { type: 'card' },
        metadata: {},
        routing: { source: 'local', reason: 'test' },
        createdAt: new Date().toISOString(),
        providerMetadata: {},
      },
    ],
  },
});

// First charge succeeds
const result1 = await mockAdapter.charge(request);
console.log(result1.status); // 'completed'

// Second charge throws error
try {
  await mockAdapter.charge(request);
} catch (error) {
  console.log(error.message); // 'Insufficient funds'
}

// Third charge requires action
const result3 = await mockAdapter.charge(request);
console.log(result3.status); // 'requires_action'

Dynamic Scenarios

Use functions for dynamic scenario behavior:
const mockAdapter = new MockAdapter({
  scenarios: {
    charge: [
      // Dynamic response based on request
      (request) => ({
        id: `txn_${Date.now()}`,
        status: request.amount > 10000 ? 'declined' : 'completed',
        provider: 'mock',
        providerId: `pi_${Date.now()}`,
        amount: request.amount,
        currency: request.currency,
        paymentMethod: { type: request.paymentMethod.type },
        metadata: request.metadata ?? {},
        routing: { source: 'local', reason: 'test' },
        createdAt: new Date().toISOString(),
        providerMetadata: {},
      }),
    ],
  },
});

Enqueueing Scenarios

Add scenarios dynamically during tests:
const mockAdapter = new MockAdapter();

// Enqueue a successful charge
mockAdapter.enqueue('charge', {
  id: 'txn_1',
  status: 'completed',
  provider: 'mock',
  providerId: 'pi_1',
  amount: 1000,
  currency: 'USD',
  paymentMethod: { type: 'card' },
  metadata: {},
  routing: { source: 'local', reason: 'test' },
  createdAt: new Date().toISOString(),
  providerMetadata: {},
});

// Enqueue a failure
mockAdapter.enqueue('charge', new Error('Card declined'));

const result1 = await mockAdapter.charge(request); // succeeds
try {
  await mockAdapter.charge(request); // throws error
} catch (error) {
  console.log(error.message); // 'Card declined'
}

Adapter Compliance Harness

The compliance harness validates that adapter implementations conform to VaultSaaS SDK contracts by checking return value structure and types.

Using the Harness

import { createAdapterComplianceHarness } from '@vaultsaas/core/testing';
import { StripeAdapter } from '@vaultsaas/core';

const adapter = new StripeAdapter({
  apiKey: process.env.STRIPE_TEST_KEY,
});

const harness = createAdapterComplianceHarness(adapter, {
  expectedProvider: 'stripe',
});

// All methods automatically validate return values
try {
  const result = await harness.charge({
    amount: 1000,
    currency: 'USD',
    paymentMethod: {
      type: 'card',
      number: '4242424242424242',
      expMonth: 12,
      expYear: 2030,
      cvc: '123',
    },
  });
  console.log('Charge validated:', result.id);
} catch (error) {
  if (error.name === 'AdapterComplianceError') {
    console.error('Compliance error:', error.message, error.field);
  }
}

Validation Functions

You can also use individual validation functions:
import {
  validatePaymentResult,
  validateRefundResult,
  validateVoidResult,
  validateTransactionStatus,
  validatePaymentMethods,
  validateWebhookEvent,
} from '@vaultsaas/core/testing';

const result = await adapter.charge(request);
validatePaymentResult(result, 'charge', 'stripe');

const methods = await adapter.listPaymentMethods('US', 'USD');
validatePaymentMethods(methods, 'listPaymentMethods', 'stripe');

Webhook Testing

Generate properly signed webhook payloads for testing:

Stripe Webhooks

import { createStripeSignedWebhookPayload } from '@vaultsaas/core/testing';
import { StripeAdapter } from '@vaultsaas/core';

const adapter = new StripeAdapter({
  apiKey: 'sk_test_123',
  webhookSecret: 'whsec_test_secret',
});

const webhookPayload = {
  id: 'evt_test_123',
  type: 'payment_intent.succeeded',
  created: Math.floor(Date.now() / 1000),
  data: {
    object: {
      id: 'pi_123',
      status: 'succeeded',
      amount: 1000,
      currency: 'usd',
    },
  },
};

const { payload, headers } = createStripeSignedWebhookPayload(
  webhookPayload,
  'whsec_test_secret',
);

const event = await adapter.handleWebhook(payload, headers);
console.log(event.type); // 'payment.completed'

DLocal Webhooks

import { createDLocalSignedWebhookPayload } from '@vaultsaas/core/testing';
import { DLocalAdapter } from '@vaultsaas/core';

const adapter = new DLocalAdapter({
  xLogin: 'test_login',
  xTransKey: 'test_key',
  secretKey: 'test_secret',
  webhookSecret: 'webhook_secret',
});

const webhookPayload = {
  id: 'evt_123',
  type: 'payment.approved',
  payment_id: 'pay_123',
  data: {
    status: 'APPROVED',
    amount: 1000,
  },
};

const { payload, headers } = createDLocalSignedWebhookPayload(
  webhookPayload,
  'webhook_secret',
);

const event = await adapter.handleWebhook(payload, headers);
console.log(event.type); // 'payment.completed'

Generic Webhooks

import { createSignedWebhookPayload } from '@vaultsaas/core/testing';

const { payload, headers } = createSignedWebhookPayload(
  { event: 'payment.success' },
  'secret_key',
  {
    provider: 'generic',
    headerName: 'x-custom-signature',
  },
);

Testing Custom Adapters

1

Create Mock HTTP Client

Use a mock fetchFn to avoid real API calls:
const mockFetch = async (url: string, options: RequestInit): Promise<Response> => {
  return new Response(
    JSON.stringify({
      id: 'txn_123',
      status: 'succeeded',
      amount: 1000,
    }),
    {
      status: 200,
      headers: { 'content-type': 'application/json' },
    },
  );
};

const adapter = new CustomAdapter({
  apiKey: 'test_key',
  fetchFn: mockFetch,
});
2

Test with Compliance Harness

Wrap your adapter in the compliance harness:
import { createAdapterComplianceHarness } from '@vaultsaas/core/testing';

const harness = createAdapterComplianceHarness(adapter, {
  expectedProvider: 'custom',
});

// Automatically validates return values
const result = await harness.charge({
  amount: 1000,
  currency: 'USD',
  paymentMethod: { type: 'card', number: '4242424242424242', expMonth: 12, expYear: 2030, cvc: '123' },
});
3

Test All Operations

Test each adapter method:
describe('CustomAdapter', () => {
  it('should charge successfully', async () => {
    const result = await harness.charge(chargeRequest);
    expect(result.status).toBe('completed');
  });

  it('should authorize successfully', async () => {
    const result = await harness.authorize(authorizeRequest);
    expect(result.status).toBe('authorized');
  });

  it('should capture successfully', async () => {
    const result = await harness.capture({ transactionId: 'txn_123' });
    expect(result.status).toBe('completed');
  });

  it('should refund successfully', async () => {
    const result = await harness.refund({ transactionId: 'txn_123', amount: 500 });
    expect(result.status).toBe('completed');
  });

  it('should handle webhooks', async () => {
    const event = await harness.handleWebhook(payload, headers);
    expect(event.type).toBe('payment.completed');
  });
});

Testing VaultClient

Test your integration end-to-end with MockAdapter:
import { VaultClient, MockAdapter } from '@vaultsaas/core';
import type { PaymentResult } from '@vaultsaas/core';

describe('Payment Flow', () => {
  it('should process payment successfully', async () => {
    const vault = new VaultClient({
      providers: {
        mock: {
          adapter: MockAdapter,
          config: {},
        },
      },
      routing: {
        rules: [{ match: { default: true }, provider: 'mock' }],
      },
    });

    // Configure mock to return success
    const mockAdapter = vault.providers.mock.adapter as MockAdapter;
    mockAdapter.enqueue('charge', {
      id: 'txn_success',
      status: 'completed',
      provider: 'mock',
      providerId: 'pi_123',
      amount: 1000,
      currency: 'USD',
      paymentMethod: { type: 'card' },
      metadata: {},
      routing: { source: 'local', reason: 'test' },
      createdAt: new Date().toISOString(),
      providerMetadata: {},
    });

    const result = await vault.charge({
      amount: 1000,
      currency: 'USD',
      paymentMethod: {
        type: 'card',
        number: '4242424242424242',
        expMonth: 12,
        expYear: 2030,
        cvc: '123',
      },
    });

    expect(result.status).toBe('completed');
    expect(result.amount).toBe(1000);
  });
});

Best Practices

Use MockAdapter

Test payment flows without hitting real APIs. Use scenarios to test different outcomes.

Validate Responses

Use compliance harness to ensure adapters return correctly structured data.

Test Webhooks

Use webhook helpers to generate properly signed payloads for webhook handler tests.

Mock HTTP

Provide custom fetchFn to adapters during tests to avoid real network calls.

API Reference

See the testing utilities source code:
  • MockAdapter: /home/daytona/workspace/source/src/testing/mock-adapter.ts:118
  • Compliance Harness: /home/daytona/workspace/source/src/testing/adapter-compliance.ts:520
  • Webhook Helpers: /home/daytona/workspace/source/src/testing/webhook-helper.ts:32

Build docs developers (and LLMs) love