diff --git a/roles/jd-server/src/lib/mod.rs b/roles/jd-server/src/lib/mod.rs index a76c80cf1..5fb12c75e 100644 --- a/roles/jd-server/src/lib/mod.rs +++ b/roles/jd-server/src/lib/mod.rs @@ -98,3 +98,76 @@ where _ => Err(serde::de::Error::custom("Unsupported duration unit")), } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + fn load_config(path: &str) -> Configuration { + let config_path = PathBuf::from(path); + assert!( + config_path.exists(), + "No config file found at {:?}", + config_path + ); + + let config_string = + std::fs::read_to_string(config_path).expect("Failed to read the config file"); + toml::from_str(&config_string).expect("Failed to parse config") + } + + #[test] + fn test_get_coinbase_output_non_empty() { + let config = load_config("config-examples/jds-config-hosted-example.toml"); + let outputs = get_coinbase_output(&config).expect("Failed to get coinbase output"); + + let expected_output = CoinbaseOutput_ { + output_script_type: "P2WPKH".to_string(), + output_script_value: + "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), + }; + let expected_script: Script = expected_output.try_into().unwrap(); + let expected_transaction_output = TxOut { + value: 0, + script_pubkey: expected_script, + }; + + assert_eq!(outputs[0], expected_transaction_output); + } + + #[test] + fn test_get_coinbase_output_empty() { + let mut config = load_config("config-examples/jds-config-hosted-example.toml"); + config.coinbase_outputs.clear(); + + let result = get_coinbase_output(&config); + assert!( + matches!(result, Err(Error::EmptyCoinbaseOutputs)), + "Expected an error for empty coinbase outputs" + ); + } + + #[test] + fn test_try_from_valid_input() { + let input = CoinbaseOutput { + output_script_type: "P2PKH".to_string(), + output_script_value: + "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), + }; + let result: Result = (&input).try_into(); + assert!(result.is_ok()); + } + + #[test] + fn test_try_from_invalid_input() { + let input = CoinbaseOutput { + output_script_type: "INVALID".to_string(), + output_script_value: + "036adc3bdf21e6f9a0f0fb0066bf517e5b7909ed1563d6958a10993849a7554075".to_string(), + }; + let result: Result = (&input).try_into(); + assert!(matches!(result, Err(Error::UnknownOutputScriptType))); + } +} diff --git a/roles/jd-server/src/lib/status.rs b/roles/jd-server/src/lib/status.rs index 83a50026f..fe9981617 100644 --- a/roles/jd-server/src/lib/status.rs +++ b/roles/jd-server/src/lib/status.rs @@ -36,7 +36,7 @@ pub struct Status { pub state: State, } -/// this function is used to discern which componnent experienced the event. +/// this function is used to discern which component experienced the event. /// With this knowledge we can wrap the status message with information (`State` variants) so /// the main status loop can decide what should happen async fn send_status( @@ -129,3 +129,282 @@ pub async fn handle_error(sender: &Sender, e: JdsError) -> error_handling::Error } } } + +#[cfg(test)] +mod tests { + use std::{convert::TryInto, io::Error}; + + use super::*; + use async_channel::{bounded, RecvError}; + use roles_logic_sv2::mining_sv2::OpenMiningChannelError; + + #[tokio::test] + async fn test_send_status_downstream_listener_shutdown() { + let (tx, rx) = bounded(1); + let sender = Sender::DownstreamListener(tx); + let error = JdsError::ChannelRecv(async_channel::RecvError); + + send_status(&sender, error, error_handling::ErrorBranch::Continue).await; + match rx.recv().await { + Ok(status) => match status.state { + State::DownstreamShutdown(e) => { + assert_eq!(e.to_string(), "Channel recv failed: `RecvError`") + } + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_send_status_upstream_shutdown() { + let (tx, rx) = bounded(1); + let sender = Sender::Upstream(tx); + let error = JdsError::MempoolError(crate::mempool::error::JdsMempoolError::EmptyMempool); + let error_string = error.to_string(); + send_status(&sender, error, error_handling::ErrorBranch::Continue).await; + + match rx.recv().await { + Ok(status) => match status.state { + State::TemplateProviderShutdown(e) => assert_eq!(e.to_string(), error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_io_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::Io(Error::new(std::io::ErrorKind::Interrupted, "IO error")); + let error_string = error.to_string(); + + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_channel_send_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::ChannelSend(Box::new("error")); + let error_string = error.to_string(); + + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_channel_receive_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::ChannelRecv(RecvError); + let error_string = error.to_string(); + + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::DownstreamShutdown(e) => assert_eq!(e.to_string(), error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_binary_sv2_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::BinarySv2(binary_sv2::Error::IoError); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_codec_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::Codec(codec_sv2::Error::InvalidStepForInitiator); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_noise_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::Noise(noise_sv2::Error::HandshakeNotFinalized); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_roles_logic_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::RolesLogic(roles_logic_sv2::Error::BadPayloadSize); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_custom_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::Custom("error".to_string()); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_framing_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::Framing(codec_sv2::framing_sv2::Error::ExpectedHandshakeFrame); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_poison_lock_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::PoisonLock("error".to_string()); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_impossible_to_reconstruct_block_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::ImpossibleToReconstructBlock("Impossible".to_string()); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_no_last_declared_job_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::NoLastDeclaredJob; + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::Healthy(e) => assert_eq!(e, error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_last_mempool_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let error = JdsError::MempoolError(crate::mempool::error::JdsMempoolError::EmptyMempool); + let error_string = error.to_string(); + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::TemplateProviderShutdown(e) => assert_eq!(e.to_string(), error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } + + #[tokio::test] + async fn test_handle_error_sv2_protocol_error() { + let (tx, rx) = bounded(1); + let sender = Sender::Downstream(tx); + let inner: [u8; 32] = rand::random(); + let value = inner.to_vec().try_into().unwrap(); + let error = JdsError::Sv2ProtocolError(( + 12, + Mining::OpenMiningChannelError(OpenMiningChannelError { + request_id: 1, + error_code: value, + }), + )); + let error_string = "12"; + handle_error(&sender, error).await; + match rx.recv().await { + Ok(status) => match status.state { + State::DownstreamInstanceDropped(e) => assert_eq!(e.to_string(), error_string), + _ => panic!("Unexpected state received"), + }, + Err(_) => panic!("Failed to receive status"), + } + } +}