Testing package
@atmosphere-money/testing package for fixtures, signed events, and app integration tests.
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
| Payments | Dedicated fixtures for payment.completed, payment.failed, payment.refunded, and payment.disputed. |
|---|---|
| Subscriptions | subscription.invoice_paid, subscription.updated, subscription.cancelled. |
| Catalog | Dedicated product.archived fixture plus generic product.updated and product.deleted fixtures. |
| Tickets | Dedicated tickets.issued and ticket.checked_in fixtures plus generic hold/refund/void fixtures. |
| Receivers | HTTP webhook signatures and optional XRPC receiver event envelopes. |
Install
npm install -D @atmosphere-money/testing@betaSigned webhook fixture
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.
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.
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
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
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.