v0.1.0:42 — webhook idempotency test + free-purchase test

Two new API integration tests, both targeting production-correctness
invariants worth locking down:

- free_purchase_issues_license_inline: exercises the price=0 shortcut
  (price_sats_override=0 on a "free" tier policy). Verifies the daemon
  synthesizes a settled invoice locally, issues a license inline, and
  the inlined license_key validates round-trip via /v1/validate.

- webhook_settles_invoice_and_issues_license_idempotently: the most
  important new test in this set. A pending invoice + an InvoiceSettled
  webhook → license issued, status flipped. Re-delivering the SAME
  webhook (which providers DO retry, sometimes aggressively) must NOT
  duplicate the license. A duplicated license here means duplicated
  revenue and duplicated revocation surface area — both bad. This test
  pins the invariant.

MockPaymentProvider added to tests/api.rs: a test-only PaymentProvider
impl that bypasses HMAC verification and parses test-supplied JSON
bodies into ProviderWebhookEvent variants. Lets us drive deterministic
settle/expire/invalid events without a real BTCPay roundtrip. Never
compiled into the production binary.

Paid-purchase test deferred: purchase::start still uses the legacy
state.btcpay_client() compat accessor that downcasts to the concrete
BtcpayProvider, which the mock can't satisfy. Documented inline. Slots
in trivially after the trait migration on the v0.3 backlog.

Version bump to v0.1.0:42 with release notes covering everything since
:41 was published: lib.rs library refactor, the original 5 API tests
from 81066df, the 2 new ones above, KEYSAT_INTEGRATION.md restoration.
No daemon-behaviour changes for operators; straight drop-in upgrade
from :41.

Test count: 20 (9 unit + 4 migration + 7 API), up from 13 in :41.
This commit is contained in:
Grant
2026-05-08 09:24:57 -05:00
parent 81066dfe62
commit c11764898b
2 changed files with 322 additions and 2 deletions
+17 -1
View File
@@ -9,8 +9,24 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v0_1_0 = VersionInfo.of({
version: '0.1.0:41',
version: '0.1.0:42',
releaseNotes: [
`Alpha-iteration revision 42 of v0.1.0 — Test infrastructure expansion. No daemon-behaviour changes for operators; this release ships safety nets for future development. Skip if v0.1.0:41 is running cleanly — the next release with new functionality will roll forward whatever shipped here.`,
``,
`**API integration tests.** New \`tests/api.rs\` with seven integration tests that drive real HTTP requests through the daemon's \`axum::Router\` against a real SQLite tempfile. Covers: health endpoint, admin auth (401 vs 403 paths), admin product creation (full happy path: auth → handler → DB → audit log → response), license validation (parse-fail surface plus full crypto round-trip), free-tier purchase with inline license issuance, and BTCPay webhook idempotency (re-delivered settle webhooks MUST NOT duplicate the license — the production-correctness invariant we most needed to lock down).`,
``,
`**Library refactor.** Adds \`src/lib.rs\` that re-exports the daemon's modules so integration tests under \`tests/\` can drive \`AppState\` + \`Router\` directly. Production binary at \`src/main.rs\` is now a thin wrapper that imports from the library. No runtime behaviour change — same module sources, same compiled binary modulo build metadata.`,
``,
`**MockPaymentProvider.** A test-only \`PaymentProvider\` impl that bypasses HMAC signature verification and parses test-supplied JSON bodies into \`ProviderWebhookEvent\` variants. Used by the webhook test to drive deterministic settle / expire / invalid events without a real BTCPay round-trip. Lives entirely in \`tests/api.rs\` — never compiled into the production binary.`,
``,
`**Test count.** 9 unit + 4 migration + 7 API = 20 tests, up from 13 in v0.1.0:41. \`cargo test\` exercises the full set in under a second.`,
``,
`**KEYSAT_INTEGRATION.md restored.** v0.1.0:41's release notes erroneously claimed the file was "removed from this repo" — that was a misread of intent. The 1378-line integration guide is back at the daemon repo's root where integrators (and LLMs running fresh against a target codebase) expect to find it via \`https://raw.githubusercontent.com/keysat-xyz/keysat/main/KEYSAT_INTEGRATION.md\`. The 12-line stub at the parent licensing/ folder was removed since it referred to a "moved" arrangement that no longer applies.`,
``,
`**Known limitation: paid-purchase test deferred.** The HTTP \`/v1/purchase\` endpoint's paid path still uses the legacy \`state.btcpay_client()\` compat accessor (which downcasts the active provider specifically to the concrete \`BtcpayProvider\` type) rather than the abstract \`PaymentProvider\` trait. A \`MockPaymentProvider\` can't satisfy that downcast. Migrating \`purchase::start\` to \`state.payment_provider().await?.create_invoice(...)\` is a small refactor on the v0.3 backlog; once it lands, a paid-purchase integration test slots right in. The webhook handler IS already on the trait surface, which is why the webhook idempotency test works.`,
``,
`**Upgrade path.** v0.1.0:41 → v0.1.0:42 is a straight drop-in. No new migrations (the migration set still ends at 0009). No schema changes. No on-disk format changes. If you hit a checksum-mismatch error on boot, you're upgrading from a version older than :41 — see :41's release notes for the recovery command.`,
``,
`Alpha-iteration revision 41 of v0.1.0 — Second hotfix to migration 0009. v40 passed on a clean install but crashed on any database with existing rows in discount_redemptions. Also lands the migration test infrastructure that would have caught this before it shipped.`,
``,
`**The bug.** v40 rebuilt only the discount_codes table (using \`PRAGMA defer_foreign_keys = 1\` to avoid intra-transaction FK errors). On a clean install that works. On any install with even one row in discount_redemptions — the child table whose foreign key points back into discount_codes — it doesn't. SQLite's deferred FK check fires at COMMIT, sees the dropped parent's row-deletion bookkeeping as unsatisfied (regardless of whether the rebuilt table contains the same IDs), and rolls the whole transaction back with "FOREIGN KEY constraint failed (787)". Daemon kept restart-looping; API never came up.`,