#[cfg(test)]
mod tests {
use crate::core::error::WorkflowError;
use crate::clients::notion::{
NotionClientBuilder, NotionClientNode, NotionConfig,
};
use crate::core::nodes::Node;
use crate::core::nodes::external_mcp_client::RetryConfig;
use crate::core::task::TaskContext;
use serde_json::Value;
#[test]
fn test_notion_config_creation() {
// Test HTTP configuration
let http_config = NotionConfig::new_http(
"http://localhost:8002".to_string(),
Some("test-api-key".to_string()),
);
assert_eq!(http_config.base_config.service_name, "notion");
assert!(matches!(
http_config.base_config.transport,
crate::transport::TransportType::Http { .. }
));
assert!(http_config.base_config.auth.is_some());
// Test WebSocket configuration
let ws_config = NotionConfig::new_websocket("ws://localhost:8002".to_string());
assert_eq!(ws_config.base_config.service_name, "notion");
assert!(matches!(
ws_config.base_config.transport,
crate::transport::TransportType::WebSocket { .. }
));
// Test stdio configuration
let stdio_config =
NotionConfig::new_stdio("python".to_string(), vec!["notion_server.py".to_string()]);
assert_eq!(stdio_config.base_config.service_name, "notion");
assert!(matches!(
stdio_config.base_config.transport,
crate::transport::TransportType::Stdio { .. }
));
}
#[test]
fn test_notion_client_builder() {
let client = NotionClientBuilder::new_http("http://localhost:8002".to_string())
.with_api_key("test-key".to_string())
.with_workspace_id("workspace-123".to_string())
.with_default_database_id("db-456".to_string())
.with_retry_config(RetryConfig {
max_retries: 5,
initial_delay_ms: 500,
max_delay_ms: 10000,
backoff_multiplier: 1.5,
})
.build();
assert_eq!(
client.config.workspace_id,
Some("workspace-123".to_string())
);
assert_eq!(
client.config.default_database_id,
Some("db-456".to_string())
);
assert_eq!(client.config.base_config.retry_config.max_retries, 5);
}
#[test]
fn test_node_implementation() {
let client = NotionClientNode::new(NotionConfig::new_http(
"http://localhost:8002".to_string(),
None,
));
// Test node name
assert!(client.node_name().contains("NotionClientNode"));
assert!(client.node_name().contains("notion"));
// Test process method
let mut context = TaskContext::new(
"test-task".to_string(),
Value::String("test-workflow".to_string()),
);
let result = client.process(context.clone()).unwrap();
assert_eq!(
result.get_data("notion_client_available").unwrap(),
Some(Value::Bool(true))
);
assert!(
result
.get_data::<Value>("notion_client_config")
.unwrap()
.is_some()
);
}
#[tokio::test]
async fn test_search_pages_arguments() {
let mut client = NotionClientNode::new(NotionConfig::new_http(
"http://localhost:8002".to_string(),
None,
));
// Mock a successful tool call - in real tests this would use a mock MCP client
// For now, we're testing that the arguments are properly formatted
// This would normally call the MCP server, but since we can't mock it here,
// we'll just verify the method compiles and can be called
// In integration tests, we'll test against a real MCP server
}
#[test]
fn test_error_parsing() {
let client = NotionClientNode::new(NotionConfig::new_http(
"http://localhost:8002".to_string(),
None,
));
// Test unauthorized error
let auth_error = WorkflowError::MCPError {
message: "Request failed with status 401 unauthorized".to_string(),
};
let parsed = client.parse_notion_error(&auth_error);
match parsed {
WorkflowError::MCPError { message } => {
assert!(message.contains("Notion authentication failed"));
}
_ => panic!("Expected MCPError"),
}
// Test not found error
let not_found_error = WorkflowError::MCPError {
message: "Page not_found with status 404".to_string(),
};
let parsed = client.parse_notion_error(¬_found_error);
match parsed {
WorkflowError::MCPError { message } => {
assert!(message.contains("Notion resource not found"));
}
_ => panic!("Expected MCPError"),
}
// Test rate limit error
let rate_error = WorkflowError::MCPError {
message: "Request failed with rate_limit status 429".to_string(),
};
let parsed = client.parse_notion_error(&rate_error);
match parsed {
WorkflowError::MCPError { message } => {
assert!(message.contains("Notion rate limit exceeded"));
}
_ => panic!("Expected MCPError"),
}
}
#[test]
fn test_config_serialization() {
let config = NotionConfig::new_http(
"http://localhost:8002".to_string(),
Some("api-key".to_string()),
);
// Test that config can be serialized
let serialized = serde_json::to_string(&config).unwrap();
let deserialized: NotionConfig = serde_json::from_str(&serialized).unwrap();
assert_eq!(
config.base_config.service_name,
deserialized.base_config.service_name
);
}
}
#[cfg(test)]
mod mock_tests {
use super::*;
use crate::core::error::WorkflowError;
use crate::protocol::{CallToolResult, ToolContent, ToolDefinition};
use async_trait::async_trait;
use serde_json::json;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
/// Mock MCP client for testing
#[derive(Debug)]
struct MockMCPClient {
tools: Vec<ToolDefinition>,
responses: Arc<Mutex<HashMap<String, CallToolResult>>>,
connected: bool,
}
impl MockMCPClient {
fn new() -> Self {
let mut tools = vec![];
// Add mock Notion tools
tools.push(ToolDefinition {
name: "search_pages".to_string(),
description: Some("Search for pages in Notion".to_string()),
input_schema: json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": "number",
"description": "Maximum number of results"
}
},
"required": ["query"]
}),
});
tools.push(ToolDefinition {
name: "create_page".to_string(),
description: Some("Create a new page in Notion".to_string()),
input_schema: json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Page title"
},
"content": {
"type": "string",
"description": "Page content"
},
"parent_id": {
"type": "string",
"description": "Parent page or database ID"
}
},
"required": ["title", "content"]
}),
});
Self {
tools,
responses: Arc::new(Mutex::new(HashMap::new())),
connected: false,
}
}
fn add_response(&self, tool_name: &str, response: CallToolResult) {
let responses = self.responses.clone();
let tool_name = tool_name.to_string();
tokio::spawn(async move {
let mut responses = responses.lock().await;
responses.insert(tool_name, response);
});
}
}
#[async_trait]
impl crate::clients::MCPClient for MockMCPClient {
async fn connect(&mut self) -> Result<(), WorkflowError> {
self.connected = true;
Ok(())
}
async fn initialize(
&mut self,
_client_name: &str,
_client_version: &str,
) -> Result<(), WorkflowError> {
if !self.connected {
return Err(WorkflowError::MCPConnectionError {
message: "Not connected".to_string(),
});
}
Ok(())
}
async fn list_tools(&mut self) -> Result<Vec<ToolDefinition>, WorkflowError> {
if !self.connected {
return Err(WorkflowError::MCPConnectionError {
message: "Not connected".to_string(),
});
}
Ok(self.tools.clone())
}
async fn call_tool(
&mut self,
name: &str,
_arguments: Option<HashMap<String, serde_json::Value>>,
) -> Result<CallToolResult, WorkflowError> {
if !self.connected {
return Err(WorkflowError::MCPConnectionError {
message: "Not connected".to_string(),
});
}
let responses = self.responses.lock().await;
if let Some(response) = responses.get(name) {
Ok(response.clone())
} else {
Ok(CallToolResult {
content: vec![ToolContent::Text {
text: json!({
"status": "success",
"tool": name,
"message": "Mock response"
})
.to_string(),
}],
is_error: Some(false),
})
}
}
async fn disconnect(&mut self) -> Result<(), WorkflowError> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
}
}