diff --git a/src/communication/rpc.rs b/src/communication/rpc.rs index ce0bed5..0358ac1 100644 --- a/src/communication/rpc.rs +++ b/src/communication/rpc.rs @@ -170,7 +170,7 @@ impl dyn RpcClient { /// /// * `method` - The URI representing the method to invoke. /// * `call_options` - Options to include in the request message. - /// * `proto_message` - The protobuf `Message` to include in the request message. + /// * `request_message` - The protobuf `Message` to include in the request message. /// /// # Returns /// @@ -184,13 +184,13 @@ impl dyn RpcClient { &self, method: UUri, call_options: CallOptions, - proto_message: T, + request_message: T, ) -> Result where T: MessageFull, R: MessageFull, { - let payload = UPayload::try_from_protobuf(proto_message) + let payload = UPayload::try_from_protobuf(request_message) .map_err(|e| ServiceInvocationError::InvalidArgument(e.to_string()))?; let result = self @@ -286,3 +286,58 @@ pub trait RpcServer { request_handler: Arc, ) -> Result<(), RegistrationError>; } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use protobuf::well_known_types::wrappers::StringValue; + + use crate::{communication::CallOptions, UUri}; + + use super::*; + + #[tokio::test] + async fn test_invoke_proto_method_fails_for_unexpected_return_type() { + let mut rpc_client = MockRpcClient::new(); + rpc_client + .expect_invoke_method() + .once() + .returning(|_method, _options, _payload| { + let error = UStatus::fail_with_code(UCode::INTERNAL, "internal error"); + let response_payload = UPayload::try_from_protobuf(error).unwrap(); + Ok(Some(response_payload)) + }); + let client: Arc = Arc::new(rpc_client); + let mut request = StringValue::new(); + request.value = "hello".to_string(); + let result = client + .invoke_proto_method::( + UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(), + CallOptions::for_rpc_request(5_000, None, None, None), + request, + ) + .await; + assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_)))); + } + + #[tokio::test] + async fn test_invoke_proto_method_fails_for_missing_response_payload() { + let mut rpc_client = MockRpcClient::new(); + rpc_client + .expect_invoke_method() + .once() + .return_const(Ok(None)); + let client: Arc = Arc::new(rpc_client); + let mut request = StringValue::new(); + request.value = "hello".to_string(); + let result = client + .invoke_proto_method::( + UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(), + CallOptions::for_rpc_request(5_000, None, None, None), + request, + ) + .await; + assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_)))); + } +} diff --git a/src/core/usubscription.rs b/src/core/usubscription.rs index 662dd2a..77a9d62 100644 --- a/src/core/usubscription.rs +++ b/src/core/usubscription.rs @@ -27,6 +27,35 @@ pub use crate::up_core_api::usubscription::{ use crate::{UStatus, UUri}; impl Hash for SubscriberInfo { + /// Creates a hash value based on the URI property. + /// + /// # Examples + /// + /// ```rust + /// use std::hash::{DefaultHasher, Hash, Hasher}; + /// use up_rust::UUri; + /// use up_rust::core::usubscription::SubscriberInfo; + /// + /// let mut hasher = DefaultHasher::new(); + /// let info = SubscriberInfo { + /// uri: Some(UUri::try_from_parts("", 0x1000, 0x01, 0x9a00).unwrap()).into(), + /// ..Default::default() + /// }; + /// + /// info.hash(&mut hasher); + /// let hash_one = hasher.finish(); + /// + /// let mut hasher = DefaultHasher::new(); + /// let info = SubscriberInfo { + /// uri: Some(UUri::try_from_parts("", 0x1000, 0x02, 0xf100).unwrap()).into(), + /// ..Default::default() + /// }; + /// + /// info.hash(&mut hasher); + /// let hash_two = hasher.finish(); + /// + /// assert_ne!(hash_one, hash_two); + /// ``` fn hash(&self, state: &mut H) { self.uri.hash(state); } @@ -39,6 +68,19 @@ impl Eq for SubscriberInfo {} /// # Returns /// /// `true` if the given instance is equal to [`SubscriberInfo::default`], `false` otherwise. +/// +/// # Examples +/// +/// ```rust +/// use up_rust::UUri; +/// use up_rust::core::usubscription::SubscriberInfo; +/// +/// let mut info = SubscriberInfo::default(); +/// assert!(info.is_empty()); +/// +/// info.uri = Some(UUri::try_from_parts("", 0x1000, 0x01, 0x9a00).unwrap()).into(); +/// assert!(!info.is_empty()); +/// ``` impl SubscriberInfo { pub fn is_empty(&self) -> bool { self.eq(&SubscriberInfo::default()) diff --git a/src/umessage.rs b/src/umessage.rs index fe9a759..969a548 100644 --- a/src/umessage.rs +++ b/src/umessage.rs @@ -220,9 +220,11 @@ pub(crate) fn deserialize_protobuf_bytes( #[cfg(test)] mod test { - use protobuf::well_known_types::{any::Any, wrappers::StringValue}; + use std::io; - use crate::UStatus; + use protobuf::well_known_types::{any::Any, duration::Duration, wrappers::StringValue}; + + use crate::{UAttributes, UStatus}; use super::*; @@ -230,13 +232,20 @@ mod test { fn test_deserialize_protobuf_bytes_succeeds() { let mut data = StringValue::new(); data.value = "hello world".to_string(); - let any = Any::pack(&data).unwrap(); + let any = Any::pack(&data.clone()).unwrap(); let buf: Bytes = any.write_to_bytes().unwrap().into(); + let result = deserialize_protobuf_bytes::( &buf, &UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, ); assert!(result.is_ok_and(|v| v.value == *"hello world")); + + let result = deserialize_protobuf_bytes::( + &data.write_to_bytes().unwrap().into(), + &UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF, + ); + assert!(result.is_ok_and(|v| v.value == *"hello world")); } #[test] @@ -251,4 +260,92 @@ mod test { ); assert!(result.is_err_and(|e| matches!(e, UMessageError::PayloadError(_)))); } + + #[test] + fn test_deserialize_protobuf_bytes_fails_for_unsupported_format() { + let result = deserialize_protobuf_bytes::( + &"hello".into(), + &UPayloadFormat::UPAYLOAD_FORMAT_TEXT, + ); + assert!(result.is_err_and(|e| matches!(e, UMessageError::PayloadError(_)))); + } + + #[test] + fn test_deserialize_protobuf_bytes_fails_for_invalid_encoding() { + let any = Any { + type_url: "type.googleapis.com/google.protobuf.Duration".to_string(), + value: vec![0x0A], + ..Default::default() + }; + let buf = any.write_to_bytes().unwrap(); + let result = deserialize_protobuf_bytes::( + &buf.into(), + &UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY, + ); + assert!(result.is_err_and(|e| matches!(e, UMessageError::DataSerializationError(_)))) + } + + #[test] + fn extract_payload_succeeds() { + let payload = StringValue { + value: "hello".to_string(), + ..Default::default() + }; + let buf = Any::pack(&payload) + .and_then(|a| a.write_to_bytes()) + .unwrap(); + let msg = UMessage { + attributes: Some(UAttributes { + payload_format: UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY.into(), + ..Default::default() + }) + .into(), + payload: Some(buf.into()), + ..Default::default() + }; + assert!(msg + .extract_protobuf::() + .is_ok_and(|v| v.value == *"hello")); + } + + #[test] + fn extract_payload_fails_for_no_payload() { + let msg = UMessage { + attributes: Some(UAttributes { + payload_format: UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY.into(), + ..Default::default() + }) + .into(), + ..Default::default() + }; + assert!(msg + .extract_protobuf::() + .is_err_and(|e| matches!(e, UMessageError::PayloadError(_)))); + } + + #[test] + fn test_from_attributes_error() { + let attributes_error = UAttributesError::validation_error("failed to validate"); + let message_error = UMessageError::from(attributes_error); + assert!(matches!( + message_error, + UMessageError::AttributesValidationError(UAttributesError::ValidationError(_)) + )); + } + + #[test] + fn test_from_protobuf_error() { + let protobuf_error = protobuf::Error::from(io::Error::last_os_error()); + let message_error = UMessageError::from(protobuf_error); + assert!(matches!( + message_error, + UMessageError::DataSerializationError(_) + )); + } + + #[test] + fn test_from_error_msg() { + let message_error = UMessageError::from("an error occurred"); + assert!(matches!(message_error, UMessageError::PayloadError(_))); + } } diff --git a/src/uri.rs b/src/uri.rs index 8c3e25b..40e0375 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -73,6 +73,23 @@ impl From<&UUri> for String { /// # Returns /// /// The output of [`UUri::to_uri`] without inlcuding the uProtocol scheme. + /// + /// # Examples + /// + /// ```rust + /// use up_rust::UUri; + /// + /// let uuri = UUri { + /// authority_name: String::from("VIN.vehicles"), + /// ue_id: 0x0000_800A, + /// ue_version_major: 0x02, + /// resource_id: 0x0000_1a50, + /// ..Default::default() + /// }; + /// + /// let uri_string = String::from(&uuri); + /// assert_eq!(uri_string, "//VIN.vehicles/800A/2/1A50"); + /// ```` fn from(uri: &UUri) -> Self { UUri::to_uri(uri, false) } @@ -222,6 +239,23 @@ impl TryFrom for UUri { /// # Returns /// /// A `Result` containing either the `UUri` representation of the URI or a `SerializationError`. + /// + /// # Examples + /// + /// ```rust + /// use up_rust::UUri; + /// + /// let uri = UUri { + /// authority_name: "".to_string(), + /// ue_id: 0x001A_8000, + /// ue_version_major: 0x02, + /// resource_id: 0x0000_1a50, + /// ..Default::default() + /// }; + /// + /// let uri_from = UUri::try_from("/1A8000/2/1A50".to_string()).unwrap(); + /// assert_eq!(uri, uri_from); + /// ```` fn try_from(uri: String) -> Result { UUri::from_str(uri.as_str()) } @@ -328,7 +362,7 @@ impl UUri { /// ```rust /// use up_rust::UUri; /// - /// assert!(UUri::try_from_parts("vin", 0x5a6b, 0x01, 0x0001).is_ok()); + /// assert!(UUri::try_from_parts("vin", 0x0000_5a6b, 0x01, 0x0001).is_ok()); /// ``` // [impl->dsn~uri-authority-name-length~1] // [impl->dsn~uri-host-only~2] @@ -390,6 +424,16 @@ impl UUri { /// # Returns /// /// 'true' if this URI is equal to `UUri::default()`, `false` otherwise. + /// + /// # Examples + /// + /// ```rust + /// use up_rust::UUri; + /// + /// let uuri = UUri::try_from_parts("MYVIN", 0xa13b, 0x01, 0x7f4e).unwrap(); + /// assert!(!uuri.is_empty()); + /// assert!(UUri::default().is_empty()); + /// ``` pub fn is_empty(&self) -> bool { self.eq(&UUri::default()) } @@ -786,6 +830,13 @@ mod tests { #[test_case("up://MYVIN/1A23/1/a13#foobar"; "for URI with fragement")] #[test_case("up://MYVIN:1000/1A23/1/A13"; "for authority with port")] #[test_case("up://user:pwd@MYVIN/1A23/1/A13"; "for authority with userinfo")] + #[test_case("5up://MYVIN/55A1/1/1"; "for invalid scheme")] + #[test_case("up://MY#VIN/55A1/1/1"; "for invalid authority")] + #[test_case("up://MYVIN/55T1/1/1"; "for non-hex entity ID")] + #[test_case("up://MYVIN/55A1//1"; "for empty version")] + #[test_case("up://MYVIN/55A1/T/1"; "for non-hex version")] + #[test_case("up://MYVIN/55A1/1/"; "for empty resource ID")] + #[test_case("up://MYVIN/55A1/1/1T"; "for non-hex resource ID")] fn test_from_string_fails(string: &str) { let parsing_result = UUri::from_str(string); assert!(parsing_result.is_err()); @@ -903,9 +954,10 @@ mod tests { } // [utest->dsn~uri-host-only~2] - #[test_case("MYVIN:1000"; "for authority with port")] - #[test_case("user:pwd@MYVIN"; "for authority with userinfo")] - fn test_try_from_parts_fails(authority: &str) { + #[test_case("MYVIN:1000"; "with port")] + #[test_case("user:pwd@MYVIN"; "with userinfo")] + #[test_case("MY%VIN"; "with reserved character")] + fn test_try_from_parts_fails_for_invalid_authority(authority: &str) { assert!(UUri::try_from_parts(authority, 0xa100, 0x01, 0x6501).is_err()); } diff --git a/src/utransport.rs b/src/utransport.rs index 70c1d5b..e0573b7 100644 --- a/src/utransport.rs +++ b/src/utransport.rs @@ -291,7 +291,7 @@ mod tests { sync::Arc, }; - use super::MockUListener; + use super::*; #[tokio::test] async fn test_deref_returns_wrapped_listener() { @@ -368,6 +368,33 @@ mod tests { assert_ne!(hash_one, hash_two); } + #[tokio::test] + async fn test_utransport_default_implementations() { + struct EmptyTransport {} + #[async_trait::async_trait] + impl UTransport for EmptyTransport { + async fn send(&self, _message: UMessage) -> Result<(), UStatus> { + todo!() + } + } + + let transport = EmptyTransport {}; + let listener = Arc::new(MockUListener::new()); + + assert!(transport + .receive(&UUri::any(), None) + .await + .is_err_and(|e| e.get_code() == UCode::UNIMPLEMENTED)); + assert!(transport + .register_listener(&UUri::any(), None, listener.clone()) + .await + .is_err_and(|e| e.get_code() == UCode::UNIMPLEMENTED)); + assert!(transport + .unregister_listener(&UUri::any(), None, listener) + .await + .is_err_and(|e| e.get_code() == UCode::UNIMPLEMENTED)); + } + #[test] fn test_comparable_listener_pointer_address() { let bar = Arc::new(MockUListener::new()); diff --git a/src/uuid.rs b/src/uuid.rs index 4979471..92f2081 100644 --- a/src/uuid.rs +++ b/src/uuid.rs @@ -322,6 +322,11 @@ impl FromStr for UUID { /// assert!("a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8" /// .parse::() /// .is_err()); + /// + /// // parsing a string that is not a UUID fails + /// assert!("this-is-not-a-UUID" + /// .parse::() + /// .is_err()); /// ``` fn from_str(uuid_str: &str) -> Result { let mut uuid = [0u8; 16]; @@ -342,9 +347,9 @@ mod tests { #[test] fn test_from_u64_pair() { // timestamp = 1, ver = 0b0111 - let msb = 0x0000000000017000u64; + let msb = 0x0000000000017000_u64; // variant = 0b10 - let lsb = 0x8000000000000000u64; + let lsb = 0x8000000000000000_u64; let conversion_attempt = UUID::from_u64_pair(msb, lsb); assert!(conversion_attempt.is_ok()); let uuid = conversion_attempt.unwrap(); @@ -352,9 +357,15 @@ mod tests { assert_eq!(uuid.get_time(), Some(0x1_u64)); // timestamp = 1, (invalid) ver = 0b0000 - let msb = 0x0000000000010000u64; + let msb = 0x0000000000010000_u64; + // variant= 0b10 + let lsb = 0x80000000000000ab_u64; + assert!(UUID::from_u64_pair(msb, lsb).is_err()); + + // timestamp = 1, ver = 0b0111 + let msb = 0x0000000000017000_u64; // (invalid) variant= 0b00 - let lsb = 0x00000000000000abu64; + let lsb = 0x00000000000000ab_u64; assert!(UUID::from_u64_pair(msb, lsb).is_err()); } @@ -373,6 +384,22 @@ mod tests { assert_eq!(uuid.get_time(), Some(0x1_u64)); } + #[test] + fn test_into_string() { + // timestamp = 1, ver = 0b0111 + let msb = 0x0000000000017000_u64; + // variant = 0b10, random = 0x0010101010101a1a + let lsb = 0x8010101010101a1a_u64; + let uuid = UUID { + msb, + lsb, + ..Default::default() + }; + + assert_eq!(String::from(&uuid), "00000000-0001-7000-8010-101010101a1a"); + assert_eq!(String::from(uuid), "00000000-0001-7000-8010-101010101a1a"); + } + // [utest->req~uuid-proto~1] #[test] fn test_protobuf_serialization() {