diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 0000000..2887c08 --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,9 @@ +pub mod api; +pub mod auth; +pub mod config; +pub mod database; +pub mod errors; +pub mod middleware; +pub mod repositories; +pub mod services; +pub mod utils; \ No newline at end of file diff --git a/backend/src/services/node_manager.rs b/backend/src/services/node_manager.rs index 2ca781d..c8b8e84 100644 --- a/backend/src/services/node_manager.rs +++ b/backend/src/services/node_manager.rs @@ -82,7 +82,7 @@ pub struct LndNode { } /// Parses the node features from the format returned by LND gRPC to LDK NodeFeatures -fn parse_node_features(features: HashSet) -> NodeFeatures { +pub fn parse_node_features(features: HashSet) -> NodeFeatures { let mut flags = vec![0; 256]; for f in features.into_iter() { diff --git a/backend/src/utils/crypto.rs b/backend/src/utils/crypto.rs index c2c2d6c..1588a8c 100644 --- a/backend/src/utils/crypto.rs +++ b/backend/src/utils/crypto.rs @@ -3,8 +3,10 @@ //! ## Usage //! //! ```rust -//! let encrypted = StringCrypto::encrypt("secret data")?; -//! let decrypted = StringCrypto::decrypt(&encrypted)?; +//! use backend::utils::crypto::StringCrypto; +//! let encrypted = StringCrypto::encrypt("secret data").unwrap(); +//! let decrypted = StringCrypto::decrypt(&encrypted).unwrap(); +//! assert_eq!(decrypted, "secret data"); //! ``` use crate::config::Config; diff --git a/backend/src/utils/generate_random_string.rs b/backend/src/utils/generate_random_string.rs index 6e3b1f8..7e5a21f 100644 --- a/backend/src/utils/generate_random_string.rs +++ b/backend/src/utils/generate_random_string.rs @@ -17,6 +17,7 @@ use rand::{Rng, distributions::Alphanumeric}; /// # Examples /// /// ``` +/// use backend::utils::generate_random_string::generate_random_string; /// let token = generate_random_string(32); /// assert_eq!(token.len(), 32); /// diff --git a/backend/src/utils/mod.rs b/backend/src/utils/mod.rs index b6095f9..38d6ea9 100644 --- a/backend/src/utils/mod.rs +++ b/backend/src/utils/mod.rs @@ -306,7 +306,7 @@ pub enum InvoiceStatus { Failed, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub enum ChannelState { Opening, // funding tx not confirmed #[default] diff --git a/backend/src/utils/sats_to_usd.rs b/backend/src/utils/sats_to_usd.rs index 4a0a680..ad84c99 100644 --- a/backend/src/utils/sats_to_usd.rs +++ b/backend/src/utils/sats_to_usd.rs @@ -43,7 +43,7 @@ impl PriceConverter { Self::round_to_2_decimals(btc_amount * btc_price) } - fn round_to_2_decimals(value: f64) -> f64 { + pub fn round_to_2_decimals(value: f64) -> f64 { (value * 100.0).round() / 100.0 } @@ -71,7 +71,7 @@ impl PriceConverter { } } - async fn check_cache(&self) -> Option { + pub async fn check_cache(&self) -> Option { let cache = self.cache.read().await; cache.as_ref().and_then(|c| { c.last_updated @@ -99,7 +99,7 @@ impl PriceConverter { Ok(price_data.usd) } - async fn update_cache(&self, price: f64) { + pub async fn update_cache(&self, price: f64) { let mut cache = self.cache.write().await; *cache = Some(PriceCache { price, diff --git a/backend/tests/handlers_common_test.rs b/backend/tests/handlers_common_test.rs new file mode 100644 index 0000000..5074c6e --- /dev/null +++ b/backend/tests/handlers_common_test.rs @@ -0,0 +1,29 @@ +use backend::utils::handlers_common::{parse_payment_hash, parse_public_key}; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + +fn valid_public_key() -> String { + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[0xcd; 32]).expect("valid secret key"); + PublicKey::from_secret_key(&secp, &secret_key).to_string() +} + +const VALID_HASH: &str = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; + +#[test] +fn parse_payment_hash_validates_length_and_format() { + let (_, body) = parse_payment_hash("a1b2").unwrap_err(); + assert!(body.contains("invalid_payment_hash_length")); + + let (_, body) = parse_payment_hash("xyz!").unwrap_err(); + assert!(body.contains("invalid_payment_hash")); + + assert!(parse_payment_hash(VALID_HASH).is_ok()); +} + +#[test] +fn parse_public_key_rejects_invalid_and_accepts_valid() { + let (_, body) = parse_public_key("not-a-key").unwrap_err(); + assert!(body.contains("invalid_public_key")); + + assert!(parse_public_key(&valid_public_key()).is_ok()); +} \ No newline at end of file diff --git a/backend/tests/lightning_types_test.rs b/backend/tests/lightning_types_test.rs new file mode 100644 index 0000000..d931ad2 --- /dev/null +++ b/backend/tests/lightning_types_test.rs @@ -0,0 +1,94 @@ +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use std::str::FromStr; + +use backend::utils::*; + +fn create_test_pubkey() -> PublicKey { + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[0xcd; 32]).unwrap(); + PublicKey::from_secret_key(&secp, &secret_key) +} + +fn create_alt_test_pubkey() -> PublicKey { + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[0xab; 32]).unwrap(); + PublicKey::from_secret_key(&secp, &secret_key) +} + +#[test] +fn test_node_id_validation() { + let pk = create_test_pubkey(); + let pk2 = create_alt_test_pubkey(); + let mut alias = String::from("test_alias"); + + // Valid pubkey validation + let node_id = NodeId::PublicKey(pk); + assert!(node_id.validate(&pk, &mut alias).is_ok()); + + // Mismatched pubkey should fail + assert!(node_id.validate(&pk2, &mut alias).is_err()); + + // Valid alias validation + let node_id = NodeId::Alias("test_alias".to_string()); + assert!(node_id.validate(&pk, &mut alias).is_ok()); + + // Mismatched alias should fail + alias = String::from("wrong_alias"); + assert!(node_id.validate(&pk, &mut alias).is_err()); +} + +#[test] +fn test_payment_state_parsing() { + // Case-insensitive parsing + assert_eq!( + PaymentState::from_str("inflight").unwrap(), + PaymentState::Inflight + ); + assert_eq!( + PaymentState::from_str("FAILED").unwrap(), + PaymentState::Failed + ); + assert_eq!( + PaymentState::from_str("Settled").unwrap(), + PaymentState::Settled + ); + + // Invalid input should fail + assert!(PaymentState::from_str("invalid").is_err()); +} + +#[test] +fn test_payment_type_parsing() { + // Case-insensitive parsing + assert_eq!( + PaymentType::from_str("OUTGOING").unwrap().as_str(), + "outgoing" + ); + assert_eq!( + PaymentType::from_str("incoming").unwrap().as_str(), + "incoming" + ); + assert_eq!( + PaymentType::from_str("Forwarded").unwrap().as_str(), + "forwarded" + ); + + // Invalid input should fail + assert!(PaymentType::from_str("invalid").is_err()); +} + +#[test] +fn test_channel_state_parsing() { + // Case-insensitive parsing + assert_eq!( + ChannelState::from_str("ACTIVE").unwrap(), + ChannelState::Active + ); + assert_eq!( + ChannelState::from_str("disabled").unwrap(), + ChannelState::Disabled + ); + + // Invalid input should fail + assert!(ChannelState::from_str("invalid").is_err()); +} diff --git a/backend/tests/node_manager_test.rs b/backend/tests/node_manager_test.rs new file mode 100644 index 0000000..3b23cda --- /dev/null +++ b/backend/tests/node_manager_test.rs @@ -0,0 +1,30 @@ +use backend::services::node_manager::{parse_channel_point, parse_node_features}; +use std::collections::HashSet; +use std::str::FromStr; +use bitcoin::Txid; + +#[test] +fn test_parse_node_features_empty() { + let features = HashSet::new(); + let parsed = parse_node_features(features); + assert!(!parsed.supports_basic_mpp()); + assert!(!parsed.requires_payment_secret()); +} + +#[test] +fn test_parse_channel_point_valid() { + let txid = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; + let channel_point = format!("{}:1", txid); + let result = parse_channel_point(&channel_point).unwrap(); + + assert_eq!(result.vout, 1); + assert_eq!(result.txid, Txid::from_str(txid).unwrap()); +} + +#[test] +fn test_parse_channel_point_invalid_formats() { + assert!(parse_channel_point(":1").is_err()); + assert!(parse_channel_point("txid:").is_err()); + assert!(parse_channel_point("txid").is_err()); + assert!(parse_channel_point("invalid_txid:1").is_err()); +} \ No newline at end of file diff --git a/backend/tests/sats_to_usd_test.rs b/backend/tests/sats_to_usd_test.rs new file mode 100644 index 0000000..326a52c --- /dev/null +++ b/backend/tests/sats_to_usd_test.rs @@ -0,0 +1,39 @@ +use backend::utils::sats_to_usd::PriceConverter; + +#[test] +fn test_sats_to_usd_with_price() { + assert_eq!( + PriceConverter::sats_to_usd_with_price(100_000_000, 50_000.0), + 50_000.0 + ); + assert_eq!( + PriceConverter::sats_to_usd_with_price(50_000_000, 50_000.0), + 25_000.0 + ); + assert_eq!(PriceConverter::sats_to_usd_with_price(1_000, 50_000.0), 0.5); + assert_eq!(PriceConverter::sats_to_usd_with_price(0, 50_000.0), 0.0); +} + +#[test] +fn test_round_to_2_decimals() { + assert_eq!(PriceConverter::round_to_2_decimals(123.456), 123.46); + assert_eq!(PriceConverter::round_to_2_decimals(123.454), 123.45); + assert_eq!(PriceConverter::round_to_2_decimals(99.999), 100.0); +} + +#[tokio::test] +async fn test_cache_update_and_retrieval() { + let converter = PriceConverter::new(); + converter.update_cache(50_000.0).await; + + assert_eq!(converter.check_cache().await, Some(50_000.0)); +} + +#[tokio::test] +async fn test_sats_to_usd_with_cached_price() { + let converter = PriceConverter::new(); + converter.update_cache(60_000.0).await; + + let result = converter.sats_to_usd(100_000_000).await.unwrap(); + assert_eq!(result, 60_000.0); +} \ No newline at end of file