Files
galaxy/app/src/ai/mcp/mod_tests.rs
T

989 lines
34 KiB
Rust

use std::collections::HashMap;
use serde_json;
use warp_managed_secrets::ManagedSecretValue;
#[cfg(not(target_family = "wasm"))]
use crate::ai::mcp::parsing::normalize_codex_toml_to_json;
use crate::ai::mcp::parsing::resolve_json;
use crate::ai::mcp::{
mcp_provider_from_file_path, CLIServer, JsonTemplate, MCPProvider, MCPServer, MCPServerExt,
ParsedTemplatableMCPServerResult, ServerSentEvents, StaticEnvVar, StaticHeader,
TemplatableMCPServer, TemplatableMCPServerInstallation, TemplateVariable, TransportType,
VariableType, VariableValue,
};
#[test]
fn mcp_provider_from_file_path_recognizes_warp_home_path() {
if let Some(galaxy_home_mcp_config_file_path) =
galaxy_core::paths::galaxy_home_mcp_config_file_path()
{
assert_eq!(
mcp_provider_from_file_path(&galaxy_home_mcp_config_file_path),
Some(MCPProvider::Warp)
);
}
}
/// Helper function to create a test TemplatableMCPServerInstallation with custom values
fn create_test_installation(
name: &str,
template_json: &str,
variables: Vec<(&str, &str)>,
) -> TemplatableMCPServerInstallation {
let template_variables = variables
.iter()
.map(|(key, _)| TemplateVariable {
key: key.to_string(),
allowed_values: None,
})
.collect();
let variable_values = variables
.into_iter()
.map(|(key, value)| {
(
key.to_string(),
VariableValue {
variable_type: VariableType::Text,
value: value.to_string(),
},
)
})
.collect();
let templatable_mcp_server = TemplatableMCPServer {
uuid: uuid::Uuid::new_v4(),
name: name.to_string(),
description: None,
template: JsonTemplate {
json: template_json.to_string(),
variables: template_variables,
},
version: 1234567890,
gallery_data: None,
};
TemplatableMCPServerInstallation::new(
uuid::Uuid::new_v4(),
templatable_mcp_server,
variable_values,
)
}
#[test]
fn test_sse_server_with_headers() {
// Test that ServerSentEvents transport type with headers serializes correctly
let static_headers = vec![
StaticHeader {
name: "Authorization".to_string(),
value: "Bearer token123".to_string(),
},
StaticHeader {
name: "X-Custom-Header".to_string(),
value: "custom-value".to_string(),
},
];
let sse_server = ServerSentEvents {
url: "https://example.com/sse".to_string(),
headers: static_headers,
};
let mcp_server = MCPServer {
transport_type: TransportType::ServerSentEvents(sse_server),
name: "sse-server-with-headers".to_string(),
uuid: uuid::Uuid::new_v4(),
};
// Test to_user_json includes headers
let user_json = mcp_server.to_user_json();
assert!(
user_json.contains("Bearer token123"),
"User JSON should contain Authorization header value: {user_json}",
);
assert!(
user_json.contains("X-Custom-Header"),
"User JSON should contain custom header name: {user_json}",
);
assert!(
user_json.contains("custom-value"),
"User JSON should contain custom header value: {user_json}",
);
// Test from_user_json can parse headers
let parsed_servers =
MCPServer::from_user_json(&user_json).expect("Failed to parse MCP server from user JSON");
assert_eq!(parsed_servers.len(), 1);
let parsed_server = &parsed_servers[0];
if let TransportType::ServerSentEvents(parsed_sse) = &parsed_server.transport_type {
assert_eq!(parsed_sse.url, "https://example.com/sse");
assert_eq!(parsed_sse.headers.len(), 2);
assert!(parsed_sse
.headers
.iter()
.any(|h| h.name == "Authorization" && h.value == "Bearer token123"));
assert!(parsed_sse
.headers
.iter()
.any(|h| h.name == "X-Custom-Header" && h.value == "custom-value"));
} else {
panic!("Expected ServerSentEvents transport type");
}
}
#[test]
fn test_sse_server_headers_default() {
// Test that headers default to empty map when not specified
let json = r#"{
"test-server": {
"url": "https://example.com/sse"
}
}"#;
let servers = MCPServer::from_user_json(json).expect("Failed to parse MCP servers");
assert_eq!(servers.len(), 1);
if let TransportType::ServerSentEvents(sse_server) = &servers[0].transport_type {
assert_eq!(sse_server.url, "https://example.com/sse");
assert!(
sse_server.headers.is_empty(),
"Headers should default to empty"
);
} else {
panic!("Expected ServerSentEvents transport type");
}
}
#[test]
fn test_hash_consistency() {
// Create two identical installations and verify they produce the same hash
let installation1 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
let installation2 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
assert_eq!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Identical installations should produce the same hash"
);
}
#[test]
fn test_hash_different_names() {
// Verify that different names produce different hashes
let installation1 = create_test_installation(
"server-one",
r#"{"server-one":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
let installation2 = create_test_installation(
"server-two",
r#"{"server-two":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
assert_ne!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Installations with different names should produce different hashes"
);
}
#[test]
fn test_hash_different_variable_values() {
// Verify that different variable values produce different hashes
let installation1 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
let installation2 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "different-secret")],
);
assert_ne!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Installations with different variable values should produce different hashes"
);
}
#[test]
fn test_hash_different_json_templates() {
// Verify that different JSON templates produce different hashes
let installation1 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
let installation2 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"python","args":["server.py"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
assert_ne!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Installations with different JSON templates should produce different hashes"
);
}
#[test]
fn test_hash_different_variables() {
// Verify that different variables produce different hashes
let installation1 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
let installation2 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"TOKEN":"{{TOKEN}}"}}}"#,
vec![("TOKEN", "secret123")],
);
assert_ne!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Installations with different variables should produce different hashes"
);
}
#[test]
fn test_hash_multiple_variables_order_independent() {
// Verify that variable order doesn't affect hash (BTreeMap ensures consistent ordering)
let installation1 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"API_KEY":"{{API_KEY}}","TOKEN":"{{TOKEN}}"}}}"#,
vec![("API_KEY", "secret123"), ("TOKEN", "token456")],
);
let installation2 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"API_KEY":"{{API_KEY}}","TOKEN":"{{TOKEN}}"}}}"#,
vec![("TOKEN", "token456"), ("API_KEY", "secret123")],
);
assert_eq!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Hash should be order-independent for multiple variables"
);
}
#[test]
fn test_hash_ignores_installation_uuid() {
// Verify that installation UUID doesn't affect hash (only name, template, and variable values)
let installation1 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
let installation2 = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","args":["server"],"env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "secret123")],
);
// Even though these have different UUIDs (created separately), hashes should be the same
assert_ne!(
installation1.uuid(),
installation2.uuid(),
"UUIDs should be different"
);
assert_eq!(
installation1.hash().expect("hash should succeed"),
installation2.hash().expect("hash should succeed"),
"Hash should not depend on installation UUID"
);
}
#[test]
fn test_to_parsed_templatable_mcp_server_result() {
let mcp_server = MCPServer {
transport_type: TransportType::CLIServer(CLIServer {
command: "npx".to_string(),
args: vec!["@modelcontextprotocol/server-postgres".to_string()],
cwd_parameter: None,
static_env_vars: vec![StaticEnvVar {
name: "API_KEY".to_string(),
value: "SOME_SECRET".to_string(),
}],
}),
name: "test-server".to_string(),
uuid: uuid::Uuid::new_v4(),
};
let parsed_result = mcp_server.to_parsed_templatable_mcp_server_result();
let actual_json_value = serde_json::from_str::<serde_json::Value>(
parsed_result.templatable_mcp_server.template.json.as_str(),
)
.unwrap();
let expected_json_value = serde_json::from_str::<serde_json::Value>(r#"{"test-server":{"command":"npx","args":["@modelcontextprotocol/server-postgres"],"env":{"API_KEY":"{{API_KEY}}"},"working_directory":null}}"#).unwrap();
assert_eq!(parsed_result.templatable_mcp_server.name, "test-server");
assert_eq!(actual_json_value, expected_json_value);
assert_eq!(
parsed_result
.templatable_mcp_server
.template
.variables
.len(),
1
);
assert_eq!(
parsed_result.templatable_mcp_server.template.variables[0].key,
"API_KEY"
);
let variable_values = parsed_result
.templatable_mcp_server_installation
.as_ref()
.unwrap()
.variable_values();
assert_eq!(variable_values.len(), 1);
assert_eq!(variable_values["API_KEY"].variable_type, VariableType::Text);
assert_eq!(variable_values["API_KEY"].value, "SOME_SECRET");
}
#[test]
fn test_to_parsed_templatable_mcp_server_result_sse_headers() {
let mcp_server = MCPServer {
transport_type: TransportType::ServerSentEvents(ServerSentEvents {
url: "https://example.com/sse".to_string(),
headers: vec![
StaticHeader {
name: "Authorization".to_string(),
value: "Bearer token123".to_string(),
},
StaticHeader {
name: "X-Custom-Header".to_string(),
value: "custom-value".to_string(),
},
],
}),
name: "sse-server".to_string(),
uuid: uuid::Uuid::new_v4(),
};
let parsed_result = mcp_server.to_parsed_templatable_mcp_server_result();
let actual_json_value = serde_json::from_str::<serde_json::Value>(
parsed_result.templatable_mcp_server.template.json.as_str(),
)
.unwrap();
let expected_json_value = serde_json::from_str::<serde_json::Value>(
r#"{"sse-server":{"url":"https://example.com/sse","headers":{"Authorization":"{{Authorization}}","X-Custom-Header":"{{X-Custom-Header}}"}}}"#,
)
.unwrap();
assert_eq!(actual_json_value, expected_json_value);
let mut variable_keys = parsed_result
.templatable_mcp_server
.template
.variables
.iter()
.map(|v| v.key.as_str())
.collect::<Vec<_>>();
variable_keys.sort();
assert_eq!(variable_keys, vec!["Authorization", "X-Custom-Header"]);
let variable_values = parsed_result
.templatable_mcp_server_installation
.as_ref()
.unwrap()
.variable_values();
assert_eq!(variable_values["Authorization"].value, "Bearer token123");
assert_eq!(
variable_values["Authorization"].variable_type,
VariableType::Text
);
assert_eq!(variable_values["X-Custom-Header"].value, "custom-value");
assert_eq!(
variable_values["X-Custom-Header"].variable_type,
VariableType::Text
);
}
#[test]
fn test_parse_cli_server_without_args() {
// MCP configs should work without an explicit "args" field.
// The args field should default to an empty array.
let json = r#"{
"my-server": {
"command": "uvx",
"env": {
"API_KEY": "secret123"
}
}
}"#;
let servers = MCPServer::from_user_json(json).expect("Failed to parse MCP servers");
assert_eq!(servers.len(), 1);
if let TransportType::CLIServer(cli_server) = &servers[0].transport_type {
assert_eq!(cli_server.command, "uvx");
assert!(
cli_server.args.is_empty(),
"Args should default to empty when not specified"
);
} else {
panic!("Expected CLIServer transport type");
}
}
#[test]
fn test_parse_cli_server_preserves_explicit_working_directory() {
// An explicitly-set `working_directory` in a `.mcp.json`-style config must
// round-trip into `CLIServer.cwd_parameter` so the file-based spawner does
// not overwrite it with the discovery-root default.
let json = r#"{
"my-server": {
"command": "node",
"args": ["./tooling/mcp/server.js"],
"working_directory": "/explicit/override/path"
}
}"#;
let servers = MCPServer::from_user_json(json).expect("Failed to parse MCP servers");
assert_eq!(servers.len(), 1);
let TransportType::CLIServer(cli_server) = &servers[0].transport_type else {
panic!("Expected CLIServer transport type");
};
assert_eq!(
cli_server.cwd_parameter.as_deref(),
Some("/explicit/override/path"),
"Explicit working_directory must be preserved through parsing"
);
}
#[test]
fn test_parse_templatable_cli_server_without_args_and_resolve_json() {
// Templatable MCP configs should work without an explicit "args" field.
let json = r#"{
"my-server": {
"command": "uvx",
"env": {
"API_KEY": "secret123"
}
}
}"#;
let parsed = ParsedTemplatableMCPServerResult::from_user_json(json)
.expect("Failed to parse templatable MCP server JSON");
assert_eq!(parsed.len(), 1);
let installation = parsed[0]
.templatable_mcp_server_installation
.as_ref()
.expect("Installation should be present when all variables are provided");
// The resolved JSON should parse successfully via MCPServer::from_user_json
let resolved = resolve_json(installation);
let servers = MCPServer::from_user_json(&resolved).expect("Failed to parse resolved MCP JSON");
assert_eq!(servers.len(), 1);
if let TransportType::CLIServer(cli_server) = &servers[0].transport_type {
assert_eq!(cli_server.command, "uvx");
assert!(
cli_server.args.is_empty(),
"Args should default to empty when not specified"
);
} else {
panic!("Expected CLIServer transport type");
}
}
// ── Codex TOML normalizer tests ────────────────────────────────────────
/// Basic STDIO server: `command` + `args` round-trips cleanly.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_basic_stdio_server() {
let toml = r#"
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let parsed = ParsedTemplatableMCPServerResult::from_user_json(&json)
.expect("from_user_json should succeed");
assert_eq!(parsed.len(), 1);
let server = &parsed[0].templatable_mcp_server;
assert_eq!(server.name, "context7");
// No env vars → installation should still be present (no missing variables)
assert!(
parsed[0].templatable_mcp_server_installation.is_some(),
"installation should be present when there are no template variables"
);
}
/// `env_vars` entries are lowered to `${NAME}` placeholders in the env map.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_env_vars_become_placeholders() {
let toml = r#"
[mcp_servers.my_stdio]
command = "npx"
args = ["-y", "@example/mcp-server"]
env_vars = ["MY_API_KEY"]
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
let env = &value["mcp_servers"]["my_stdio"]["env"];
assert_eq!(
env["MY_API_KEY"].as_str(),
Some("${MY_API_KEY}"),
"env_vars entry should become a ${{NAME}} placeholder"
);
}
/// Explicit `env` values win over `env_vars` placeholders on collision.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_explicit_env_wins_over_env_vars_on_collision() {
let toml = r#"
[mcp_servers.my_stdio]
command = "npx"
args = ["-y", "@example/mcp-server"]
env_vars = ["MY_API_KEY"]
[mcp_servers.my_stdio.env]
MY_API_KEY = "literal-value"
LOG_LEVEL = "info"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
let env = &value["mcp_servers"]["my_stdio"]["env"];
// Explicit env wins: literal value, not the placeholder
assert_eq!(
env["MY_API_KEY"].as_str(),
Some("literal-value"),
"explicit env entry should override env_vars placeholder"
);
assert_eq!(
env["LOG_LEVEL"].as_str(),
Some("info"),
"non-colliding explicit env entry should be present"
);
}
/// `cwd` is mapped to `working_directory` in the output JSON.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_cwd_maps_to_working_directory() {
let toml = r#"
[mcp_servers.my_stdio]
command = "npx"
args = ["-y", "@example/mcp-server"]
cwd = "/home/user/project"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
assert_eq!(
value["mcp_servers"]["my_stdio"]["working_directory"].as_str(),
Some("/home/user/project"),
"cwd should be mapped to working_directory"
);
}
/// A TOML with one STDIO and one HTTP server produces both in the output.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_mixed_stdio_and_http_servers() {
let toml = r#"
[mcp_servers.my_stdio]
command = "npx"
args = ["-y", "@example/mcp-server"]
[mcp_servers.my_http]
url = "https://example.com/mcp"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
let servers = value["mcp_servers"]
.as_object()
.expect("mcp_servers should be an object");
assert!(
servers.contains_key("my_stdio"),
"STDIO server should be present"
);
assert!(
servers.contains_key("my_http"),
"HTTP server should be present"
);
}
/// An HTTP server with only a `url` field round-trips correctly.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_http_url_only() {
let toml = r#"
[mcp_servers.my_http]
url = "https://example.com/mcp"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
let server = &value["mcp_servers"]["my_http"];
assert_eq!(
server["url"].as_str(),
Some("https://example.com/mcp"),
"url should be present in output"
);
}
/// `bearer_token_env_var` is lowered to `Authorization: "Bearer ${VAR}"`.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_http_bearer_token_env_var() {
let toml = r#"
[mcp_servers.my_http]
url = "https://example.com/mcp"
bearer_token_env_var = "MCP_TOKEN"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
assert_eq!(
value["mcp_servers"]["my_http"]["headers"]["Authorization"].as_str(),
Some("Bearer ${MCP_TOKEN}"),
"bearer_token_env_var should produce Authorization: Bearer ${{VAR}}"
);
}
/// `env_http_headers` entries become `header: "${VAR}"` placeholders.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_http_env_http_headers() {
let toml = r#"
[mcp_servers.my_http]
url = "https://example.com/mcp"
env_http_headers = { "X-Api-Key" = "MCP_API_KEY" }
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
assert_eq!(
value["mcp_servers"]["my_http"]["headers"]["X-Api-Key"].as_str(),
Some("${MCP_API_KEY}"),
"env_http_headers entry should become a ${{VAR}} placeholder"
);
}
/// `http_headers` static values are passed through verbatim.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_http_static_headers() {
let toml = r#"
[mcp_servers.my_http]
url = "https://example.com/mcp"
http_headers = { "X-Client" = "codex" }
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
assert_eq!(
value["mcp_servers"]["my_http"]["headers"]["X-Client"].as_str(),
Some("codex"),
"http_headers static value should pass through verbatim"
);
}
/// `http_headers` wins over `env_http_headers` on collision.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_http_static_headers_win_over_env_headers_on_collision() {
let toml = r#"
[mcp_servers.my_http]
url = "https://example.com/mcp"
env_http_headers = { "X-Api-Key" = "MCP_API_KEY" }
http_headers = { "X-Api-Key" = "static-override" }
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
assert_eq!(
value["mcp_servers"]["my_http"]["headers"]["X-Api-Key"].as_str(),
Some("static-override"),
"http_headers should override env_http_headers on collision"
);
}
/// Entries with neither `command` nor `url` are skipped.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_unknown_entry_skipped() {
let toml = r#"
[mcp_servers.my_stdio]
command = "npx"
[mcp_servers.mystery]
some_unknown_field = "value"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let value: serde_json::Value =
serde_json::from_str(&json).expect("normalized output should be valid JSON");
let servers = value["mcp_servers"]
.as_object()
.expect("mcp_servers should be an object");
assert!(
servers.contains_key("my_stdio"),
"STDIO server should be present"
);
assert!(
!servers.contains_key("mystery"),
"entry with neither command nor url should be skipped"
);
}
/// Full round-trip: TOML with env + env_vars parses into a working installation
/// whose resolved JSON is consumable by `MCPServer::from_user_json`.
#[cfg(not(target_family = "wasm"))]
#[test]
fn test_codex_toml_round_trip_through_from_user_json() {
let toml = r#"
[mcp_servers.my_stdio]
command = "npx"
args = ["-y", "@example/mcp-server"]
[mcp_servers.my_stdio.env]
LOG_LEVEL = "info"
"#;
let json = normalize_codex_toml_to_json(toml).expect("normalization should succeed");
let parsed = ParsedTemplatableMCPServerResult::from_user_json(&json)
.expect("from_user_json should succeed");
assert_eq!(parsed.len(), 1);
let installation = parsed[0]
.templatable_mcp_server_installation
.as_ref()
.expect("installation should be present when all variables have values");
// The installation's variable values should contain LOG_LEVEL
let variable_values = installation.variable_values();
assert_eq!(
variable_values["LOG_LEVEL"].value, "info",
"explicit env value should be stored in installation"
);
// Resolved JSON should parse as a valid MCPServer
let resolved = resolve_json(installation);
let servers =
MCPServer::from_user_json(&resolved).expect("resolved JSON should parse as MCPServer");
assert_eq!(servers.len(), 1);
if let TransportType::CLIServer(cli) = &servers[0].transport_type {
assert_eq!(cli.command, "npx");
assert_eq!(cli.args, vec!["-y", "@example/mcp-server"]);
} else {
panic!("Expected CLIServer transport type");
}
}
#[test]
fn test_parse_templatable_sse_headers_and_resolve_json() {
let json = r#"{
"sse-server": {
"url": "https://example.com/sse",
"headers": {
"Authorization": "Bearer token123",
"X-Custom-Header": "custom-value"
}
}
}"#;
let parsed = ParsedTemplatableMCPServerResult::from_user_json(json)
.expect("Failed to parse templatable MCP server JSON");
assert_eq!(parsed.len(), 1);
let templatable = &parsed[0].templatable_mcp_server;
let installation = parsed[0]
.templatable_mcp_server_installation
.as_ref()
.expect("Installation should be present when all variables are provided");
let template_value =
serde_json::from_str::<serde_json::Value>(templatable.template.json.as_str()).unwrap();
let expected_template_value = serde_json::from_str::<serde_json::Value>(
r#"{"sse-server":{"url":"https://example.com/sse","headers":{"Authorization":"{{Authorization}}","X-Custom-Header":"{{X-Custom-Header}}"}}}"#,
)
.unwrap();
assert_eq!(template_value, expected_template_value);
let variable_values = installation.variable_values();
assert_eq!(variable_values["Authorization"].value, "Bearer token123");
assert_eq!(variable_values["X-Custom-Header"].value, "custom-value");
let resolved_value =
serde_json::from_str::<serde_json::Value>(&resolve_json(installation)).unwrap();
let expected_resolved_value = serde_json::from_str::<serde_json::Value>(
r#"{"sse-server":{"url":"https://example.com/sse","headers":{"Authorization":"Bearer token123","X-Custom-Header":"custom-value"}}}"#,
)
.unwrap();
assert_eq!(resolved_value, expected_resolved_value);
}
// --- Runtime handlebars secret resolution tests ---
fn make_secrets(pairs: Vec<(&str, &str)>) -> HashMap<String, ManagedSecretValue> {
pairs
.into_iter()
.map(|(k, v)| {
(
k.to_string(),
ManagedSecretValue::RawValue {
value: v.to_string(),
},
)
})
.collect()
}
#[test]
fn test_apply_secrets_resolves_explicit_handlebars_in_env_value() {
// Parser templatizes "API_KEY": "{{secret_one}}" → template has {{API_KEY}},
// variable value is API_KEY = "{{secret_one}}". apply_secrets renders
// the explicit {{...}} ref against the secrets map.
let mut installation = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "{{secret_one}}")],
);
let secrets = make_secrets(vec![("secret_one", "real_api_key_value")]);
installation.apply_secrets(&secrets);
assert_eq!(
installation.variable_values()["API_KEY"].value,
"real_api_key_value"
);
}
#[test]
fn test_apply_secrets_resolves_bearer_header_with_handlebars() {
// "Authorization": "Bearer {{my_token}}" → variable value is
// Authorization = "Bearer {{my_token}}". apply_secrets renders the
// embedded ref while keeping the Bearer prefix.
let mut installation = create_test_installation(
"sse-server",
r#"{"sse-server":{"url":"https://example.com","headers":{"Authorization":"{{Authorization}}"}}}"#,
vec![("Authorization", "Bearer {{my_token}}")],
);
let secrets = make_secrets(vec![("my_token", "tok_abc123")]);
installation.apply_secrets(&secrets);
assert_eq!(
installation.variable_values()["Authorization"].value,
"Bearer tok_abc123"
);
}
#[test]
fn test_apply_secrets_skips_plain_values() {
// Plain values like "info" contain no {{...}} and should be left unchanged
// when no secret matches the key name.
let mut installation = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"LOG_LEVEL":"{{LOG_LEVEL}}"}}}"#,
vec![("LOG_LEVEL", "info")],
);
let secrets = make_secrets(vec![("some_secret", "value")]);
installation.apply_secrets(&secrets);
assert_eq!(installation.variable_values()["LOG_LEVEL"].value, "info");
}
#[test]
fn test_apply_secrets_explicit_refs_take_priority_over_key_match() {
// If a value contains {{secret_one}} and a secret named API_KEY also exists,
// the explicit {{secret_one}} should win.
let mut installation = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "{{secret_one}}")],
);
let secrets = make_secrets(vec![
("secret_one", "correct_value"),
("API_KEY", "wrong_value_from_key_match"),
]);
installation.apply_secrets(&secrets);
assert_eq!(
installation.variable_values()["API_KEY"].value,
"correct_value"
);
}
#[test]
fn test_apply_secrets_mixed_explicit_and_implicit() {
// Mixed case: one variable uses explicit {{...}} ref, another uses
// implicit key-name matching.
let mut installation = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"API_KEY":"{{API_KEY}}","LOG_LEVEL":"{{LOG_LEVEL}}"}}}"#,
vec![("API_KEY", "{{secret_one}}"), ("LOG_LEVEL", "info")],
);
let secrets = make_secrets(vec![
("secret_one", "resolved_secret"),
("LOG_LEVEL", "debug_from_secret"),
]);
installation.apply_secrets(&secrets);
// API_KEY resolved via explicit handlebars
assert_eq!(
installation.variable_values()["API_KEY"].value,
"resolved_secret"
);
// LOG_LEVEL resolved via implicit key-name matching
assert_eq!(
installation.variable_values()["LOG_LEVEL"].value,
"debug_from_secret"
);
}
#[test]
fn test_apply_secrets_missing_secret_leaves_placeholder() {
// If the referenced secret doesn't exist, the {{...}} placeholder
// should remain in the value.
let mut installation = create_test_installation(
"test-server",
r#"{"test-server":{"command":"npx","env":{"API_KEY":"{{API_KEY}}"}}}"#,
vec![("API_KEY", "{{nonexistent_secret}}")],
);
let secrets = make_secrets(vec![]);
installation.apply_secrets(&secrets);
assert_eq!(
installation.variable_values()["API_KEY"].value,
"{{nonexistent_secret}}"
);
}