81066dfe62
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).
116 lines
3.3 KiB
TOML
116 lines
3.3 KiB
TOML
[package]
|
|
name = "keysat"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
rust-version = "1.75"
|
|
description = "Keysat — self-hosted Bitcoin-paid software licensing server for Start9"
|
|
license-file = "LICENSE"
|
|
publish = false
|
|
|
|
[[bin]]
|
|
name = "keysat"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
# HTTP server
|
|
axum = { version = "0.7", features = ["macros"] }
|
|
tower = "0.4"
|
|
tower-http = { version = "0.5", features = ["trace", "cors", "limit"] }
|
|
hyper = "1"
|
|
|
|
# Async runtime
|
|
tokio = { version = "1", features = ["full"] }
|
|
|
|
# Trait objects with async methods. The new `PaymentProvider` trait
|
|
# (src/payment/mod.rs) uses `#[async_trait]` for object-safe async fns.
|
|
# Could move to native AFIT in Rust 1.75+ but `async_trait` is one
|
|
# attribute and behaves identically.
|
|
async-trait = "0.1"
|
|
|
|
# Database (SQLite via sqlx). `macros` is required for the `sqlx::migrate!`
|
|
# macro that bakes ./migrations/*.sql into the binary at compile time. We
|
|
# only use the macros that don't need a live DB (no `query!` calls in the
|
|
# codebase), so enabling `macros` doesn't impose any compile-time database
|
|
# requirement.
|
|
sqlx = { version = "0.7", default-features = false, features = [
|
|
"runtime-tokio-rustls",
|
|
"sqlite",
|
|
"migrate",
|
|
"macros",
|
|
"chrono",
|
|
"uuid",
|
|
] }
|
|
|
|
# Cryptography. ed25519-dalek 2.x re-exports the `pkcs8` trait module from
|
|
# the lightweight `ed25519` crate, which doesn't include the `LineEnding`
|
|
# enum we need. So we depend on the underlying `pkcs8` crate directly with
|
|
# its `pem` feature for that one type.
|
|
ed25519-dalek = { version = "2", features = ["rand_core", "pkcs8", "pem"] }
|
|
pkcs8 = { version = "0.10", features = ["pem"] }
|
|
rand = "0.8"
|
|
sha2 = "0.10"
|
|
hmac = "0.12"
|
|
subtle = "2"
|
|
# Web-UI password hashing. Argon2id is the modern PHC-recommended default;
|
|
# this is the reference implementation in pure Rust (no FFI).
|
|
argon2 = "0.5"
|
|
|
|
# Encoding
|
|
data-encoding = "2" # Crockford base32 for license keys
|
|
base64 = "0.22"
|
|
hex = "0.4"
|
|
|
|
# Serialization
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
toml = "0.8"
|
|
|
|
# Time & IDs
|
|
chrono = { version = "0.4", features = ["serde"] }
|
|
uuid = { version = "1", features = ["v4", "serde"] }
|
|
|
|
# HTTP client (BTCPay)
|
|
reqwest = { version = "0.12", default-features = false, features = [
|
|
"json",
|
|
"rustls-tls",
|
|
] }
|
|
|
|
# Errors
|
|
anyhow = "1"
|
|
thiserror = "1"
|
|
|
|
# Logging
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
|
|
|
# Config
|
|
dotenvy = "0.15"
|
|
|
|
# URL/HTML helpers (used by BTCPay authorize-flow endpoints)
|
|
urlencoding = "2"
|
|
url = "2"
|
|
html-escape = "0.2"
|
|
|
|
# Embed the static admin web UI assets into the binary at compile time.
|
|
# The web/ directory is bundled directly into the runtime binary via
|
|
# rust-embed so that at runtime axum can serve /admin/* without needing
|
|
# any files copied alongside the binary.
|
|
rust-embed = { version = "8", features = ["mime-guess"] }
|
|
mime_guess = "2"
|
|
|
|
[dev-dependencies]
|
|
# Each integration test gets its own throwaway sqlite file. tempfile gives
|
|
# us NamedTempFile so the OS cleans up if a test panics mid-run.
|
|
tempfile = "3"
|
|
|
|
# tower in main deps is feature-minimal (only what axum needs). Tests use
|
|
# `ServiceExt::oneshot` to drive the router without bringing up an HTTP
|
|
# listener — that lives behind the `util` feature.
|
|
tower = { version = "0.4", features = ["util"] }
|
|
|
|
[profile.release]
|
|
opt-level = 3
|
|
lto = "thin"
|
|
codegen-units = 1
|
|
strip = true
|