diff --git a/node/src/accountant/db_access_objects/failed_payable_dao.rs b/node/src/accountant/db_access_objects/failed_payable_dao.rs index 296bfe8d2..3202807b3 100644 --- a/node/src/accountant/db_access_objects/failed_payable_dao.rs +++ b/node/src/accountant/db_access_objects/failed_payable_dao.rs @@ -382,7 +382,7 @@ mod tests { make_read_only_db_connection, FailedTxBuilder, }; use crate::accountant::db_access_objects::utils::current_unix_timestamp; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{ PreviousAttempts, ValidationFailureClockReal, }; @@ -591,8 +591,8 @@ mod tests { fn failure_reason_from_str_works() { // Submission error assert_eq!( - FailureReason::from_str(r#"{"Submission":{"Local":{"Decoder"}}}"#).unwrap(), - FailureReason::Submission(AppRpcErrorKind::Decoder) + FailureReason::from_str(r#"{"Submission":{"Local":"Decoder"}}"#).unwrap(), + FailureReason::Submission(AppRpcErrorKind::Local(LocalErrorKind::Decoder)) ); // Reverted @@ -640,8 +640,8 @@ mod tests { ); assert_eq!( - FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":{"ServerUnreachable":{"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}}}}"#).unwrap(), - FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), &validation_failure_clock))) + FailureStatus::from_str(r#"{"RecheckRequired":{"Reattempting":[{"error":{"AppRpc":{"Remote":"Unreachable"}},"firstSeen":{"secs_since_epoch":1755080031,"nanos_since_epoch":612180914},"attempts":1}]}}"#).unwrap(), + FailureStatus::RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), &validation_failure_clock))) ); assert_eq!( @@ -652,9 +652,8 @@ mod tests { // Invalid Variant assert_eq!( FailureStatus::from_str("\"UnknownStatus\"").unwrap_err(), - "unknown variant `UnknownStatus`, \ - expected one of `RetryRequired`, `RecheckRequired`, `Concluded` \ - at line 1 column 15 in '\"UnknownStatus\"'" + "unknown variant `UnknownStatus`, expected one of `RetryRequired`, `RecheckRequired`, \ + `Concluded` at line 1 column 15 in '\"UnknownStatus\"'" ); // Invalid Input @@ -724,7 +723,9 @@ mod tests { .reason(PendingTooLong) .status(RecheckRequired(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -775,13 +776,19 @@ mod tests { subject .insert_new_records(&vec![tx1.clone(), tx2.clone(), tx3.clone(), tx4.clone()]) .unwrap(); + let timestamp = SystemTime::now(); + let clock = ValidationFailureClockMock::default() + .now_result(timestamp) + .now_result(timestamp); let hashmap = HashMap::from([ (tx1.hash, Concluded), ( tx2.hash, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), - &ValidationFailureClockReal::default(), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), + &clock, ))), ), (tx3.hash, Concluded), @@ -797,8 +804,8 @@ mod tests { assert_eq!( updated_txs[1].status, RecheckRequired(ValidationStatus::Reattempting(PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), - &ValidationFailureClockReal::default() + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable)), + &clock ))) ); assert_eq!(tx3.status, RetryRequired); diff --git a/node/src/accountant/db_access_objects/sent_payable_dao.rs b/node/src/accountant/db_access_objects/sent_payable_dao.rs index a79e8ffbd..09e293edf 100644 --- a/node/src/accountant/db_access_objects/sent_payable_dao.rs +++ b/node/src/accountant/db_access_objects/sent_payable_dao.rs @@ -442,7 +442,7 @@ mod tests { use crate::accountant::db_access_objects::test_utils::{make_read_only_db_connection, TxBuilder}; use crate::blockchain::blockchain_interface::blockchain_interface_web3::lower_level_interface_web3::{TransactionBlock}; use crate::blockchain::errors::BlockchainErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, RemoteErrorKind}; use crate::blockchain::errors::validation_status::{PreviousAttempts, ValidationFailureClockReal}; use crate::blockchain::test_utils::{make_block_hash, make_tx_hash, ValidationFailureClockMock}; @@ -458,11 +458,15 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ) .add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -694,7 +698,9 @@ mod tests { .hash(make_tx_hash(2)) .status(TxStatus::Pending(ValidationStatus::Reattempting( PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::ServerUnreachable), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote( + RemoteErrorKind::Unreachable, + )), &ValidationFailureClockReal::default(), ), ))) @@ -1189,8 +1195,8 @@ mod tests { ); assert_eq!( - TxStatus::from_str(r#"{"Pending":{"Reattempting":{"InvalidResponse":{"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}}}}"#).unwrap(), - TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::InvalidResponse), &validation_failure_clock))) + TxStatus::from_str(r#"{"Pending":{"Reattempting":[{"error":{"AppRpc":{"Remote":"InvalidResponse"}},"firstSeen":{"secs_since_epoch":12456,"nanos_since_epoch":0},"attempts":1}]}}"#).unwrap(), + TxStatus::Pending(ValidationStatus::Reattempting(PreviousAttempts::new(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse)), &validation_failure_clock))) ); assert_eq!( diff --git a/node/src/blockchain/errors/mod.rs b/node/src/blockchain/errors/mod.rs index ab18ae9df..5cd1a6f3c 100644 --- a/node/src/blockchain/errors/mod.rs +++ b/node/src/blockchain/errors/mod.rs @@ -23,19 +23,19 @@ pub enum BlockchainErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::internal_errors::InternalErrorKind; - use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; use crate::blockchain::errors::BlockchainErrorKind; #[test] fn blockchain_error_serialization_deserialization() { vec![ ( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), - "{\"AppRpc\":\"Decoder\"}", + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), + r#"{"AppRpc":{"Local":"Decoder"}}"#, ), ( BlockchainErrorKind::Internal(InternalErrorKind::PendingTooLongNotReplaced), - "{\"Internal\":\"PendingTooLongNotReplaced\"}", + r#"{"Internal":"PendingTooLongNotReplaced"}"#, ), ] .into_iter() diff --git a/node/src/blockchain/errors/rpc_errors.rs b/node/src/blockchain/errors/rpc_errors.rs index 4d8482e17..e717fbf25 100644 --- a/node/src/blockchain/errors/rpc_errors.rs +++ b/node/src/blockchain/errors/rpc_errors.rs @@ -14,7 +14,7 @@ pub enum AppRpcError { pub enum LocalError { Decoder(String), Internal, - Io(String), + IO(String), Signing(String), Transport(String), } @@ -33,7 +33,7 @@ impl From for AppRpcError { // Local Errors Web3Error::Decoder(error) => AppRpcError::Local(LocalError::Decoder(error)), Web3Error::Internal => AppRpcError::Local(LocalError::Internal), - Web3Error::Io(error) => AppRpcError::Local(LocalError::Io(error.to_string())), + Web3Error::Io(error) => AppRpcError::Local(LocalError::IO(error.to_string())), Web3Error::Signing(error) => { // This variant cannot be tested due to import limitations. AppRpcError::Local(LocalError::Signing(error.to_string())) @@ -53,18 +53,25 @@ impl From for AppRpcError { } } -#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum AppRpcErrorKind { - // Local + Local(LocalErrorKind), + Remote(RemoteErrorKind), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum LocalErrorKind { Decoder, Internal, IO, Signing, Transport, +} - // Remote +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RemoteErrorKind { InvalidResponse, - ServerUnreachable, + Unreachable, Web3RpcError(i64), // Keep only the stable error code } @@ -72,16 +79,18 @@ impl From<&AppRpcError> for AppRpcErrorKind { fn from(err: &AppRpcError) -> Self { match err { AppRpcError::Local(local) => match local { - LocalError::Decoder(_) => Self::Decoder, - LocalError::Internal => Self::Internal, - LocalError::Io(_) => Self::IO, - LocalError::Signing(_) => Self::Signing, - LocalError::Transport(_) => Self::Transport, + LocalError::Decoder(_) => Self::Local(LocalErrorKind::Decoder), + LocalError::Internal => Self::Local(LocalErrorKind::Internal), + LocalError::IO(_) => Self::Local(LocalErrorKind::IO), + LocalError::Signing(_) => Self::Local(LocalErrorKind::Signing), + LocalError::Transport(_) => Self::Local(LocalErrorKind::Transport), }, AppRpcError::Remote(remote) => match remote { - RemoteError::InvalidResponse(_) => Self::InvalidResponse, - RemoteError::Unreachable => Self::ServerUnreachable, - RemoteError::Web3RpcError { code, .. } => Self::Web3RpcError(*code), + RemoteError::InvalidResponse(_) => Self::Remote(RemoteErrorKind::InvalidResponse), + RemoteError::Unreachable => Self::Remote(RemoteErrorKind::Unreachable), + RemoteError::Web3RpcError { code, .. } => { + Self::Remote(RemoteErrorKind::Web3RpcError(*code)) + } }, } } @@ -90,7 +99,7 @@ impl From<&AppRpcError> for AppRpcErrorKind { #[cfg(test)] mod tests { use crate::blockchain::errors::rpc_errors::{ - AppRpcError, AppRpcErrorKind, LocalError, RemoteError, + AppRpcError, AppRpcErrorKind, LocalError, LocalErrorKind, RemoteError, RemoteErrorKind, }; use web3::error::Error as Web3Error; @@ -110,7 +119,7 @@ mod tests { std::io::ErrorKind::Other, "IO error" ))), - AppRpcError::Local(LocalError::Io("IO error".to_string())) + AppRpcError::Local(LocalError::IO("IO error".to_string())) ); assert_eq!( AppRpcError::from(Web3Error::Transport("Transport error".to_string())), @@ -145,60 +154,58 @@ mod tests { AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Decoder( "Decoder error".to_string() ))), - AppRpcErrorKind::Decoder + AppRpcErrorKind::Local(LocalErrorKind::Decoder) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Internal)), - AppRpcErrorKind::Internal + AppRpcErrorKind::Local(LocalErrorKind::Internal) ); assert_eq!( - AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Io("IO error".to_string()))), - AppRpcErrorKind::IO + AppRpcErrorKind::from(&AppRpcError::Local(LocalError::IO("IO error".to_string()))), + AppRpcErrorKind::Local(LocalErrorKind::IO) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Signing( "Signing error".to_string() ))), - AppRpcErrorKind::Signing + AppRpcErrorKind::Local(LocalErrorKind::Signing) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Local(LocalError::Transport( "Transport error".to_string() ))), - AppRpcErrorKind::Transport + AppRpcErrorKind::Local(LocalErrorKind::Transport) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::InvalidResponse( "Invalid response".to_string() ))), - AppRpcErrorKind::InvalidResponse + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Unreachable)), - AppRpcErrorKind::ServerUnreachable + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable) ); assert_eq!( AppRpcErrorKind::from(&AppRpcError::Remote(RemoteError::Web3RpcError { code: 55, message: "Booga".to_string() })), - AppRpcErrorKind::Web3RpcError(55) + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(55)) ); } #[test] fn app_rpc_error_kind_serialization_deserialization() { let errors = vec![ - // Local Errors - AppRpcErrorKind::Decoder, - AppRpcErrorKind::Internal, - AppRpcErrorKind::IO, - AppRpcErrorKind::Signing, - AppRpcErrorKind::Transport, - // Remote Errors - AppRpcErrorKind::InvalidResponse, - AppRpcErrorKind::ServerUnreachable, - AppRpcErrorKind::Web3RpcError(42), + AppRpcErrorKind::Local(LocalErrorKind::Decoder), + AppRpcErrorKind::Local(LocalErrorKind::Internal), + AppRpcErrorKind::Local(LocalErrorKind::IO), + AppRpcErrorKind::Local(LocalErrorKind::Signing), + AppRpcErrorKind::Local(LocalErrorKind::Transport), + AppRpcErrorKind::Remote(RemoteErrorKind::InvalidResponse), + AppRpcErrorKind::Remote(RemoteErrorKind::Unreachable), + AppRpcErrorKind::Remote(RemoteErrorKind::Web3RpcError(42)), ]; errors.into_iter().for_each(|error| { @@ -206,8 +213,7 @@ mod tests { let deserialized: AppRpcErrorKind = serde_json::from_str(&serialized).unwrap(); assert_eq!( error, deserialized, - "Failed serde attempt for {:?} that should look \ - like {:?}", + "Failed serde attempt for {:?} that should look like {:?}", deserialized, error ); }); diff --git a/node/src/blockchain/errors/validation_status.rs b/node/src/blockchain/errors/validation_status.rs index 72ca28346..34cb2c5e3 100644 --- a/node/src/blockchain/errors/validation_status.rs +++ b/node/src/blockchain/errors/validation_status.rs @@ -1,9 +1,14 @@ // Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. -use crate::blockchain::errors::rpc_errors::AppRpcErrorKind; use crate::blockchain::errors::BlockchainErrorKind; +use serde::de::{SeqAccess, Visitor}; +use serde::ser::SerializeSeq; +use serde::{ + Deserialize as ManualDeserialize, Deserializer, Serialize as ManualSerialize, Serializer, +}; use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt::Formatter; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -12,12 +17,74 @@ pub enum ValidationStatus { Reattempting(PreviousAttempts), } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct PreviousAttempts { - #[serde(flatten)] inner: HashMap, } +// had to implement it manually in an array JSON layout, as the original, default HashMap +// serialization threw errors because the values of keys were represented by nested enums that +// serde doesn't translate into a complex JSON value (unlike the plain string required for a key) +impl ManualSerialize for PreviousAttempts { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct Entry<'a> { + #[serde(rename = "error")] + error_kind: &'a BlockchainErrorKind, + #[serde(flatten)] + stats: &'a ErrorStats, + } + + let mut seq = serializer.serialize_seq(Some(self.inner.len()))?; + for (error_kind, stats) in self.inner.iter() { + seq.serialize_element(&Entry { error_kind, stats })?; + } + seq.end() + } +} + +impl<'de> ManualDeserialize<'de> for PreviousAttempts { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(PreviousAttemptsVisitor) + } +} + +struct PreviousAttemptsVisitor; + +impl<'de> Visitor<'de> for PreviousAttemptsVisitor { + type Value = PreviousAttempts; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("PreviousAttempts") + } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + #[derive(Deserialize)] + struct EntryOwned { + #[serde(rename = "error")] + error_kind: BlockchainErrorKind, + #[serde(flatten)] + stats: ErrorStats, + } + + let mut error_stats_map: HashMap = hashmap!(); + while let Some(entry) = seq.next_element::()? { + error_stats_map.insert(entry.error_kind, entry.stats); + } + Ok(PreviousAttempts { + inner: error_stats_map, + }) + } +} + impl PreviousAttempts { pub fn new(error: BlockchainErrorKind, clock: &dyn ValidationFailureClock) -> Self { Self { @@ -75,6 +142,11 @@ impl ValidationFailureClock for ValidationFailureClockReal { mod tests { use super::*; use crate::blockchain::errors::internal_errors::InternalErrorKind; + use crate::blockchain::errors::rpc_errors::{AppRpcErrorKind, LocalErrorKind}; + use crate::blockchain::test_utils::ValidationFailureClockMock; + use crate::test_utils::serde_serializer_mock::{SerdeSerializerMock, SerializeSeqMock}; + use serde::ser::Error as SerdeError; + use std::time::{Duration, UNIX_EPOCH}; #[test] fn previous_attempts_and_validation_failure_clock_work_together_fine() { @@ -82,7 +154,7 @@ mod tests { // new() let timestamp_a = SystemTime::now(); let subject = PreviousAttempts::new( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); // add_attempt() @@ -93,22 +165,24 @@ mod tests { ); let timestamp_c = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), &validation_failure_clock, ); let timestamp_d = SystemTime::now(); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Decoder)), &validation_failure_clock, ); let subject = subject.add_attempt( - BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO), + BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::IO)), &validation_failure_clock, ); let decoder_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Decoder)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Decoder, + ))) .unwrap(); assert!( timestamp_a <= decoder_error_stats.first_seen @@ -136,7 +210,9 @@ mod tests { assert_eq!(internal_error_stats.attempts, 1); let io_error_stats = subject .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::IO)) + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::IO, + ))) .unwrap(); assert!( timestamp_c <= io_error_stats.first_seen && io_error_stats.first_seen <= timestamp_d, @@ -146,9 +222,106 @@ mod tests { io_error_stats.first_seen ); assert_eq!(io_error_stats.attempts, 2); - let other_error_stats = subject - .inner - .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Signing)); + let other_error_stats = + subject + .inner + .get(&BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local( + LocalErrorKind::Signing, + ))); assert_eq!(other_error_stats, None); } + + #[test] + fn previous_attempts_custom_serialize_seq_happy_path() { + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = serde_json::to_string(&PreviousAttempts::new(err, &clock)).unwrap(); + + assert_eq!( + result, + r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":{"secs_since_epoch":1234567890,"nanos_since_epoch":0},"attempts":1}]"# + ); + } + + #[test] + fn previous_attempts_custom_serialize_seq_initialization_err() { + let mock = SerdeSerializerMock::default() + .serialize_seq_result(Err(serde_json::Error::custom("lethally acid bobbles"))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "lethally acid bobbles"); + } + + #[test] + fn previous_attempts_custom_serialize_seq_element_err() { + let mock = SerdeSerializerMock::default() + .serialize_seq_result(Ok(SerializeSeqMock::default().serialize_element_result( + Err(serde_json::Error::custom("jelly gummies gone off")), + ))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "jelly gummies gone off"); + } + + #[test] + fn previous_attempts_custom_serialize_end_err() { + let mock = + SerdeSerializerMock::default().serialize_seq_result(Ok(SerializeSeqMock::default() + .serialize_element_result(Ok(())) + .end_result(Err(serde_json::Error::custom("funny belly ache"))))); + let err = BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)); + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + + let result = PreviousAttempts::new(err, &clock).serialize(mock); + + assert_eq!(result.unwrap_err().to_string(), "funny belly ache"); + } + + #[test] + fn previous_attempts_custom_deserialize_happy_path() { + let str = r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":{"secs_since_epoch":1234567890,"nanos_since_epoch":0},"attempts":1}]"#; + + let result = serde_json::from_str::(str); + + let timestamp = UNIX_EPOCH + .checked_add(Duration::from_secs(1234567890)) + .unwrap(); + let clock = ValidationFailureClockMock::default().now_result(timestamp); + assert_eq!( + result.unwrap().inner, + hashmap!(BlockchainErrorKind::AppRpc(AppRpcErrorKind::Local(LocalErrorKind::Internal)) => ErrorStats::now(&clock)) + ); + } + + #[test] + fn previous_attempts_custom_deserialize_sad_path() { + let str = + r#"[{"error":{"AppRpc":{"Local":"Internal"}},"firstSeen":"Yesterday","attempts":1}]"#; + + let result = serde_json::from_str::(str); + + assert_eq!( + result.unwrap_err().to_string(), + "invalid type: string \"Yesterday\", expected struct SystemTime at line 1 column 79" + ); + } } diff --git a/node/src/test_utils/mod.rs b/node/src/test_utils/mod.rs index b36199b75..588eb87e6 100644 --- a/node/src/test_utils/mod.rs +++ b/node/src/test_utils/mod.rs @@ -14,6 +14,7 @@ pub mod persistent_configuration_mock; pub mod recorder; pub mod recorder_counter_msgs; pub mod recorder_stop_conditions; +pub mod serde_serializer_mock; pub mod stream_connector_mock; pub mod tcp_wrapper_mocks; pub mod tokio_wrapper_mocks; diff --git a/node/src/test_utils/serde_serializer_mock.rs b/node/src/test_utils/serde_serializer_mock.rs new file mode 100644 index 000000000..7130cd0c0 --- /dev/null +++ b/node/src/test_utils/serde_serializer_mock.rs @@ -0,0 +1,348 @@ +// Copyright (c) 2025, MASQ (https://masq.ai) and/or its affiliates. All rights reserved. + +#![cfg(test)] + +use serde::ser::{ + SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, +}; +use serde::{Serialize, Serializer}; +use serde_json::Error; +use std::cell::RefCell; + +#[derive(Default)] +pub struct SerdeSerializerMock { + serialize_seq_results: RefCell>>, +} + +impl Serializer for SerdeSerializerMock { + type Ok = (); + type Error = Error; + type SerializeSeq = SerializeSeqMock; + type SerializeTuple = SerializeTupleMock; + type SerializeTupleStruct = SerializeTupleStructMock; + type SerializeTupleVariant = SerializeTupleVariantMock; + type SerializeMap = SerializeMapMock; + type SerializeStruct = SerializeStructMock; + type SerializeStructVariant = SerializeStructVariantMock; + + fn serialize_bool(self, _v: bool) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i8(self, _v: i8) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i16(self, _v: i16) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i32(self, _v: i32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_i64(self, _v: i64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u8(self, _v: u8) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u16(self, _v: u16) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u32(self, _v: u32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_u64(self, _v: u64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_f32(self, _v: f32) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_f64(self, _v: f64) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_char(self, _v: char) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_str(self, _v: &str) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_none(self) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_some(self, _value: &T) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_unit(self) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_seq(self, _len: Option) -> Result { + self.serialize_seq_results.borrow_mut().remove(0) + } + + fn serialize_tuple(self, _len: usize) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_map(self, _len: Option) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + unimplemented!("Not yet needed") + } +} + +impl SerdeSerializerMock { + pub fn serialize_seq_result(self, serializer: Result) -> Self { + self.serialize_seq_results.borrow_mut().push(serializer); + self + } +} + +#[derive(Default)] +pub struct SerializeSeqMock { + serialize_element_results: RefCell>>, + end_results: RefCell>>, +} + +impl SerializeSeq for SerializeSeqMock { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + self.serialize_element_results.borrow_mut().remove(0) + } + + fn end(self) -> Result { + self.end_results.borrow_mut().remove(0) + } +} + +impl SerializeSeqMock { + pub fn serialize_element_result(self, result: Result<(), Error>) -> Self { + self.serialize_element_results.borrow_mut().push(result); + self + } + + pub fn end_result(self, result: Result<(), Error>) -> Self { + self.end_results.borrow_mut().push(result); + self + } +} + +pub struct SerializeTupleMock {} + +impl SerializeTuple for SerializeTupleMock { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeTupleStructMock {} + +impl SerializeTupleStruct for SerializeTupleStructMock { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeTupleVariantMock {} + +impl SerializeTupleVariant for SerializeTupleVariantMock { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeMapMock {} + +impl SerializeMap for SerializeMapMock { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, _key: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn serialize_value(&mut self, _value: &T) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeStructMock {} + +impl SerializeStruct for SerializeStructMock { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +} + +pub struct SerializeStructVariantMock {} + +impl SerializeStructVariant for SerializeStructVariantMock { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + _key: &'static str, + _value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + unimplemented!("Not yet needed") + } + + fn end(self) -> Result { + unimplemented!("Not yet needed") + } +}