punchout-simulator — Test Strategy

What is tested, at which layer, with which techniques — and what is deliberately left out · 2026-06-11

v1.0 — June 2026

Goals and philosophy

The simulator exists to make other systems' integration behaviour observable and reproducible — so its own correctness has to be cheap to verify on every change. The test suite is built around three principles:

There are no mocking frameworks and no test doubles beyond these seams: tests call the real implementations on temporary data directories.

The test pyramid

All tests live in test/**/*.test.ts and run under Vitest (environment: node, 20-second timeout, one process-isolated environment per file). The suite is currently 19 files across four layers:

Test layers (bottom = most tests, fastest)Integration over real HTTPintegration.test.ts — full Mode-A roundtrip,ephemeral port, seeded demo dataHTTP app-level (no sockets)security / rate-limit / static —Hono app.request() against the real appStore & persistencemigrations / products / profiles /log-dedup / log-summary / cart-storePure unitscXML core: build / parse / validate / multipart / xml-formathelpers: csv / geo · proxy: proxy / proxy-detect (fixtures)

Fig. 1: Four layers — pure protocol units at the base, one real-HTTP integration proof at the top.

Layer 1 — pure units (cXML core and helpers)

Suite What it pins down
build.test.ts Document emission: SetupRequest/OrderRequest shape, address and classification blocks, line-item totals (including the mixed-currency 0 total).
parse.test.ts Inbound parsing with fast-xml-parser, including XXE rejection.
validate.test.ts Field-level validation: credential checks only on header-bearing documents, single-currency rule and its allowMixedCurrency downgrade, address completeness per profile address mode, operation/items coherence.
multipart.test.ts Hand-assembled multipart/related for attachments, both binary and base64.
xml-format.test.ts The local prettifier used by the Copy/Prettify toolbar and the PDF export.
csv.test.ts / geo.test.ts Product-list CSV import (header aliases, decimal-comma) and country/postal-code data.

Layer 2 — store and persistence

migrations.test.ts covers every boot migration (legacy flat connections → normalized entities, inline catalog → product lists, unspsc → classifications, address-mode backfill) against real config.json files in temp directories. products.test.ts and profiles.test.ts cover entity CRUD semantics (e.g. the 409 on deleting a referenced product list); log-dedup.test.ts, log-summary.test.ts and cart-store.test.ts cover the session-log and in-memory-cart behaviour.

Layer 3 — HTTP app-level, no sockets

These suites call app.request() on the real Hono app — full middleware and routing, no network:

Layer 4 — integration over real HTTP

integration.test.ts boots the real server on an ephemeral port (port: 0) with a fresh temp data directory, seeds the demo entities, and drives a full Mode-A roundtrip against the built-in mock supplier: setup preview → send → StartPage → catalog → checkout → cart return through /punchout → order. The flow's outbound calls use real fetch, so this is the same path production traffic takes — including the loopback proxy bypass.

Techniques worth copying

Gates

The same three commands gate every path to a release:

Gate What runs When
Local npm run typecheck · npm test Before committing; also part of the release verify gate.
CI (ci.yml) typecheckbuildtest on a Node 20 + 24 matrix Every push and pull request on main. Node 20 is the declared minimum (engines), Node 24 the current runtime.
Publish (publish.yml) typecheckbuildtest, then npm publish --provenance On GitHub Release — the suite re-runs even though CI is green, so nothing unpublishable can ship.
Note

The Node 20 leg of the matrix is what caught the undici 8 incompatibility (it requires Node ≥ 22.19) that led to pinning undici@^6 — the matrix exists precisely to keep the declared minimum honest.

What is deliberately not covered

Tip

When adding a feature, put the logic at the lowest layer that can express it: protocol behaviour in the cXML core with unit tests, storage behaviour behind the store suites, and extend integration.test.ts only when the wiring between layers is the thing that changed.