diff --git a/crates/rmcp/src/error.rs b/crates/rmcp/src/error.rs index e0da2b3d..5b91ab52 100644 --- a/crates/rmcp/src/error.rs +++ b/crates/rmcp/src/error.rs @@ -54,3 +54,75 @@ impl RmcpError { } } } + +#[cfg(test)] +mod tests { + use std::io; + + use super::*; + use crate::model::{ErrorCode, ErrorData}; + + #[test] + fn test_error_data_display_without_data() { + let error = ErrorData { + code: ErrorCode(-32600), + message: "Invalid Request".into(), + data: None, + }; + assert_eq!(format!("{}", error), "-32600: Invalid Request"); + } + + #[test] + fn test_error_data_display_with_data() { + let error = ErrorData { + code: ErrorCode(-32600), + message: "Invalid Request".into(), + data: Some(serde_json::json!({"detail": "missing field"})), + }; + assert_eq!( + format!("{}", error), + "-32600: Invalid Request({\"detail\":\"missing field\"})" + ); + } + + #[test] + fn test_rmcp_error_transport_creation() { + struct DummyTransport; + let io_error = io::Error::other("connection failed"); + let error = RmcpError::transport_creation::(io_error); + + match error { + RmcpError::TransportCreation { + into_transport_type_name, + into_transport_type_id, + .. + } => { + assert!(into_transport_type_name.contains("DummyTransport")); + assert_eq!( + into_transport_type_id, + std::any::TypeId::of::() + ); + } + _ => panic!("Expected TransportCreation variant"), + } + } + + #[test] + fn test_rmcp_error_display() { + struct DummyTransport; + let io_error = io::Error::other("connection failed"); + let error = RmcpError::transport_creation::(io_error); + let display = format!("{}", error); + assert!(display.contains("Transport creation error")); + } + + #[test] + fn test_error_data_is_std_error() { + let error = ErrorData { + code: ErrorCode(-32600), + message: "Invalid Request".into(), + data: None, + }; + let _: &dyn std::error::Error = &error; + } +} diff --git a/crates/rmcp/src/model/annotated.rs b/crates/rmcp/src/model/annotated.rs index f9921146..55072cd9 100644 --- a/crates/rmcp/src/model/annotated.rs +++ b/crates/rmcp/src/model/annotated.rs @@ -222,3 +222,244 @@ pub trait AnnotateAble: sealed::Sealed { self.with_timestamp(Utc::now()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_annotations_default() { + let annotations = Annotations::default(); + assert_eq!(annotations.audience, None); + assert_eq!(annotations.priority, None); + assert_eq!(annotations.last_modified, None); + } + + #[test] + fn test_annotations_for_resource() { + let timestamp = Utc::now(); + let annotations = Annotations::for_resource(0.5, timestamp); + assert_eq!(annotations.priority, Some(0.5)); + assert_eq!(annotations.last_modified, Some(timestamp)); + assert_eq!(annotations.audience, None); + } + + #[test] + #[should_panic(expected = "Priority")] + fn test_annotations_for_resource_invalid_priority_high() { + let timestamp = Utc::now(); + Annotations::for_resource(1.5, timestamp); + } + + #[test] + #[should_panic(expected = "Priority")] + fn test_annotations_for_resource_invalid_priority_low() { + let timestamp = Utc::now(); + Annotations::for_resource(-0.1, timestamp); + } + + #[test] + fn test_annotated_new() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content.clone(), None); + assert_eq!(annotated.raw, content); + assert_eq!(annotated.annotations, None); + } + + #[test] + fn test_annotated_deref() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content.clone(), None); + assert_eq!(annotated.text, "test"); + } + + #[test] + fn test_annotated_deref_mut() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let mut annotated = Annotated::new(content, None); + annotated.text = "modified".to_string(); + assert_eq!(annotated.text, "modified"); + } + + #[test] + fn test_annotated_remove_annotation() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let mut annotated = Annotated::new(content, Some(Annotations::default())); + assert!(annotated.annotations.is_some()); + let removed = annotated.remove_annotation(); + assert!(removed.is_some()); + assert!(annotated.annotations.is_none()); + } + + #[test] + fn test_annotated_getters() { + let timestamp = Utc::now(); + let annotations = Annotations { + audience: Some(vec![Role::User]), + priority: Some(0.7), + last_modified: Some(timestamp), + }; + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content, Some(annotations)); + + assert_eq!(annotated.audience(), Some(&vec![Role::User])); + assert_eq!(annotated.priority(), Some(0.7)); + assert_eq!(annotated.timestamp(), Some(timestamp)); + } + + #[test] + fn test_annotated_with_audience() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content, None); + let with_audience = annotated.with_audience(vec![Role::User, Role::Assistant]); + + assert_eq!( + with_audience.audience(), + Some(&vec![Role::User, Role::Assistant]) + ); + } + + #[test] + fn test_annotated_with_priority() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content, None); + let with_priority = annotated.with_priority(0.9); + + assert_eq!(with_priority.priority(), Some(0.9)); + } + + #[test] + fn test_annotated_with_timestamp() { + let timestamp = Utc::now(); + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content, None); + let with_timestamp = annotated.with_timestamp(timestamp); + + assert_eq!(with_timestamp.timestamp(), Some(timestamp)); + } + + #[test] + fn test_annotated_with_timestamp_now() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = Annotated::new(content, None); + let with_timestamp = annotated.with_timestamp_now(); + + assert!(with_timestamp.timestamp().is_some()); + } + + #[test] + fn test_annotate_able_optional_annotate() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = content.optional_annotate(None); + assert_eq!(annotated.annotations, None); + } + + #[test] + fn test_annotate_able_annotate() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotations = Annotations::default(); + let annotated = content.annotate(annotations); + assert!(annotated.annotations.is_some()); + } + + #[test] + fn test_annotate_able_no_annotation() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = content.no_annotation(); + assert_eq!(annotated.annotations, None); + } + + #[test] + fn test_annotate_able_with_audience() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = content.with_audience(vec![Role::User]); + assert_eq!(annotated.audience(), Some(&vec![Role::User])); + } + + #[test] + fn test_annotate_able_with_priority() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = content.with_priority(0.5); + assert_eq!(annotated.priority(), Some(0.5)); + } + + #[test] + fn test_annotate_able_with_timestamp() { + let timestamp = Utc::now(); + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = content.with_timestamp(timestamp); + assert_eq!(annotated.timestamp(), Some(timestamp)); + } + + #[test] + fn test_annotate_able_with_timestamp_now() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let annotated = content.with_timestamp_now(); + assert!(annotated.timestamp().is_some()); + } + + #[test] + fn test_chaining_annotations() { + let content = RawTextContent { + text: "test".to_string(), + meta: None, + }; + let timestamp = Utc::now(); + let annotated = Annotated::new(content, None) + .with_audience(vec![Role::User]) + .with_priority(0.8) + .with_timestamp(timestamp); + + assert_eq!(annotated.audience(), Some(&vec![Role::User])); + assert_eq!(annotated.priority(), Some(0.8)); + assert_eq!(annotated.timestamp(), Some(timestamp)); + } +} diff --git a/crates/rmcp/src/model/capabilities.rs b/crates/rmcp/src/model/capabilities.rs index 7399141f..e026c963 100644 --- a/crates/rmcp/src/model/capabilities.rs +++ b/crates/rmcp/src/model/capabilities.rs @@ -343,4 +343,137 @@ mod test { }) ); } + + #[test] + fn test_prompts_capability_default() { + let cap = PromptsCapability::default(); + assert_eq!(cap.list_changed, None); + } + + #[test] + fn test_resources_capability_default() { + let cap = ResourcesCapability::default(); + assert_eq!(cap.subscribe, None); + assert_eq!(cap.list_changed, None); + } + + #[test] + fn test_tools_capability_default() { + let cap = ToolsCapability::default(); + assert_eq!(cap.list_changed, None); + } + + #[test] + fn test_roots_capabilities_default() { + let cap = RootsCapabilities::default(); + assert_eq!(cap.list_changed, None); + } + + #[test] + fn test_elicitation_capability_default() { + let cap = ElicitationCapability::default(); + assert_eq!(cap.schema_validation, None); + } + + #[test] + fn test_server_capabilities_builder_build() { + let builder = ServerCapabilities::builder() + .enable_logging() + .enable_prompts(); + let caps: ServerCapabilities = builder.build(); + assert!(caps.logging.is_some()); + assert!(caps.prompts.is_some()); + } + + #[test] + fn test_server_capabilities_prompts_list_changed() { + let caps = ServerCapabilities::builder() + .enable_prompts() + .enable_prompts_list_changed() + .build(); + assert_eq!(caps.prompts.as_ref().unwrap().list_changed, Some(true)); + } + + #[test] + fn test_server_capabilities_resources_list_changed() { + let caps = ServerCapabilities::builder() + .enable_resources() + .enable_resources_list_changed() + .build(); + assert_eq!(caps.resources.as_ref().unwrap().list_changed, Some(true)); + } + + #[test] + fn test_server_capabilities_resources_subscribe() { + let caps = ServerCapabilities::builder() + .enable_resources() + .enable_resources_subscribe() + .build(); + assert_eq!(caps.resources.as_ref().unwrap().subscribe, Some(true)); + } + + #[test] + fn test_server_capabilities_completions() { + let caps = ServerCapabilities::builder().enable_completions().build(); + assert!(caps.completions.is_some()); + } + + #[test] + fn test_client_capabilities_default() { + let caps = ClientCapabilities::default(); + assert_eq!(caps.experimental, None); + assert_eq!(caps.roots, None); + assert_eq!(caps.sampling, None); + } + + #[test] + fn test_client_capabilities_builder_from() { + let builder = ClientCapabilities::builder().enable_experimental(); + let caps: ClientCapabilities = builder.into(); + assert!(caps.experimental.is_some()); + } + + #[test] + fn test_server_capabilities_enable_with() { + let mut exp_caps = ExperimentalCapabilities::default(); + exp_caps.insert("feature1".to_string(), JsonObject::default()); + + let caps = ServerCapabilities::builder() + .enable_experimental_with(exp_caps.clone()) + .build(); + assert_eq!(caps.experimental, Some(exp_caps)); + } + + #[test] + fn test_client_capabilities_elicitation() { + let caps = ClientCapabilities::builder().enable_elicitation().build(); + assert!(caps.elicitation.is_some()); + } + + #[test] + #[cfg(feature = "elicitation")] + fn test_client_capabilities_elicitation_schema_validation() { + let caps = ClientCapabilities::builder() + .enable_elicitation() + .enable_elicitation_schema_validation() + .build(); + assert_eq!( + caps.elicitation.as_ref().unwrap().schema_validation, + Some(true) + ); + } + + #[test] + fn test_server_capabilities_clone() { + let caps1 = ServerCapabilities::builder().enable_logging().build(); + let caps2 = caps1.clone(); + assert_eq!(caps1, caps2); + } + + #[test] + fn test_client_capabilities_clone() { + let caps1 = ClientCapabilities::builder().enable_roots().build(); + let caps2 = caps1.clone(); + assert_eq!(caps1, caps2); + } } diff --git a/crates/rmcp/src/model/content.rs b/crates/rmcp/src/model/content.rs index 7c60fafd..ac15b017 100644 --- a/crates/rmcp/src/model/content.rs +++ b/crates/rmcp/src/model/content.rs @@ -290,4 +290,326 @@ mod tests { panic!("Expected ResourceLink variant"); } } + + #[test] + fn test_raw_content_text() { + let content = RawContent::text("Hello"); + match content { + RawContent::Text(text) => assert_eq!(text.text, "Hello"), + _ => panic!("Expected Text variant"), + } + } + + #[test] + fn test_raw_content_image() { + let content = RawContent::image("base64data", "image/png"); + match content { + RawContent::Image(image) => { + assert_eq!(image.data, "base64data"); + assert_eq!(image.mime_type, "image/png"); + } + _ => panic!("Expected Image variant"), + } + } + + #[test] + fn test_raw_content_json() { + let data = json!({"key": "value"}); + let content = RawContent::json(data).unwrap(); + match content { + RawContent::Text(text) => assert!(text.text.contains("key")), + _ => panic!("Expected Text variant"), + } + } + + #[test] + fn test_raw_content_as_text() { + let content = RawContent::text("test"); + assert!(content.as_text().is_some()); + assert!(content.as_image().is_none()); + assert!(content.as_resource().is_none()); + } + + #[test] + fn test_raw_content_as_image() { + let content = RawContent::image("data", "image/png"); + assert!(content.as_image().is_some()); + assert!(content.as_text().is_none()); + assert!(content.as_resource().is_none()); + } + + #[test] + fn test_raw_content_as_resource_link() { + use super::super::resource::RawResource; + let resource = RawResource::new("file:///test.txt", "test.txt"); + let content = RawContent::resource_link(resource); + assert!(content.as_resource_link().is_some()); + assert!(content.as_text().is_none()); + } + + #[test] + fn test_raw_content_embedded_text() { + let content = RawContent::embedded_text("file:///test.txt", "content"); + match content { + RawContent::Resource(embedded) => match embedded.resource { + ResourceContents::TextResourceContents { text, .. } => { + assert_eq!(text, "content"); + } + _ => panic!("Expected TextResourceContents"), + }, + _ => panic!("Expected Resource variant"), + } + } + + #[test] + fn test_content_text() { + let content = Content::text("Hello"); + assert!(content.as_text().is_some()); + } + + #[test] + fn test_content_image() { + let content = Content::image("data", "image/png"); + assert!(content.as_image().is_some()); + } + + #[test] + fn test_content_json() { + let data = json!({"test": "value"}); + let content = Content::json(data).unwrap(); + assert!(content.as_text().is_some()); + } + + #[test] + fn test_embedded_resource_get_text() { + let resource = RawEmbeddedResource { + meta: None, + resource: ResourceContents::TextResourceContents { + uri: "file:///test.txt".to_string(), + mime_type: Some("text/plain".to_string()), + text: "content".to_string(), + meta: None, + }, + }; + let embedded: EmbeddedResource = resource.no_annotation(); + assert_eq!(embedded.get_text(), "content"); + } + + #[test] + fn test_embedded_resource_get_text_blob() { + let resource = RawEmbeddedResource { + meta: None, + resource: ResourceContents::BlobResourceContents { + uri: "file:///test.bin".to_string(), + mime_type: Some("application/octet-stream".to_string()), + blob: "blobdata".to_string(), + meta: None, + }, + }; + let embedded: EmbeddedResource = resource.no_annotation(); + assert_eq!(embedded.get_text(), String::new()); + } + + #[test] + fn test_into_contents_content() { + let content = Content::text("test"); + let contents = content.into_contents(); + assert_eq!(contents.len(), 1); + } + + #[test] + fn test_into_contents_string() { + let contents = "test".to_string().into_contents(); + assert_eq!(contents.len(), 1); + assert!(contents[0].as_text().is_some()); + } + + #[test] + fn test_into_contents_unit() { + let contents = ().into_contents(); + assert_eq!(contents.len(), 0); + } + + #[test] + fn test_raw_text_content_with_meta() { + let meta = Some(super::super::Meta::default()); + let content = RawTextContent { + text: "test".to_string(), + meta, + }; + assert!(content.meta.is_some()); + } + + #[test] + fn test_raw_image_content_with_meta() { + let meta = Some(super::super::Meta::default()); + let content = RawImageContent { + data: "data".to_string(), + mime_type: "image/png".to_string(), + meta, + }; + assert!(content.meta.is_some()); + } + + #[test] + fn test_raw_content_resource() { + let resource_contents = ResourceContents::text("test content", "file:///test.txt"); + let content = RawContent::resource(resource_contents.clone()); + match content { + RawContent::Resource(embedded) => { + assert_eq!(embedded.resource, resource_contents); + } + _ => panic!("Expected Resource variant"), + } + } + + #[test] + fn test_content_resource() { + let resource_contents = ResourceContents::text("test", "file:///test.txt"); + let content = Content::resource(resource_contents); + assert!(content.as_resource().is_some()); + } + + #[test] + fn test_content_embedded_text() { + let content = Content::embedded_text("file:///test.txt", "test content"); + match content.raw { + RawContent::Resource(embedded) => match embedded.resource { + ResourceContents::TextResourceContents { text, .. } => { + assert_eq!(text, "test content"); + } + _ => panic!("Expected TextResourceContents"), + }, + _ => panic!("Expected Resource variant"), + } + } + + #[test] + fn test_content_resource_link() { + use super::super::resource::RawResource; + let resource = RawResource::new("file:///test.txt", "test.txt"); + let content = Content::resource_link(resource); + assert!(content.as_resource_link().is_some()); + } + + #[test] + fn test_raw_audio_content_creation() { + let audio = RawAudioContent { + data: "audiodata".to_string(), + mime_type: "audio/mp3".to_string(), + }; + assert_eq!(audio.data, "audiodata"); + assert_eq!(audio.mime_type, "audio/mp3"); + } + + #[test] + fn test_raw_content_audio_variant() { + let audio = RawAudioContent { + data: "audiodata".to_string(), + mime_type: "audio/wav".to_string(), + }; + let content = RawContent::Audio(audio.clone()); + match content { + RawContent::Audio(a) => assert_eq!(a.data, "audiodata"), + _ => panic!("Expected Audio variant"), + } + } + + #[test] + fn test_raw_content_as_methods_return_none_for_wrong_type() { + let text_content = RawContent::text("test"); + assert!(text_content.as_image().is_none()); + assert!(text_content.as_resource().is_none()); + assert!(text_content.as_resource_link().is_none()); + + let image_content = RawContent::image("data", "image/png"); + assert!(image_content.as_text().is_none()); + assert!(image_content.as_resource().is_none()); + } + + #[test] + fn test_embedded_resource_get_text_returns_empty_for_non_text() { + let resource = RawEmbeddedResource { + meta: None, + resource: ResourceContents::BlobResourceContents { + uri: "file:///test.bin".to_string(), + mime_type: None, + blob: "data".to_string(), + meta: None, + }, + }; + let embedded: EmbeddedResource = resource.no_annotation(); + assert_eq!(embedded.get_text(), ""); + } + + #[test] + fn test_raw_content_image_with_different_mime_types() { + let jpeg = RawContent::image("data", "image/jpeg"); + let png = RawContent::image("data", "image/png"); + let webp = RawContent::image("data", "image/webp"); + + match jpeg { + RawContent::Image(img) => assert_eq!(img.mime_type, "image/jpeg"), + _ => panic!("Expected Image variant"), + } + match png { + RawContent::Image(img) => assert_eq!(img.mime_type, "image/png"), + _ => panic!("Expected Image variant"), + } + match webp { + RawContent::Image(img) => assert_eq!(img.mime_type, "image/webp"), + _ => panic!("Expected Image variant"), + } + } + + #[test] + fn test_json_content_json_array() { + let data = json!([1, 2, 3]); + let result = RawContent::json(data); + assert!(result.is_ok()); + match result.unwrap() { + RawContent::Text(text) => assert!(text.text.contains("1")), + _ => panic!("Expected Text variant"), + } + } + + #[test] + fn test_raw_content_text_with_meta_preserved() { + let mut meta = super::super::Meta::default(); + meta.insert("key".to_string(), json!("value")); + + let content = RawTextContent { + text: "test".to_string(), + meta: Some(meta.clone()), + }; + assert_eq!( + content.meta.as_ref().unwrap().get("key"), + Some(&json!("value")) + ); + } + + #[test] + fn test_raw_content_audio_variant_not_confused_with_image() { + let audio = RawContent::Audio(RawAudioContent { + data: "audiodata".to_string(), + mime_type: "audio/mp3".to_string(), + }); + + assert!(matches!(audio, RawContent::Audio(_))); + assert!(!matches!(audio, RawContent::Image(_))); + assert!(audio.as_image().is_none()); + } + + #[test] + fn test_raw_content_json_nested_object() { + let data = json!({"outer": {"inner": "value"}}); + let content = RawContent::json(data).unwrap(); + match content { + RawContent::Text(text) => { + assert!(text.text.contains("outer")); + assert!(text.text.contains("inner")); + } + _ => panic!("Expected Text variant"), + } + } } diff --git a/crates/rmcp/src/model/extension.rs b/crates/rmcp/src/model/extension.rs index 039fdf2e..374b974e 100644 --- a/crates/rmcp/src/model/extension.rs +++ b/crates/rmcp/src/model/extension.rs @@ -310,28 +310,175 @@ impl Clone for Box { } } -#[test] -fn test_extensions() { - #[derive(Clone, Debug, PartialEq)] - struct MyType(i32); +#[cfg(test)] +mod tests { + use super::*; - let mut extensions = Extensions::new(); + #[test] + fn test_extensions() { + #[derive(Clone, Debug, PartialEq)] + struct MyType(i32); - extensions.insert(5i32); - extensions.insert(MyType(10)); + let mut extensions = Extensions::new(); - assert_eq!(extensions.get(), Some(&5i32)); - assert_eq!(extensions.get_mut(), Some(&mut 5i32)); + extensions.insert(5i32); + extensions.insert(MyType(10)); - let ext2 = extensions.clone(); + assert_eq!(extensions.get(), Some(&5i32)); + assert_eq!(extensions.get_mut(), Some(&mut 5i32)); - assert_eq!(extensions.remove::(), Some(5i32)); - assert!(extensions.get::().is_none()); + let ext2 = extensions.clone(); - // clone still has it - assert_eq!(ext2.get(), Some(&5i32)); - assert_eq!(ext2.get(), Some(&MyType(10))); + assert_eq!(extensions.remove::(), Some(5i32)); + assert!(extensions.get::().is_none()); - assert_eq!(extensions.get::(), None); - assert_eq!(extensions.get(), Some(&MyType(10))); + // clone still has it + assert_eq!(ext2.get(), Some(&5i32)); + assert_eq!(ext2.get(), Some(&MyType(10))); + + assert_eq!(extensions.get::(), None); + assert_eq!(extensions.get(), Some(&MyType(10))); + } + + #[test] + fn test_extensions_new() { + let ext = Extensions::new(); + assert!(ext.is_empty()); + assert_eq!(ext.len(), 0); + } + + #[test] + fn test_extensions_insert_replace() { + let mut ext = Extensions::new(); + assert_eq!(ext.insert(5i32), None); + assert_eq!(ext.insert(10i32), Some(5i32)); + } + + #[test] + fn test_extensions_get_or_insert() { + let mut ext = Extensions::new(); + *ext.get_or_insert(1i32) += 2; + assert_eq!(ext.get::(), Some(&3i32)); + } + + #[test] + fn test_extensions_get_or_insert_with() { + let mut ext = Extensions::new(); + *ext.get_or_insert_with(|| 5i32) += 3; + assert_eq!(ext.get::(), Some(&8i32)); + } + + #[test] + fn test_extensions_get_or_insert_default() { + let mut ext = Extensions::new(); + *ext.get_or_insert_default::() += 10; + assert_eq!(ext.get::(), Some(&10i32)); + } + + #[test] + fn test_extensions_clear() { + let mut ext = Extensions::new(); + ext.insert(5i32); + ext.insert("test"); + assert!(!ext.is_empty()); + ext.clear(); + assert!(ext.is_empty()); + } + + #[test] + fn test_extensions_len() { + let mut ext = Extensions::new(); + assert_eq!(ext.len(), 0); + ext.insert(5i32); + assert_eq!(ext.len(), 1); + ext.insert("test"); + assert_eq!(ext.len(), 2); + } + + #[test] + fn test_extensions_is_empty() { + let mut ext = Extensions::new(); + assert!(ext.is_empty()); + ext.insert(5i32); + assert!(!ext.is_empty()); + } + + #[test] + fn test_extensions_extend() { + let mut ext_a = Extensions::new(); + ext_a.insert(8u8); + ext_a.insert(16u16); + + let mut ext_b = Extensions::new(); + ext_b.insert(4u8); + ext_b.insert("hello"); + + ext_a.extend(ext_b); + assert_eq!(ext_a.len(), 3); + assert_eq!(ext_a.get::(), Some(&4u8)); + assert_eq!(ext_a.get::(), Some(&16u16)); + } + + #[test] + fn test_extensions_extend_empty() { + let mut ext_a = Extensions::new(); + ext_a.insert(5i32); + let ext_b = Extensions::new(); + ext_a.extend(ext_b); + assert_eq!(ext_a.len(), 1); + assert_eq!(ext_a.get::(), Some(&5i32)); + } + + #[test] + fn test_extensions_get_mut() { + let mut ext = Extensions::new(); + ext.insert(String::from("Hello")); + ext.get_mut::().unwrap().push_str(" World"); + assert_eq!(ext.get::().unwrap(), "Hello World"); + } + + #[test] + fn test_extensions_remove() { + let mut ext = Extensions::new(); + ext.insert(5i32); + assert_eq!(ext.remove::(), Some(5i32)); + assert!(ext.get::().is_none()); + } + + #[test] + fn test_extensions_remove_nonexistent() { + let mut ext = Extensions::new(); + assert_eq!(ext.remove::(), None); + } + + #[test] + fn test_extensions_multiple_types() { + let mut ext = Extensions::new(); + ext.insert(5i32); + ext.insert("test"); + ext.insert(1.23f64); + assert_eq!(ext.get::(), Some(&5i32)); + assert_eq!(ext.get::<&str>(), Some(&"test")); + assert_eq!(ext.get::(), Some(&1.23f64)); + } + + #[test] + fn test_extensions_debug() { + let ext = Extensions::new(); + let debug_str = format!("{:?}", ext); + assert!(debug_str.contains("Extensions")); + } + + #[test] + fn test_extensions_default() { + let ext = Extensions::default(); + assert!(ext.is_empty()); + } + + #[test] + fn test_id_hasher() { + let mut hasher = IdHasher::default(); + hasher.write_u64(12345); + assert_eq!(hasher.finish(), 12345); + } } diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index fd93362b..1f5f319d 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -186,3 +186,162 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_meta_new() { + let meta = Meta::new(); + assert!(meta.0.is_empty()); + } + + #[test] + fn test_meta_default() { + let meta = Meta::default(); + assert!(meta.0.is_empty()); + } + + #[test] + fn test_meta_static_empty() { + let meta = Meta::static_empty(); + assert!(meta.0.is_empty()); + } + + #[test] + fn test_meta_set_and_get_progress_token_string() { + let mut meta = Meta::new(); + let token = ProgressToken(NumberOrString::String("token123".into())); + meta.set_progress_token(token.clone()); + assert_eq!(meta.get_progress_token(), Some(token)); + } + + #[test] + fn test_meta_set_and_get_progress_token_number() { + let mut meta = Meta::new(); + let token = ProgressToken(NumberOrString::Number(42)); + meta.set_progress_token(token.clone()); + assert_eq!(meta.get_progress_token(), Some(token)); + } + + #[test] + fn test_meta_get_progress_token_none() { + let meta = Meta::new(); + assert_eq!(meta.get_progress_token(), None); + } + + #[test] + fn test_meta_extend() { + let mut meta1 = Meta::new(); + meta1 + .0 + .insert("key1".to_string(), Value::String("value1".to_string())); + + let mut meta2 = Meta::new(); + meta2 + .0 + .insert("key2".to_string(), Value::String("value2".to_string())); + + meta1.extend(meta2); + assert_eq!(meta1.0.len(), 2); + assert_eq!( + meta1.0.get("key1"), + Some(&Value::String("value1".to_string())) + ); + assert_eq!( + meta1.0.get("key2"), + Some(&Value::String("value2".to_string())) + ); + } + + #[test] + fn test_meta_deref() { + let mut meta = Meta::new(); + meta.0 + .insert("key".to_string(), Value::String("value".to_string())); + assert_eq!(meta.get("key"), Some(&Value::String("value".to_string()))); + } + + #[test] + fn test_meta_deref_mut() { + let mut meta = Meta::new(); + meta.insert("key".to_string(), Value::String("value".to_string())); + assert_eq!(meta.0.get("key"), Some(&Value::String("value".to_string()))); + } + + #[test] + fn test_meta_clone() { + let mut meta = Meta::new(); + meta.0 + .insert("key".to_string(), Value::String("value".to_string())); + let cloned = meta.clone(); + assert_eq!(cloned.0, meta.0); + } + + #[test] + fn test_meta_partial_eq() { + let mut meta1 = Meta::new(); + meta1 + .0 + .insert("key".to_string(), Value::String("value".to_string())); + let mut meta2 = Meta::new(); + meta2 + .0 + .insert("key".to_string(), Value::String("value".to_string())); + assert_eq!(meta1, meta2); + } + + #[test] + fn test_progress_token_string_variant() { + let token = ProgressToken(NumberOrString::String("test".into())); + let mut meta = Meta::new(); + meta.set_progress_token(token.clone()); + assert_eq!(meta.get_progress_token(), Some(token)); + } + + #[test] + fn test_progress_token_number_variant() { + let token = ProgressToken(NumberOrString::Number(100)); + let mut meta = Meta::new(); + meta.set_progress_token(token.clone()); + assert_eq!(meta.get_progress_token(), Some(token)); + } + + #[test] + fn test_meta_extend_overwrites() { + let mut meta1 = Meta::new(); + meta1 + .0 + .insert("key".to_string(), Value::String("value1".to_string())); + + let mut meta2 = Meta::new(); + meta2 + .0 + .insert("key".to_string(), Value::String("value2".to_string())); + + meta1.extend(meta2); + assert_eq!( + meta1.0.get("key"), + Some(&Value::String("value2".to_string())) + ); + } + + #[test] + fn test_meta_with_multiple_fields() { + let mut meta = Meta::new(); + meta.0 + .insert("field1".to_string(), Value::String("value1".to_string())); + meta.0 + .insert("field2".to_string(), Value::Number(42.into())); + meta.0.insert("field3".to_string(), Value::Bool(true)); + + assert_eq!(meta.0.len(), 3); + assert_eq!( + meta.0.get("field1"), + Some(&Value::String("value1".to_string())) + ); + assert_eq!(meta.0.get("field2"), Some(&Value::Number(42.into()))); + assert_eq!(meta.0.get("field3"), Some(&Value::Bool(true))); + } +} diff --git a/crates/rmcp/src/model/prompt.rs b/crates/rmcp/src/model/prompt.rs index 3c69758c..17890c4c 100644 --- a/crates/rmcp/src/model/prompt.rs +++ b/crates/rmcp/src/model/prompt.rs @@ -263,4 +263,278 @@ mod tests { panic!("Expected ResourceLink variant"); } } + + #[test] + fn test_prompt_new() { + let prompt = Prompt::new("test_prompt", Some("A test prompt"), None); + assert_eq!(prompt.name, "test_prompt"); + assert_eq!(prompt.description, Some("A test prompt".to_string())); + assert_eq!(prompt.arguments, None); + assert_eq!(prompt.title, None); + assert_eq!(prompt.icons, None); + } + + #[test] + fn test_prompt_with_arguments() { + let arg = PromptArgument { + name: "input".to_string(), + title: Some("Input".to_string()), + description: Some("Input parameter".to_string()), + required: Some(true), + }; + let prompt = Prompt::new("test", Some("desc"), Some(vec![arg.clone()])); + assert_eq!(prompt.arguments.as_ref().unwrap().len(), 1); + assert_eq!(prompt.arguments.as_ref().unwrap()[0].name, "input"); + } + + #[test] + fn test_prompt_message_new_text() { + let message = PromptMessage::new_text(PromptMessageRole::User, "Hello"); + assert_eq!(message.role, PromptMessageRole::User); + match message.content { + PromptMessageContent::Text { text } => assert_eq!(text, "Hello"), + _ => panic!("Expected Text content"), + } + } + + #[test] + fn test_prompt_message_content_text() { + let content = PromptMessageContent::text("test message"); + match content { + PromptMessageContent::Text { text } => assert_eq!(text, "test message"), + _ => panic!("Expected Text content"), + } + } + + #[test] + fn test_prompt_message_new_text_with_meta() { + let meta = Some(crate::model::Meta::default()); + let message = + PromptMessage::new_text_with_meta(PromptMessageRole::Assistant, "Response", meta); + assert_eq!(message.role, PromptMessageRole::Assistant); + match message.content { + PromptMessageContent::Text { text } => assert_eq!(text, "Response"), + _ => panic!("Expected Text content"), + } + } + + #[test] + #[cfg(feature = "base64")] + fn test_prompt_message_new_image() { + let data = vec![1, 2, 3, 4]; + let message = + PromptMessage::new_image(PromptMessageRole::User, &data, "image/png", None, None); + assert_eq!(message.role, PromptMessageRole::User); + match message.content { + PromptMessageContent::Image { image } => { + assert_eq!(image.mime_type, "image/png"); + assert!(!image.data.is_empty()); + } + _ => panic!("Expected Image content"), + } + } + + #[test] + fn test_prompt_message_new_resource() { + let message = PromptMessage::new_resource( + PromptMessageRole::Assistant, + "file:///test.txt".to_string(), + Some("text/plain".to_string()), + Some("content".to_string()), + None, + None, + None, + ); + assert_eq!(message.role, PromptMessageRole::Assistant); + match message.content { + PromptMessageContent::Resource { .. } => {} + _ => panic!("Expected Resource content"), + } + } + + #[test] + fn test_prompt_argument_optional_fields() { + let arg = PromptArgument { + name: "param".to_string(), + title: None, + description: None, + required: None, + }; + assert_eq!(arg.name, "param"); + assert_eq!(arg.title, None); + } + + #[test] + fn test_prompt_message_role_variants() { + let user_role = PromptMessageRole::User; + let assistant_role = PromptMessageRole::Assistant; + assert_eq!(user_role, PromptMessageRole::User); + assert_eq!(assistant_role, PromptMessageRole::Assistant); + } + + #[test] + fn test_prompt_message_content_resource_link_constructor() { + use super::super::resource::RawResource; + let resource = RawResource::new("file:///test.txt", "test.txt"); + let content = PromptMessageContent::resource_link(resource.no_annotation()); + match content { + PromptMessageContent::ResourceLink { link } => { + assert_eq!(link.uri, "file:///test.txt"); + } + _ => panic!("Expected ResourceLink content"), + } + } + + #[test] + fn test_prompt_with_title() { + let prompt = Prompt { + name: "test".to_string(), + title: Some("Test Title".to_string()), + description: None, + arguments: None, + icons: None, + }; + assert_eq!(prompt.title, Some("Test Title".to_string())); + } + + #[test] + fn test_prompt_with_icons() { + let icon = Icon { + src: "icon.png".to_string(), + mime_type: Some("image/png".to_string()), + sizes: None, + }; + let prompt = Prompt { + name: "test".to_string(), + title: None, + description: None, + arguments: None, + icons: Some(vec![icon]), + }; + assert_eq!(prompt.icons.as_ref().unwrap().len(), 1); + } + + #[test] + fn test_prompt_argument_all_fields() { + let arg = PromptArgument { + name: "param".to_string(), + title: Some("Parameter".to_string()), + description: Some("A parameter".to_string()), + required: Some(true), + }; + assert_eq!(arg.name, "param"); + assert_eq!(arg.title, Some("Parameter".to_string())); + assert_eq!(arg.description, Some("A parameter".to_string())); + assert_eq!(arg.required, Some(true)); + } + + #[test] + fn test_prompt_message_role_serialization() { + let json_user = serde_json::to_string(&PromptMessageRole::User).unwrap(); + let json_assistant = serde_json::to_string(&PromptMessageRole::Assistant).unwrap(); + assert!(json_user.contains("user")); + assert!(json_assistant.contains("assistant")); + } + + #[test] + fn test_prompt_message_new_text_with_meta_ignores_meta() { + let meta = Some(crate::model::Meta::default()); + let message1 = PromptMessage::new_text_with_meta(PromptMessageRole::User, "test", meta); + let message2 = PromptMessage::new_text(PromptMessageRole::User, "test"); + + // Should be equivalent since meta is ignored for text content + assert_eq!(message1, message2); + } + + #[test] + fn test_prompt_arguments_required_vs_optional() { + let required_arg = PromptArgument { + name: "required".to_string(), + title: None, + description: None, + required: Some(true), + }; + + let optional_arg = PromptArgument { + name: "optional".to_string(), + title: None, + description: None, + required: Some(false), + }; + + assert_ne!(required_arg.required, optional_arg.required); + assert!(required_arg.required.unwrap()); + assert!(!optional_arg.required.unwrap()); + } + + #[test] + fn test_prompt_new_with_none_description() { + let prompt = Prompt::new::<_, String>("test", None, None); + assert_eq!(prompt.name, "test"); + assert_eq!(prompt.description, None); + } + + #[test] + fn test_prompt_message_new_resource_blob() { + let message = PromptMessage::new_resource( + PromptMessageRole::User, + "file:///binary.dat".to_string(), + Some("application/octet-stream".to_string()), + None, + None, + None, + None, + ); + match message.content { + PromptMessageContent::Resource { resource } => match &resource.resource { + ResourceContents::BlobResourceContents { uri, .. } => { + assert_eq!(uri, "file:///binary.dat"); + } + _ => panic!("Expected BlobResourceContents"), + }, + _ => panic!("Expected Resource content"), + } + } + + #[test] + fn test_prompt_message_roles_are_distinct() { + let user_msg = PromptMessage::new_text(PromptMessageRole::User, "user text"); + let assistant_msg = PromptMessage::new_text(PromptMessageRole::Assistant, "assistant text"); + + assert_ne!(user_msg.role, assistant_msg.role); + assert!(matches!(user_msg.role, PromptMessageRole::User)); + assert!(matches!(assistant_msg.role, PromptMessageRole::Assistant)); + } + + #[test] + fn test_prompt_argument_without_required_field() { + let arg = PromptArgument { + name: "param".to_string(), + title: None, + description: None, + required: None, + }; + // When required is None, the argument's requirement is unspecified + assert!(arg.required.is_none()); + } + + #[test] + fn test_prompt_with_multiple_arguments() { + let args = vec![ + PromptArgument { + name: "arg1".to_string(), + title: None, + description: None, + required: Some(true), + }, + PromptArgument { + name: "arg2".to_string(), + title: None, + description: None, + required: Some(false), + }, + ]; + let prompt = Prompt::new("test", Some("desc"), Some(args)); + assert_eq!(prompt.arguments.as_ref().unwrap().len(), 2); + } } diff --git a/crates/rmcp/src/model/resource.rs b/crates/rmcp/src/model/resource.rs index a342ad4e..f528400e 100644 --- a/crates/rmcp/src/model/resource.rs +++ b/crates/rmcp/src/model/resource.rs @@ -141,4 +141,283 @@ mod tests { assert!(json.contains("mimeType")); assert!(!json.contains("mime_type")); } + + #[test] + fn test_raw_resource_new() { + let resource = RawResource::new("file:///test.txt", "test"); + assert_eq!(resource.uri, "file:///test.txt"); + assert_eq!(resource.name, "test"); + assert_eq!(resource.title, None); + assert_eq!(resource.description, None); + assert_eq!(resource.mime_type, None); + assert_eq!(resource.size, None); + } + + #[test] + fn test_resource_contents_text() { + let contents = ResourceContents::text("Hello", "file:///test.txt"); + match contents { + ResourceContents::TextResourceContents { + text, + uri, + mime_type, + .. + } => { + assert_eq!(text, "Hello"); + assert_eq!(uri, "file:///test.txt"); + assert_eq!(mime_type, Some("text".to_string())); + } + _ => panic!("Expected TextResourceContents"), + } + } + + #[test] + fn test_resource_contents_blob() { + let blob_contents = ResourceContents::BlobResourceContents { + uri: "file:///binary.dat".to_string(), + mime_type: Some("application/octet-stream".to_string()), + blob: "base64data".to_string(), + meta: None, + }; + let json = serde_json::to_string(&blob_contents).unwrap(); + assert!(json.contains("blob")); + assert!(json.contains("mimeType")); + } + + #[test] + fn test_resource_template() { + let template = RawResourceTemplate { + uri_template: "file:///{path}".to_string(), + name: "template".to_string(), + title: Some("Template".to_string()), + description: Some("A template".to_string()), + mime_type: Some("text/plain".to_string()), + }; + let json = serde_json::to_string(&template).unwrap(); + assert!(json.contains("uriTemplate")); + } + + #[test] + fn test_resource_with_size() { + let resource = RawResource { + uri: "file:///large.txt".to_string(), + name: "large".to_string(), + title: None, + description: None, + mime_type: None, + size: Some(1024), + icons: None, + }; + assert_eq!(resource.size, Some(1024)); + } + + #[test] + fn test_resource_clone() { + let resource1 = RawResource::new("file:///test.txt", "test"); + let resource2 = resource1.clone(); + assert_eq!(resource1, resource2); + } + + #[test] + fn test_resource_contents_with_meta() { + let mut meta = Meta::new(); + meta.insert("key".to_string(), serde_json::json!("value")); + + let contents = ResourceContents::TextResourceContents { + uri: "file:///test.txt".to_string(), + mime_type: Some("text/plain".to_string()), + text: "content".to_string(), + meta: Some(meta), + }; + + let json = serde_json::to_value(&contents).unwrap(); + assert_eq!(json["_meta"]["key"], "value"); + } + + #[test] + fn test_raw_resource_with_all_fields() { + let icon = Icon { + src: "icon.png".to_string(), + mime_type: Some("image/png".to_string()), + sizes: None, + }; + let resource = RawResource { + uri: "file:///test.txt".to_string(), + name: "test".to_string(), + title: Some("Test Resource".to_string()), + description: Some("A test resource".to_string()), + mime_type: Some("text/plain".to_string()), + size: Some(100), + icons: Some(vec![icon]), + }; + assert_eq!(resource.uri, "file:///test.txt"); + assert_eq!(resource.title, Some("Test Resource".to_string())); + assert_eq!(resource.size, Some(100)); + assert_eq!(resource.icons.as_ref().unwrap().len(), 1); + } + + #[test] + fn test_raw_resource_template_serialization() { + let template = RawResourceTemplate { + uri_template: "file:///{path}".to_string(), + name: "template".to_string(), + title: None, + description: None, + mime_type: None, + }; + let json = serde_json::to_string(&template).unwrap(); + assert!(json.contains("uriTemplate")); + assert!(json.contains("template")); + } + + #[test] + fn test_raw_resource_template_with_all_fields() { + let template = RawResourceTemplate { + uri_template: "file:///{path}".to_string(), + name: "template".to_string(), + title: Some("Template".to_string()), + description: Some("A template".to_string()), + mime_type: Some("text/plain".to_string()), + }; + assert_eq!(template.title, Some("Template".to_string())); + assert_eq!(template.description, Some("A template".to_string())); + } + + #[test] + fn test_resource_contents_text_with_none_mime() { + let contents = ResourceContents::TextResourceContents { + uri: "file:///test.txt".to_string(), + mime_type: None, + text: "content".to_string(), + meta: None, + }; + match contents { + ResourceContents::TextResourceContents { mime_type, .. } => { + assert_eq!(mime_type, None); + } + _ => panic!("Expected TextResourceContents"), + } + } + + #[test] + fn test_resource_contents_blob_with_meta() { + let mut meta = Meta::new(); + meta.insert("key".to_string(), serde_json::json!("value")); + + let contents = ResourceContents::BlobResourceContents { + uri: "file:///binary.dat".to_string(), + mime_type: Some("application/octet-stream".to_string()), + blob: "blobdata".to_string(), + meta: Some(meta), + }; + + let json = serde_json::to_value(&contents).unwrap(); + assert_eq!(json["_meta"]["key"], "value"); + } + + #[test] + fn test_resource_with_different_uris() { + let file_resource = RawResource::new("file:///path/to/file.txt", "file"); + let http_resource = RawResource::new("http://example.com/resource", "http"); + let custom_resource = RawResource::new("custom://resource/id", "custom"); + + assert_eq!(file_resource.uri, "file:///path/to/file.txt"); + assert_eq!(http_resource.uri, "http://example.com/resource"); + assert_eq!(custom_resource.uri, "custom://resource/id"); + } + + #[test] + fn test_resource_size_affects_equality() { + let resource1 = RawResource { + uri: "file:///test.txt".to_string(), + name: "test".to_string(), + title: None, + description: None, + mime_type: None, + size: Some(100), + icons: None, + }; + + let resource2 = RawResource { + uri: "file:///test.txt".to_string(), + name: "test".to_string(), + title: None, + description: None, + mime_type: None, + size: Some(200), + icons: None, + }; + + assert_ne!(resource1, resource2); + } + + #[test] + fn test_resource_contents_text_sets_mime_type() { + let contents = ResourceContents::text("content", "file:///test.txt"); + match contents { + ResourceContents::TextResourceContents { mime_type, .. } => { + assert_eq!(mime_type, Some("text".to_string())); + } + _ => panic!("Expected TextResourceContents"), + } + } + + #[test] + fn test_resource_template_with_variable_substitution_pattern() { + let template = RawResourceTemplate { + uri_template: "file:///{user}/documents/{filename}".to_string(), + name: "user_document".to_string(), + title: Some("User Document".to_string()), + description: Some("Access user documents by name".to_string()), + mime_type: None, + }; + + assert!(template.uri_template.contains("{user}")); + assert!(template.uri_template.contains("{filename}")); + } + + #[test] + fn test_resource_deserialization() { + let json = r#"{ + "uri": "file:///test.txt", + "name": "test", + "mimeType": "text/plain" + }"#; + let resource: RawResource = serde_json::from_str(json).unwrap(); + assert_eq!(resource.uri, "file:///test.txt"); + assert_eq!(resource.name, "test"); + assert_eq!(resource.mime_type, Some("text/plain".to_string())); + } + + #[test] + fn test_resource_contents_deserialization_text() { + let json = r#"{ + "uri": "file:///test.txt", + "text": "content", + "mimeType": "text/plain" + }"#; + let contents: ResourceContents = serde_json::from_str(json).unwrap(); + match contents { + ResourceContents::TextResourceContents { text, .. } => { + assert_eq!(text, "content"); + } + _ => panic!("Expected TextResourceContents"), + } + } + + #[test] + fn test_resource_contents_deserialization_blob() { + let json = r#"{ + "uri": "file:///binary.dat", + "blob": "blobdata", + "mimeType": "application/octet-stream" + }"#; + let contents: ResourceContents = serde_json::from_str(json).unwrap(); + match contents { + ResourceContents::BlobResourceContents { blob, .. } => { + assert_eq!(blob, "blobdata"); + } + _ => panic!("Expected BlobResourceContents"), + } + } } diff --git a/crates/rmcp/src/model/serde_impl.rs b/crates/rmcp/src/model/serde_impl.rs index 09222d52..1df0305c 100644 --- a/crates/rmcp/src/model/serde_impl.rs +++ b/crates/rmcp/src/model/serde_impl.rs @@ -253,7 +253,8 @@ where mod test { use serde_json::json; - use crate::model::ListToolsRequest; + use super::*; + use crate::model::{Extensions, ListToolsRequest, Meta}; #[test] fn test_deserialize_lost_tools_request() { @@ -264,4 +265,142 @@ mod test { )) .unwrap(); } + + #[test] + fn test_request_serialize_without_meta() { + let req = Request { + method: "test_method".to_string(), + params: json!({"key": "value"}), + extensions: Extensions::new(), + }; + let serialized = serde_json::to_value(&req).unwrap(); + assert_eq!(serialized["method"], "test_method"); + assert_eq!(serialized["params"]["key"], "value"); + assert!(serialized["params"]["_meta"].is_null()); + } + + #[test] + fn test_request_serialize_with_meta() { + let mut extensions = Extensions::new(); + let mut meta = Meta::new(); + meta.insert("custom".to_string(), json!("data")); + extensions.insert(meta); + + let req = Request { + method: "test_method".to_string(), + params: json!({"key": "value"}), + extensions, + }; + let serialized = serde_json::to_value(&req).unwrap(); + assert_eq!(serialized["params"]["_meta"]["custom"], "data"); + } + + #[test] + fn test_request_deserialize() { + let json_val = json!({ + "method": "test_method", + "params": {"key": "value"} + }); + let req: Request = serde_json::from_value(json_val).unwrap(); + assert_eq!(req.method, "test_method"); + assert_eq!(req.params["key"], "value"); + } + + #[test] + fn test_request_optional_param_serialize() { + let req = RequestOptionalParam { + method: "test_method".to_string(), + params: Some(json!({"key": "value"})), + extensions: Extensions::new(), + }; + let serialized = serde_json::to_value(&req).unwrap(); + assert_eq!(serialized["method"], "test_method"); + assert!(serialized["params"].is_object()); + } + + #[test] + fn test_request_optional_param_deserialize_with_params() { + let json_val = json!({ + "method": "test_method", + "params": {"key": "value"} + }); + let req: RequestOptionalParam = + serde_json::from_value(json_val).unwrap(); + assert_eq!(req.method, "test_method"); + assert!(req.params.is_some()); + } + + #[test] + fn test_request_optional_param_deserialize_without_params() { + let json_val = json!({ + "method": "test_method" + }); + let req: RequestOptionalParam = + serde_json::from_value(json_val).unwrap(); + assert_eq!(req.method, "test_method"); + assert!(req.params.is_none()); + } + + #[test] + fn test_request_no_param_serialize() { + let req = RequestNoParam { + method: "test_method".to_string(), + extensions: Extensions::new(), + }; + let serialized = serde_json::to_value(&req).unwrap(); + assert_eq!(serialized["method"], "test_method"); + assert!(serialized.get("params").is_none()); + } + + #[test] + fn test_request_no_param_deserialize() { + let json_val = json!({ + "method": "test_method" + }); + let req: RequestNoParam = serde_json::from_value(json_val).unwrap(); + assert_eq!(req.method, "test_method"); + } + + #[test] + fn test_notification_serialize() { + let notif = Notification { + method: "test_notification".to_string(), + params: json!({"data": "test"}), + extensions: Extensions::new(), + }; + let serialized = serde_json::to_value(¬if).unwrap(); + assert_eq!(serialized["method"], "test_notification"); + assert_eq!(serialized["params"]["data"], "test"); + } + + #[test] + fn test_notification_deserialize() { + let json_val = json!({ + "method": "test_notification", + "params": {"data": "test"} + }); + let notif: Notification = + serde_json::from_value(json_val).unwrap(); + assert_eq!(notif.method, "test_notification"); + assert_eq!(notif.params["data"], "test"); + } + + #[test] + fn test_notification_no_param_serialize() { + let notif = NotificationNoParam { + method: "test_notification".to_string(), + extensions: Extensions::new(), + }; + let serialized = serde_json::to_value(¬if).unwrap(); + assert_eq!(serialized["method"], "test_notification"); + } + + #[test] + fn test_notification_no_param_deserialize() { + let json_val = json!({ + "method": "test_notification" + }); + let notif: NotificationNoParam = serde_json::from_value(json_val).unwrap(); + assert_eq!(notif.method, "test_notification"); + } } diff --git a/crates/rmcp/src/model/tool.rs b/crates/rmcp/src/model/tool.rs index 282d8aa3..66c27641 100644 --- a/crates/rmcp/src/model/tool.rs +++ b/crates/rmcp/src/model/tool.rs @@ -177,3 +177,355 @@ impl Tool { Value::Object(self.input_schema.as_ref().clone()) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn test_tool_annotations_new() { + let annotations = ToolAnnotations::new(); + assert_eq!(annotations.title, None); + assert_eq!(annotations.read_only_hint, None); + assert_eq!(annotations.destructive_hint, None); + assert_eq!(annotations.idempotent_hint, None); + assert_eq!(annotations.open_world_hint, None); + } + + #[test] + fn test_tool_annotations_with_title() { + let annotations = ToolAnnotations::with_title("Test Tool"); + assert_eq!(annotations.title, Some("Test Tool".to_string())); + } + + #[test] + fn test_tool_annotations_read_only() { + let annotations = ToolAnnotations::new().read_only(true); + assert_eq!(annotations.read_only_hint, Some(true)); + } + + #[test] + fn test_tool_annotations_destructive() { + let annotations = ToolAnnotations::new().destructive(false); + assert_eq!(annotations.destructive_hint, Some(false)); + } + + #[test] + fn test_tool_annotations_idempotent() { + let annotations = ToolAnnotations::new().idempotent(true); + assert_eq!(annotations.idempotent_hint, Some(true)); + } + + #[test] + fn test_tool_annotations_open_world() { + let annotations = ToolAnnotations::new().open_world(false); + assert_eq!(annotations.open_world_hint, Some(false)); + } + + #[test] + fn test_tool_annotations_is_destructive_default() { + let annotations = ToolAnnotations::new(); + assert!(annotations.is_destructive()); + } + + #[test] + fn test_tool_annotations_is_destructive_explicit() { + let annotations = ToolAnnotations::new().destructive(false); + assert!(!annotations.is_destructive()); + } + + #[test] + fn test_tool_annotations_is_idempotent_default() { + let annotations = ToolAnnotations::new(); + assert!(!annotations.is_idempotent()); + } + + #[test] + fn test_tool_annotations_is_idempotent_explicit() { + let annotations = ToolAnnotations::new().idempotent(true); + assert!(annotations.is_idempotent()); + } + + #[test] + fn test_tool_annotations_chaining() { + let annotations = ToolAnnotations::with_title("Test") + .read_only(true) + .destructive(false) + .idempotent(true) + .open_world(false); + + assert_eq!(annotations.title, Some("Test".to_string())); + assert_eq!(annotations.read_only_hint, Some(true)); + assert_eq!(annotations.destructive_hint, Some(false)); + assert_eq!(annotations.idempotent_hint, Some(true)); + assert_eq!(annotations.open_world_hint, Some(false)); + } + + #[test] + fn test_tool_new() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let tool = Tool::new("test_tool", "A test tool", schema); + + assert_eq!(tool.name, "test_tool"); + assert_eq!(tool.description, Some(Cow::Borrowed("A test tool"))); + assert_eq!(tool.title, None); + assert_eq!(tool.annotations, None); + assert_eq!(tool.icons, None); + } + + #[test] + fn test_tool_annotate() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let tool = Tool::new("test_tool", "desc", schema); + let annotations = ToolAnnotations::with_title("Test Title"); + let annotated_tool = tool.annotate(annotations); + + assert!(annotated_tool.annotations.is_some()); + assert_eq!( + annotated_tool.annotations.unwrap().title, + Some("Test Title".to_string()) + ); + } + + #[test] + fn test_tool_schema_as_json_value() { + let schema_obj = json!({"type": "object", "properties": {}}) + .as_object() + .unwrap() + .clone(); + let schema = Arc::new(schema_obj); + let tool = Tool::new("test_tool", "desc", schema); + + let json_value = tool.schema_as_json_value(); + assert!(json_value.is_object()); + assert_eq!(json_value["type"], "object"); + } + + #[test] + fn test_tool_annotations_default() { + let annotations = ToolAnnotations::default(); + assert_eq!(annotations.title, None); + assert_eq!(annotations.read_only_hint, None); + } + + #[test] + fn test_tool_with_title() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let tool = Tool { + name: "test_tool".into(), + title: Some("Test Tool".to_string()), + description: Some("desc".into()), + input_schema: schema, + output_schema: None, + annotations: None, + icons: None, + }; + assert_eq!(tool.title, Some("Test Tool".to_string())); + } + + #[test] + fn test_tool_with_icons() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let icon = Icon { + src: "icon.png".to_string(), + mime_type: Some("image/png".to_string()), + sizes: None, + }; + let tool = Tool { + name: "test_tool".into(), + title: None, + description: Some("desc".into()), + input_schema: schema, + output_schema: None, + annotations: None, + icons: Some(vec![icon]), + }; + assert_eq!(tool.icons.as_ref().unwrap().len(), 1); + } + + #[test] + fn test_tool_with_output_schema() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let output_schema = Arc::new(json!({"type": "string"}).as_object().unwrap().clone()); + let tool = Tool { + name: "test_tool".into(), + title: None, + description: Some("desc".into()), + input_schema: schema, + output_schema: Some(output_schema.clone()), + annotations: None, + icons: None, + }; + assert!(tool.output_schema.is_some()); + } + + #[test] + fn test_tool_with_different_schemas_not_equal() { + let schema1 = Arc::new( + json!({"type": "object", "properties": {"a": {"type": "string"}}}) + .as_object() + .unwrap() + .clone(), + ); + let schema2 = Arc::new( + json!({"type": "object", "properties": {"b": {"type": "number"}}}) + .as_object() + .unwrap() + .clone(), + ); + + let tool1 = Tool::new("test_tool", "desc", schema1); + let tool2 = Tool::new("test_tool", "desc", schema2); + + assert_ne!(tool1, tool2); + } + + #[test] + fn test_tool_annotations_with_all_hints() { + let annotations = ToolAnnotations::new() + .read_only(true) + .destructive(false) + .idempotent(true) + .open_world(false); + + assert_eq!(annotations.read_only_hint, Some(true)); + assert_eq!(annotations.destructive_hint, Some(false)); + assert_eq!(annotations.idempotent_hint, Some(true)); + assert_eq!(annotations.open_world_hint, Some(false)); + } + + #[test] + fn test_tool_annotations_destructive_defaults_to_true() { + let annotations1 = ToolAnnotations::new(); + let annotations2 = ToolAnnotations::new().destructive(true); + + assert!(annotations1.is_destructive()); + assert!(annotations2.is_destructive()); + } + + #[test] + fn test_tool_annotations_idempotent_defaults_to_false() { + let annotations1 = ToolAnnotations::new(); + let annotations2 = ToolAnnotations::new().idempotent(false); + + assert!(!annotations1.is_idempotent()); + assert!(!annotations2.is_idempotent()); + } + + #[test] + fn test_tool_annotations_contradictory_hints() { + // A tool can be both read-only and destructive (contradictory but allowed) + let annotations = ToolAnnotations::new().read_only(true).destructive(true); + + assert_eq!(annotations.read_only_hint, Some(true)); + assert_eq!(annotations.destructive_hint, Some(true)); + } + + #[test] + fn test_tool_serialization() { + let schema = Arc::new( + json!({"type": "object", "properties": {}}) + .as_object() + .unwrap() + .clone(), + ); + let tool = Tool::new("test_tool", "A test tool", schema); + let json = serde_json::to_string(&tool).unwrap(); + assert!(json.contains("test_tool")); + assert!(json.contains("A test tool")); + } + + #[test] + fn test_tool_deserialization() { + let json = r#"{ + "name": "test_tool", + "description": "A test tool", + "inputSchema": {"type": "object"} + }"#; + let tool: Tool = serde_json::from_str(json).unwrap(); + assert_eq!(tool.name, "test_tool"); + assert_eq!(tool.description, Some(Cow::Borrowed("A test tool"))); + } + + #[test] + fn test_tool_with_annotations_serialization() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let tool = + Tool::new("test_tool", "desc", schema).annotate(ToolAnnotations::new().read_only(true)); + let json = serde_json::to_value(&tool).unwrap(); + assert_eq!(json["annotations"]["readOnlyHint"], true); + } + + #[test] + fn test_tool_annotations_serialization() { + let annotations = ToolAnnotations::with_title("Test") + .read_only(true) + .destructive(false); + let json = serde_json::to_string(&annotations).unwrap(); + assert!(json.contains("Test")); + assert!(json.contains("readOnlyHint")); + } + + #[test] + fn test_tool_name_cow_borrowed() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let tool = Tool::new("static_name", "desc", schema); + assert_eq!(tool.name, "static_name"); + } + + #[test] + fn test_tool_name_cow_owned() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let name = String::from("dynamic_name"); + let tool = Tool::new(name, "desc", schema); + assert_eq!(tool.name, "dynamic_name"); + } + + #[test] + fn test_tool_annotations_is_idempotent_when_destructive() { + let annotations = ToolAnnotations::new().destructive(true).idempotent(false); + assert!(!annotations.is_idempotent()); + } + + #[test] + fn test_tool_schema_as_json_value_complex() { + let schema_obj = json!({ + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "number"} + }, + "required": ["name"] + }) + .as_object() + .unwrap() + .clone(); + let schema = Arc::new(schema_obj); + let tool = Tool::new("test_tool", "desc", schema); + + let json_value = tool.schema_as_json_value(); + assert_eq!(json_value["type"], "object"); + assert!(json_value["properties"]["name"].is_object()); + assert!(json_value["required"].is_array()); + } + + #[test] + fn test_tool_annotations_different_titles_not_equal() { + let annotations1 = ToolAnnotations::with_title("Title1"); + let annotations2 = ToolAnnotations::with_title("Title2"); + assert_ne!(annotations1, annotations2); + } + + #[test] + fn test_tool_without_annotations_vs_with_annotations() { + let schema = Arc::new(json!({"type": "object"}).as_object().unwrap().clone()); + let tool_without = Tool::new("test_tool", "desc", schema.clone()); + let tool_with = Tool::new("test_tool", "desc", schema).annotate(ToolAnnotations::new()); + + assert!(tool_without.annotations.is_none()); + assert!(tool_with.annotations.is_some()); + assert_ne!(tool_without, tool_with); + } +}