use std::io; use std::path::Path; use std::time::Duration; use assert_fs::prelude::*; use assert_fs::TempDir; use distant_core::protocol::{ ChangeKindSet, Environment, FileType, Metadata, Permissions, SetPermissionsOptions, }; use distant_core::{DistantChannelExt, DistantClient}; use once_cell::sync::Lazy; use predicates::prelude::*; use rstest::*; use test_log::test; use crate::sshd::*; const SETUP_DIR_TIMEOUT: Duration = Duration::from_secs(1); const SETUP_DIR_POLL: Duration = Duration::from_millis(50); static TEMP_SCRIPT_DIR: Lazy = Lazy::new(|| TempDir::new().unwrap()); static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); static ECHO_ARGS_TO_STDOUT_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh"); script .write_str(indoc::indoc!( r#" #/usr/bin/env bash printf "%s" "$*" "# )) .unwrap(); script }); static ECHO_ARGS_TO_STDERR_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh"); script .write_str(indoc::indoc!( r#" #/usr/bin/env bash printf "%s" "$*" 1>&2 "# )) .unwrap(); script }); static ECHO_STDIN_TO_STDOUT_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh"); script .write_str(indoc::indoc!( r#" #/usr/bin/env bash while IFS= read; do echo "$REPLY"; done "# )) .unwrap(); script }); static SLEEP_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("sleep.sh"); script .write_str(indoc::indoc!( r#" #!/usr/bin/env bash sleep "$1" "# )) .unwrap(); script }); static DOES_NOT_EXIST_BIN: Lazy = Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin")); #[rstest] #[test(tokio::test)] async fn read_file_should_fail_if_file_missing(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let path = temp.child("missing-file").path().to_path_buf(); let _ = client.read_file(path).await.unwrap_err(); } #[rstest] #[test(tokio::test)] async fn read_file_should_send_blob_with_file_contents(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); let bytes = client.read_file(file.path().to_path_buf()).await.unwrap(); assert_eq!(bytes, b"some file contents"); } #[rstest] #[test(tokio::test)] async fn read_file_text_should_send_error_if_fails_to_read_file( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let path = temp.child("missing-file").path().to_path_buf(); let _ = client.read_file_text(path).await.unwrap_err(); } #[rstest] #[test(tokio::test)] async fn read_file_text_should_send_text_with_file_contents(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); let text = client .read_file_text(file.path().to_path_buf()) .await .unwrap(); assert_eq!(text, "some file contents"); } #[rstest] #[test(tokio::test)] async fn write_file_should_send_error_if_fails_to_write_file(#[future] client: Ctx) { let mut client = client.await; // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let _ = client .write_file(file.path().to_path_buf(), b"some text".to_vec()) .await .unwrap_err(); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn write_file_should_send_ok_when_successful(#[future] client: Ctx) { let mut client = client.await; // Path should point to a file that does not exist, but all // other components leading up to it do let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); client .write_file(file.path().to_path_buf(), b"some text".to_vec()) .await .unwrap(); // Also verify that we actually did create the file // with the associated contents file.assert("some text"); } #[rstest] #[test(tokio::test)] async fn write_file_text_should_send_error_if_fails_to_write_file( #[future] client: Ctx, ) { let mut client = client.await; // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let _ = client .write_file_text(file.path().to_path_buf(), "some text".to_string()) .await .unwrap_err(); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn write_file_text_should_send_ok_when_successful(#[future] client: Ctx) { let mut client = client.await; // Path should point to a file that does not exist, but all // other components leading up to it do let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); client .write_file_text(file.path().to_path_buf(), "some text".to_string()) .await .unwrap(); // Also verify that we actually did create the file // with the associated contents file.assert("some text"); } #[rstest] #[test(tokio::test)] async fn append_file_should_send_error_if_fails_to_create_file( #[future] client: Ctx, ) { let mut client = client.await; // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let _ = client .append_file(file.path().to_path_buf(), b"some extra contents".to_vec()) .await .unwrap_err(); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn append_file_should_create_file_if_missing(#[future] client: Ctx) { let mut client = client.await; // Don't create the file directly, but define path // where the file should be let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); client .append_file(file.path().to_path_buf(), b"some extra contents".to_vec()) .await .unwrap(); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did create to the file file.assert("some extra contents"); } #[rstest] #[test(tokio::test)] async fn append_file_should_send_ok_when_successful(#[future] client: Ctx) { let mut client = client.await; // Create a temporary file and fill it with some contents let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); client .append_file(file.path().to_path_buf(), b"some extra contents".to_vec()) .await .unwrap(); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did append to the file file.assert("some file contentssome extra contents"); } #[rstest] #[test(tokio::test)] async fn append_file_text_should_send_error_if_fails_to_create_file( #[future] client: Ctx, ) { let mut client = client.await; // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let _ = client .append_file_text(file.path().to_path_buf(), "some extra contents".to_string()) .await .unwrap_err(); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn append_file_text_should_create_file_if_missing(#[future] client: Ctx) { let mut client = client.await; // Don't create the file directly, but define path // where the file should be let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); client .append_file_text(file.path().to_path_buf(), "some extra contents".to_string()) .await .unwrap(); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did create to the file file.assert("some extra contents"); } #[rstest] #[test(tokio::test)] async fn append_file_text_should_send_ok_when_successful(#[future] client: Ctx) { let mut client = client.await; // Create a temporary file and fill it with some contents let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); client .append_file_text(file.path().to_path_buf(), "some extra contents".to_string()) .await .unwrap(); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did append to the file file.assert("some file contentssome extra contents"); } #[rstest] #[test(tokio::test)] async fn dir_read_should_send_error_if_directory_does_not_exist( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("test-dir"); let _ = client .read_dir( dir.path().to_path_buf(), /* depth */ 0, /* absolute */ false, /* canonicalize */ false, /* include_root */ false, ) .await .unwrap_err(); } // /root/ // /root/file1 // /root/link1 -> /root/sub1/file2 // /root/sub1/ // /root/sub1/file2 async fn setup_dir() -> assert_fs::TempDir { let root_dir = assert_fs::TempDir::new().unwrap(); let file1 = root_dir.child("file1"); file1.touch().unwrap(); let sub1 = root_dir.child("sub1"); sub1.create_dir_all().unwrap(); let file2 = sub1.child("file2"); file2.touch().unwrap(); let link1 = root_dir.child("link1"); link1.symlink_to_file(file2.path()).unwrap(); // Wait to ensure that everything was set up tokio::time::timeout(SETUP_DIR_TIMEOUT, async { macro_rules! all_exist { () => {{ let root_dir_exists = root_dir.exists(); let sub1_exists = sub1.exists(); let file1_exists = file1.exists(); let file2_exists = file2.exists(); let link1_exists = link1.exists(); root_dir_exists && sub1_exists && file1_exists && file2_exists && link1_exists }}; } while !all_exist!() { tokio::time::sleep(SETUP_DIR_POLL).await; } }) .await .expect("Failed to setup dir"); root_dir } // NOTE: CI fails this on Windows, but it's running Windows with bash and strange paths, so ignore // it only for the CI #[rstest] #[test(tokio::test)] #[cfg_attr(all(windows, ci), ignore)] async fn dir_read_should_support_depth_limits(#[future] client: Ctx) { let mut client = client.await; // Create directory with some nested items let root_dir = setup_dir().await; let (entries, _) = client .read_dir( root_dir.path().to_path_buf(), /* depth */ 1, /* absolute */ false, /* canonicalize */ false, /* include_root */ false, ) .await .unwrap(); assert_eq!(entries.len(), 3, "Wrong number of entries found"); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, Path::new("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, Path::new("link1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, Path::new("sub1")); assert_eq!(entries[2].depth, 1); } // NOTE: CI fails this on Windows, but it's running Windows with bash and strange paths, so ignore // it only for the CI #[rstest] #[test(tokio::test)] #[cfg_attr(all(windows, ci), ignore)] async fn dir_read_should_support_unlimited_depth_using_zero(#[future] client: Ctx) { let mut client = client.await; // Create directory with some nested items let root_dir = setup_dir().await; let (entries, _) = client .read_dir( root_dir.path().to_path_buf(), /* depth */ 0, /* absolute */ false, /* canonicalize */ false, /* include_root */ false, ) .await .unwrap(); assert_eq!(entries.len(), 4, "Wrong number of entries found"); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, Path::new("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, Path::new("link1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, Path::new("sub1")); assert_eq!(entries[2].depth, 1); assert_eq!(entries[3].file_type, FileType::File); assert_eq!(entries[3].path, Path::new("sub1").join("file2")); assert_eq!(entries[3].depth, 2); } // NOTE: This is failing on windows as canonicalization of root path is not correct! #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn dir_read_should_support_including_directory_in_returned_entries( #[future] client: Ctx, ) { let mut client = client.await; // Create directory with some nested items let root_dir = setup_dir().await; let (entries, _) = client .read_dir( root_dir.path().to_path_buf(), /* depth */ 1, /* absolute */ false, /* canonicalize */ false, /* include_root */ true, ) .await .unwrap(); assert_eq!(entries.len(), 4, "Wrong number of entries found"); // NOTE: Root entry is always absolute, resolved path assert_eq!(entries[0].file_type, FileType::Dir); assert_eq!( entries[0].path, dunce::canonicalize(root_dir.path()).unwrap() ); assert_eq!(entries[0].depth, 0); assert_eq!(entries[1].file_type, FileType::File); assert_eq!(entries[1].path, Path::new("file1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Symlink); assert_eq!(entries[2].path, Path::new("link1")); assert_eq!(entries[2].depth, 1); assert_eq!(entries[3].file_type, FileType::Dir); assert_eq!(entries[3].path, Path::new("sub1")); assert_eq!(entries[3].depth, 1); } // NOTE: This is failing on windows as canonicalization of root path is not correct! #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn dir_read_should_support_returning_absolute_paths(#[future] client: Ctx) { let mut client = client.await; // Create directory with some nested items let root_dir = setup_dir().await; let (entries, _) = client .read_dir( root_dir.path().to_path_buf(), /* depth */ 1, /* absolute */ true, /* canonicalize */ false, /* include_root */ false, ) .await .unwrap(); assert_eq!(entries.len(), 3, "Wrong number of entries found"); let root_path = dunce::canonicalize(root_dir.path()).unwrap(); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, root_path.join("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, root_path.join("link1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, root_path.join("sub1")); assert_eq!(entries[2].depth, 1); } // NOTE: This is failing on windows as the symlink does not get resolved! #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn dir_read_should_support_returning_canonicalized_paths( #[future] client: Ctx, ) { let mut client = client.await; // Create directory with some nested items let root_dir = setup_dir().await; let (entries, _) = client .read_dir( root_dir.path().to_path_buf(), /* depth */ 1, /* absolute */ false, /* canonicalize */ true, /* include_root */ false, ) .await .unwrap(); assert_eq!(entries.len(), 3, "Wrong number of entries found"); println!("{:?}", entries); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, Path::new("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Dir); assert_eq!(entries[1].path, Path::new("sub1")); assert_eq!(entries[1].depth, 1); // Symlink should be resolved from $ROOT/link1 -> $ROOT/sub1/file2 assert_eq!(entries[2].file_type, FileType::Symlink); assert_eq!(entries[2].path, Path::new("sub1").join("file2")); assert_eq!(entries[2].depth, 1); } #[rstest] #[test(tokio::test)] async fn create_dir_should_send_error_if_fails(#[future] client: Ctx) { let mut client = client.await; // Make a path that has multiple non-existent components // so the creation will fail let root_dir = setup_dir().await; let path = root_dir.path().join("nested").join("new-dir"); let _ = client .create_dir(path.to_path_buf(), /* all */ false) .await .unwrap_err(); // Also verify that the directory was not actually created assert!(!path.exists(), "Path unexpectedly exists"); } #[rstest] #[test(tokio::test)] async fn create_dir_should_send_ok_when_successful(#[future] client: Ctx) { let mut client = client.await; let root_dir = setup_dir().await; let path = root_dir.path().join("new-dir"); client .create_dir(path.to_path_buf(), /* all */ false) .await .unwrap(); // Also verify that the directory was actually created assert!(path.exists(), "Directory not created"); } #[rstest] #[test(tokio::test)] async fn create_dir_should_support_creating_multiple_dir_components( #[future] client: Ctx, ) { let mut client = client.await; let root_dir = setup_dir().await; let path = root_dir.path().join("nested").join("new-dir"); client .create_dir(path.to_path_buf(), /* all */ true) .await .unwrap(); // Also verify that the directory was actually created assert!(path.exists(), "Directory not created"); } #[rstest] #[test(tokio::test)] async fn remove_should_send_error_on_failure(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("missing-file"); let _ = client .remove(file.path().to_path_buf(), /* false */ false) .await .unwrap_err(); // Also, verify that path does not exist file.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn remove_should_support_deleting_a_directory(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); client .remove(dir.path().to_path_buf(), /* false */ false) .await .unwrap(); // Also, verify that path does not exist dir.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn remove_should_delete_nonempty_directory_if_force_is_true( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); dir.child("file").touch().unwrap(); client .remove(dir.path().to_path_buf(), /* false */ true) .await .unwrap(); // Also, verify that path does not exist dir.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn remove_should_support_deleting_a_single_file(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("some-file"); file.touch().unwrap(); client .remove(file.path().to_path_buf(), /* false */ false) .await .unwrap(); // Also, verify that path does not exist file.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn copy_should_send_error_on_failure(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); let dst = temp.child("dst"); let _ = client .copy(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap_err(); // Also, verify that destination does not exist dst.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn copy_should_support_copying_an_entire_directory(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let src_file = src.child("file"); src_file.write_str("some contents").unwrap(); let dst = temp.child("dst"); let dst_file = dst.child("file"); client .copy(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap(); // Verify that we have source and destination directories and associated contents src.assert(predicate::path::is_dir()); src_file.assert(predicate::path::is_file()); dst.assert(predicate::path::is_dir()); dst_file.assert(predicate::path::eq_file(src_file.path())); } #[rstest] #[test(tokio::test)] async fn copy_should_support_copying_an_empty_directory(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let dst = temp.child("dst"); client .copy(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap(); // Verify that we still have source and destination directories src.assert(predicate::path::is_dir()); dst.assert(predicate::path::is_dir()); } #[rstest] #[test(tokio::test)] async fn copy_should_support_copying_a_directory_that_only_contains_directories( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let src_dir = src.child("dir"); src_dir.create_dir_all().unwrap(); let dst = temp.child("dst"); let dst_dir = dst.child("dir"); client .copy(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap(); // Verify that we have source and destination directories and associated contents src.assert(predicate::path::is_dir().name("src")); src_dir.assert(predicate::path::is_dir().name("src/dir")); dst.assert(predicate::path::is_dir().name("dst")); dst_dir.assert(predicate::path::is_dir().name("dst/dir")); } #[rstest] #[test(tokio::test)] async fn copy_should_support_copying_a_single_file(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.write_str("some text").unwrap(); let dst = temp.child("dst"); client .copy(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap(); // Verify that we still have source and that destination has source's contents src.assert(predicate::path::is_file()); dst.assert(predicate::path::eq_file(src.path())); } #[rstest] #[test(tokio::test)] async fn rename_should_fail_if_path_missing(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); let dst = temp.child("dst"); let _ = client .rename(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap_err(); // Also, verify that destination does not exist dst.assert(predicate::path::missing()); } #[rstest] #[test(tokio::test)] async fn rename_should_support_renaming_an_entire_directory(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let src_file = src.child("file"); src_file.write_str("some contents").unwrap(); let dst = temp.child("dst"); let dst_file = dst.child("file"); client .rename(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap(); // Verify that we moved the contents src.assert(predicate::path::missing()); src_file.assert(predicate::path::missing()); dst.assert(predicate::path::is_dir()); dst_file.assert("some contents"); } #[rstest] #[test(tokio::test)] async fn rename_should_support_renaming_a_single_file(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.write_str("some text").unwrap(); let dst = temp.child("dst"); client .rename(src.path().to_path_buf(), dst.path().to_path_buf()) .await .unwrap(); // Verify that we moved the file src.assert(predicate::path::missing()); dst.assert("some text"); } #[rstest] #[test(tokio::test)] async fn watch_should_fail_as_unsupported(#[future] client: Ctx) { // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc. let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.touch().unwrap(); let err = client .watch( file.path().to_path_buf(), /* recursive */ false, /* only */ ChangeKindSet::default(), /* except */ ChangeKindSet::default(), ) .await .unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::Unsupported, "{:?}", err); } #[rstest] #[test(tokio::test)] async fn exists_should_send_true_if_path_exists(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.touch().unwrap(); let exists = client.exists(file.path().to_path_buf()).await.unwrap(); assert!(exists, "Expected exists to be true, but was false"); } #[rstest] #[test(tokio::test)] async fn exists_should_send_false_if_path_does_not_exist(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); let exists = client.exists(file.path().to_path_buf()).await.unwrap(); assert!(!exists, "Expected exists to be false, but was true"); } #[rstest] #[test(tokio::test)] async fn metadata_should_send_error_on_failure(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); let _ = client .metadata( file.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ false, ) .await .unwrap_err(); } #[rstest] #[test(tokio::test)] async fn metadata_should_send_back_metadata_on_file_if_exists( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let metadata = client .metadata( file.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ false, ) .await .unwrap(); assert!( matches!( metadata, Metadata { canonicalized_path: None, file_type: FileType::File, len: 9, readonly: false, .. } ), "{:?}", metadata ); } #[cfg(unix)] #[rstest] #[test(tokio::test)] async fn metadata_should_include_unix_specific_metadata_on_unix_platform( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let metadata = client .metadata( file.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ false, ) .await .unwrap(); #[allow(clippy::match_single_binding)] match metadata { Metadata { unix, windows, .. } => { assert!(unix.is_some(), "Unexpectedly missing unix metadata on unix"); assert!( windows.is_none(), "Unexpectedly got windows metadata on unix" ); } } } #[cfg(windows)] #[rstest] #[test(tokio::test)] async fn metadata_should_not_include_windows_as_ssh_cannot_retrieve_that_information( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let metadata = client .metadata( file.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ false, ) .await .unwrap(); #[allow(clippy::match_single_binding)] match metadata { Metadata { unix, windows, .. } => { assert!( windows.is_none(), "Unexpectedly got windows metadata on windows (support added?)" ); // NOTE: Still includes unix metadata assert!( unix.is_some(), "Unexpectedly missing unix metadata from sshd (even on windows)" ); } } } #[rstest] #[test(tokio::test)] async fn metadata_should_send_back_metadata_on_dir_if_exists(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); let metadata = client .metadata( dir.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ false, ) .await .unwrap(); assert!( matches!( metadata, Metadata { canonicalized_path: None, file_type: FileType::Dir, readonly: false, .. } ), "{:?}", metadata ); } #[rstest] #[test(tokio::test)] async fn metadata_should_send_back_metadata_on_symlink_if_exists( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); let metadata = client .metadata( symlink.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ false, ) .await .unwrap(); assert!( matches!( metadata, Metadata { canonicalized_path: None, file_type: FileType::Symlink, readonly: false, .. } ), "{:?}", metadata ); } #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn metadata_should_include_canonicalized_path_if_flag_specified( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); let metadata = client .metadata( symlink.path().to_path_buf(), /* canonicalize */ true, /* resolve_file_type */ false, ) .await .unwrap(); // NOTE: This is failing on windows as the symlink does not get resolved! match metadata { Metadata { canonicalized_path: Some(path), file_type: FileType::Symlink, readonly: false, .. } => assert_eq!( path, dunce::canonicalize(file.path()).unwrap(), "Symlink canonicalized path does not match referenced file" ), x => panic!("Unexpected response: {:?}", x), } } #[rstest] #[test(tokio::test)] async fn metadata_should_resolve_file_type_of_symlink_if_flag_specified( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); let metadata = client .metadata( symlink.path().to_path_buf(), /* canonicalize */ false, /* resolve_file_type */ true, ) .await .unwrap(); assert!( matches!( metadata, Metadata { file_type: FileType::File, .. } ), "{:?}", metadata ); } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_set_readonly_flag_if_specified( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); // Verify that not readonly by default let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File is already set to readonly"); // Change the file permissions client .set_permissions( file.path().to_path_buf(), Permissions::readonly(), Default::default(), ) .await .unwrap(); // Retrieve permissions to verify set let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "File not set to readonly"); } #[allow(unused_attributes)] #[rstest] #[test(tokio::test)] #[cfg_attr(not(unix), ignore)] #[ignore] async fn set_permissions_should_set_unix_permissions_if_on_unix_platform( #[future] client: Ctx, ) { #[allow(unused_mut, unused_variables)] let mut client = client.await; #[cfg(unix)] { use std::os::unix::prelude::*; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); // Verify that permissions do not match our readonly state let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); let mode = permissions.mode() & 0o777; assert_ne!(mode, 0o400, "File is already set to 0o400"); // Change the file permissions client .set_permissions( file.path().to_path_buf(), Permissions::from_unix_mode(0o400), Default::default(), ) .await .unwrap(); // Retrieve file permissions to verify set let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); // Drop the upper bits that mode can have (only care about read/write/exec) let mode = permissions.mode() & 0o777; assert_eq!(mode, 0o400, "Wrong permissions on file: {:o}", mode); } #[cfg(not(unix))] { unreachable!(); } } #[allow(unused_attributes)] #[rstest] #[test(tokio::test)] #[cfg_attr(unix, ignore)] #[ignore] async fn set_permissions_should_set_readonly_flag_if_not_on_unix_platform( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); // Verify that not readonly by default let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File is already set to readonly"); // Change the file permissions to be readonly (in general) client .set_permissions( file.path().to_path_buf(), Permissions::from_unix_mode(0o400), Default::default(), ) .await .unwrap(); #[cfg(not(unix))] { // Retrieve file permissions to verify set let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "File not marked as readonly"); } #[cfg(unix)] { unreachable!(); } } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_not_recurse_if_option_false(#[future] client: Ctx) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); // Verify that dir is not readonly by default let permissions = tokio::fs::symlink_metadata(temp.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Temp dir is already set to readonly" ); // Verify that file is not readonly by default let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File is already set to readonly"); // Verify that symlink is not readonly by default let permissions = tokio::fs::symlink_metadata(symlink.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Symlink is already set to readonly" ); // Change the permissions of the directory and not the contents underneath client .set_permissions( temp.path().to_path_buf(), Permissions::readonly(), SetPermissionsOptions { recursive: false, ..Default::default() }, ) .await .unwrap(); // Retrieve permissions of the file, symlink, and directory to verify set let permissions = tokio::fs::symlink_metadata(temp.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "Temp directory not set to readonly"); let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File unexpectedly set to readonly"); let permissions = tokio::fs::symlink_metadata(symlink.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Symlink unexpectedly set to readonly" ); } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_traverse_symlinks_while_recursing_if_following_symlinks_enabled( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let temp2 = assert_fs::TempDir::new().unwrap(); let file2 = temp2.child("file"); file2.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_dir(temp2.path()).unwrap(); // Verify that symlink is not readonly by default let permissions = tokio::fs::symlink_metadata(file2.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File2 is already set to readonly"); // Change the main directory permissions client .set_permissions( temp.path().to_path_buf(), Permissions::readonly(), SetPermissionsOptions { follow_symlinks: true, recursive: true, ..Default::default() }, ) .await .unwrap(); // Retrieve permissions referenced by another directory let permissions = tokio::fs::symlink_metadata(file2.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "File2 not set to readonly"); } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_not_traverse_symlinks_while_recursing_if_following_symlinks_disabled( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let temp2 = assert_fs::TempDir::new().unwrap(); let file2 = temp2.child("file"); file2.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_dir(temp2.path()).unwrap(); // Verify that symlink is not readonly by default let permissions = tokio::fs::symlink_metadata(file2.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File2 is already set to readonly"); // Change the main directory permissions client .set_permissions( temp.path().to_path_buf(), Permissions::readonly(), SetPermissionsOptions { follow_symlinks: false, recursive: true, ..Default::default() }, ) .await .unwrap(); // Retrieve permissions referenced by another directory let permissions = tokio::fs::symlink_metadata(file2.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "File2 unexpectedly set to readonly" ); } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_skip_symlinks_if_exclude_symlinks_enabled( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); // Verify that symlink is not readonly by default let permissions = tokio::fs::symlink_metadata(symlink.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Symlink is already set to readonly" ); // Change the symlink permissions client .set_permissions( symlink.path().to_path_buf(), Permissions::readonly(), SetPermissionsOptions { exclude_symlinks: true, ..Default::default() }, ) .await .unwrap(); // Retrieve permissions to verify not set let permissions = tokio::fs::symlink_metadata(symlink.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Symlink (or file underneath) set to readonly" ); } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_support_recursive_if_option_specified( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); // Verify that dir is not readonly by default let permissions = tokio::fs::symlink_metadata(temp.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Temp dir is already set to readonly" ); // Verify that file is not readonly by default let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File is already set to readonly"); // Change the permissions of the file pointed to by the symlink client .set_permissions( temp.path().to_path_buf(), Permissions::readonly(), SetPermissionsOptions { recursive: true, ..Default::default() }, ) .await .unwrap(); // Retrieve permissions of the file, symlink, and directory to verify set let permissions = tokio::fs::symlink_metadata(temp.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "Temp directory not set to readonly"); let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "File not set to readonly"); } #[rstest] #[test(tokio::test)] #[ignore] async fn set_permissions_should_support_following_symlinks_if_option_specified( #[future] client: Ctx, ) { let mut client = client.await; let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); // Verify that file is not readonly by default let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(!permissions.readonly(), "File is already set to readonly"); // Verify that symlink is not readonly by default let permissions = tokio::fs::symlink_metadata(symlink.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Symlink is already set to readonly" ); // Change the permissions of the file pointed to by the symlink client .set_permissions( symlink.path().to_path_buf(), Permissions::readonly(), SetPermissionsOptions { follow_symlinks: true, ..Default::default() }, ) .await .unwrap(); // Retrieve permissions of the file and symlink to verify set let permissions = tokio::fs::symlink_metadata(file.path()) .await .unwrap() .permissions(); assert!(permissions.readonly(), "File not set to readonly"); let permissions = tokio::fs::symlink_metadata(symlink.path()) .await .unwrap() .permissions(); assert!( !permissions.readonly(), "Symlink unexpectedly set to readonly" ); } #[rstest] #[test(tokio::test)] async fn proc_spawn_should_not_fail_even_if_process_not_found( #[future] client: Ctx, ) { let mut client = client.await; // NOTE: This is a distinction from standard distant and ssh distant let _ = client .spawn( /* cmd */ DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); } #[rstest] #[test(tokio::test)] async fn proc_spawn_should_return_id_of_spawned_process(#[future] client: Ctx) { let mut client = client.await; let proc = client .spawn( /* cmd */ format!( "{} {}", *SCRIPT_RUNNER, ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap() ), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); assert!(proc.id() > 0); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn proc_spawn_should_send_back_stdout_periodically_when_available( #[future] client: Ctx, ) { let mut client = client.await; let mut proc = client .spawn( /* cmd */ format!( "{} {} some stdout", *SCRIPT_RUNNER, ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap() ), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); assert_eq!( proc.stdout.as_mut().unwrap().read().await.unwrap(), b"some stdout" ); assert!( proc.wait().await.unwrap().success, "Process should have completed successfully" ); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn proc_spawn_should_send_back_stderr_periodically_when_available( #[future] client: Ctx, ) { let mut client = client.await; let mut proc = client .spawn( /* cmd */ format!( "{} {} some stderr", *SCRIPT_RUNNER, ECHO_ARGS_TO_STDERR_SH.to_str().unwrap() ), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); assert_eq!( proc.stderr.as_mut().unwrap().read().await.unwrap(), b"some stderr" ); assert!( proc.wait().await.unwrap().success, "Process should have completed successfully" ); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn proc_spawn_should_send_done_signal_when_completed(#[future] client: Ctx) { let mut client = client.await; let proc = client .spawn( /* cmd */ format!("{} {} 0.1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); let _ = proc.wait().await.unwrap(); } #[rstest] #[test(tokio::test)] async fn proc_spawn_should_clear_process_from_state_when_killed( #[future] client: Ctx, ) { let mut client = client.await; let mut proc = client .spawn( /* cmd */ format!("{} {} 1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); // Send kill signal proc.kill().await.unwrap(); // Verify killed, which should be success false let status = proc.wait().await.unwrap(); assert!(!status.success, "Process succeeded when killed") } #[rstest] #[test(tokio::test)] async fn proc_kill_should_fail_if_process_not_running(#[future] client: Ctx) { let mut client = client.await; let mut proc = client .spawn( /* cmd */ format!("{} {} 1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); // Send kill signal proc.kill().await.unwrap(); // Wait for process to be dead let mut killer = proc.clone_killer(); let _ = proc.wait().await.unwrap(); // Now send it again, which should fail let _ = killer.kill().await.unwrap_err(); } #[rstest] #[test(tokio::test)] async fn proc_stdin_should_fail_if_process_not_running(#[future] client: Ctx) { let mut client = client.await; let mut proc = client .spawn( /* cmd */ format!("{} {} 1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); // Send kill signal proc.kill().await.unwrap(); // Wait for process to be dead let mut stdin = proc.stdin.take().unwrap(); let _ = proc.wait().await.unwrap(); // Now send stdin, which should fail let _ = stdin.write_str("some data").await.unwrap_err(); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[rstest] #[test(tokio::test)] #[cfg_attr(windows, ignore)] async fn proc_stdin_should_send_stdin_to_process(#[future] client: Ctx) { let mut client = client.await; // First, run a program that listens for stdin let mut proc = client .spawn( /* cmd */ format!( "{} {}", *SCRIPT_RUNNER, ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap() ), /* environment */ Environment::new(), /* current_dir */ None, /* pty */ None, ) .await .unwrap(); // Second, send stdin to the remote process proc.stdin .as_mut() .unwrap() .write_str("hello world\n") .await .unwrap(); // Third, check the async response of stdout to verify we got stdin assert_eq!( proc.stdout.as_mut().unwrap().read_string().await.unwrap(), "hello world\n" ); } #[rstest] #[test(tokio::test)] async fn system_info_should_return_system_info_based_on_binary( #[future] client: Ctx, ) { let mut client = client.await; let system_info = client.system_info().await.unwrap(); assert_eq!(system_info.family, std::env::consts::FAMILY.to_string()); // We only support setting the os when the family is windows if system_info.family == "windows" { assert_eq!(system_info.os, "windows"); } else { assert_eq!(system_info.os, ""); } assert_eq!(system_info.arch, ""); assert_eq!(system_info.main_separator, std::path::MAIN_SEPARATOR); // We don't have an easy way to tell the remote username and shell in most cases, // so we just check that they are not empty assert_ne!(system_info.username, ""); assert_ne!(system_info.shell, ""); }