Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub server: ServerConfig,
pub users: Vec<UserConfig>,
}

#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct ServerConfig {
pub domain: String,
pub listen_addr: String,
pub log_dir: String,
}

#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct UserConfig {
pub name: String,
pub nwcs: Vec<String>,
Expand All @@ -37,3 +37,58 @@ impl Config {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

fn load_config_from_str(contents: &str) -> Result<Config> {
let config: Config = toml::from_str(contents)?;
config.validate()?;
Ok(config)
}

#[test]
fn load_valid_config() -> Result<()> {
let contents = r#"
[server]
domain = "example.com"
listen_addr = "127.0.0.1:8080"
log_dir = "/tmp/thor"

[[users]]
name = "alice"
nwcs = ["nwc://example"]
"#;
let config = load_config_from_str(contents)?;
assert_eq!(config.server.domain, "example.com");
assert_eq!(config.server.listen_addr, "127.0.0.1:8080");
assert_eq!(config.server.log_dir, "/tmp/thor");
assert_eq!(config.users.len(), 1);
assert_eq!(config.users[0].name, "alice");
assert_eq!(config.users[0].nwcs, vec!["nwc://example".to_string()]);
Ok(())
}

#[test]
fn load_config_rejects_empty_nwcs() {
let contents = r#"
[server]
domain = "example.com"
listen_addr = "127.0.0.1:8080"
log_dir = "/tmp/thor"

[[users]]
name = "alice"
nwcs = []
"#;
let res = load_config_from_str(contents);
assert!(res.is_err());

let err = res.unwrap_err();
assert!(
err.to_string().contains("user alice has no NWC configured"),
"unexpected error: {err}"
);
}
}
94 changes: 94 additions & 0 deletions src/http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,97 @@ pub struct InvoiceResponse {
struct Amount {
amount: u64,
}

#[cfg(test)]
mod tests {
use super::*;
use axum::response::IntoResponse;
use std::collections::HashMap;

struct DummyCreator {
result: std::result::Result<String, String>,
}

#[async_trait::async_trait]
impl InvoiceCreator for DummyCreator {
async fn create_invoice(
&self,
_amount_msat: u64,
_description_hash: &str,
) -> Result<String> {
match &self.result {
Ok(invoice) => Ok(invoice.clone()),
Err(msg) => Err(anyhow::anyhow!("{msg}")),
}
}
}

fn create_app_state(user: &str, creators: Vec<Box<dyn InvoiceCreator>>) -> AppState {
let mut users = HashMap::new();
users.insert(user.to_string(), creators);
AppState {
domain: "example.com".to_string(),
users,
}
}

#[tokio::test]
async fn get_lnurlp_info_unknown_user_returns_bad_request() {
let state = Arc::new(AppState {
domain: "example.com".to_string(),
users: HashMap::new(),
});
let res = get_lnurlp_info(State(state), Path("alice".to_string())).await;
assert!(res.is_err());
let response = res.unwrap_err().into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

#[test]
fn generate_metadata_includes_identifier() -> Result<()> {
let creator = Box::new(DummyCreator {
result: Ok("lnbc1test".to_string()),
});
let state = create_app_state("alice", vec![creator]);
let metadata = generate_metadata(&state, "alice")?;
let parsed: Vec<Vec<String>> = serde_json::from_str(&metadata).unwrap();
assert!(parsed.iter().any(|entry| {
entry.len() == 2 && entry[0] == "text/identifier" && entry[1] == "alice@example.com"
}));
Ok(())
}

#[tokio::test]
async fn create_invoice_rejects_zero_amount() {
let creator = Box::new(DummyCreator {
result: Ok("lnbc1test".to_string()),
});
let state = Arc::new(create_app_state("alice", vec![creator]));
let err = create_invoice(
State(state),
Path("alice".to_string()),
Query(Amount { amount: 0 }),
)
.await
.unwrap_err();
let response = err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn create_invoice_returns_invoice() {
let creator = Box::new(DummyCreator {
result: Ok("lnbc1test".to_string()),
});
let state = Arc::new(create_app_state("alice", vec![creator]));
let response = create_invoice(
State(state),
Path("alice".to_string()),
Query(Amount { amount: 1500 }),
)
.await
.unwrap();
assert_eq!(response.0.pr, "lnbc1test");
assert!(response.0.routes.is_empty());
}
}