Mike Bennett

Deliverable

API test collection

A Postman collection against the billing API in the demo repo: assertions that check contracts rather than status codes, a database check after the charge, and a Newman run that fails where it should.

Sample deliverable. Written for this site against a fictional product with no client data and no confidential material.

Structure

Billing API
├── 00 Setup
│   ├── POST  /sandbox/reset
│   ├── POST  /sandbox/clock                 → pins the clock to {{clockStart}}
│   ├── POST  /v1/accounts                   → sets {{customerId}}
│   └── POST  /v1/auth/token                 → sets {{accessToken}}
├── 01 Subscriptions
│   ├── POST  /v1/subscriptions
│   ├── POST  /v1/subscriptions              (idempotency replay)
│   └── GET   /sandbox/processor/charges
├── 02 Database
│   ├── GET   /sandbox/db/subscriptions
│   ├── GET   /sandbox/db/invoices
│   └── GET   /sandbox/ledger
├── 03 Plan change
│   ├── POST  /sandbox/clock                 → advances 12 days
│   └── PATCH /v1/subscriptions/:id/plan
├── 04 Invoices
│   └── GET   /v1/invoices?status=open
├── 05 Webhooks
│   ├── GET   /sandbox/events
│   ├── POST  /v1/webhooks/events            (replay)
│   └── GET   /sandbox/ledger
└── 06 Negative
    ├── GET   /v1/subscriptions/:id          (no token)
    ├── POST  /v1/subscriptions              (malformed body)
    └── POST  /v1/webhooks/events            (missing field)

Ordered folders, not a flat list. The run has to be reproducible from a cold environment, which means setup comes first and nothing depends on state left by a previous run.

Assertions

RequestAssertsWhy
POST /v1/subscriptionsStatus 201 · exact key set · state is active · Location header present · responds inside 800 msThe key-set assertion catches field renames that a status-code check sails past.
POST /v1/subscriptions (replay, same Idempotency-Key)Status 200 · returns the original subscription IDPasses on both builds. The defect is invisible in the response, which is why the next check exists.
GET /sandbox/processor/chargesExactly one charge captured for the subscription, for the plan priceBUG-1142 lives here, not in the UI or the API response: the processor was charged twice.
GET /sandbox/db/subscriptions · /db/invoices · /ledgerOne subscription row · one paid invoice equal to the plan price · one ledger entry for the paymentThe database check after a charge. On rc3 it passes, so the app has no record of the second charge: the processor and the database disagree.
PATCH /v1/subscriptions/:id/planStatus 200 · tier updated · effective date is the day of the change · proration equals the finance-supplied valueBUG-1156 lives here. Expected values come from the environment, not from the code under test.
GET /v1/invoices?status=openStatus 200 · array · every item belongs to the authenticated accountThe ownership assertion is an authorization test disguised as a list endpoint test.
POST /v1/webhooks/events (replay of a delivered payment)Duplicate event acknowledged · ledger entry count unchanged on the next requestIdempotency at the consumer, verified independently of delivery.
GET /v1/subscriptions/:id (no token)Status 401 · no subscription data in the bodyEvery collection gets at least one negative auth case.
POST /v1/subscriptions (malformed body)Status 422 · error names the missing cardA bad request must say what is wrong, not fail with a 500.
POST /v1/webhooks/events (missing field)Status 400 · event rejectedA 4xx tells the sender not to retry, so a bad event cannot cause a retry storm.

Test script

pm.test("201 with a usable subscription", () => {
  pm.response.to.have.status(201);
  const body = pm.response.json();
  pm.expect(body).to.have.all.keys(
    "id", "status", "plan", "current_period_end", "customer_id"
  );
  pm.expect(body.status).to.eql("active");
  pm.collectionVariables.set("subscriptionId", body.id);
});

pm.test("no second charge on the processor", () => {
  const body = pm.response.json();
  pm.expect(body.count, "charges captured for this subscription").to.eql(1);
  pm.expect(body.total, "total captured, in cents").to.eql(9900);
});

pm.test("proration matches the finance-supplied value", () => {
  const expected = Number(pm.environment.get("expectedProration"));
  const actual = pm.response.json().amount_due / 100;
  pm.expect(actual).to.eql(expected);
});

Asserting the full key set rather than a single field is deliberate. It fails when a field is dropped or renamed, which is the change most likely to break a downstream consumer silently. The other two tests come from different requests in the collection.

Environment

VariableValueNotes
baseUrlhttp://127.0.0.1:5055The local demo app. On an engagement, staging and the time-shifted environment each get their own file. No production environment ships with the collection.
accessTokenset at runtimeNever committed. Populated by the Setup folder.
clockStart · clockUpgrade2026-01-01 · 2026-01-13Pins the sandbox clock to the first day of a 31-day period, then moves it 12 days on.
expectedProration47.42Supplied by finance for the 31-day scenario.
cardNumber · cardExpiry4242 4242 4242 4242 · 12/30The sandbox processor's test card.

CI run

scripts/newman.sh rc3      # or: fixed

newman run postman/billing-api.postman_collection.json \
  --environment postman/local.postman_environment.json \
  --reporters cli,junit \
  --reporter-junit-export results/newman-rc3.xml
BuildRequestsAssertionsFailed
4.2.1-rc319232
4.2.1 (fixed)19230

The two rc3 failures are the point of the exercise. Each one is a seeded defect, and neither is visible from the response of the request that causes it.

DefectRequestFailed assertionDetail
BUG-1142Processor view: charges for the subscriptionno second charge on the processorcharges captured for this subscription: expected 2 to deeply equal 1
BUG-1156Upgrade to the pro planproration matches the finance-supplied valueexpected 49 to deeply equal 47.42

JUnit XML output, the format Xray and Zephyr import, so results land in the test management tool instead of a terminal.

Source

The collection and its environment are in demo repository under postman/, next to the app they run against. Clone it, run make newman, and the two failures above are what you should see. The automation framework checks the same defects from the browser and with direct SQL.