Document P0 webhook fix + test seam; ship :54; track EVALUATION.md

This commit is contained in:
Keysat
2026-06-12 22:42:29 -05:00
parent be7dfa5d8c
commit ffdb59aa8f
4 changed files with 131 additions and 39 deletions
+18 -3
View File
@@ -51,9 +51,24 @@ card+lightning+onchain), NOT stored per row.
3. else earliest-`connected_at` (deterministic) + a warning.
`payment::build_provider(&row, ...)` constructs a **real** `BtcpayProvider` /
`ZapriteProvider` from the DB row — there is **no mock seam** in this path, so
integration tests can't drive it with `MockPaymentProvider` without one. The legacy
`state.payment` arc is a compat shim and is NOT consulted by the new resolver.
`ZapriteProvider` from the DB row. Tests swap in a mock via the always-compiled
`AppState::provider_override` seam (`None` in production), honored by
`AppState::provider_from_row` at every resolution site — see [testing](testing.md).
The legacy `state.payment` arc is a compat shim and is NOT consulted by the new
resolver.
## Webhook settle confirmation (anti-forgery)
Zaprite webhooks carry no signature, so the settle handler does **not** trust the
webhook body's claim. `api/webhook.rs::handle_inner` re-fetches
`provider.get_invoice_status` and requires `Settled` before persisting status or
taking ANY settle-derived action (license issuance, tier-change, subscription
renewal — the guard sits ahead of all of them). On a provider-API error it acks
`200` without issuing — the reconcile loop re-confirms and issues on its next tick
(fail-closed on issuance, and a 2xx avoids a provider retry-storm). **Not yet
done**: a literal paid-amount/currency check (the trait exposes only a status
enum); trusting the provider's own `Settled` determination is the current
boundary — see the auditor's open P1.
## Provider connect
+16 -13
View File
@@ -37,21 +37,24 @@ purchase returning 500 on `:52`) shipped this way because the new merchant-profi
resolution path had no passing test. When adding/altering a repo query, add a test
that actually executes it. See [payments](payments.md).
## Known-failing tests (3 in tests/api.rs)
## Driving the purchase / settle path — the provider seam
Test-debt from the `:52` payments transition; the backend is sound, these are a
test-strategy decision, not bugs:
Integration tests are a separate crate, so `#[cfg(test)]` doesn't reach the lib
and `payment::build_provider` only ever makes real BTCPay/Zaprite clients. To
drive the real resolver (`resolve_provider_for_profile_rail` /
`payment_provider_by_id`) with a `MockPaymentProvider`, set the always-compiled
`AppState::provider_override` field (`None` in production; honored by
`provider_from_row`). `install_mock_provider` / `make_test_state_with_mock_provider`
wire a mock into BOTH that field AND the legacy `state.payment` singleton (the
back-compat `/v1/{kind}/webhook` route reads the singleton), and seed a real
`payment_providers` row on the default profile so profile/rail/row resolution
still runs for real. `MockPaymentProvider::new_unconfirmed()` /
`new_status_unavailable()` vary the `get_invoice_status` answer to exercise the
webhook settle-confirmation guard (see [payments](payments.md)).
- `paid_purchase_creates_invoice_via_provider`, `paid_purchase_in_usd_records_listed_currency_and_rate`
— fail with a legitimate 400 ("no payment providers connected"); the fixture
seeds no provider. Reaching 200 needs the mock wired through
`resolve_provider_for_profile_rail` (`build_provider` only makes real clients;
`#[cfg(test)]` does not apply to integration tests against the lib).
- `payment_provider_preference_round_trip` — inserts into the dropped
`btcpay_config`/`zaprite_config`; superseded by
`merchant_profile_provider_resolution_queries_round_trip` (delete or rewrite).
The other 43 api tests + all other suites pass.
The 3 `:52`-era known-failing tests are **resolved**: the two `paid_purchase_*`
greened via the seam, the dead `payment_provider_preference_round_trip` deleted.
api suite is now **47 pass / 0 fail**; all other suites green.
## Cross-language wire-format tests