Skip to content

Commit 146bc6c

Browse files
committed
refactor: internalize async-ssh2-tokio as tokio_client module with comprehensive test coverage
1 parent 071f9a1 commit 146bc6c

10 files changed

Lines changed: 1122 additions & 17 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ license = "Apache-2.0"
88

99
[dependencies]
1010
tokio = { version = "1", features = ["full"] }
11-
async-ssh2-tokio = "0.9"
11+
russh = "0.52.1"
12+
russh-sftp = "2.1.1"
1213
clap = { version = "4", features = ["derive", "env"] }
1314
anyhow = "1"
1415
thiserror = "2"

src/ssh/client.rs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use super::tokio_client::{AuthMethod, Client};
1516
use anyhow::{Context, Result};
16-
use async_ssh2_tokio::{AuthMethod, Client};
1717
use std::path::Path;
1818
use std::time::Duration;
1919

@@ -336,3 +336,123 @@ impl CommandResult {
336336
self.exit_status == 0
337337
}
338338
}
339+
340+
#[cfg(test)]
341+
mod tests {
342+
use super::*;
343+
use tempfile::TempDir;
344+
345+
#[test]
346+
fn test_ssh_client_creation() {
347+
let client = SshClient::new("example.com".to_string(), 22, "user".to_string());
348+
assert_eq!(client.host, "example.com");
349+
assert_eq!(client.port, 22);
350+
assert_eq!(client.username, "user");
351+
}
352+
353+
#[test]
354+
fn test_command_result_success() {
355+
let result = CommandResult {
356+
host: "test.com".to_string(),
357+
output: b"Hello World\n".to_vec(),
358+
stderr: Vec::new(),
359+
exit_status: 0,
360+
};
361+
362+
assert!(result.is_success());
363+
assert_eq!(result.stdout_string(), "Hello World\n");
364+
assert_eq!(result.stderr_string(), "");
365+
}
366+
367+
#[test]
368+
fn test_command_result_failure() {
369+
let result = CommandResult {
370+
host: "test.com".to_string(),
371+
output: Vec::new(),
372+
stderr: b"Command not found\n".to_vec(),
373+
exit_status: 127,
374+
};
375+
376+
assert!(!result.is_success());
377+
assert_eq!(result.stdout_string(), "");
378+
assert_eq!(result.stderr_string(), "Command not found\n");
379+
}
380+
381+
#[test]
382+
fn test_command_result_with_utf8() {
383+
let result = CommandResult {
384+
host: "test.com".to_string(),
385+
output: "한글 테스트\n".as_bytes().to_vec(),
386+
stderr: "エラー\n".as_bytes().to_vec(),
387+
exit_status: 1,
388+
};
389+
390+
assert!(!result.is_success());
391+
assert_eq!(result.stdout_string(), "한글 테스트\n");
392+
assert_eq!(result.stderr_string(), "エラー\n");
393+
}
394+
395+
#[test]
396+
fn test_determine_auth_method_with_key() {
397+
let temp_dir = TempDir::new().unwrap();
398+
let key_path = temp_dir.path().join("test_key");
399+
std::fs::write(&key_path, "fake key content").unwrap();
400+
401+
let client = SshClient::new("test.com".to_string(), 22, "user".to_string());
402+
let auth = client
403+
.determine_auth_method(Some(&key_path), false)
404+
.unwrap();
405+
406+
match auth {
407+
AuthMethod::PrivateKeyFile { key_file_path, .. } => {
408+
assert_eq!(key_file_path, key_path);
409+
}
410+
_ => panic!("Expected PrivateKeyFile auth method"),
411+
}
412+
}
413+
414+
#[cfg(not(target_os = "windows"))]
415+
#[test]
416+
fn test_determine_auth_method_with_agent() {
417+
unsafe {
418+
std::env::set_var("SSH_AUTH_SOCK", "/tmp/ssh-agent.sock");
419+
}
420+
421+
let client = SshClient::new("test.com".to_string(), 22, "user".to_string());
422+
let auth = client.determine_auth_method(None, true).unwrap();
423+
424+
match auth {
425+
AuthMethod::Agent => {}
426+
_ => panic!("Expected Agent auth method"),
427+
}
428+
429+
unsafe {
430+
std::env::remove_var("SSH_AUTH_SOCK");
431+
}
432+
}
433+
434+
#[test]
435+
fn test_determine_auth_method_fallback_to_default() {
436+
// Create a fake home directory with default key
437+
let temp_dir = TempDir::new().unwrap();
438+
let ssh_dir = temp_dir.path().join(".ssh");
439+
std::fs::create_dir_all(&ssh_dir).unwrap();
440+
let default_key = ssh_dir.join("id_rsa");
441+
std::fs::write(&default_key, "fake key").unwrap();
442+
443+
unsafe {
444+
std::env::set_var("HOME", temp_dir.path().to_str().unwrap());
445+
std::env::remove_var("SSH_AUTH_SOCK");
446+
}
447+
448+
let client = SshClient::new("test.com".to_string(), 22, "user".to_string());
449+
let auth = client.determine_auth_method(None, false).unwrap();
450+
451+
match auth {
452+
AuthMethod::PrivateKeyFile { key_file_path, .. } => {
453+
assert_eq!(key_file_path, default_key);
454+
}
455+
_ => panic!("Expected PrivateKeyFile auth method"),
456+
}
457+
}
458+
}

src/ssh/known_hosts.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use async_ssh2_tokio::ServerCheckMethod;
15+
use super::tokio_client::ServerCheckMethod;
1616
use directories::BaseDirs;
1717
use std::path::PathBuf;
1818
use std::str::FromStr;
@@ -123,3 +123,83 @@ impl FromStr for StrictHostKeyChecking {
123123
})
124124
}
125125
}
126+
127+
#[cfg(test)]
128+
mod tests {
129+
use super::*;
130+
131+
#[test]
132+
fn test_strict_host_key_checking_from_str() {
133+
assert_eq!(
134+
StrictHostKeyChecking::from_str("yes").unwrap(),
135+
StrictHostKeyChecking::Yes
136+
);
137+
assert_eq!(
138+
StrictHostKeyChecking::from_str("true").unwrap(),
139+
StrictHostKeyChecking::Yes
140+
);
141+
assert_eq!(
142+
StrictHostKeyChecking::from_str("no").unwrap(),
143+
StrictHostKeyChecking::No
144+
);
145+
assert_eq!(
146+
StrictHostKeyChecking::from_str("false").unwrap(),
147+
StrictHostKeyChecking::No
148+
);
149+
assert_eq!(
150+
StrictHostKeyChecking::from_str("accept-new").unwrap(),
151+
StrictHostKeyChecking::AcceptNew
152+
);
153+
assert_eq!(
154+
StrictHostKeyChecking::from_str("tofu").unwrap(),
155+
StrictHostKeyChecking::AcceptNew
156+
);
157+
assert_eq!(
158+
StrictHostKeyChecking::from_str("invalid").unwrap(),
159+
StrictHostKeyChecking::AcceptNew
160+
);
161+
}
162+
163+
#[test]
164+
fn test_strict_host_key_checking_to_bool() {
165+
assert!(StrictHostKeyChecking::Yes.to_bool());
166+
assert!(!StrictHostKeyChecking::No.to_bool());
167+
assert!(!StrictHostKeyChecking::AcceptNew.to_bool());
168+
}
169+
170+
#[test]
171+
fn test_strict_host_key_checking_default() {
172+
assert_eq!(
173+
StrictHostKeyChecking::default(),
174+
StrictHostKeyChecking::AcceptNew
175+
);
176+
}
177+
178+
#[test]
179+
fn test_get_default_known_hosts_path() {
180+
let path = get_default_known_hosts_path();
181+
assert!(path.is_some());
182+
if let Some(p) = path {
183+
assert!(p.to_str().unwrap().contains(".ssh/known_hosts"));
184+
}
185+
}
186+
187+
#[test]
188+
fn test_get_check_method() {
189+
// Test with No mode
190+
let method = get_check_method(StrictHostKeyChecking::No);
191+
assert!(matches!(method, ServerCheckMethod::NoCheck));
192+
193+
// Test with AcceptNew mode (should use NoCheck since library doesn't support TOFU)
194+
let method = get_check_method(StrictHostKeyChecking::AcceptNew);
195+
assert!(matches!(method, ServerCheckMethod::NoCheck));
196+
197+
// Test with Yes mode
198+
let method = get_check_method(StrictHostKeyChecking::Yes);
199+
// Result depends on whether known_hosts file exists
200+
assert!(matches!(
201+
method,
202+
ServerCheckMethod::DefaultKnownHostsFile | ServerCheckMethod::NoCheck
203+
));
204+
}
205+
}

src/ssh/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod client;
1616
pub mod handler;
1717
pub mod known_hosts;
1818
pub mod pool;
19+
pub mod tokio_client;
1920

2021
pub use client::SshClient;
2122
pub use handler::BsshHandler;

src/ssh/pool.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
// See the License for the specific language governing permissions and
2323
// limitations under the License.
2424

25+
use super::tokio_client::Client;
2526
use anyhow::Result;
26-
use async_ssh2_tokio::Client;
2727
use std::sync::Arc;
2828
use std::time::Duration;
2929
use tokio::sync::RwLock;

0 commit comments

Comments
 (0)