use crate::client::builder::ClientBuilder;
use crate::error::Error;
use crate::types::{
CallToolResult, ClientCapabilities, ListToolsResult, MessageContent, ReadResourceResult,
ServerCapabilities, Tool,
};
use tokio;
async fn create_test_client() -> Result<crate::client::Client, Error> {
ClientBuilder::new("uvx")
.arg("notes-simple")
.spawn_and_initialize()
.await
}
#[tokio::test]
async fn test_notes_simple_basic_functionality() -> Result<(), Error> {
let client = create_test_client().await?;
let caps: Option<ServerCapabilities> = client.capabilities().await;
assert!(
caps.is_some(),
"Server should return capabilities after initialization"
);
Ok(())
}
#[tokio::test]
async fn test_list_tools_schema() -> Result<(), Error> {
let client = create_test_client().await?;
let tools_result = client.list_tools().await?;
assert!(
!tools_result.tools.is_empty(),
"Expected at least one tool from the server"
);
let maybe_add_note_tool = tools_result.tools.iter().find(|t| t.name == "add-note");
assert!(
maybe_add_note_tool.is_some(),
"Expected the 'add-note' tool to be listed"
);
let add_note_tool = maybe_add_note_tool.unwrap();
assert_eq!(
add_note_tool.description, "Add a new note",
"Tool 'add-note' should have the correct description"
);
let schema = &add_note_tool
.input_schema
.as_object()
.expect("Expected inputSchema for add-note");
assert_eq!(schema.get("type").and_then(|v| v.as_str()), Some("object"));
assert!(
schema.get("properties").is_some(),
"Expected 'properties' in inputSchema"
);
let required = schema
.get("required")
.and_then(|r| r.as_array())
.expect("Expected 'required' array");
let required_fields: Vec<String> = required
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
assert!(
required_fields.contains(&"name".to_string())
&& required_fields.contains(&"content".to_string()),
"Expected 'name' and 'content' to be required fields"
);
Ok(())
}
#[tokio::test]
async fn test_call_add_note_success() -> Result<(), Error> {
let client = create_test_client().await?;
let arguments = serde_json::json!({
"name": "my-test-note",
"content": "This is a test note"
});
let call_result = client.call_tool("add-note", arguments).await?;
assert_eq!(call_result.is_error, false, "Tool call should succeed");
assert!(
!call_result.content.is_empty(),
"Expected some text content after calling add-note"
);
if let Some(MessageContent::Text { text }) = call_result.content.first() {
assert!(
text.contains("my-test-note"),
"Response text should mention the newly added note name"
);
}
Ok(())
}
#[tokio::test]
async fn test_call_add_note_missing_args() -> Result<(), Error> {
let client = create_test_client().await?;
let arguments = serde_json::json!({ "name": "only-name-provided" });
let bad_result = client.call_tool("add-note", arguments).await;
assert!(
bad_result.is_err(),
"Expected error when missing required fields (tool-level isError)"
);
Ok(())
}
#[tokio::test]
async fn test_call_add_note_wrong_types() -> Result<(), Error> {
let client = create_test_client().await?;
let arguments = serde_json::json!({
"name": "numeric-content",
"content": 123
});
let bad_result = client.call_tool("add-note", arguments).await;
assert!(
bad_result.is_err(),
"Expected error for a numeric content field (tool-level isError)"
);
Ok(())
}
#[tokio::test]
async fn test_resource_list_after_adding_note() -> Result<(), Error> {
let client = create_test_client().await?;
let arguments = serde_json::json!({
"name": "listed-note",
"content": "Note content"
});
client.call_tool("add-note", arguments).await?;
let resources_value = client.request("resources/list", None).await?;
let resources_array = resources_value
.get("resources")
.and_then(|val| val.as_array())
.expect("Expected 'resources' array in the server's response");
let found_note = resources_array.iter().any(|res| {
res.get("name")
.and_then(|val| val.as_str())
.map(|name| name.contains("listed-note"))
.unwrap_or(false)
});
assert!(
found_note,
"Expected 'listed-note' among the listed resources"
);
Ok(())
}
#[tokio::test]
async fn test_read_resource_of_added_note() -> Result<(), Error> {
let client = create_test_client().await?;
let note_name = "readable-note";
let content_str = "Hello, I am a readable note";
let arguments = serde_json::json!({
"name": note_name,
"content": content_str
});
client.call_tool("add-note", arguments).await?;
let resource_uri = format!("note://internal/{}", note_name);
let read_result = client.read_resource(&resource_uri).await?;
assert!(
!read_result.contents.is_empty(),
"Expected at least one resource content block"
);
match &read_result.contents[0] {
crate::types::ResourceContents::Text { text, .. } => {
assert_eq!(text, content_str, "Read content should match the original")
}
_ => panic!("Expected text resource content"),
};
Ok(())
}
#[tokio::test]
async fn test_call_tool_invalid_name() -> Result<(), Error> {
let client = create_test_client().await?;
let bad_result = client
.call_tool("this_tool_does_not_exist", serde_json::json!({}))
.await;
assert!(
bad_result.is_err(),
"Expected error when calling a non-existent tool (tool-level isError)"
);
Ok(())
}
#[tokio::test]
async fn test_resource_list_changed_notification_handling() -> Result<(), Error> {
let client = create_test_client().await?;
let arguments = serde_json::json!({
"name": "note-with-notification",
"content": "Triggering list_changed"
});
let _ = client.call_tool("add-note", arguments).await?;
let tools_result = client.list_tools().await?;
assert!(
!tools_result.tools.is_empty(),
"Expected the client to still be able to list tools"
);
Ok(())
}
#[tokio::test]
async fn test_ping_request() -> Result<(), Error> {
let client = create_test_client().await?;
let ping_result = client.request("ping", None).await;
match ping_result {
Ok(val) => {
assert!(
val.as_object().map_or(true, |map| map.is_empty()),
"Expected an empty result object for ping"
);
}
Err(e) => {
tracing::warn!("Ping not implemented by server, got error: {:?}", e);
}
}
Ok(())
}
#[tokio::test]
async fn test_set_log_level() -> Result<(), Error> {
let client = create_test_client().await?;
let set_result = client
.request(
"logging/setLevel",
Some(serde_json::json!({ "level": "info" })),
)
.await;
match set_result {
Ok(_) => {
tracing::info!("Successfully set log level to info");
}
Err(e) => {
tracing::warn!("Server does not support logging/setLevel: {:?}", e);
}
}
Ok(())
}