Add API endpoint integration tests + library scaffolding

Closes the next-biggest test gap after migration tests. The daemon has
54+ HTTP endpoints, all previously untested at the request/response
level — same shape of blind spot that allowed the v0.1.0:39 migration
bug to ship.

What's new:

- src/lib.rs — exposes the daemon's modules as a library so integration
  tests can import them (`pub mod api;`, etc.). Module source files are
  unchanged; main.rs now imports via `use keysat::...` instead of
  declaring `mod api;` directly. No runtime behaviour change in the
  binary.

- tests/api.rs — 5 integration tests that drive real HTTP requests
  through axum::Router::oneshot against a real SQLite tempfile pool
  (same options as src/db/mod.rs::init):
    1. health_endpoint_returns_200 — framework smoke test
    2. admin_endpoint_rejects_missing_or_wrong_auth — 401 vs 403 paths
    3. admin_creates_product_with_correct_token — full happy path
       (auth → handler → DB insert → audit log → response)
    4. validate_rejects_unsigned_garbage — early parse-fail surfaces
       as `ok: false, reason: "bad_format"` (HTTP still 200)
    5. validate_accepts_well_formed_license — issues a license via
       repo, signs a matching LicensePayload with the daemon's
       actual key, encodes to wire format, validates via the
       endpoint, asserts ok=true plus populated metadata fields

Test count: 9 unit + 4 migrations + 5 API = 18 (was 13).

Cargo.toml dev-deps gain `tower = { version = "0.4", features = ["util"] }`
for ServiceExt::oneshot. The main `tower` dep is feature-minimal because
axum only needs a subset.

Out of scope (explicit follow-ups):

- Purchase happy path (needs a MockPaymentProvider implementing the
  trait; ~250 LOC of mock + ~200 LOC of test).
- Webhook handler with idempotency assertions (same MockPaymentProvider
  dependency).
- Tier-cap enforcement (mechanically simple; small follow-up PR).
- Discount-code atomic reserve race (better as a SQL-layer unit test
  than an HTTP integration test).
- Rate-limiting (interacts with shared state; needs careful isolation).
- Cookie/session auth (already covered in session_layer.rs).
This commit is contained in:
Grant
2026-05-08 09:14:27 -05:00
parent 4ac856bb10
commit 81066dfe62
5 changed files with 430 additions and 23 deletions
+31
View File
@@ -0,0 +1,31 @@
//! Keysat library — the daemon's internal modules, exposed as a library
//! so integration tests under `tests/` can drive the API directly without
//! re-implementing the bootstrap.
//!
//! The binary at `src/main.rs` is a thin wrapper that loads runtime config
//! from environment variables, starts the HTTP server, and spawns
//! background tasks. Tests bypass that wrapper and construct `AppState`
//! programmatically.
pub mod api;
pub mod btcpay;
pub mod config;
pub mod crypto;
pub mod db;
pub mod error;
pub mod license_self;
pub mod models;
pub mod payment;
pub mod rate_limit;
pub mod reconcile;
pub mod tipping;
pub mod webhooks;
/// Hex-encoded SHA-256 of a string — used everywhere we need a deterministic
/// id from a raw value (machine fingerprints, admin key hashes).
pub fn hex_sha256(s: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
hex::encode(hasher.finalize())
}
+6 -23
View File
@@ -1,29 +1,12 @@
//! Entry point. Wires config → logging → DB → keypair → HTTP server.
mod api;
mod btcpay;
mod config;
mod crypto;
mod db;
mod error;
mod license_self;
mod models;
mod payment;
mod rate_limit;
mod reconcile;
mod tipping;
mod webhooks;
/// Hex-encoded SHA-256 of a string — used everywhere we need a deterministic
/// id from a raw value (machine fingerprints, admin key hashes).
pub fn hex_sha256(s: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
hex::encode(hasher.finalize())
}
//!
//! The actual modules (api, btcpay, db, etc.) live in `src/lib.rs` so that
//! integration tests under `tests/` can also reach them. Both the binary
//! and the library compile from the same source files; nothing here
//! changes between targets.
use anyhow::Context;
use keysat::{api, btcpay, config, crypto, db, license_self, payment, reconcile, webhooks};
use std::sync::Arc;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};