#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic, missing_docs)]
use assert_cmd::Command;
fn handshake_then_call(tool: &str, arguments: &serde_json::Value) -> String {
let call = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {"name": tool, "arguments": arguments}
});
format!(
"{}\n{}\n{}\n",
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.0"}}}"#,
r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
call,
)
}
fn run_mcp_without_credentials(input: String) -> String {
let output = Command::cargo_bin("mkt")
.unwrap()
.args(["mcp", "serve"])
.env("MKT_CONFIG_DIR", "/tmp/mkt-test-nonexistent")
.env_remove("MKT_META_ACCESS_TOKEN")
.env_remove("MKT_GOOGLE_DEVELOPER_TOKEN")
.env_remove("MKT_TIKTOK_ACCESS_TOKEN")
.env_remove("MKT_LINKEDIN_ACCESS_TOKEN")
.write_stdin(input)
.timeout(std::time::Duration::from_secs(20))
.output()
.unwrap();
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn call_response(stdout: &str) -> serde_json::Value {
stdout
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.find(|v| v["id"] == 2)
.unwrap_or_else(|| panic!("no response with id 2 in: {stdout}"))
}
#[test]
fn tools_call_unknown_provider_is_invalid_params() {
let stdout = run_mcp_without_credentials(handshake_then_call(
"campaign_list",
&serde_json::json!({"provider": "myspace"}),
));
let response = call_response(&stdout);
let message = response["error"]["message"].as_str().unwrap_or_default();
assert!(
message.contains("unknown provider"),
"error should say the provider is unknown: {response}"
);
assert!(
message.contains("meta") && message.contains("linkedin"),
"error should list valid providers for self-correction: {response}"
);
}
#[test]
fn tools_call_without_credentials_reports_structured_auth_error() {
let stdout = run_mcp_without_credentials(handshake_then_call(
"campaign_list",
&serde_json::json!({"provider": "meta"}),
));
let response = call_response(&stdout);
let message = response["error"]["message"].as_str().unwrap_or_default();
assert!(
message.contains("auth_error"),
"auth failures must carry the stable error_type tag: {response}"
);
assert!(
message.contains("doctor") || message.contains("MKT_"),
"auth failures must include an actionable suggestion: {response}"
);
}
#[test]
fn tools_call_set_status_without_credentials_fails() {
let stdout = run_mcp_without_credentials(handshake_then_call(
"campaign_set_status",
&serde_json::json!({"provider": "meta", "campaign_id": "c1", "status": "active"}),
));
let response = call_response(&stdout);
assert!(
response.get("error").is_some(),
"activating a campaign without credentials must be an error: {response}"
);
}
#[test]
fn tools_call_health_never_leaks_token_values() {
let input = handshake_then_call("provider_health", &serde_json::json!({"provider": "meta"}));
let output = Command::cargo_bin("mkt")
.unwrap()
.args(["mcp", "serve"])
.env("MKT_CONFIG_DIR", "/tmp/mkt-test-nonexistent")
.env("MKT_META_ACCESS_TOKEN", "super-secret-value-do-not-print")
.write_stdin(input)
.timeout(std::time::Duration::from_secs(20))
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stdout.contains("super-secret-value-do-not-print")
&& !stderr.contains("super-secret-value-do-not-print"),
"MCP output must never contain raw token values"
);
}
#[test]
fn tools_call_answers_for_all_providers() {
for provider in ["meta", "google", "tiktok", "linkedin"] {
let stdout = run_mcp_without_credentials(handshake_then_call(
"provider_health",
&serde_json::json!({"provider": provider}),
));
let response = call_response(&stdout);
assert!(
response.get("error").is_some() || response.get("result").is_some(),
"{provider}: expected a JSON-RPC response, got: {response}"
);
}
}