mod options { mod ssh_command { use crate::client::ssh::{connect::Options, ProgramKind}; #[test] fn no_field_means_ssh() { assert_eq!(Options::default().ssh_command(), "ssh"); } #[test] fn command_field_determines_ssh_command() { assert_eq!( Options { command: Some("field-value".into()), ..Default::default() } .ssh_command(), "field-value" ); assert_eq!( Options { command: Some("field-value".into()), kind: Some(ProgramKind::TortoisePlink), ..Default::default() } .ssh_command(), "field-value" ); } #[test] fn kind_serves_as_fallback() { assert_eq!( Options { kind: Some(ProgramKind::TortoisePlink), ..Default::default() } .ssh_command(), "tortoiseplink.exe" ); } } } mod program_kind { mod from_os_str { use std::ffi::OsStr; use crate::client::ssh::ProgramKind; #[test] fn known_variants_are_derived_from_basename() { for name_or_path in [ "ssh", "ssh.exe", "SSH", "SSH.exe", "/bin/ssh", "/bin/SSH", #[cfg(windows)] "c:\\bin\\ssh.exe", ] { assert_eq!( ProgramKind::from(OsStr::new(name_or_path)), ProgramKind::Ssh, "{name_or_path:?} could not be identified correctly" ); } assert_eq!( ProgramKind::from(OsStr::new("TortoisePlink.exe")), ProgramKind::TortoisePlink ); assert_eq!(ProgramKind::from(OsStr::new("putty")), ProgramKind::Putty); assert_eq!( ProgramKind::from(OsStr::new("../relative/Plink.exe")), ProgramKind::Plink ); } #[test] fn unknown_variants_fallback_to_simple() { assert_eq!( ProgramKind::from(OsStr::new("something-unknown-that-does-not-exist-for-sure-foobar")), ProgramKind::Simple, "in theory, we could fail right here but we don't and leave non-existing programs to fail during handshake" ); } #[test] fn ssh_disguised_within_a_script_cannot_be_detected_due_to_invocation_with_dash_g() { assert_eq!( ProgramKind::from(OsStr::new("ssh -VVV")), ProgramKind::Simple, "we don't execute the command here but assume simple, even though we could determine it's ssh if we would do what git does here" ); } } mod prepare_invocation { use std::ffi::OsStr; use crate::{ client::{ssh, ssh::ProgramKind}, Protocol, }; #[test] fn ssh() { for (url, protocol, expected) in [ ("ssh://user@host:42/p", Protocol::V1, &["ssh", "-p42", "user@host"][..]), ("ssh://user@host/p", Protocol::V1, &["ssh", "user@host"][..]), ("ssh://host/p", Protocol::V1, &["ssh", "host"][..]), ( "ssh://user@host:42/p", Protocol::V2, &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "-p42", "user@host"][..], ), ( "ssh://user@host/p", Protocol::V2, &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@host"][..], ), ( "ssh://host/p", Protocol::V2, &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "host"][..], ), ] { assert_eq!(call_args(ProgramKind::Ssh, url, protocol), quoted(expected)); } } #[test] fn tortoise_plink_has_batch_command() { assert_eq!( call_args(ProgramKind::TortoisePlink, "ssh://user@host:42/p", Protocol::V2), quoted(&["tortoiseplink.exe", "-batch", "-P", "42", "user@host"]) ); } #[test] fn port_for_all() { for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with(r#""-P" "43" "user@host""#)); } } #[test] fn simple_cannot_handle_any_arguments() { match try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2) { Err(ssh::invocation::Error { .. }) => {} _ => panic!("BUG: unexpected outcome"), } assert_eq!( call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2), quoted(&["simple", "user@host"]), "simple can only do simple invocations" ); } #[test] fn ssh_env_v2() { let prepare = call(ProgramKind::Ssh, "ssh://host/p", Protocol::V2); assert_eq!( prepare.env, &[ ("GIT_PROTOCOL".into(), "version=2".into()), ("LANG".into(), "C".into()), ("LC_ALL".into(), "C".into()) ] ); assert!(!prepare.use_shell); } #[test] fn disallow_shell_is_honored() -> Result { let url = git_url::parse("ssh://host/path".into()).expect("valid url"); let disallow_shell = false; let prepare = ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?; assert!(prepare.use_shell, "shells are used when needed"); let disallow_shell = true; let prepare = ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?; assert!( !prepare.use_shell, "but we can enforce it not to be used as well for historical reasons" ); Ok(()) } fn quoted(input: &[&str]) -> String { input.iter().map(|s| format!("\"{s}\"")).collect::>().join(" ") } fn try_call( kind: ProgramKind, url: &str, version: Protocol, ) -> std::result::Result { let ssh_cmd = kind.exe().unwrap_or_else(|| OsStr::new("simple")); let url = git_url::parse(url.into()).expect("valid url"); kind.prepare_invocation(ssh_cmd, &url, version, false) } fn call(kind: ProgramKind, url: &str, version: Protocol) -> git_command::Prepare { try_call(kind, url, version).expect("no error") } fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> String { format!("{:?}", std::process::Command::from(call(kind, url, version))) } type Result = std::result::Result<(), ssh::invocation::Error>; } mod line_to_err { use std::io::ErrorKind; use crate::client::ssh::ProgramKind; #[test] fn all() { for (kind, line, expected) in [ ( ProgramKind::Ssh, "byron@github.com: Permission denied (publickey).", ErrorKind::PermissionDenied, ), ( ProgramKind::Ssh, "ssh: Could not resolve hostname hostfoobar: nodename nor servname provided, or not known", ErrorKind::ConnectionRefused, ), ( ProgramKind::Ssh, "ssh: connect to host example.org port 22: No route to host", ErrorKind::NotFound, ), // connection closed by remote on windows ( ProgramKind::Ssh, "banner exchange: Connection to 127.0.0.1 port 61024: Software caused connection abort", ErrorKind::NotFound, ), // connection closed by remote on unix ( ProgramKind::Ssh, "Connection closed by 127.0.0.1 port 8888", // ErrorKind::NotFound, ), // this kind is basically unknown but we try our best, and simple equals ssh ( ProgramKind::Simple, "something permission denied something", ErrorKind::PermissionDenied, ), ( ProgramKind::Simple, "something resolve hostname hostfoobar: nodename nor servname something", ErrorKind::ConnectionRefused, ), ( ProgramKind::Simple, "something connect to host something", ErrorKind::NotFound, ), ] { assert_eq!(kind.line_to_err(line.into()).map(|err| err.kind()), Ok(expected)); } } #[test] fn tortoiseplink_putty_plink() { for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { assert_eq!( kind .line_to_err("publickey".into()) .map(|err| err.kind()), Ok(std::io::ErrorKind::PermissionDenied), "this program pops up error messages in a window, no way to extract information from it. Maybe there is other ways to use it, 'publickey' they mention all" ); } } } }