use anyhow::Result; use assert_cmd::{cargo::cargo_bin, Command}; use get_port::{tcp::TcpPort, Ops, Range}; use once_cell::sync::Lazy; use rexpect::session::{spawn_command, PtySession}; use std::{io, path, sync::Mutex}; use tempfile::TempDir; use thiserror::Error; struct TestContext { workdir: TempDir, ssh_port: u16, docker_process: PtySession, } impl Drop for TestContext { fn drop(&mut self) { self.docker_process.send_line("exit").unwrap(); } } #[derive(Error, Debug, Clone)] pub enum DockerBuildError { #[error(transparent)] StripPrefixError(#[from] path::StripPrefixError), #[error("IO Error: `{0}`")] IoError(String), #[error("Failed to build dockerfile")] CliError, } impl From for DockerBuildError { fn from(e: io::Error) -> Self { DockerBuildError::IoError(e.to_string()) } } static BUILD_DOCKER_RESULT: Lazy> = Lazy::new(|| { let mut command = std::process::Command::new("docker"); let absolute_shell_path = cargo_bin(env!("CARGO_PKG_NAME")); let relative_shell_path = absolute_shell_path.strip_prefix(std::fs::canonicalize(".")?)?; command.args([ "build", "-t", "shackle-server", "--build-arg", &format!("SHELL={}", relative_shell_path.display()), "./", ]); let status = command.status()?; if status.success() { Ok(()) } else { Err(DockerBuildError::CliError) } }); fn build_docker_image() -> Result<(), DockerBuildError> { BUILD_DOCKER_RESULT.clone() } #[derive(Error, Debug, Clone)] pub enum PortAssignmentError { #[error("Mutex Error: `{0}`")] MutexError(String), #[error("Couldn't find an available port")] NoMorePorts, } static LAST_USED_PORT: Lazy> = Lazy::new(|| Mutex::new(2022)); fn find_unused_port() -> Result { let mut last_used = LAST_USED_PORT .lock() .map_err(|e| PortAssignmentError::MutexError(e.to_string()))?; let port = TcpPort::in_range( "127.0.0.1", Range { min: *last_used + 1, max: 3022, }, ) .ok_or(PortAssignmentError::NoMorePorts)?; *last_used = port; Ok(port) } fn spawn_ssh_server() -> Result { build_docker_image()?; let workdir = tempfile::tempdir()?; let ssh_port = find_unused_port()?; let mut command = std::process::Command::new("docker"); command.args([ "run", "-it", "-p", &format!("{}:22", ssh_port), "shackle-server", ]); command.current_dir(&workdir); let mut docker_process = spawn_command(command, Some(3000))?; docker_process.exp_string("Ready")?; Ok(TestContext { workdir, ssh_port, docker_process, }) } fn connect_to_ssh_server_interactively(c: &TestContext) -> Result { let mut command = std::process::Command::new("ssh"); command.args([ "-p", &c.ssh_port.to_string(), "shukkie@localhost", "-o", "StrictHostKeyChecking=false", ]); command.current_dir(&c.workdir); let mut p = spawn_command(command, Some(3000))?; expect_prompt(&mut p)?; Ok(p) } fn expect_prompt(p: &mut PtySession) -> Result<()> { p.exp_string("> ")?; Ok(()) } fn make_new_repo(c: &TestContext, repo_name: &str) -> Result<()> { let mut p = connect_to_ssh_server_interactively(&c)?; p.send_line(&format!("git-init {}", repo_name))?; expect_prompt(&mut p)?; p.send_line("exit")?; p.exp_eof()?; Ok(()) } #[test] fn shows_a_prompt() -> Result<()> { let c = spawn_ssh_server()?; connect_to_ssh_server_interactively(&c)?; Ok(()) } #[test] #[ignore] // requires non-interactive commands fn git_clone_works_with_an_empty_repo() -> Result<()> { let c = spawn_ssh_server()?; let repo_name = "my-new-repo"; make_new_repo(&c, repo_name)?; Command::new("git") .args([ "clone", "-v", "--progress", &format!("ssh://shukkie@localhost:{}/{}.git", c.ssh_port, repo_name), ]) .current_dir(&c.workdir) .timeout(std::time::Duration::from_secs(3)) .assert() .success(); Ok(()) }