//! Tests for `CapabilityExecutor` and `ResponseCache`
use super::*;
use crate::capability::response_cache::ResponseCache;
#[test]
fn test_build_url() {
let executor = CapabilityExecutor::new();
let config = RestConfig {
base_url: "https://api.example.com".to_string(),
path: "/users/{id}/posts/{post_id}".to_string(),
..Default::default()
};
let params = serde_json::json!({
"id": "123",
"post_id": 456
});
let url = executor.build_url(&config, ¶ms).unwrap();
assert_eq!(url, "https://api.example.com/users/123/posts/456");
}
#[test]
fn test_substitute_string() {
let executor = CapabilityExecutor::new();
let template = "Hello {name}, your score is {score}";
let params = serde_json::json!({
"name": "World",
"score": 100
});
let result = executor.substitute_string(template, ¶ms).unwrap();
assert_eq!(result, "Hello World, your score is 100");
}
#[test]
fn test_extract_path() {
let executor = CapabilityExecutor::new();
let value = serde_json::json!({
"data": {
"users": [
{"name": "Alice"},
{"name": "Bob"}
]
}
});
let result = executor.extract_path(&value, "data.users").unwrap();
assert!(result.is_array());
assert_eq!(result.as_array().unwrap().len(), 2);
let result = executor.extract_path(&value, "data.users.0.name").unwrap();
assert_eq!(result, "Alice");
}
#[test]
fn test_cache() {
let cache = ResponseCache::new();
let value = serde_json::json!({"test": true});
cache.set("key1", &value, 60);
assert_eq!(cache.get("key1"), Some(value));
assert_eq!(cache.get("nonexistent"), None);
}
#[test]
fn test_fetch_from_file_simple() {
let executor = CapabilityExecutor::new();
let dir = std::env::temp_dir().join("mcp-gateway-test-cred");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("tokens.json");
std::fs::write(
&file,
r#"{"access_token": "test-token-123", "refresh_token": "refresh-456"}"#,
)
.unwrap();
let spec = format!("{}:access_token", file.display());
let result = executor.fetch_from_file(&spec).unwrap();
assert_eq!(result, "test-token-123");
let spec = format!("{}:refresh_token", file.display());
let result = executor.fetch_from_file(&spec).unwrap();
assert_eq!(result, "refresh-456");
// Cleanup
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_fetch_from_file_nested() {
let executor = CapabilityExecutor::new();
let dir = std::env::temp_dir().join("mcp-gateway-test-cred-nested");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("config.json");
std::fs::write(
&file,
r#"{"auth": {"google": {"token": "nested-token"}, "count": 42}}"#,
)
.unwrap();
let spec = format!("{}:auth.google.token", file.display());
let result = executor.fetch_from_file(&spec).unwrap();
assert_eq!(result, "nested-token");
// Numeric values work too
let spec = format!("{}:auth.count", file.display());
let result = executor.fetch_from_file(&spec).unwrap();
assert_eq!(result, "42");
// Cleanup
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_fetch_from_file_missing_field() {
let executor = CapabilityExecutor::new();
let dir = std::env::temp_dir().join("mcp-gateway-test-cred-missing");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("tokens.json");
std::fs::write(&file, r#"{"access_token": "value"}"#).unwrap();
let spec = format!("{}:nonexistent", file.display());
let result = executor.fetch_from_file(&spec);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
// Cleanup
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn test_fetch_from_file_missing_file() {
let executor = CapabilityExecutor::new();
let result = executor.fetch_from_file("/nonexistent/path/tokens.json:field");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Failed to read"));
}
#[test]
fn test_fetch_from_file_invalid_format() {
let executor = CapabilityExecutor::new();
// No colon separator for field
let result = executor.fetch_from_file("/path/to/file.json");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid file credential format"));
}
#[test]
fn test_substitute_params_skips_unresolved_placeholders() {
let executor = CapabilityExecutor::new();
let mut template = std::collections::HashMap::new();
template.insert("resolved".to_string(), "{name}".to_string());
template.insert("unresolved".to_string(), "{missing_param}".to_string());
template.insert("static_val".to_string(), "hello".to_string());
let params = serde_json::json!({"name": "world"});
let result = executor.substitute_params(&template, ¶ms).unwrap();
// resolved and static_val should be present, unresolved should be filtered
let keys: Vec<&str> = result.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"resolved"));
assert!(keys.contains(&"static_val"));
assert!(!keys.contains(&"unresolved"));
}
// ── static_params integration tests ──────────────────────────────────────────
#[test]
fn static_params_flow_into_url_substitution() {
// GIVEN: RestConfig with static_params containing a path placeholder value
// WHEN: build_url is called with empty caller params
// THEN: static_params are substituted into the URL path
let executor = CapabilityExecutor::new();
let config = RestConfig {
base_url: "https://api.example.com".to_string(),
path: "/v1/forecast/{format}".to_string(),
static_params: [("format".to_string(), serde_json::json!("json"))]
.into_iter()
.collect(),
..Default::default()
};
let caller_params = serde_json::json!({});
let effective = config.merge_with_static_params(&caller_params);
let url = executor.build_url(&config, &effective).unwrap();
assert_eq!(url, "https://api.example.com/v1/forecast/json");
}
#[test]
fn static_params_flow_into_query_params() {
// GIVEN: RestConfig with static_params and a query params template referencing them
// WHEN: substitute_params is called with merged effective params
// THEN: static values appear in the resolved query parameters
let executor = CapabilityExecutor::new();
let config = RestConfig {
base_url: "https://api.example.com".to_string(),
path: "/forecast".to_string(),
params: [
("current".to_string(), "{current}".to_string()),
("timezone".to_string(), "{timezone}".to_string()),
]
.into_iter()
.collect(),
static_params: [
(
"current".to_string(),
serde_json::json!("temperature_2m,weather_code"),
),
("timezone".to_string(), serde_json::json!("auto")),
]
.into_iter()
.collect(),
..Default::default()
};
let caller_params = serde_json::json!({});
let effective = config.merge_with_static_params(&caller_params);
let resolved = executor.substitute_params(&config.params, &effective).unwrap();
let map: std::collections::HashMap<_, _> = resolved.into_iter().collect();
assert_eq!(map["current"], "temperature_2m,weather_code");
assert_eq!(map["timezone"], "auto");
}
#[test]
fn caller_params_override_static_params_on_collision() {
// GIVEN: static_params defines "timezone" = "auto"
// WHEN: caller supplies "timezone" = "America/New_York"
// THEN: caller value wins
let config = RestConfig {
static_params: [("timezone".to_string(), serde_json::json!("auto"))]
.into_iter()
.collect(),
..Default::default()
};
let caller_params = serde_json::json!({ "timezone": "America/New_York" });
let effective = config.merge_with_static_params(&caller_params);
assert_eq!(effective["timezone"], "America/New_York");
}
#[test]
fn static_params_preserved_when_caller_provides_no_collision() {
// GIVEN: static_params has "format" key, caller provides "lat" key
// WHEN: merging
// THEN: both keys are present in the effective params
let config = RestConfig {
static_params: [("format".to_string(), serde_json::json!("json"))]
.into_iter()
.collect(),
..Default::default()
};
let caller_params = serde_json::json!({ "lat": 60.17 });
let effective = config.merge_with_static_params(&caller_params);
assert_eq!(effective["format"], "json");
assert!((effective["lat"].as_f64().unwrap() - 60.17).abs() < f64::EPSILON);
}
#[test]
fn empty_static_params_returns_borrowed_caller_params() {
// GIVEN: RestConfig with no static_params
// WHEN: merging with caller params
// THEN: returned Cow is Borrowed (zero allocation)
let config = RestConfig::default();
let caller_params = serde_json::json!({ "q": "rust" });
let effective = config.merge_with_static_params(&caller_params);
assert!(matches!(effective, std::borrow::Cow::Borrowed(_)));
assert_eq!(effective["q"], "rust");
}
#[test]
fn static_params_support_numeric_and_boolean_values() {
// GIVEN: static_params with integer, float, and boolean values
// WHEN: merging with empty caller params
// THEN: all typed values are preserved
let config = RestConfig {
static_params: [
("count".to_string(), serde_json::json!(10)),
("ratio".to_string(), serde_json::json!(0.5)),
("enabled".to_string(), serde_json::json!(true)),
]
.into_iter()
.collect(),
..Default::default()
};
let empty = serde_json::json!({});
let effective = config.merge_with_static_params(&empty);
assert_eq!(effective["count"], serde_json::json!(10));
assert!((effective["ratio"].as_f64().unwrap() - 0.5).abs() < f64::EPSILON);
assert_eq!(effective["enabled"], serde_json::json!(true));
}
#[test]
fn static_params_deserialization_from_yaml() {
// GIVEN: YAML config with static_params containing mixed types
// WHEN: deserializing RestConfig
// THEN: all static_params are correctly parsed
let yaml = r"
base_url: https://api.open-meteo.com
path: /v1/forecast
static_params:
current: 'temperature_2m,weather_code'
timezone: auto
forecast_days: 7
";
let config: RestConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.static_params["current"], "temperature_2m,weather_code");
assert_eq!(config.static_params["timezone"], "auto");
assert_eq!(config.static_params["forecast_days"], serde_json::json!(7));
}
#[test]
fn static_params_serialization_round_trips() {
// GIVEN: RestConfig with static_params
// WHEN: serialized to JSON and back
// THEN: static_params are preserved exactly
let mut static_params = std::collections::HashMap::new();
static_params.insert("key".to_string(), serde_json::json!("value"));
static_params.insert("num".to_string(), serde_json::json!(42));
let config = RestConfig {
base_url: "https://example.com".to_string(),
static_params,
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let restored: RestConfig = serde_json::from_str(&json).unwrap();
assert_eq!(restored.static_params["key"], "value");
assert_eq!(restored.static_params["num"], serde_json::json!(42));
}
#[test]
fn static_params_with_array_value_preserved() {
// GIVEN: static_params containing a JSON array value
// WHEN: merging
// THEN: array is preserved in the effective params
let config = RestConfig {
static_params: [(
"fields".to_string(),
serde_json::json!(["id", "name", "email"]),
)]
.into_iter()
.collect(),
..Default::default()
};
let empty = serde_json::json!({});
let effective = config.merge_with_static_params(&empty);
assert_eq!(
effective["fields"],
serde_json::json!(["id", "name", "email"])
);
}
#[test]
fn build_url_with_static_params_substitution_in_endpoint() {
// GIVEN: endpoint-style URL with static params embedded as placeholders
// WHEN: merged with empty caller params and URL built
// THEN: static values are substituted into the endpoint URL
let executor = CapabilityExecutor::new();
let config = RestConfig {
endpoint: "https://api.example.com/v1/data?format={fmt}&version={ver}".to_string(),
static_params: [
("fmt".to_string(), serde_json::json!("json")),
("ver".to_string(), serde_json::json!("2")),
]
.into_iter()
.collect(),
..Default::default()
};
let caller_params = serde_json::json!({});
let effective = config.merge_with_static_params(&caller_params);
let url = executor.build_url(&config, &effective).unwrap();
assert!(url.contains("format=json"), "URL should contain format=json");
assert!(url.contains("version=2"), "URL should contain version=2");
}