feat(stripe): make the Stripe API base URL injectable so the REST paths are testable #43

Merged
longjacksonle merged 1 commit from feat/DUNITE-11-injectable-api-base into main 2026-08-12 04:04:08 +02:00

DUNITE-11. Every raw-REST call in the crate hardcoded https://api.stripe.com, and the async-stripe client was built with Client::new, which pins the same host. Nothing could be pointed at a mock server, which is why DUNITE-10 shipped one acceptance criterion short: "a unit test feeds a 403 error envelope to the list path and asserts it errors rather than returning an empty list" was untestable, so the tests covered stripe_error_envelope in isolation and left the wiring that actually regressed uncovered.

Change

  • StripeService::with_api_base(config, base) is the new constructor. StripeService::new(config) delegates to it with the new STRIPE_API_BASE constant, so no consumer changes.
  • The base lives on the service, not on StripeConfig. That was the approach flagged as a blocker in DUNITE-10: StripeConfig is built by struct literal in both consumers, so a new field breaks them at compile time, and it would then have to be threaded through each app's env/DB config loader for something only tests ever set. The host a service instance talks to is not part of the app's Stripe configuration.
  • The async-stripe client is built with Client::from_url off the same base, so SDK-backed methods (products, prices, subscriptions, invoices, checkout) reach the mock too, not just the three raw webhook-endpoint calls.
  • reload() preserves the base: a credential save swaps keys, never the host.
  • A trailing slash on the injected base is trimmed, so http://host:9000/ and http://host:9000 behave identically.
  • grep https://api.stripe.com over the crate now returns the constant and nothing else.

with_api_base panics on a base that is not a parseable absolute URL, because Client::from_url does; that is documented on the method, and failing at construction beats every later call failing opaquely.

Testing

just fmt, just check, just lint, just test all clean. 22 tests in the crate now, 10 of them new:

  • DUNITE-10's outstanding test: a 403 with more_permissions_required makes list_webhook_endpoints return an error carrying the code, the status and the request id, not Ok(vec![]).
  • A 200 with a populated data array, and a 200 with an empty one, so the error test cannot pass by breaking the happy path.
  • create_webhook_endpoint returns the one-time signing secret; delete_webhook_endpoint surfaces resource_missing on a 404.
  • get_invoice served from the mock, proving the Client::from_url half of the wiring reaches async-stripe.
  • A 404-vs-500 pair on get_invoice pinning DUNITE-10's fix: only a genuine missing invoice reports NotFound, a Stripe fault keeps its status.
  • Base normalisation, the production default, and reload() leaving the base alone.

No network and no Stripe credentials: wiremock binds localhost.

DUNITE-11. Every raw-REST call in the crate hardcoded `https://api.stripe.com`, and the async-stripe client was built with `Client::new`, which pins the same host. Nothing could be pointed at a mock server, which is why DUNITE-10 shipped one acceptance criterion short: "a unit test feeds a 403 error envelope to the list path and asserts it errors rather than returning an empty list" was untestable, so the tests covered `stripe_error_envelope` in isolation and left the wiring that actually regressed uncovered. ## Change - `StripeService::with_api_base(config, base)` is the new constructor. `StripeService::new(config)` delegates to it with the new `STRIPE_API_BASE` constant, so **no consumer changes**. - The base lives on the service, not on `StripeConfig`. That was the approach flagged as a blocker in DUNITE-10: `StripeConfig` is built by struct literal in both consumers, so a new field breaks them at compile time, and it would then have to be threaded through each app's env/DB config loader for something only tests ever set. The host a service instance talks to is not part of the app's Stripe configuration. - The async-stripe client is built with `Client::from_url` off the same base, so SDK-backed methods (products, prices, subscriptions, invoices, checkout) reach the mock too, not just the three raw webhook-endpoint calls. - `reload()` preserves the base: a credential save swaps keys, never the host. - A trailing slash on the injected base is trimmed, so `http://host:9000/` and `http://host:9000` behave identically. - `grep https://api.stripe.com` over the crate now returns the constant and nothing else. `with_api_base` panics on a base that is not a parseable absolute URL, because `Client::from_url` does; that is documented on the method, and failing at construction beats every later call failing opaquely. ## Testing `just fmt`, `just check`, `just lint`, `just test` all clean. 22 tests in the crate now, 10 of them new: - **DUNITE-10's outstanding test**: a 403 with `more_permissions_required` makes `list_webhook_endpoints` return an error carrying the code, the status and the request id, not `Ok(vec![])`. - A 200 with a populated `data` array, and a 200 with an empty one, so the error test cannot pass by breaking the happy path. - `create_webhook_endpoint` returns the one-time signing secret; `delete_webhook_endpoint` surfaces `resource_missing` on a 404. - `get_invoice` served from the mock, proving the `Client::from_url` half of the wiring reaches async-stripe. - A 404-vs-500 pair on `get_invoice` pinning DUNITE-10's fix: only a genuine missing invoice reports `NotFound`, a Stripe fault keeps its status. - Base normalisation, the production default, and `reload()` leaving the base alone. No network and no Stripe credentials: wiremock binds localhost.
feat(stripe): make the Stripe API base URL injectable
All checks were successful
Check / fmt + clippy + test (pull_request) Successful in 22s
create-release / create-release (pull_request) Has been skipped
554e783bbd
DUNITE-11. Every raw-REST call in `dunite-stripe` hardcoded `https://api.stripe.com`, and the async-stripe client was built with `Client::new`, which pins the same host. Nothing in the crate could be pointed at a mock, so DUNITE-10 shipped without its outstanding acceptance criterion: the test that a 403 makes `list_webhook_endpoints` error rather than return `Ok(vec![])` covered the envelope parser in isolation, not the wiring that actually regressed.

`StripeService::with_api_base(config, base)` is the new constructor; `StripeService::new` delegates to it with the `STRIPE_API_BASE` constant. The base lives on the service, not on `StripeConfig`, so no consumer changes: `StripeConfig` is built by struct literal in both apps and would otherwise break at compile time, and the field would then have to be threaded through each app's env/DB config loader for something only tests set. `reload()` keeps the base it was constructed with, since a credential save must never move the host. A trailing slash is trimmed so `http://host:9000/` and `http://host:9000` behave the same, and the async-stripe client is built with `Client::from_url` off the same base, which makes the SDK-backed methods testable too rather than just the three raw ones.

Adds the 10 tests that unlocks, including the one DUNITE-10 could not write: a 403 with `more_permissions_required` now proves `list_webhook_endpoints` errors and carries the code, the status and the request id; a 200 with a populated `data` array and a 200 with an empty one guard that the error test fails on the status rather than on a broken happy path; create returns the one-time signing secret; delete surfaces `resource_missing`; `get_invoice` served from the mock proves the `from_url` half of the wiring; and a 404-vs-500 pair pins DUNITE-10's fix that only a genuine missing invoice reports `NotFound`.

`grep https://api.stripe.com` over the crate now returns the constant and nothing else.
longjacksonle deleted branch feat/DUNITE-11-injectable-api-base 2026-08-12 04:04:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/dunite!43
No description provided.