use ssh_mcp::transfer::{
CompactTransferResponse, ResolvedPaths, StagingLocal, StagingRemote, TransferCounts,
TransferKind, TransferOperation, TransferParams, TransferResponse, TransferStaging,
TransferTransport,
};
use std::path::PathBuf;
fn sample_params() -> TransferParams {
TransferParams {
operation: TransferOperation::Put,
local_path: "config.yml".to_string(),
remote_path: "/etc/app/config.yml".to_string(),
transport: TransferTransport::Auto,
kind: Some(TransferKind::File),
overwrite: true,
timeout_ms: Some(30000),
verbose: false,
..Default::default()
}
}
fn sample_counts() -> TransferCounts {
TransferCounts {
bytes: 1024,
files: 1,
directories: 0,
}
}
fn sample_success_response() -> TransferResponse {
TransferResponse {
ok: true,
error: None,
params: sample_params(),
kind: Some(TransferKind::File),
transport_used: TransferTransport::ExecRaw,
fallback_chain: vec![
TransferTransport::Rsync,
TransferTransport::Sftp,
TransferTransport::ExecRaw,
],
remote_home: Some("/home/user".to_string()),
local_root: "/tmp/test".to_string(),
resolved_paths: Some(ResolvedPaths {
local_path: PathBuf::from("/tmp/test/config.yml"),
}),
staging: Some(TransferStaging {
local: Some(StagingLocal {
staging_path: "/tmp/test/.config.yml.staging".to_string(),
backup_path: None,
final_path: "/tmp/test/config.yml".to_string(),
}),
remote: Some(StagingRemote {
staging_path: "/home/user/.staging/config.yml".to_string(),
backup_path: None,
final_path: "/etc/app/config.yml".to_string(),
staging_base_home: "/home/user/.staging".to_string(),
}),
}),
counts: Some(sample_counts()),
elapsed_ms: Some(150),
semantics: Some("test semantics".to_string()),
}
}
fn sample_error_response() -> TransferResponse {
TransferResponse {
ok: false,
error: Some("Permission denied".to_string()),
params: sample_params(),
kind: None,
transport_used: TransferTransport::ExecRaw,
fallback_chain: vec![TransferTransport::ExecRaw],
remote_home: None,
local_root: "/tmp/test".to_string(),
resolved_paths: None,
staging: None,
counts: None,
elapsed_ms: Some(50),
semantics: None,
}
}
#[test]
fn test_compact_response_includes_paths() {
let response = sample_success_response();
let compact = response.to_compact();
assert_eq!(compact.local_path, "config.yml");
assert_eq!(compact.remote_path, "/etc/app/config.yml");
}
#[test]
fn test_compact_response_success_fields() {
let response = sample_success_response();
let compact = response.to_compact();
assert!(compact.ok);
assert_eq!(compact.error, None);
assert_eq!(compact.kind, Some(TransferKind::File));
assert_eq!(compact.local_path, "config.yml");
assert_eq!(compact.remote_path, "/etc/app/config.yml");
assert!(compact.counts.is_some());
let counts = compact.counts.unwrap();
assert_eq!(counts.bytes, 1024);
assert_eq!(counts.files, 1);
assert_eq!(counts.directories, 0);
assert_eq!(compact.elapsed_ms, Some(150));
}
#[test]
fn test_compact_response_error_fields() {
let response = sample_error_response();
let compact = response.to_compact();
assert!(!compact.ok);
assert_eq!(compact.error, Some("Permission denied".to_string()));
assert_eq!(compact.kind, None);
assert_eq!(compact.local_path, "config.yml");
assert_eq!(compact.remote_path, "/etc/app/config.yml");
assert_eq!(compact.counts, None);
assert_eq!(compact.elapsed_ms, Some(50));
}
#[test]
fn test_to_json_false_includes_paths() {
let response = sample_success_response();
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(json["local_path"].as_str(), Some("config.yml"));
assert_eq!(json["remote_path"].as_str(), Some("/etc/app/config.yml"));
assert_eq!(json["ok"].as_bool(), Some(true));
assert_eq!(json["kind"].as_str(), Some("file"));
}
#[test]
fn test_to_json_false_excludes_verbose_fields() {
let response = sample_success_response();
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(
json["transport_used"].as_str(),
Some("exec-raw"),
"transport_used should be present in compact JSON"
);
assert!(
json.get("fallback_chain").is_some(),
"fallback_chain should be present when non-empty"
);
assert!(
json.get("staging").is_none(),
"staging should not be in compact JSON"
);
assert!(
json.get("params").is_none(),
"params should not be in compact JSON"
);
assert!(
json.get("remote_home").is_none(),
"remote_home should not be in compact JSON"
);
assert!(
json.get("local_root").is_none(),
"local_root should not be in compact JSON"
);
assert!(
json.get("resolved_paths").is_none(),
"resolved_paths should not be in compact JSON"
);
assert!(
json.get("semantics").is_none(),
"semantics should not be in compact JSON"
);
}
#[test]
fn test_to_json_false_excludes_error_on_success() {
let response = sample_success_response();
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert!(
json.get("error").is_none(),
"error should be skipped when None"
);
}
#[test]
fn test_to_json_false_includes_error_on_failure() {
let response = sample_error_response();
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(json["error"].as_str(), Some("Permission denied"));
assert_eq!(json["ok"].as_bool(), Some(false));
}
#[test]
fn test_to_json_true_includes_all_fields() {
let response = sample_success_response();
let json_str = response
.to_json(true)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(json["ok"].as_bool(), Some(true));
assert_eq!(json["kind"].as_str(), Some("file"));
assert_eq!(
json["transport_used"].as_str(),
Some("exec-raw"),
"transport_used should be present in verbose JSON"
);
assert_eq!(
json["remote_home"].as_str(),
Some("/home/user"),
"remote_home should be present in verbose JSON"
);
assert_eq!(
json["local_root"].as_str(),
Some("/tmp/test"),
"local_root should be present in verbose JSON"
);
assert_eq!(
json["semantics"].as_str(),
Some("test semantics"),
"semantics should be present in verbose JSON"
);
assert!(
json.get("params").is_some(),
"params should be present in verbose JSON"
);
let params = json["params"]
.as_object()
.expect("params should be an object");
assert_eq!(params["local_path"].as_str(), Some("config.yml"));
assert_eq!(params["remote_path"].as_str(), Some("/etc/app/config.yml"));
assert_eq!(params["operation"].as_str(), Some("put"));
assert_eq!(params["transport"].as_str(), Some("auto"));
assert!(
json.get("resolved_paths").is_some(),
"resolved_paths should be present in verbose JSON"
);
assert!(
json.get("staging").is_some(),
"staging should be present in verbose JSON"
);
let staging = json["staging"]
.as_object()
.expect("staging should be an object");
assert!(
staging.get("local").is_some(),
"staging.local should be present"
);
assert!(
staging.get("remote").is_some(),
"staging.remote should be present"
);
assert!(
json.get("counts").is_some(),
"counts should be present in verbose JSON"
);
let counts = json["counts"]
.as_object()
.expect("counts should be an object");
assert_eq!(counts["bytes"].as_u64(), Some(1024));
assert_eq!(counts["files"].as_u64(), Some(1));
assert_eq!(json["elapsed_ms"].as_u64(), Some(150));
}
#[test]
fn test_to_json_true_error_response() {
let response = sample_error_response();
let json_str = response
.to_json(true)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(json["ok"].as_bool(), Some(false));
assert_eq!(json["error"].as_str(), Some("Permission denied"));
assert!(
json.get("params").is_some(),
"params should be present even in error"
);
assert!(
json.get("staging").is_none() || json["staging"].is_null(),
"staging should be absent/null on error"
);
assert!(
json.get("counts").is_none() || json["counts"].is_null(),
"counts should be absent/null on error"
);
}
#[test]
fn test_compact_response_counts_skipped_when_none() {
let mut response = sample_error_response();
response.counts = None;
response.elapsed_ms = None;
let compact = response.to_compact();
let json_str = serde_json::to_string(&compact).expect("Compact serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert!(
json.get("counts").is_none(),
"counts should be skipped when None"
);
assert!(
json.get("elapsed_ms").is_none(),
"elapsed_ms should be skipped when None"
);
}
#[test]
fn test_fallback_chain_absent_for_explicit_transport() {
let mut params = sample_params();
params.transport = TransferTransport::ExecRaw;
let response = TransferResponse {
ok: true,
error: None,
params,
kind: Some(TransferKind::File),
transport_used: TransferTransport::ExecRaw,
fallback_chain: vec![], remote_home: Some("/home/user".to_string()),
local_root: "/tmp/test".to_string(),
resolved_paths: Some(ResolvedPaths {
local_path: PathBuf::from("/tmp/test/config.yml"),
}),
staging: None,
counts: Some(sample_counts()),
elapsed_ms: Some(100),
semantics: None,
};
let compact = response.to_compact();
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert!(
json.get("fallback_chain").is_none(),
"fallback_chain should be absent for explicit transport"
);
assert!(
compact.fallback_chain.is_empty(),
"fallback_chain should be empty for explicit transport"
);
}
#[test]
fn test_fallback_chain_present_for_auto_transport() {
let mut params = sample_params();
params.transport = TransferTransport::Auto;
let response = TransferResponse {
ok: true,
error: None,
params,
kind: Some(TransferKind::File),
transport_used: TransferTransport::Rsync,
fallback_chain: vec![
TransferTransport::Rsync,
TransferTransport::Sftp,
TransferTransport::Scp,
TransferTransport::ExecRaw,
],
remote_home: Some("/home/user".to_string()),
local_root: "/tmp/test".to_string(),
resolved_paths: Some(ResolvedPaths {
local_path: PathBuf::from("/tmp/test/config.yml"),
}),
staging: None,
counts: Some(sample_counts()),
elapsed_ms: Some(100),
semantics: None,
};
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert!(
json.get("fallback_chain").is_some(),
"fallback_chain should be present for Auto transport"
);
let chain = json["fallback_chain"]
.as_array()
.expect("fallback_chain should be an array");
assert!(!chain.is_empty(), "fallback_chain should not be empty");
}
#[test]
fn test_compact_response_directory_kind() {
let mut response = sample_success_response();
response.kind = Some(TransferKind::Directory);
response.params.local_path = "mydir".to_string();
response.params.remote_path = "/opt/app/mydir".to_string();
let compact = response.to_compact();
assert_eq!(compact.kind, Some(TransferKind::Directory));
assert_eq!(compact.local_path, "mydir");
assert_eq!(compact.remote_path, "/opt/app/mydir");
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(json["kind"].as_str(), Some("directory"));
assert_eq!(json["local_path"].as_str(), Some("mydir"));
assert_eq!(json["remote_path"].as_str(), Some("/opt/app/mydir"));
}
#[test]
fn test_to_json_invalid_utf8_paths() {
let mut params = sample_params();
params.local_path = "file with spaces.txt".to_string();
params.remote_path = "/path/with spaces/and-unicode-文件.txt".to_string();
let response = TransferResponse {
ok: true,
error: None,
params,
kind: Some(TransferKind::File),
transport_used: TransferTransport::ExecRaw,
fallback_chain: vec![TransferTransport::ExecRaw],
remote_home: None,
local_root: "/tmp".to_string(),
resolved_paths: None,
staging: None,
counts: None,
elapsed_ms: None,
semantics: None,
};
let compact = response.to_compact();
assert_eq!(compact.local_path, "file with spaces.txt");
assert_eq!(
compact.remote_path,
"/path/with spaces/and-unicode-文件.txt"
);
let json_str = response
.to_json(false)
.expect("JSON serialization should succeed");
let json: serde_json::Value = serde_json::from_str(&json_str).expect("Should parse as JSON");
assert_eq!(json["local_path"].as_str(), Some("file with spaces.txt"));
assert_eq!(
json["remote_path"].as_str(),
Some("/path/with spaces/and-unicode-文件.txt")
);
}
#[test]
fn test_compact_serialization_roundtrip() {
let response = sample_success_response();
let compact = response.to_compact();
let json_str = serde_json::to_string(&compact).expect("Compact serialization should succeed");
let deserialized: CompactTransferResponse =
serde_json::from_str(&json_str).expect("Should deserialize");
assert_eq!(deserialized.ok, compact.ok);
assert_eq!(deserialized.error, compact.error);
assert_eq!(deserialized.kind, compact.kind);
assert_eq!(deserialized.transport_used, compact.transport_used);
assert_eq!(deserialized.fallback_chain, compact.fallback_chain);
assert_eq!(deserialized.local_path, compact.local_path);
assert_eq!(deserialized.remote_path, compact.remote_path);
assert_eq!(deserialized.elapsed_ms, compact.elapsed_ms);
assert!(deserialized.counts.is_some());
let orig_counts = compact.counts.unwrap();
let deser_counts = deserialized.counts.unwrap();
assert_eq!(deser_counts.bytes, orig_counts.bytes);
assert_eq!(deser_counts.files, orig_counts.files);
assert_eq!(deser_counts.directories, orig_counts.directories);
}