fix: make stripe webhooks explicitly toggleable with mandatory secret validation
This commit is contained in:
@@ -28,5 +28,6 @@ LIVEKIT_API_SECRET=
|
||||
|
||||
# Billing
|
||||
NWC_URL= # Nostr Wallet Connect URL for generating Lightning invoices
|
||||
STRIPE_SECRET_KEY= # Stripe API secret key (sk_...)
|
||||
STRIPE_WEBHOOK_SECRET= # Secret for verifying Stripe webhook signatures (whsec_...)
|
||||
STRIPE_SECRET_KEY= # Required Stripe API secret key (sk_...)
|
||||
STRIPE_WEBHOOKS_ENABLED=false # Local-friendly default; set true to enable Stripe webhooks and require STRIPE_WEBHOOK_SECRET
|
||||
STRIPE_WEBHOOK_SECRET= # Required webhook signing secret when STRIPE_WEBHOOKS_ENABLED=true (whsec_...)
|
||||
|
||||
+25
-22
@@ -30,27 +30,30 @@ backend/
|
||||
|
||||
Environment variables:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite://<backend>/data/caravel.db` |
|
||||
| `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` |
|
||||
| `PORT` | API bind port | `3000` |
|
||||
| `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ |
|
||||
| `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ |
|
||||
| `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ |
|
||||
| `ZOOID_API_SECRET` | Nostr secret key used for authentication of requests to the zooid API | _required_ |
|
||||
| `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty |
|
||||
| `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ |
|
||||
| `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ |
|
||||
| `LIVEKIT_API_SECRET` | LiveKit API secret sent to zooid | _optional_ |
|
||||
| `NWC_URL` | Platform NWC URL used to generate BOLT11 invoices | _required for invoice generation_ |
|
||||
| `ROBOT_SECRET` | Robot Nostr secret key | _required_ |
|
||||
| `ROBOT_NAME` | Robot display name (kind `0`) | _optional_ |
|
||||
| `ROBOT_DESCRIPTION` | Robot description (kind `0`) | _optional_ |
|
||||
| `ROBOT_PICTURE` | Robot picture URL (kind `0`) | _optional_ |
|
||||
| `ROBOT_OUTBOX_RELAYS` | Comma-separated relays published as kind `10002` | _required_ |
|
||||
| `ROBOT_INDEXER_RELAYS` | Comma-separated relays used for recipient relay discovery | _required_ |
|
||||
| `ROBOT_MESSAGING_RELAYS` | Comma-separated relays published as kind `10050` | _required_ |
|
||||
| Variable | Description | Default |
|
||||
| ------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| `DATABASE_URL` | SQLite URL. Relative paths are resolved under `backend/`. | `sqlite://<backend>/data/caravel.db` |
|
||||
| `HOST` | API bind host (also used for NIP-98 `u` host check) | `127.0.0.1` |
|
||||
| `PORT` | API bind port | `3000` |
|
||||
| `ADMINS` | Comma-separated admin pubkeys (hex) | _optional_ |
|
||||
| `ALLOW_ORIGINS` | Comma-separated CORS origins. If empty, CORS is permissive. | _optional_ |
|
||||
| `ZOOID_API_URL` | Zooid API base URL used by infra worker | _required for infra sync_ |
|
||||
| `ZOOID_API_SECRET` | Nostr secret key used for authentication of requests to the zooid API | _required_ |
|
||||
| `RELAY_DOMAIN` | Base domain appended to relay subdomains | empty |
|
||||
| `LIVEKIT_URL` | LiveKit URL sent to zooid when relay livekit is enabled | _optional_ |
|
||||
| `LIVEKIT_API_KEY` | LiveKit API key sent to zooid | _optional_ |
|
||||
| `LIVEKIT_API_SECRET` | LiveKit API secret sent to zooid | _optional_ |
|
||||
| `NWC_URL` | Platform NWC URL used to generate BOLT11 invoices | _required for invoice generation_ |
|
||||
| `STRIPE_SECRET_KEY` | Stripe API secret key used for billing API operations | _required_ |
|
||||
| `STRIPE_WEBHOOKS_ENABLED` | Enables `POST /stripe/webhook` handling (`true`/`false`) | debug builds: `false`, release builds: `true` |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret used to verify `Stripe-Signature` headers | _required when `STRIPE_WEBHOOKS_ENABLED=true`_ |
|
||||
| `ROBOT_SECRET` | Robot Nostr secret key | _required_ |
|
||||
| `ROBOT_NAME` | Robot display name (kind `0`) | _optional_ |
|
||||
| `ROBOT_DESCRIPTION` | Robot description (kind `0`) | _optional_ |
|
||||
| `ROBOT_PICTURE` | Robot picture URL (kind `0`) | _optional_ |
|
||||
| `ROBOT_OUTBOX_RELAYS` | Comma-separated relays published as kind `10002` | _required_ |
|
||||
| `ROBOT_INDEXER_RELAYS` | Comma-separated relays used for recipient relay discovery | _required_ |
|
||||
| `ROBOT_MESSAGING_RELAYS` | Comma-separated relays published as kind `10050` | _required_ |
|
||||
|
||||
Relay list env vars are comma-separated and trimmed. If a relay has no `ws://` or `wss://` scheme, `wss://` is prepended.
|
||||
|
||||
@@ -66,7 +69,7 @@ Public exceptions:
|
||||
|
||||
- `GET /plans`
|
||||
- `GET /plans/:id`
|
||||
- `POST /stripe/webhook` (validated with Stripe signatures instead)
|
||||
- `POST /stripe/webhook` (only when enabled; validated with Stripe signatures)
|
||||
|
||||
- `GET /identity` — get auth identity (`pubkey`, `is_admin`)
|
||||
- `GET /tenants` — list tenants (admin)
|
||||
|
||||
+2
-1
@@ -173,11 +173,12 @@ Notes:
|
||||
|
||||
## `async fn stripe_webhook(...) -> Response`
|
||||
|
||||
- Serves `POST /stripe/webhook`
|
||||
- Serves `POST /stripe/webhook` when `STRIPE_WEBHOOKS_ENABLED=true`
|
||||
- No NIP-98 authentication — uses Stripe signature verification instead
|
||||
- Reads raw request body and `Stripe-Signature` header
|
||||
- Calls `billing.handle_webhook(payload, signature)`
|
||||
- Returns `200` on success, `400` on signature verification failure
|
||||
- Route is not registered when Stripe webhooks are disabled
|
||||
|
||||
--- Utilities
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Billing encapsulates logic related to synchronizing state with Stripe, processin
|
||||
Members:
|
||||
|
||||
- `nwc_url: String` - a nostr wallet connect URL used to **create** bolt11 invoices (i.e. receive payments), from `NWC_URL`
|
||||
- `stripe_webhooks_enabled: bool` - enables/disables Stripe webhook handling, from `STRIPE_WEBHOOKS_ENABLED`
|
||||
- `stripe_webhook_secret: String` - secret for verifying Stripe webhook signatures, from `STRIPE_WEBHOOK_SECRET`
|
||||
- `query: Query`
|
||||
- `command: Command`
|
||||
@@ -13,6 +14,10 @@ Members:
|
||||
## `pub fn new(query: Query, command: Command, robot: Robot) -> Self`
|
||||
|
||||
- Reads environment and populates members
|
||||
- Panics if `STRIPE_SECRET_KEY` is missing/empty
|
||||
- Parses `STRIPE_WEBHOOKS_ENABLED` as a boolean (`true/false`, `1/0`, `yes/no`, `on/off`)
|
||||
- Defaults `STRIPE_WEBHOOKS_ENABLED` to `false` in debug builds and `true` in release builds
|
||||
- Panics if `STRIPE_WEBHOOK_SECRET` is missing/empty while `STRIPE_WEBHOOKS_ENABLED=true`
|
||||
|
||||
## `pub fn start(&self)`
|
||||
|
||||
@@ -33,6 +38,7 @@ Manages the Stripe subscription and subscription items for a relay's tenant. Onl
|
||||
|
||||
## `pub fn handle_webhook(&self, payload: &str, signature: &str) -> Result<()>`
|
||||
|
||||
- Returns an error when Stripe webhooks are disabled
|
||||
- Verify the webhook signature using `self.stripe_webhook_secret`
|
||||
- Parse the event and dispatch by type:
|
||||
- `invoice.created` -> `self.handle_invoice_created`
|
||||
@@ -109,4 +115,3 @@ Skip invoices with `amount_due` of 0.
|
||||
|
||||
- Look up tenant by `stripe_customer_id`
|
||||
- Clear `stripe_subscription_id` via `command.clear_tenant_subscription`
|
||||
|
||||
|
||||
+11
-4
@@ -135,11 +135,12 @@ impl Api {
|
||||
}
|
||||
|
||||
pub fn router(self) -> Router {
|
||||
let stripe_webhooks_enabled = self.billing.webhooks_enabled();
|
||||
let state = AppState {
|
||||
api: Arc::new(self),
|
||||
};
|
||||
|
||||
Router::new()
|
||||
let router = Router::new()
|
||||
.route("/identity", get(get_identity))
|
||||
.route("/plans", get(list_plans))
|
||||
.route("/plans/:id", get(get_plan))
|
||||
@@ -157,9 +158,15 @@ impl Api {
|
||||
.route(
|
||||
"/tenants/:pubkey/stripe/session",
|
||||
get(create_stripe_session),
|
||||
)
|
||||
.route("/stripe/webhook", post(stripe_webhook))
|
||||
.with_state(state)
|
||||
);
|
||||
|
||||
let router = if stripe_webhooks_enabled {
|
||||
router.route("/stripe/webhook", post(stripe_webhook))
|
||||
} else {
|
||||
router
|
||||
};
|
||||
|
||||
router.with_state(state)
|
||||
}
|
||||
|
||||
fn extract_auth_pubkey(&self, headers: &HeaderMap) -> std::result::Result<String, ApiError> {
|
||||
|
||||
+185
-5
@@ -18,6 +18,23 @@ const STRIPE_API: &str = "https://api.stripe.com/v1";
|
||||
const COINBASE_SPOT_API: &str = "https://api.coinbase.com/v2/prices";
|
||||
const WEBHOOK_TOLERANCE_SECS: i64 = 300;
|
||||
|
||||
fn parse_env_bool(name: &str, default: bool) -> bool {
|
||||
match std::env::var(name) {
|
||||
Ok(raw) => {
|
||||
let normalized = raw.trim().to_ascii_lowercase();
|
||||
match normalized.as_str() {
|
||||
"" => default,
|
||||
"1" | "true" | "yes" | "on" => true,
|
||||
"0" | "false" | "no" | "off" => false,
|
||||
_ => panic!(
|
||||
"invalid {name} environment variable value: {raw} (expected one of true,false,1,0,yes,no,on,off)"
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(_) => default,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InvoiceLookupError {
|
||||
StripeClient { status: reqwest::StatusCode },
|
||||
@@ -79,6 +96,7 @@ struct CoinbaseSpotPriceData {
|
||||
pub struct Billing {
|
||||
nwc_url: String,
|
||||
stripe_secret_key: String,
|
||||
stripe_webhooks_enabled: bool,
|
||||
stripe_webhook_secret: String,
|
||||
btc_quote_api_base: String,
|
||||
http: reqwest::Client,
|
||||
@@ -94,12 +112,22 @@ impl Billing {
|
||||
if stripe_secret_key.trim().is_empty() {
|
||||
panic!("missing STRIPE_SECRET_KEY environment variable");
|
||||
}
|
||||
let default_webhooks_enabled = !cfg!(debug_assertions);
|
||||
let stripe_webhooks_enabled =
|
||||
parse_env_bool("STRIPE_WEBHOOKS_ENABLED", default_webhooks_enabled);
|
||||
let stripe_webhook_secret = std::env::var("STRIPE_WEBHOOK_SECRET").unwrap_or_default();
|
||||
if stripe_webhooks_enabled && stripe_webhook_secret.trim().is_empty() {
|
||||
panic!("missing STRIPE_WEBHOOK_SECRET environment variable");
|
||||
}
|
||||
if !stripe_webhooks_enabled {
|
||||
tracing::warn!("stripe webhooks disabled; /stripe/webhook route is inactive");
|
||||
}
|
||||
let btc_quote_api_base =
|
||||
std::env::var("BTC_PRICE_API_BASE").unwrap_or_else(|_| COINBASE_SPOT_API.to_string());
|
||||
Self {
|
||||
nwc_url,
|
||||
stripe_secret_key,
|
||||
stripe_webhooks_enabled,
|
||||
stripe_webhook_secret,
|
||||
btc_quote_api_base,
|
||||
http: reqwest::Client::new(),
|
||||
@@ -109,6 +137,10 @@ impl Billing {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn webhooks_enabled(&self) -> bool {
|
||||
self.stripe_webhooks_enabled
|
||||
}
|
||||
|
||||
pub async fn start(self) {
|
||||
let mut rx = self.command.notify.subscribe();
|
||||
|
||||
@@ -237,6 +269,10 @@ impl Billing {
|
||||
}
|
||||
|
||||
pub async fn handle_webhook(&self, payload: &str, signature: &str) -> Result<()> {
|
||||
if !self.stripe_webhooks_enabled {
|
||||
return Err(anyhow!("stripe webhooks are disabled"));
|
||||
}
|
||||
|
||||
self.verify_webhook_signature(payload, signature)?;
|
||||
|
||||
let event: StripeEvent = serde_json::from_str(payload)?;
|
||||
@@ -949,7 +985,8 @@ mod tests {
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::sync::OnceLock;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
@@ -964,6 +1001,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
fn set_stripe_webhook_secret(value: Option<&str>) {
|
||||
match value {
|
||||
Some(v) => unsafe { std::env::set_var("STRIPE_WEBHOOK_SECRET", v) },
|
||||
None => unsafe { std::env::remove_var("STRIPE_WEBHOOK_SECRET") },
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
fn set_stripe_webhooks_enabled(value: Option<&str>) {
|
||||
match value {
|
||||
Some(v) => unsafe { std::env::set_var("STRIPE_WEBHOOKS_ENABLED", v) },
|
||||
None => unsafe { std::env::remove_var("STRIPE_WEBHOOKS_ENABLED") },
|
||||
}
|
||||
}
|
||||
|
||||
struct StripeSecretKeyGuard {
|
||||
previous: Option<String>,
|
||||
}
|
||||
@@ -982,6 +1035,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct StripeWebhookSecretGuard {
|
||||
previous: Option<String>,
|
||||
}
|
||||
|
||||
impl StripeWebhookSecretGuard {
|
||||
fn set(value: Option<&str>) -> Self {
|
||||
let previous = std::env::var("STRIPE_WEBHOOK_SECRET").ok();
|
||||
set_stripe_webhook_secret(value);
|
||||
Self { previous }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StripeWebhookSecretGuard {
|
||||
fn drop(&mut self) {
|
||||
set_stripe_webhook_secret(self.previous.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
struct StripeWebhooksEnabledGuard {
|
||||
previous: Option<String>,
|
||||
}
|
||||
|
||||
impl StripeWebhooksEnabledGuard {
|
||||
fn set(value: Option<&str>) -> Self {
|
||||
let previous = std::env::var("STRIPE_WEBHOOKS_ENABLED").ok();
|
||||
set_stripe_webhooks_enabled(value);
|
||||
Self { previous }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StripeWebhooksEnabledGuard {
|
||||
fn drop(&mut self) {
|
||||
set_stripe_webhooks_enabled(self.previous.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_pool() -> SqlitePool {
|
||||
let connect_options = SqliteConnectOptions::from_str("sqlite::memory:")
|
||||
.expect("valid sqlite memory url")
|
||||
@@ -1003,8 +1092,10 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn billing_new_panics_without_stripe_secret_key() {
|
||||
let _lock = env_lock().lock().expect("acquire env lock");
|
||||
let _env = StripeSecretKeyGuard::set(None);
|
||||
let _lock = env_lock().lock().await;
|
||||
let _secret_env = StripeSecretKeyGuard::set(None);
|
||||
let _webhook_env = StripeWebhookSecretGuard::set(Some("whsec_test_dummy"));
|
||||
let _webhooks_enabled_env = StripeWebhooksEnabledGuard::set(Some("true"));
|
||||
|
||||
let pool = test_pool().await;
|
||||
let query = Query::new(pool.clone());
|
||||
@@ -1033,10 +1124,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn billing_new_panics_without_stripe_webhook_secret() {
|
||||
let _lock = env_lock().lock().await;
|
||||
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
|
||||
let _webhook_env = StripeWebhookSecretGuard::set(None);
|
||||
let _webhooks_enabled_env = StripeWebhooksEnabledGuard::set(Some("true"));
|
||||
|
||||
let pool = test_pool().await;
|
||||
let query = Query::new(pool.clone());
|
||||
let command = Command::new(pool);
|
||||
let robot = Robot::test_stub();
|
||||
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
Billing::new(query, command, robot)
|
||||
}));
|
||||
|
||||
let panic_payload = match result {
|
||||
Ok(_) => panic!("constructor should panic when STRIPE_WEBHOOK_SECRET is missing"),
|
||||
Err(payload) => payload,
|
||||
};
|
||||
let panic_msg = if let Some(msg) = panic_payload.downcast_ref::<&str>() {
|
||||
(*msg).to_string()
|
||||
} else if let Some(msg) = panic_payload.downcast_ref::<String>() {
|
||||
msg.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
assert!(
|
||||
panic_msg.contains("missing STRIPE_WEBHOOK_SECRET environment variable"),
|
||||
"unexpected panic: {panic_msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn billing_new_panics_with_blank_stripe_webhook_secret() {
|
||||
let _lock = env_lock().lock().await;
|
||||
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
|
||||
let _webhook_env = StripeWebhookSecretGuard::set(Some(" "));
|
||||
let _webhooks_enabled_env = StripeWebhooksEnabledGuard::set(Some("true"));
|
||||
|
||||
let pool = test_pool().await;
|
||||
let query = Query::new(pool.clone());
|
||||
let command = Command::new(pool);
|
||||
let robot = Robot::test_stub();
|
||||
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
Billing::new(query, command, robot)
|
||||
}));
|
||||
|
||||
let panic_payload = match result {
|
||||
Ok(_) => panic!("constructor should panic when STRIPE_WEBHOOK_SECRET is blank"),
|
||||
Err(payload) => payload,
|
||||
};
|
||||
let panic_msg = if let Some(msg) = panic_payload.downcast_ref::<&str>() {
|
||||
(*msg).to_string()
|
||||
} else if let Some(msg) = panic_payload.downcast_ref::<String>() {
|
||||
msg.clone()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
assert!(
|
||||
panic_msg.contains("missing STRIPE_WEBHOOK_SECRET environment variable"),
|
||||
"unexpected panic: {panic_msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn billing_new_accepts_non_empty_stripe_secret_key() {
|
||||
let _lock = env_lock().lock().expect("acquire env lock");
|
||||
let _env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
|
||||
let _lock = env_lock().lock().await;
|
||||
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
|
||||
let _webhook_env = StripeWebhookSecretGuard::set(Some("whsec_test_dummy"));
|
||||
let _webhooks_enabled_env = StripeWebhooksEnabledGuard::set(Some("true"));
|
||||
|
||||
let pool = test_pool().await;
|
||||
let billing = Billing::new(
|
||||
@@ -1046,5 +1207,24 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(billing.stripe_secret_key, "sk_test_dummy");
|
||||
assert!(billing.webhooks_enabled());
|
||||
assert_eq!(billing.stripe_webhook_secret, "whsec_test_dummy");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn billing_new_allows_missing_webhook_secret_when_webhooks_disabled() {
|
||||
let _lock = env_lock().lock().await;
|
||||
let _secret_env = StripeSecretKeyGuard::set(Some("sk_test_dummy"));
|
||||
let _webhook_env = StripeWebhookSecretGuard::set(None);
|
||||
let _webhooks_enabled_env = StripeWebhooksEnabledGuard::set(Some("false"));
|
||||
|
||||
let pool = test_pool().await;
|
||||
let billing = Billing::new(
|
||||
Query::new(pool.clone()),
|
||||
Command::new(pool),
|
||||
Robot::test_stub(),
|
||||
);
|
||||
|
||||
assert!(!billing.webhooks_enabled());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ impl Command {
|
||||
.fetch_one(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
_ => anyhow::bail!("unknown resource_type: {}", resource_type),
|
||||
_ => anyhow::bail!("unknown resource_type: {resource_type}"),
|
||||
};
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Infra {
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("zooid sync returned {}: {}", status, body)
|
||||
anyhow::bail!("zooid sync returned {status}: {body}")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ mod billing;
|
||||
mod command;
|
||||
mod infra;
|
||||
mod models;
|
||||
mod query;
|
||||
mod pool;
|
||||
mod query;
|
||||
mod robot;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
+1
-2
@@ -21,8 +21,7 @@ pub async fn create_pool() -> Result<SqlitePool> {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let connect_options =
|
||||
SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
|
||||
let connect_options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(true);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
|
||||
@@ -25,7 +25,7 @@ async fn quote_endpoint_can_be_stubbed_deterministically() {
|
||||
|
||||
assert_eq!(btc_price, 50_000.0);
|
||||
|
||||
let msats = fiat_minor_to_msats_from_quote(100, "USD", btc_price)
|
||||
.expect("convert quoted fiat amount");
|
||||
let msats =
|
||||
fiat_minor_to_msats_from_quote(100, "USD", btc_price).expect("convert quoted fiat amount");
|
||||
assert_eq!(msats, 2_000_000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user