Docs

Testing package

@atmosphere-money/testing package for fixtures, signed events, and app integration tests.

Closed beta@atmosphere-money/app-nodeSDK beta: 0.0.0-beta.0ATM API beta: 2026-0642 lexicons

Compatible with the closed-beta ATM app APIs and versioned ATM event headers. Check atm-api-version on every webhook or XRPC receiver event.

Package goal

@atmosphere-money/testing helps app developers test receivers, idempotency, and fulfillment without creating real checkouts. It is separate from the production App Node SDK so fixture code stays out of production bundles.

Fixture families

PaymentsDedicated fixtures for payment.completed, payment.failed, payment.refunded, and payment.disputed.
Subscriptionssubscription.invoice_paid, subscription.updated, subscription.cancelled.
CatalogDedicated product.archived fixture plus generic product.updated and product.deleted fixtures.
TicketsDedicated tickets.issued and ticket.checked_in fixtures plus generic hold/refund/void fixtures.
ReceiversHTTP webhook signatures and optional XRPC receiver event envelopes.

Install

sh
npm install -D @atmosphere-money/testing@beta

Signed webhook fixture

ts
import {
  ATM_TEST_WEBHOOK_SECRET,
  createPaymentCompletedFixture
} from "@atmosphere-money/testing";
import { constructTypedAtmWebhookEvent } from "@atmosphere-money/app-node";

const fixture = createPaymentCompletedFixture({
  payment: {
    id: "pay_test_123",
    amountCents: 1200,
    currency: "usd",
    metadata: { appOrderId: "ord_123" }
  }
});

const event = constructTypedAtmWebhookEvent({
  rawBody: fixture.rawBody,
  secret: ATM_TEST_WEBHOOK_SECRET,
  expectedType: "payment.completed",
  now: fixture.event.created,
  headers: {
    signature: fixture.headers["atm-signature"],
    deliveryId: fixture.headers["atm-delivery-id"],
    event: fixture.headers["atm-event"],
    apiVersion: fixture.headers["atm-api-version"],
    environment: fixture.headers["atm-environment"]
  }
});

Request helper

Use createAtmWebhookRequest when your route tests expect a real Web Request. It preserves the signed raw body and ATM delivery headers exactly as a webhook receiver would see them.

ts
import {
  ATM_TEST_WEBHOOK_SECRET,
  createAtmWebhookRequest,
  createPaymentCompletedFixture
} from "@atmosphere-money/testing";
import { createNodeWebhookHandler } from "@atmosphere-money/app-node";

const fixture = createPaymentCompletedFixture();
const request = createAtmWebhookRequest(fixture);

const handler = createNodeWebhookHandler({
  secret: ATM_TEST_WEBHOOK_SECRET,
  expectedType: "payment.completed",
  onEvent: async (event) => {
    const metadata = event.data.payment.metadata as
      | { appOrderId?: string }
      | undefined;
    await markOrderPaid(String(metadata?.appOrderId ?? ""));
  }
});

await handler(request);

Generic event fixture

Use createAtmEventFixture for documented event types that do not need a dedicated factory. Use the dedicated factories when one exists; they make app tests clearer.

ts
import { createAtmEventFixture } from "@atmosphere-money/testing";

const archived = createAtmEventFixture({
  type: "product.archived",
  data: {
    productUri: "at://did:plc:creator/money.atmosphere.product/product_123",
    appDid: "did:plc:app"
  }
});

Common event fixtures

ts
import {
  createPaymentDisputedFixture,
  createPaymentFailedFixture,
  createPaymentRefundedFixture,
  createProductArchivedFixture,
  createTicketCheckedInFixture
} from "@atmosphere-money/testing";

const failedPayment = createPaymentFailedFixture({
  payment: { failureCode: "card_declined" }
});
const refundedPayment = createPaymentRefundedFixture({
  payment: { refundedAmountCents: 500 }
});
const disputedPayment = createPaymentDisputedFixture({
  payment: { disputeStatus: "under_review" }
});
const archivedProduct = createProductArchivedFixture();
const checkedInTicket = createTicketCheckedInFixture();

Replay and idempotency

ts
import {
  assertFreshDelivery,
  createAtmIdempotencyKey,
  createMemoryReplayStore
} from "@atmosphere-money/testing";

const replayStore = createMemoryReplayStore();
await assertFreshDelivery(replayStore, fixture.headers["atm-delivery-id"]);

const idempotencyKey = createAtmIdempotencyKey([
  "ticket",
  (event.data.payment.metadata as { appOrderId?: string } | undefined)
    ?.appOrderId,
  "buyer",
  "did:plc:buyer"
]);

Assertions

  • Invalid signatures are rejected.
  • Duplicate delivery ids do not double-fulfill.
  • Unknown future fields are ignored safely.
  • Missing app order ids do not fulfill.
  • Ticket issuance and check-in stay idempotent.
  • Test and live webhook secrets are never mixed.

Package boundary

The testing package generates fake inputs only. Real app calls still use @atmosphere-money/app-node, and real sandbox E2E tests still run through ATM checkout and app event delivery.

Testing package - Atmosphere Money Docs