diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 915e4b9..dbf6a68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,8 @@ jobs: - uses: actions/checkout@v4 - name: Test - run: cargo test --workspace --lib --bins + run: cargo test --workspace --bins env: + STORIFY_SKIP_BEHAVIOR: "1" RUST_LOG: DEBUG RUST_BACKTRACE: full diff --git a/.github/workflows/test_behavior.yml b/.github/workflows/test_behavior.yml index 50c4bf4..e984569 100644 --- a/.github/workflows/test_behavior.yml +++ b/.github/workflows/test_behavior.yml @@ -31,7 +31,7 @@ jobs: echo "MinIO not ready" >&2; exit 1 - name: Run Behavior Tests - run: cargo test --test behavior --quiet + run: cargo test tests::behavior_suite -- --quiet env: RUST_LOG: info diff --git a/Cargo.lock b/Cargo.lock index 1152333..fd218fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -995,7 +995,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] @@ -1609,7 +1609,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.0", + "socket2 0.5.9", "thiserror", "tokio", "tracing", @@ -1646,9 +1646,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.5.9", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1908,9 +1908,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "once_cell", "ring", @@ -1941,9 +1941,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -2359,9 +2359,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2731,14 +2731,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 7bf6cf0..a7e9abf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,3 @@ predicates = "3.1.3" rand = "0.9.2" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "tracing-log"] } tempfile = "3.14.0" - -[[test]] -harness = false -name = "behavior" -path = "tests/behavior/main.rs" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cb399f2..633a5ca 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -4,6 +4,4 @@ pub mod entry; pub mod prompts; pub mod storage; -pub use context::CliContext; -pub use entry::{Args, Command, ConfigCommand, GlobalOptions, run, run_with_prompt}; -pub use prompts::Prompt; +pub use entry::{Args, run}; diff --git a/src/cli/prompts.rs b/src/cli/prompts.rs index b28c18a..d4e8116 100644 --- a/src/cli/prompts.rs +++ b/src/cli/prompts.rs @@ -6,9 +6,10 @@ use tokio::task; use crate::error::{Error, Result}; /// Interactive prompt mode for CLI operations -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum Prompt { /// Console-based interactive prompts using dialoguer + #[default] Console, /// Non-interactive mode that uses defaults or fails NonInteractive, @@ -77,12 +78,6 @@ impl Prompt { } } -impl Default for Prompt { - fn default() -> Self { - Self::Console - } -} - fn join_error(err: task::JoinError) -> Error { Error::Io { source: io::Error::other(err.to_string()), diff --git a/src/config/loader.rs b/src/config/loader.rs index 4a5bbe0..8a60f88 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -332,28 +332,6 @@ pub fn resolve(request: ConfigRequest) -> Result { Ok(resolved) } -/// Load storage configuration using environment variables only. -pub fn load_storage_config() -> Result { - let resolved = resolve(ConfigRequest { - profile: None, - profile_store_path: None, - non_interactive: false, - require_storage: true, - master_password: None, - })?; - - let storage = resolved.storage.ok_or_else(|| Error::NoConfiguration { - profiles: "none".to_string(), - })?; - Ok(storage) -} - -pub fn load_env_storage_config(provider_hint: Option) -> Result { - load_env_config(&env_value, provider_hint) - .and_then(build_config) - .map_err(with_config_hint) -} - fn load_env_config( get: &dyn Fn(&str) -> Option, provider_hint: Option, diff --git a/src/config/profile_store.rs b/src/config/profile_store.rs index 9723ed7..fc9005f 100644 --- a/src/config/profile_store.rs +++ b/src/config/profile_store.rs @@ -121,17 +121,6 @@ impl ProfileStore { default_store_path() } - pub fn open_default() -> Result { - Self::open_with_options(ProfileStoreOpenOptions::default()) - } - - pub fn open(path: Option) -> Result { - Self::open_with_options(ProfileStoreOpenOptions { - path, - ..ProfileStoreOpenOptions::default() - }) - } - pub fn open_with_password( path: Option, master_password: Option, @@ -350,15 +339,6 @@ impl ProfileStore { self.persist() } - /// Re-derive encryption key (for key rotation or master password change) - pub fn set_encryption(&mut self, master_password: Option) -> Result<()> { - let password = resolve_master_password(master_password, &self.path); - let salt = generate_salt(); - let key = derive_master_key(&password, &salt)?; - self.encryption = EncryptionMetadata::new(key, salt.to_vec()); - self.persist() - } - fn persist(&mut self) -> Result<()> { let mut payload = self.file.clone(); payload.normalize_default(); @@ -389,7 +369,7 @@ impl ProfileStore { write_atomic(&self.path, serialized.as_bytes())?; // Synchronize salt file to ensure it matches the in-memory state - // This is critical for operations like set_encryption() that generate a new salt + // This is critical for operations that regenerate the encryption metadata let salt_path = Self::salt_file_path(&self.path); Self::write_salt_file(&salt_path, salt)?; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 3a166de..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod cli; -pub mod config; -pub mod error; -pub mod storage; -pub mod utils; diff --git a/src/main.rs b/src/main.rs index 4ed5e63..6190e39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,15 @@ +mod cli; +mod config; +mod error; +mod storage; +mod utils; + +#[cfg(test)] +mod tests; + use clap::Parser; -use storify::cli::{Args, run}; +use crate::cli::{Args, run}; #[tokio::main] async fn main() { diff --git a/src/storage.rs b/src/storage.rs index abbacaa..29973e7 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -47,10 +47,14 @@ impl StorageClient { Ok(Self { operator, provider }) } + #[cfg(test)] + #[allow(dead_code)] pub fn provider(&self) -> StorageProvider { self.provider } + #[cfg(test)] + #[allow(dead_code)] pub fn operator(&self) -> &Operator { &self.operator } diff --git a/src/storage/constants.rs b/src/storage/constants.rs index 0248fbb..22b2f4c 100644 --- a/src/storage/constants.rs +++ b/src/storage/constants.rs @@ -10,4 +10,3 @@ pub const PROGRESS_UPDATE_INTERVAL: u64 = 100; pub const DEFAULT_FS_ROOT: &str = "./storage"; pub const DEFAULT_HDFS_ROOT: &str = "/"; pub const DEFAULT_COS_ENDPOINT: &str = "https://cos.myqcloud.com"; -pub const CAT_CONFIRM_SIZE_THRESHOLD: u64 = 10 * 1024 * 1024; diff --git a/src/tests/behavior/macros.rs b/src/tests/behavior/macros.rs new file mode 100644 index 0000000..b1419de --- /dev/null +++ b/src/tests/behavior/macros.rs @@ -0,0 +1,21 @@ +macro_rules! register_behavior_tests { + ($($test:ident),+ $(,)?) => { + pub fn tests(client: &StorageClient, tests: &mut Vec) { + tests.extend(async_trials!(client, $( $test ),+)); + } + + #[cfg(test)] + mod case_tests { + $( + #[test] + fn $test() { + if let Err(err) = crate::tests::behavior::run_behavior_case( + concat!("behavior::", stringify!($test)), + ) { + panic!("behavior case {} failed: {err}", stringify!($test)); + } + } + )* + } + }; +} diff --git a/src/tests/behavior/mod.rs b/src/tests/behavior/mod.rs new file mode 100644 index 0000000..3025c96 --- /dev/null +++ b/src/tests/behavior/mod.rs @@ -0,0 +1,125 @@ +use crate::error::{Error, Result}; +use libtest_mimic::Arguments; +pub use libtest_mimic::Trial; +use std::env; +use std::sync::{LazyLock, Mutex}; + +#[macro_use] +mod macros; + +pub mod operations; +pub mod utils; + +pub use utils::*; + +pub const SKIP_ENV: &str = "STORIFY_SKIP_BEHAVIOR"; + +pub fn run_behavior_suite() -> Result<()> { + if behavior_skipped("behavior suite") { + return Ok(()); + } + let _guard = BEHAVIOR_MUTEX.lock().unwrap(); + run_behavior_with_args(behavior_arguments(None)) +} + +pub fn run_behavior_case(name: &str) -> Result<()> { + if behavior_skipped(name) { + return Ok(()); + } + let _guard = BEHAVIOR_MUTEX.lock().unwrap(); + run_behavior_with_args(behavior_arguments(Some(name))) +} + +fn run_behavior_with_args(args: Arguments) -> Result<()> { + let client = TEST_RUNTIME.block_on(init_test_service())?; + + let mut tests = Vec::new(); + + operations::list::tests(&client, &mut tests); + operations::copy::tests(&client, &mut tests); + operations::delete::tests(&client, &mut tests); + operations::download::tests(&client, &mut tests); + operations::head::tests(&client, &mut tests); + operations::grep::tests(&client, &mut tests); + operations::find::tests(&client, &mut tests); + operations::tail::tests(&client, &mut tests); + operations::mkdir::tests(&client, &mut tests); + operations::mv::tests(&client, &mut tests); + operations::upload::tests(&client, &mut tests); + operations::cat::tests(&client, &mut tests); + operations::usage::tests(&client, &mut tests); + operations::stat::tests(&client, &mut tests); + operations::tree::tests(&client, &mut tests); + operations::diff::tests(&client, &mut tests); + operations::touch::tests(&client, &mut tests); + + let _ = tracing_subscriber::fmt() + .pretty() + .with_test_writer() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + + let conclusion = libtest_mimic::run(&args, tests); + + TEST_RUNTIME.block_on(TEST_FIXTURE.cleanup(client.operator())); + + if conclusion.has_failed() { + return Err(Error::InvalidArgument { + message: "storify behavior tests failed".to_string(), + }); + } + + Ok(()) +} + +fn behavior_arguments(filter: Option<&str>) -> Arguments { + let mut args = base_behavior_arguments(); + if let Some(filter) = filter { + args.filter = Some(filter.to_string()); + args.exact = true; + } + args +} + +fn base_behavior_arguments() -> Arguments { + let mut raw = env::args(); + let mut filtered = Vec::new(); + if let Some(bin) = raw.next() { + filtered.push(bin); + } + + let mut filter_removed = false; + let mut saw_double_dash = false; + + for arg in raw { + if !saw_double_dash && arg == "--" { + saw_double_dash = true; + filtered.push(arg); + continue; + } + + if !saw_double_dash && !filter_removed && !arg.starts_with('-') && is_harness_filter(&arg) { + filter_removed = true; + continue; + } + + filtered.push(arg); + } + + Arguments::from_iter(filtered) +} + +fn is_harness_filter(arg: &str) -> bool { + arg == "behavior_suite" || arg == "tests::behavior_suite" || arg.contains("case_tests::") +} + +static BEHAVIOR_MUTEX: LazyLock> = LazyLock::new(|| Mutex::new(())); + +fn behavior_skipped(label: &str) -> bool { + if env::var(SKIP_ENV).is_ok() { + eprintln!("behavior test '{label}' skipped because {SKIP_ENV} is set"); + true + } else { + false + } +} diff --git a/tests/behavior/operations/cat.rs b/src/tests/behavior/operations/cat.rs similarity index 56% rename from tests/behavior/operations/cat.rs rename to src/tests/behavior/operations/cat.rs index 8e4baf2..2a1338b 100644 --- a/tests/behavior/operations/cat.rs +++ b/src/tests/behavior/operations/cat.rs @@ -1,23 +1,18 @@ -use crate::*; +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; use assert_cmd::prelude::*; -use predicates::prelude::*; -use std::process::Stdio; -use storify::error::Result; -use storify::storage::StorageClient; -use tokio::fs; -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_cat_small_file_prints_content, - test_cat_large_file_non_interactive_aborts, - test_cat_large_file_force_streams - )); -} +register_behavior_tests!( + test_cat_small_file_prints_content, + test_cat_large_file_force_streams, +); // Verify cat prints the content of a small text file async fn test_cat_small_file_prints_content(_client: StorageClient) -> Result<()> { - let source_path = get_test_data_path("small.txt"); + let content = b"cat small file\nHello world\n".to_vec(); + let source_path = write_temp_file(&content, ".txt"); let dest_prefix = TEST_FIXTURE.new_file_path(); // Upload via CLI to ensure end-to-end path @@ -34,7 +29,7 @@ async fn test_cat_small_file_prints_content(_client: StorageClient) -> Result<() .to_string_lossy() .to_string(); let remote_path = join_remote_path(&dest_prefix, &file_name); - let expected = fs::read(&source_path).await?; + let expected = content.clone(); let assert = storify_cmd() .arg("cat") @@ -47,30 +42,10 @@ async fn test_cat_small_file_prints_content(_client: StorageClient) -> Result<() Ok(()) } -// Verify non-interactive stdin aborts when exceeding size limit (without --force) -async fn test_cat_large_file_non_interactive_aborts(_client: StorageClient) -> Result<()> { - // Prepare ~2 MiB file so we can trip a low size-limit without huge output - let env = E2eTestEnv::new().await; - let remote_path = TEST_FIXTURE.new_file_path(); - let content = vec![b'X'; 2 * 1024 * 1024]; - env.verifier.operator().write(&remote_path, content).await?; - - storify_cmd() - .arg("cat") - .arg("--size-limit") - .arg("1") // 1 MB limit so 2 MB triggers guard - .arg(&remote_path) - .stdin(Stdio::null()) // simulate non-interactive - .assert() - .success() - .stderr(predicate::str::contains("File too large")); - - Ok(()) -} - // Verify --force streams content when exceeding size limit async fn test_cat_large_file_force_streams(_client: StorageClient) -> Result<()> { - let source_path = get_test_data_path("small.txt"); + let content = "Force stream content\n".repeat(4); + let source_path = write_temp_file(content.as_bytes(), ".txt"); let dest_prefix = TEST_FIXTURE.new_file_path(); let file_name = source_path .file_name() @@ -87,7 +62,7 @@ async fn test_cat_large_file_force_streams(_client: StorageClient) -> Result<()> .success(); let remote_path = join_remote_path(&dest_prefix, &file_name); - let expected = fs::read(&source_path).await?; + let expected = content.into_bytes(); // Force with a very small size-limit to ensure the guard would trigger let assert = storify_cmd() diff --git a/src/tests/behavior/operations/copy.rs b/src/tests/behavior/operations/copy.rs new file mode 100644 index 0000000..708f1d3 --- /dev/null +++ b/src/tests/behavior/operations/copy.rs @@ -0,0 +1,62 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!( + test_copy_file_to_existing_directory, + test_copy_non_existent_file, +); + +async fn test_copy_file_to_existing_directory(client: StorageClient) -> Result<()> { + let src_dir = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&src_dir).await?; + + let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); + let src_file_path = format!("{}{}", src_dir, src_file); + client + .operator() + .write(&src_file_path, content.clone()) + .await?; + + let dest_dir = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&dest_dir).await?; + + storify_cmd() + .arg("cp") + .arg(&src_file_path) + .arg(&dest_dir) + .assert() + .success(); + + let dest_file_path = format!("{}{}", dest_dir, src_file); + let dst_content = client.operator().read(&dest_file_path).await?; + assert_eq!(content, dst_content.to_vec()); + + let src_content = client.operator().read(&src_file_path).await?; + assert_eq!(content, src_content.to_vec()); + + Ok(()) +} + +async fn test_copy_non_existent_file(client: StorageClient) -> Result<()> { + let non_existent_src = TEST_FIXTURE.new_dir_path(); + let non_exist_src_file = TEST_FIXTURE.new_file_path(); + client.operator().create_dir(&non_existent_src).await?; + let final_src_file = format!("{}{}", non_existent_src, non_exist_src_file); + + let dest_path = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&dest_path).await?; + + storify_cmd() + .arg("cp") + .arg(&final_src_file) + .arg(&dest_path) + .assert() + .failure() + .stderr(predicate::str::contains("Invalid path")); + + Ok(()) +} diff --git a/src/tests/behavior/operations/delete.rs b/src/tests/behavior/operations/delete.rs new file mode 100644 index 0000000..93feedf --- /dev/null +++ b/src/tests/behavior/operations/delete.rs @@ -0,0 +1,59 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; + +register_behavior_tests!( + test_delete_single_file, + test_delete_non_empty_directory_recursively, +); + +async fn test_delete_single_file(client: StorageClient) -> Result<()> { + let (path, content, _) = TEST_FIXTURE.new_file(client.operator()); + client.operator().write(&path, content).await?; + + storify_cmd() + .arg("rm") + .arg("--force") + .arg(&path) + .assert() + .success(); + + let result = client.operator().stat(&path).await; + assert!(result.is_err(), "File should be deleted"); + assert!( + matches!(result.unwrap_err().kind(), opendal::ErrorKind::NotFound), + "Error should be NotFound" + ); + + Ok(()) +} + +async fn test_delete_non_empty_directory_recursively(client: StorageClient) -> Result<()> { + let root_dir = TEST_FIXTURE.new_dir_path(); + let file_path = format!("{root_dir}test.txt"); + let (path, content, _) = TEST_FIXTURE.new_file_with_range(file_path, 1..1024); + client.operator().write(&path, content).await?; + + E2eTestEnv::new() + .await + .command() + .arg("rm") + .arg("-R") + .arg("--force") + .arg(&root_dir) + .assert() + .success(); + + let result = client.operator().stat(&root_dir).await; + assert!(result.is_err(), "Root directory should be deleted"); + + let file_result = client.operator().stat(&path).await; + assert!( + file_result.is_err(), + "File within directory should be deleted" + ); + + Ok(()) +} diff --git a/src/tests/behavior/operations/diff.rs b/src/tests/behavior/operations/diff.rs new file mode 100644 index 0000000..3f4cc2b --- /dev/null +++ b/src/tests/behavior/operations/diff.rs @@ -0,0 +1,61 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!( + test_diff_basic_unified_output, + test_diff_size_limit_blocks_without_force, +); + +async fn upload_text_file(env: &E2eTestEnv, content: &str) -> Result { + let path = TEST_FIXTURE.new_file_path(); + env.verifier + .operator() + .write(&path, content.as_bytes().to_vec()) + .await?; + Ok(path) +} + +async fn test_diff_basic_unified_output(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let left = upload_text_file(&env, "a\nb\nc\n").await?; + let right = upload_text_file(&env, "a\nB\nc\n").await?; + + storify_cmd() + .arg("diff") + .arg(&left) + .arg(&right) + .arg("-U") + .arg("1") + .assert() + .success() + .stdout( + // Expect unified diff markers + predicate::str::contains("@@") + .and(predicate::str::contains("-b")) + .and(predicate::str::contains("+B")), + ); + + Ok(()) +} + +async fn test_diff_size_limit_blocks_without_force(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let left = upload_text_file(&env, &"X".repeat(2 * 1024 * 1024)).await?; // 2MB + let right = upload_text_file(&env, &"Y".repeat(2 * 1024 * 1024)).await?; // 2MB + + storify_cmd() + .arg("diff") + .arg(&left) + .arg(&right) + .arg("--size-limit") + .arg("1") // 1MB total limit so blocked + .assert() + .failure() + .stderr(predicate::str::contains("Files too large")); + + Ok(()) +} diff --git a/src/tests/behavior/operations/download.rs b/src/tests/behavior/operations/download.rs new file mode 100644 index 0000000..b0390c2 --- /dev/null +++ b/src/tests/behavior/operations/download.rs @@ -0,0 +1,106 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; +use tokio::fs; +use uuid::Uuid; + +register_behavior_tests!( + test_download_existing_file_to_directory, + test_download_directory_recursive, + test_download_non_existent_file, +); + +#[derive(Clone)] +struct StagedFile { + remote_path: String, + content: Vec, + file_name: String, +} + +async fn stage_remote_file(client: &StorageClient) -> Result { + let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); + let src_path = TEST_FIXTURE.new_dir_path(); + + client.operator().create_dir(&src_path).await?; + let src_file_path = format!("{}{}", src_path, src_file); + client + .operator() + .write(&src_file_path, content.clone()) + .await?; + + Ok(StagedFile { + remote_path: src_file_path, + content, + file_name: src_file, + }) +} + +async fn stage_remote_directory(client: &StorageClient) -> Result<(String, Vec)> { + let src_dir = TEST_FIXTURE.new_dir_path(); + let file_name = "test_file.txt"; + let file_path = format!("{}{}", src_dir, file_name); + let content = b"test directory content".to_vec(); + + client.operator().create_dir(&src_dir).await?; + client.operator().write(&file_path, content.clone()).await?; + + Ok((src_dir, content)) +} + +async fn test_download_existing_file_to_directory(client: StorageClient) -> Result<()> { + let staged_file = stage_remote_file(&client).await?; + let local_dir = std::env::temp_dir().join(format!("storify-dl-{}", Uuid::new_v4())); + + storify_cmd() + .arg("get") + .arg(&staged_file.remote_path) + .arg(&local_dir) + .assert() + .success() + .stdout(predicate::str::contains("Downloaded:")); + + let downloaded_file = local_dir.join(&staged_file.file_name); + let actual_content = fs::read(&downloaded_file).await?; + assert_eq!(staged_file.content, actual_content); + + let _ = fs::remove_dir_all(&local_dir).await; + Ok(()) +} + +async fn test_download_directory_recursive(client: StorageClient) -> Result<()> { + let (remote_dir, expected_content) = stage_remote_directory(&client).await?; + let local_dest = std::env::temp_dir().join(format!("storify-dl-dir-{}", Uuid::new_v4())); + + storify_cmd() + .arg("get") + .arg(&remote_dir) + .arg(&local_dest) + .assert() + .success(); + + let dest_file = local_dest.join("test_file.txt"); + let dest_content = fs::read(&dest_file).await?; + assert_eq!(expected_content, dest_content); + + let _ = fs::remove_dir_all(&local_dest).await; + Ok(()) +} + +async fn test_download_non_existent_file(_client: StorageClient) -> Result<()> { + let remote_path = TEST_FIXTURE.new_file_path(); + let local_dir = std::env::temp_dir().join(format!("storify-dl-miss-{}", Uuid::new_v4())); + + storify_cmd() + .arg("get") + .arg(&remote_path) + .arg(&local_dir) + .assert() + .failure() + .stderr(predicate::str::contains("Failed to download")); + + assert!(!local_dir.exists()); + Ok(()) +} diff --git a/src/tests/behavior/operations/find.rs b/src/tests/behavior/operations/find.rs new file mode 100644 index 0000000..0ab7e29 --- /dev/null +++ b/src/tests/behavior/operations/find.rs @@ -0,0 +1,59 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!(test_find_by_name_glob, test_find_type_file,); + +async fn test_find_by_name_glob(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let root = TEST_FIXTURE.new_dir_path(); + let sub = format!("{root}sub/"); + env.verifier.operator().create_dir(&root).await?; + env.verifier.operator().create_dir(&sub).await?; + + let f1 = format!("{root}a.log"); + let f2 = format!("{sub}b.log"); + let f3 = format!("{sub}c.txt"); + env.verifier.operator().write(&f1, b"x".to_vec()).await?; + env.verifier.operator().write(&f2, b"y".to_vec()).await?; + env.verifier.operator().write(&f3, b"z".to_vec()).await?; + + storify_cmd() + .arg("find") + .arg(&root) + .arg("--name") + .arg("**/*.log") + .assert() + .success() + .stdout( + predicate::str::contains(&f1) + .and(predicate::str::contains(&f2)) + .and(predicate::str::contains(&f3).not()), + ); + + Ok(()) +} + +async fn test_find_type_file(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let root = TEST_FIXTURE.new_dir_path(); + let sub = format!("{root}d/"); + env.verifier.operator().create_dir(&root).await?; + env.verifier.operator().create_dir(&sub).await?; + let f = format!("{root}file.bin"); + env.verifier.operator().write(&f, b"data".to_vec()).await?; + + storify_cmd() + .arg("find") + .arg(&root) + .arg("--type") + .arg("f") + .assert() + .success() + .stdout(predicate::str::contains(&f).and(predicate::str::contains(&sub).not())); + + Ok(()) +} diff --git a/src/tests/behavior/operations/grep.rs b/src/tests/behavior/operations/grep.rs new file mode 100644 index 0000000..6d874b6 --- /dev/null +++ b/src/tests/behavior/operations/grep.rs @@ -0,0 +1,88 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!( + test_grep_basic, + test_grep_recursive_basic, + test_grep_directory_without_recursive_flag, +); + +async fn prepare_remote_file(verifier: &StorageClient, content: &[u8]) -> Result { + let path = TEST_FIXTURE.new_file_path(); + verifier.operator().write(&path, content.to_vec()).await?; + Ok(path) +} + +async fn test_grep_basic(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let content = b"alpha\nbeta\ngamma\nAlpha Beta\n"; + let remote_path = prepare_remote_file(&env.verifier, content).await?; + + storify_cmd() + .arg("grep") + .arg("beta") + .arg(&remote_path) + .assert() + .success() + .stdout(predicate::str::contains("beta").and(predicate::str::contains("Alpha Beta").not())); + + Ok(()) +} + +async fn test_grep_recursive_basic(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let root_dir = TEST_FIXTURE.new_dir_path(); + let sub_dir = format!("{root_dir}sub/"); + env.verifier.operator().create_dir(&root_dir).await?; + env.verifier.operator().create_dir(&sub_dir).await?; + + let root_file = format!("{root_dir}a.txt"); + let sub_file = format!("{sub_dir}b.txt"); + env.verifier + .operator() + .write(&root_file, b"foo\nmatch here\nbar\n".to_vec()) + .await?; + env.verifier + .operator() + .write(&sub_file, b"nope\nTARGET in sub\n".to_vec()) + .await?; + + storify_cmd() + .arg("grep") + .arg("-R") + .arg("TARGET") + .arg(&root_dir) + .assert() + .success() + .stdout( + predicate::str::contains(format!("{}:", sub_file)) + .and(predicate::str::contains("TARGET in sub")), + ); + + Ok(()) +} + +async fn test_grep_directory_without_recursive_flag(_client: StorageClient) -> Result<()> { + let env = E2eTestEnv::new().await; + let root_dir = TEST_FIXTURE.new_dir_path(); + env.verifier.operator().create_dir(&root_dir).await?; + let f = format!("{root_dir}a.txt"); + env.verifier + .operator() + .write(&f, b"hello\nworld\n".to_vec()) + .await?; + + storify_cmd() + .arg("grep") + .arg("hello") + .arg(&root_dir) + .assert() + .failure() + .stderr(predicate::str::contains("use -R")); + + Ok(()) +} diff --git a/src/tests/behavior/operations/head.rs b/src/tests/behavior/operations/head.rs new file mode 100644 index 0000000..35af27e --- /dev/null +++ b/src/tests/behavior/operations/head.rs @@ -0,0 +1,75 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!( + test_head_default_10_lines, + test_head_n_lines, + test_head_nonexistent_file, +); + +fn create_temp_file_with_content(content: &[u8]) -> String { + let path = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string()); + std::fs::write(&path, content).expect("write temp file"); + path.to_string_lossy().to_string() +} + +fn upload_and_remote_path(local_path: &str, dest_prefix: &str) -> String { + storify_cmd() + .arg("put") + .arg(local_path) + .arg(dest_prefix) + .assert() + .success(); + + let file_name = std::path::Path::new(local_path) + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + join_remote_path(dest_prefix, &file_name) +} + +async fn test_head_default_10_lines(_client: StorageClient) -> Result<()> { + let lines: Vec = (1..=20).map(|i| format!("line-{i}")).collect(); + let local = create_temp_file_with_content((lines.join("\n") + "\n").as_bytes()); + + let remote = upload_and_remote_path(&local, &TEST_FIXTURE.new_file_path()); + let assert = storify_cmd().arg("head").arg(&remote).assert().success(); + let output = String::from_utf8_lossy(&assert.get_output().stdout); + + assert_eq!(output.lines().count(), 10); + assert!(output.contains("line-10")); + assert!(!output.contains("line-11")); + Ok(()) +} + +async fn test_head_n_lines(_client: StorageClient) -> Result<()> { + let local = create_temp_file_with_content(b"A\nB\nC\nD\n"); + let remote = upload_and_remote_path(&local, &TEST_FIXTURE.new_file_path()); + + let assert = storify_cmd() + .arg("head") + .arg("-n") + .arg("2") + .arg(&remote) + .assert() + .success(); + let output = String::from_utf8_lossy(&assert.get_output().stdout); + + assert_eq!(output.lines().collect::>(), ["A", "B"]); + Ok(()) +} + +async fn test_head_nonexistent_file(_client: StorageClient) -> Result<()> { + storify_cmd() + .arg("head") + .arg("missing-file.txt") + .assert() + .failure() + .stderr(predicate::str::contains("Failed to read head of file")); + Ok(()) +} diff --git a/src/tests/behavior/operations/list.rs b/src/tests/behavior/operations/list.rs new file mode 100644 index 0000000..a802107 --- /dev/null +++ b/src/tests/behavior/operations/list.rs @@ -0,0 +1,55 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use futures::TryStreamExt; +use opendal::EntryMode; +use predicates::prelude::*; + +register_behavior_tests!( + test_list_empty_directory, + test_list_single_file, + test_list_invalid_path, +); + +async fn test_list_empty_directory(client: StorageClient) -> Result<()> { + let dir = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&dir).await?; + + storify_cmd().arg("ls").arg(&dir).assert().success(); + + let mut lister = client.operator().lister(&dir).await?; + while let Some(entry) = lister.try_next().await? { + assert!(entry.path() == dir || entry.path().is_empty()); + } + Ok(()) +} + +async fn test_list_single_file(client: StorageClient) -> Result<()> { + let (path, content, size) = TEST_FIXTURE.new_file(client.operator()); + client.operator().write(&path, content).await?; + + let parent = path.rsplit('/').nth(1).unwrap_or(""); + storify_cmd() + .arg("ls") + .arg(if parent.is_empty() { "/" } else { parent }) + .assert() + .success() + .stdout(predicate::str::contains(&path)); + + let meta = client.operator().stat(&path).await?; + assert_eq!(meta.mode(), EntryMode::FILE); + assert_eq!(meta.content_length(), size as u64); + Ok(()) +} + +async fn test_list_invalid_path(_client: StorageClient) -> Result<()> { + storify_cmd() + .arg("ls") + .arg("nonexistent-dir/") + .assert() + .success() + .stdout(predicate::str::is_empty()); + Ok(()) +} diff --git a/src/tests/behavior/operations/mkdir.rs b/src/tests/behavior/operations/mkdir.rs new file mode 100644 index 0000000..95b6413 --- /dev/null +++ b/src/tests/behavior/operations/mkdir.rs @@ -0,0 +1,46 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; +use uuid::Uuid; + +register_behavior_tests!( + test_create_single_directory, + test_create_directory_with_parents, +); + +async fn test_create_single_directory(_client: StorageClient) -> Result<()> { + let dir = format!("test-dir-{}", Uuid::new_v4()); + + storify_cmd() + .arg("mkdir") + .arg(&dir) + .assert() + .success() + .stdout(predicate::str::contains("Created directory")); + + storify_cmd().arg("ls").arg(&dir).assert().success(); + storify_cmd().arg("rm").arg("-R").arg(&dir).output().ok(); + Ok(()) +} + +async fn test_create_directory_with_parents(_client: StorageClient) -> Result<()> { + let nested = format!("parent-{}/child/sub", Uuid::new_v4()); + + storify_cmd() + .arg("mkdir") + .arg("-p") + .arg(&nested) + .assert() + .success() + .stdout(predicate::str::contains("Created directory")); + + storify_cmd() + .arg("ls") + .arg(nested.trim_end_matches("sub")) + .assert() + .success(); + Ok(()) +} diff --git a/tests/behavior/operations/mod.rs b/src/tests/behavior/operations/mod.rs similarity index 100% rename from tests/behavior/operations/mod.rs rename to src/tests/behavior/operations/mod.rs diff --git a/src/tests/behavior/operations/mv.rs b/src/tests/behavior/operations/mv.rs new file mode 100644 index 0000000..d767a7d --- /dev/null +++ b/src/tests/behavior/operations/mv.rs @@ -0,0 +1,49 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!( + test_move_file_to_existing_directory, + test_move_non_existent_file, +); + +async fn test_move_file_to_existing_directory(client: StorageClient) -> Result<()> { + let src_dir = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&src_dir).await?; + let (file, content, _) = TEST_FIXTURE.new_file(client.operator()); + let src_path = format!("{src_dir}{file}"); + client.operator().write(&src_path, content.clone()).await?; + + let dest_dir = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&dest_dir).await?; + + storify_cmd() + .arg("mv") + .arg(&src_path) + .arg(&dest_dir) + .assert() + .success(); + + let dest_path = format!("{dest_dir}{file}"); + let dst_content = client.operator().read(&dest_path).await?; + assert_eq!(content, dst_content.to_vec()); + assert!(client.operator().read(&src_path).await.is_err()); + Ok(()) +} + +async fn test_move_non_existent_file(client: StorageClient) -> Result<()> { + let dest_dir = TEST_FIXTURE.new_dir_path(); + client.operator().create_dir(&dest_dir).await?; + + storify_cmd() + .arg("mv") + .arg("missing-file") + .arg(&dest_dir) + .assert() + .failure() + .stderr(predicate::str::contains("Invalid path")); + Ok(()) +} diff --git a/src/tests/behavior/operations/stat.rs b/src/tests/behavior/operations/stat.rs new file mode 100644 index 0000000..5b11226 --- /dev/null +++ b/src/tests/behavior/operations/stat.rs @@ -0,0 +1,34 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!(test_stat_file_human, test_stat_not_found,); + +async fn test_stat_file_human(client: StorageClient) -> Result<()> { + let (path, content, _) = TEST_FIXTURE.new_file(client.operator()); + client.operator().write(&path, content).await?; + + storify_cmd() + .arg("stat") + .arg(&path) + .assert() + .success() + .stdout(predicate::str::contains("type=file")); + Ok(()) +} + +async fn test_stat_not_found(_client: StorageClient) -> Result<()> { + storify_cmd() + .arg("stat") + .arg("missing") + .assert() + .failure() + .stderr( + predicate::str::contains("OpenDAL error") + .or(predicate::str::contains("Path not found")), + ); + Ok(()) +} diff --git a/src/tests/behavior/operations/tail.rs b/src/tests/behavior/operations/tail.rs new file mode 100644 index 0000000..f2cd74d --- /dev/null +++ b/src/tests/behavior/operations/tail.rs @@ -0,0 +1,73 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!( + test_tail_default_10_lines, + test_tail_n_lines, + test_tail_nonexistent_file, +); + +fn create_temp_file_with_content(content: &[u8]) -> String { + let path = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string()); + std::fs::write(&path, content).expect("write temp file"); + path.to_string_lossy().to_string() +} + +fn upload_and_remote_path(local_path: &str, dest_prefix: &str) -> String { + storify_cmd() + .arg("put") + .arg(local_path) + .arg(dest_prefix) + .assert() + .success(); + let file_name = std::path::Path::new(local_path) + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + join_remote_path(dest_prefix, &file_name) +} + +async fn test_tail_default_10_lines(_client: StorageClient) -> Result<()> { + let lines: Vec = (1..=30).map(|i| format!("line-{i}")).collect(); + let local = create_temp_file_with_content((lines.join("\n") + "\n").as_bytes()); + let remote = upload_and_remote_path(&local, &TEST_FIXTURE.new_file_path()); + + let assert = storify_cmd().arg("tail").arg(&remote).assert().success(); + let output = String::from_utf8_lossy(&assert.get_output().stdout); + + assert!(output.starts_with("line-21")); + assert!(output.contains("line-30")); + assert_eq!(output.lines().count(), 10); + Ok(()) +} + +async fn test_tail_n_lines(_client: StorageClient) -> Result<()> { + let local = create_temp_file_with_content(b"A\nB\nC\nD\nE\n"); + let remote = upload_and_remote_path(&local, &TEST_FIXTURE.new_file_path()); + + let assert = storify_cmd() + .arg("tail") + .arg("-n") + .arg("2") + .arg(&remote) + .assert() + .success(); + let output = String::from_utf8_lossy(&assert.get_output().stdout); + assert_eq!(output.lines().collect::>(), ["D", "E"]); + Ok(()) +} + +async fn test_tail_nonexistent_file(_client: StorageClient) -> Result<()> { + storify_cmd() + .arg("tail") + .arg("missing") + .assert() + .failure() + .stderr(predicate::str::contains("Failed to read tail of file")); + Ok(()) +} diff --git a/src/tests/behavior/operations/touch.rs b/src/tests/behavior/operations/touch.rs new file mode 100644 index 0000000..eb82fd0 --- /dev/null +++ b/src/tests/behavior/operations/touch.rs @@ -0,0 +1,21 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; + +register_behavior_tests!(test_touch_create_and_truncate,); + +async fn test_touch_create_and_truncate(_client: StorageClient) -> Result<()> { + let path = TEST_FIXTURE.new_file_path(); + + storify_cmd().arg("touch").arg(&path).assert().success(); + + storify_cmd() + .arg("touch") + .args(["-t", &path]) + .assert() + .success(); + + Ok(()) +} diff --git a/src/tests/behavior/operations/tree.rs b/src/tests/behavior/operations/tree.rs new file mode 100644 index 0000000..505bb49 --- /dev/null +++ b/src/tests/behavior/operations/tree.rs @@ -0,0 +1,43 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; + +register_behavior_tests!(test_tree_nested, test_tree_depth_limit,); + +async fn test_tree_nested(client: StorageClient) -> Result<()> { + let root = TEST_FIXTURE.new_dir_path(); + let child = format!("{root}child/"); + let file = format!("{child}file.txt"); + + client.operator().create_dir(&root).await?; + client.operator().create_dir(&child).await?; + client.operator().write(&file, vec![b'x']).await?; + + storify_cmd() + .arg("tree") + .arg(&root) + .assert() + .success() + .stdout(predicate::str::contains("child/").and(predicate::str::contains("file.txt"))); + Ok(()) +} + +async fn test_tree_depth_limit(client: StorageClient) -> Result<()> { + let root = TEST_FIXTURE.new_dir_path(); + let sub = format!("{root}sub/"); + client.operator().create_dir(&root).await?; + client.operator().create_dir(&sub).await?; + + storify_cmd() + .arg("tree") + .arg("--depth") + .arg("0") + .arg(&root) + .assert() + .success() + .stdout(predicate::str::contains(&sub).not()); + Ok(()) +} diff --git a/src/tests/behavior/operations/upload.rs b/src/tests/behavior/operations/upload.rs new file mode 100644 index 0000000..b62eafe --- /dev/null +++ b/src/tests/behavior/operations/upload.rs @@ -0,0 +1,53 @@ +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; +use assert_cmd::prelude::*; +use predicates::prelude::*; +register_behavior_tests!(test_storage_client_write, e2e_test_upload_command_succeeds,); + +async fn test_storage_client_write(_client: StorageClient) -> Result<()> { + let content = b"upload small file\n".to_vec(); + let source_path = write_temp_file(&content, ".txt"); + let dest_prefix = TEST_FIXTURE.new_file_path(); + let file_name = source_path + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + storify_cmd() + .arg("put") + .arg(&source_path) + .arg(&dest_prefix) + .assert() + .success() + .stdout(predicate::str::contains("Upload")); + + let env = E2eTestEnv::new().await; + let final_dest_path = join_remote_path(&dest_prefix, &file_name); + let uploaded_content = env.verifier.operator().read(&final_dest_path).await?; + assert_eq!(content, uploaded_content.to_vec()); + Ok(()) +} + +async fn e2e_test_upload_command_succeeds(_client: StorageClient) -> Result<()> { + let content = b"upload e2e file\n".to_vec(); + let source_path = write_temp_file(&content, ".txt"); + let dest_path = TEST_FIXTURE.new_file_path(); + let final_dest_path = format!("{}", source_path.file_name().unwrap().to_string_lossy()); + let final_dest_path = join_remote_path(&dest_path, &final_dest_path); + + storify_cmd() + .arg("put") + .arg(&source_path) + .arg(&dest_path) + .assert() + .success() + .stdout(predicate::str::contains("Upload")); + + let env = E2eTestEnv::new().await; + let actual_content = env.verifier.operator().read(&final_dest_path).await?; + assert_eq!(content, actual_content.to_vec()); + + Ok(()) +} diff --git a/tests/behavior/operations/usage.rs b/src/tests/behavior/operations/usage.rs similarity index 89% rename from tests/behavior/operations/usage.rs rename to src/tests/behavior/operations/usage.rs index 91e9aee..53def82 100644 --- a/tests/behavior/operations/usage.rs +++ b/src/tests/behavior/operations/usage.rs @@ -1,12 +1,11 @@ -use crate::*; +use crate::async_trials; +use crate::error::Result; +use crate::storage::StorageClient; +use crate::tests::behavior::*; use assert_cmd::prelude::*; use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!(client, test_du_summary_total_size)); -} +register_behavior_tests!(test_du_summary_total_size,); pub async fn test_du_summary_total_size(client: StorageClient) -> Result<()> { // Prepare a directory with files of deterministic sizes diff --git a/tests/behavior/utils.rs b/src/tests/behavior/utils.rs similarity index 83% rename from tests/behavior/utils.rs rename to src/tests/behavior/utils.rs index 0189493..ef2d515 100644 --- a/tests/behavior/utils.rs +++ b/src/tests/behavior/utils.rs @@ -1,3 +1,5 @@ +use crate::error::Result; +use crate::storage::StorageClient; use assert_cmd::prelude::*; use libtest_mimic::{Failed, Trial}; use opendal::Operator; @@ -7,8 +9,6 @@ use std::env; use std::path::PathBuf; use std::process::Command; use std::sync::LazyLock; -use storify::error::Result; -use storify::storage::StorageClient; use uuid::Uuid; const TEST_DEFAULT_BUCKET: &str = "test"; @@ -25,7 +25,7 @@ pub static TEST_RUNTIME: LazyLock = LazyLock::new(|| { }); // Cache MinIO config for tests to avoid repeated env reads -static TEST_MINIO_CONFIG: LazyLock = +static TEST_MINIO_CONFIG: LazyLock = LazyLock::new(|| build_minio_config_from_env().expect("minio config")); pub async fn init_test_service() -> Result { @@ -38,15 +38,16 @@ pub async fn init_test_service() -> Result { Ok(client) } -/// Get the absolute path to a file under `tests/data/`. -pub fn get_test_data_path(file_name: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("data") - .join(file_name) +/// Write `content` to a unique temp file and return its path. +pub fn write_temp_file(content: &[u8], suffix: &str) -> PathBuf { + let dir = env::temp_dir().join("storify-tests"); + std::fs::create_dir_all(&dir).expect("create temp test dir"); + let path = dir.join(format!("{}{}", Uuid::new_v4(), suffix)); + std::fs::write(&path, content).expect("write temp test file"); + path } -fn build_minio_config_from_env() -> Result { +fn build_minio_config_from_env() -> Result { let bucket = env::var("STORAGE_BUCKET").unwrap_or_else(|_| TEST_DEFAULT_BUCKET.to_string()); let access_key_id = env::var("STORAGE_ACCESS_KEY_ID") .unwrap_or_else(|_| TEST_DEFAULT_ACCESS_KEY_ID.to_string()); @@ -59,7 +60,7 @@ fn build_minio_config_from_env() -> Result { .ok() .unwrap_or_else(|| TEST_DEFAULT_ENDPOINT.to_string()); - let mut config = storify::storage::StorageConfig::s3(bucket); + let mut config = crate::storage::StorageConfig::s3(bucket); config.access_key_id = Some(access_key_id); config.access_key_secret = Some(access_key_secret); config.region = Some(region); @@ -71,7 +72,7 @@ fn build_minio_config_from_env() -> Result { /// Apply MinIO config to a command as environment variables fn apply_minio_env<'a>( cmd: &'a mut Command, - cfg: &storify::storage::StorageConfig, + cfg: &crate::storage::StorageConfig, ) -> &'a mut Command { cmd.env("STORAGE_PROVIDER", "minio") .env("STORAGE_BUCKET", &cfg.bucket) @@ -99,17 +100,33 @@ fn apply_minio_env<'a>( /// Create a base storify Command with clean environment and logging configured fn base_cmd() -> Command { - let mut cmd = Command::cargo_bin("storify").unwrap(); + let mut cmd = match Command::cargo_bin("storify") { + Ok(cmd) => cmd, + Err(_) => { + build_storify_binary(); + Command::cargo_bin("storify").expect("storify binary should exist after build") + } + }; cmd.env_clear().env("RUST_LOG", "info"); cmd } +fn build_storify_binary() { + let status = Command::new("cargo") + .args(["build", "--bin", "storify"]) + .status() + .expect("failed to invoke cargo build for storify binary"); + if !status.success() { + panic!("cargo build --bin storify failed with status {status}"); + } +} + /// Ensure the target bucket exists for tests. Ignores 'already exists' errors. pub async fn ensure_bucket_exists(op: &Operator) -> Result<()> { match op.create_dir("").await { Ok(_) => Ok(()), Err(e) if e.kind() == opendal::ErrorKind::Unexpected => Ok(()), - Err(e) => Err(storify::error::Error::from(e)), + Err(e) => Err(crate::error::Error::from(e)), } } @@ -132,10 +149,6 @@ impl Fixture { } } - pub fn add_path(&self, path: String) { - self.paths.lock().unwrap().push(path); - } - pub fn new_dir_path(&self) -> String { let path = format!("{}/", Uuid::new_v4()); self.paths.lock().unwrap().push(path.clone()); @@ -190,7 +203,7 @@ impl Default for Fixture { /// A helper struct for managing End-to-End test environments. pub struct E2eTestEnv { - pub config: storify::storage::StorageConfig, + pub config: crate::storage::StorageConfig, pub verifier: StorageClient, } diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..452223d --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,8 @@ +pub mod behavior; + +#[test] +fn behavior_suite() { + if let Err(err) = behavior::run_behavior_suite() { + panic!("behavior suite failed: {err}"); + } +} diff --git a/tests/behavior/main.rs b/tests/behavior/main.rs deleted file mode 100644 index 1529a41..0000000 --- a/tests/behavior/main.rs +++ /dev/null @@ -1,46 +0,0 @@ -use libtest_mimic::Arguments; -use libtest_mimic::Trial; -use storify::error::Result; - -mod operations; -mod utils; - -pub use utils::*; - -fn main() -> Result<()> { - let args = Arguments::from_args(); - - let client = TEST_RUNTIME.block_on(init_test_service())?; - - let mut tests = Vec::new(); - - operations::list::tests(&client, &mut tests); - operations::copy::tests(&client, &mut tests); - operations::delete::tests(&client, &mut tests); - operations::download::tests(&client, &mut tests); - operations::head::tests(&client, &mut tests); - operations::grep::tests(&client, &mut tests); - operations::find::tests(&client, &mut tests); - operations::tail::tests(&client, &mut tests); - operations::mkdir::tests(&client, &mut tests); - operations::mv::tests(&client, &mut tests); - operations::upload::tests(&client, &mut tests); - operations::cat::tests(&client, &mut tests); - operations::usage::tests(&client, &mut tests); - operations::stat::tests(&client, &mut tests); - operations::tree::tests(&client, &mut tests); - operations::diff::tests(&client, &mut tests); - operations::touch::tests(&client, &mut tests); - - let _ = tracing_subscriber::fmt() - .pretty() - .with_test_writer() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .try_init(); - - let conclusion = libtest_mimic::run(&args, tests); - - TEST_RUNTIME.block_on(TEST_FIXTURE.cleanup(client.operator())); - - conclusion.exit() -} diff --git a/tests/behavior/operations/copy.rs b/tests/behavior/operations/copy.rs deleted file mode 100644 index f5d26b9..0000000 --- a/tests/behavior/operations/copy.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use std::path::Path; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_copy_file_to_existing_directory, - test_copy_file_to_new_path, - test_copy_across_directory, - test_copy_overwrite_existing_file, - test_copy_to_nonexistent_directory, - test_copy_non_existent_file - )); -} - -async fn test_copy_file_to_existing_directory(client: StorageClient) -> Result<()> { - let src_dir = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&src_dir).await?; - - let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); - let src_file_path = format!("{}{}", src_dir, src_file); - client - .operator() - .write(&src_file_path, content.clone()) - .await?; - - let dest_dir = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dest_dir).await?; - - storify_cmd() - .arg("cp") - .arg(&src_file_path) - .arg(&dest_dir) - .assert() - .success(); - - let dest_file_path = format!("{}{}", dest_dir, src_file); - let dst_content = client.operator().read(&dest_file_path).await?; - assert_eq!(content, dst_content.to_vec()); - - let src_content = client.operator().read(&src_file_path).await?; - assert_eq!(content, src_content.to_vec()); - - Ok(()) -} - -async fn test_copy_file_to_new_path(client: StorageClient) -> Result<()> { - let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&src_file, content.clone()).await?; - - let dest_file = TEST_FIXTURE.new_file_path(); - - storify_cmd() - .arg("cp") - .arg(&src_file) - .arg(&dest_file) - .assert() - .success(); - - let dst_content = client.operator().read(&dest_file).await?; - assert_eq!(content, dst_content.to_vec()); - - let src_content = client.operator().read(&src_file).await?; - assert_eq!(content, src_content.to_vec()); - - Ok(()) -} - -async fn test_copy_across_directory(client: StorageClient) -> Result<()> { - let (src_path, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&src_path, content.clone()).await?; - - let dest_path = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dest_path).await?; - let final_dest_path = format!( - "{}", - Path::new(&src_path).file_name().unwrap().to_string_lossy() - ); - - let final_dest_path = join_remote_path(&dest_path, &final_dest_path); - - storify_cmd() - .arg("cp") - .arg(&src_path) - .arg(&dest_path) - .assert() - .success(); - - let dest_content = client.operator().read(&final_dest_path).await?; - assert_eq!(content, dest_content.to_vec()); - - let src_content = client.operator().read(&src_path).await?; - assert_eq!(content, src_content.to_vec()); - - Ok(()) -} - -async fn test_copy_overwrite_existing_file(client: StorageClient) -> Result<()> { - let (src_file_path, src_content, _) = TEST_FIXTURE.new_file(client.operator()); - client - .operator() - .write(&src_file_path, src_content.clone()) - .await?; - - let (dst_file_path, dst_content, _) = TEST_FIXTURE.new_file(client.operator()); - client - .operator() - .write(&dst_file_path, dst_content.clone()) - .await?; - - let initial_dst_content = client.operator().read(&dst_file_path).await?; - assert_eq!(dst_content, initial_dst_content.to_vec()); - assert_ne!(src_content, dst_content); - - storify_cmd() - .arg("cp") - .arg(&src_file_path) - .arg(&dst_file_path) - .assert() - .success(); - - let final_dst_content = client.operator().read(&dst_file_path).await?; - assert_eq!(src_content, final_dst_content.to_vec()); - assert_ne!(dst_content, final_dst_content.to_vec()); - - let src_content_after = client.operator().read(&src_file_path).await?; - assert_eq!(src_content, src_content_after.to_vec()); - - Ok(()) -} - -async fn test_copy_to_nonexistent_directory(client: StorageClient) -> Result<()> { - let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&src_file, content.clone()).await?; - - let nonexistent_dir = format!("{}/", TEST_FIXTURE.new_dir_path()); - - storify_cmd() - .arg("cp") - .arg(&src_file) - .arg(&nonexistent_dir) - .assert() - .failure() - .stderr(predicate::str::contains("Invalid path")); - - Ok(()) -} - -async fn test_copy_non_existent_file(client: StorageClient) -> Result<()> { - let non_existent_src = TEST_FIXTURE.new_dir_path(); - let non_exist_src_file = TEST_FIXTURE.new_file_path(); - client.operator().create_dir(&non_existent_src).await?; - let final_src_file = format!("{}{}", non_existent_src, non_exist_src_file); - - let dest_path = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dest_path).await?; - - storify_cmd() - .arg("cp") - .arg(&final_src_file) - .arg(&dest_path) - .assert() - .failure() - .stderr(predicate::str::contains("Invalid path")); - - Ok(()) -} diff --git a/tests/behavior/operations/delete.rs b/tests/behavior/operations/delete.rs deleted file mode 100644 index 2ecd3d7..0000000 --- a/tests/behavior/operations/delete.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_delete_single_file, - test_delete_non_existent_file, - test_delete_empty_directory, - test_delete_non_empty_directory_recursively, - test_delete_multiple_files_bulk - )); -} - -async fn test_delete_single_file(client: StorageClient) -> Result<()> { - let (path, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&path, content).await?; - - storify_cmd() - .arg("rm") - .arg("--force") - .arg(&path) - .assert() - .success(); - - let result = client.operator().stat(&path).await; - assert!(result.is_err(), "File should be deleted"); - assert!( - matches!(result.unwrap_err().kind(), opendal::ErrorKind::NotFound), - "Error should be NotFound" - ); - - Ok(()) -} - -async fn test_delete_non_existent_file(_client: StorageClient) -> Result<()> { - let path = TEST_FIXTURE.new_file_path(); - - storify_cmd() - .arg("rm") - .arg("--force") - .arg(&path) - .assert() - .failure() - .stderr(predicate::str::contains("Path not found")); - - Ok(()) -} - -async fn test_delete_empty_directory(client: StorageClient) -> Result<()> { - let dir_path = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dir_path).await?; - - storify_cmd() - .arg("rm") - .arg("-R") - .arg("--force") - .arg(&dir_path) - .assert() - .success(); - - let result = client.operator().stat(&dir_path).await; - assert!(result.is_err(), "Directory should be deleted"); - assert!( - matches!(result.unwrap_err().kind(), opendal::ErrorKind::NotFound), - "Error should be NotFound for deleted directory" - ); - - Ok(()) -} - -async fn test_delete_non_empty_directory_recursively(client: StorageClient) -> Result<()> { - let root_dir = TEST_FIXTURE.new_dir_path(); - let file_path = format!("{root_dir}test.txt"); - let (path, content, _) = TEST_FIXTURE.new_file_with_range(file_path, 1..1024); - client.operator().write(&path, content).await?; - - E2eTestEnv::new() - .await - .command() - .arg("rm") - .arg("-R") - .arg("--force") - .arg(&root_dir) - .assert() - .success(); - - let result = client.operator().stat(&root_dir).await; - assert!(result.is_err(), "Root directory should be deleted"); - - let file_result = client.operator().stat(&path).await; - assert!( - file_result.is_err(), - "File within directory should be deleted" - ); - - Ok(()) -} - -async fn test_delete_multiple_files_bulk(client: StorageClient) -> Result<()> { - let mut paths = Vec::new(); - for _ in 0..5 { - let (path, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&path, content).await?; - paths.push(path); - } - - let mut cmd = storify_cmd(); - cmd.arg("rm").arg("--force"); - for p in &paths { - cmd.arg(p); - } - cmd.assert().success(); - - for path in paths { - let result = client.operator().stat(&path).await; - assert!(result.is_err(), "File {path} should be deleted"); - } - - Ok(()) -} diff --git a/tests/behavior/operations/diff.rs b/tests/behavior/operations/diff.rs deleted file mode 100644 index fec1215..0000000 --- a/tests/behavior/operations/diff.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_diff_identical_files_produces_no_output, - test_diff_basic_unified_output, - test_diff_disallows_directories, - test_diff_size_limit_blocks_without_force, - test_diff_ignore_space_trims_trailing_whitespace - )); -} - -async fn upload_text_file(env: &E2eTestEnv, content: &str) -> Result { - let path = TEST_FIXTURE.new_file_path(); - env.verifier - .operator() - .write(&path, content.as_bytes().to_vec()) - .await?; - Ok(path) -} - -async fn test_diff_identical_files_produces_no_output(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let left = upload_text_file(&env, "line1\nline2\n").await?; - let right = upload_text_file(&env, "line1\nline2\n").await?; - - storify_cmd() - .arg("diff") - .arg(&left) - .arg(&right) - .assert() - .success() - .stdout(predicate::str::is_empty()); - - Ok(()) -} - -async fn test_diff_basic_unified_output(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let left = upload_text_file(&env, "a\nb\nc\n").await?; - let right = upload_text_file(&env, "a\nB\nc\n").await?; - - storify_cmd() - .arg("diff") - .arg(&left) - .arg(&right) - .arg("-U") - .arg("1") - .assert() - .success() - .stdout( - // Expect unified diff markers - predicate::str::contains("@@") - .and(predicate::str::contains("-b")) - .and(predicate::str::contains("+B")), - ); - - Ok(()) -} - -async fn test_diff_disallows_directories(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let dir = TEST_FIXTURE.new_dir_path(); - env.verifier.operator().create_dir(&dir).await?; - let file = upload_text_file(&env, "x\n").await?; - - storify_cmd() - .arg("diff") - .arg(&dir) - .arg(&file) - .assert() - .failure() - .stderr(predicate::str::contains("diff only supports files")); - - Ok(()) -} - -async fn test_diff_size_limit_blocks_without_force(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let left = upload_text_file(&env, &"X".repeat(2 * 1024 * 1024)).await?; // 2MB - let right = upload_text_file(&env, &"Y".repeat(2 * 1024 * 1024)).await?; // 2MB - - storify_cmd() - .arg("diff") - .arg(&left) - .arg(&right) - .arg("--size-limit") - .arg("1") // 1MB total limit so blocked - .assert() - .failure() - .stderr(predicate::str::contains("Files too large")); - - Ok(()) -} - -async fn test_diff_ignore_space_trims_trailing_whitespace(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let left = upload_text_file(&env, "hello \nworld\n").await?; // trailing spaces - let right = upload_text_file(&env, "hello\nworld\n").await?; - - storify_cmd() - .arg("diff") - .arg(&left) - .arg(&right) - .arg("-w") - .assert() - .success() - .stdout(predicate::str::is_empty()); - - Ok(()) -} diff --git a/tests/behavior/operations/download.rs b/tests/behavior/operations/download.rs deleted file mode 100644 index c1c5b08..0000000 --- a/tests/behavior/operations/download.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; -use tokio::fs; -use uuid::Uuid; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_download_existing_file_to_directory, - test_download_directory_recursive, - test_download_non_existent_file, - test_download_large_file, - test_download_with_special_chars - )); -} - -#[derive(Clone)] -struct StagedFile { - remote_path: String, - content: Vec, - file_name: String, -} - -async fn stage_remote_file(client: &StorageClient) -> Result { - let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); - let src_path = TEST_FIXTURE.new_dir_path(); - - client.operator().create_dir(&src_path).await?; - let src_file_path = format!("{}{}", src_path, src_file); - client - .operator() - .write(&src_file_path, content.clone()) - .await?; - - Ok(StagedFile { - remote_path: src_file_path, - content, - file_name: src_file, - }) -} - -async fn stage_remote_directory(client: &StorageClient) -> Result<(String, Vec)> { - let src_dir = TEST_FIXTURE.new_dir_path(); - let file_name = "test_file.txt"; - let file_path = format!("{}{}", src_dir, file_name); - let content = b"test directory content".to_vec(); - - client.operator().create_dir(&src_dir).await?; - client.operator().write(&file_path, content.clone()).await?; - - Ok((src_dir, content)) -} - -async fn stage_large_file(client: &StorageClient) -> Result { - let src_file = "large_file.bin"; - let src_path = TEST_FIXTURE.new_dir_path(); - let content = vec![0x42; 1024 * 1024 * 100]; - - client.operator().create_dir(&src_path).await?; - let src_file_path = format!("{}{}", src_path, src_file); - client - .operator() - .write(&src_file_path, content.clone()) - .await?; - - Ok(StagedFile { - remote_path: src_file_path, - content, - file_name: src_file.to_string(), - }) -} - -async fn stage_special_char_file(client: &StorageClient) -> Result { - let src_file = "special!@#$%^&()_+-=;'file.txt"; - let src_path = TEST_FIXTURE.new_dir_path(); - let content = b"special character file content".to_vec(); - - client.operator().create_dir(&src_path).await?; - let src_file_path = format!("{}{}", src_path, src_file); - client - .operator() - .write(&src_file_path, content.clone()) - .await?; - - Ok(StagedFile { - remote_path: src_file_path, - content, - file_name: src_file.to_string(), - }) -} - -async fn test_download_existing_file_to_directory(client: StorageClient) -> Result<()> { - let staged_file = stage_remote_file(&client).await?; - let local_dir = std::env::temp_dir().join(format!("storify-dl-{}", Uuid::new_v4())); - - storify_cmd() - .arg("get") - .arg(&staged_file.remote_path) - .arg(&local_dir) - .assert() - .success() - .stdout(predicate::str::contains("Downloaded:")); - - let downloaded_file = local_dir.join(&staged_file.file_name); - let actual_content = fs::read(&downloaded_file).await?; - assert_eq!(staged_file.content, actual_content); - - let _ = fs::remove_dir_all(&local_dir).await; - Ok(()) -} - -async fn test_download_directory_recursive(client: StorageClient) -> Result<()> { - let (remote_dir, expected_content) = stage_remote_directory(&client).await?; - let local_dest = std::env::temp_dir().join(format!("storify-dl-dir-{}", Uuid::new_v4())); - - storify_cmd() - .arg("get") - .arg(&remote_dir) - .arg(&local_dest) - .assert() - .success(); - - let dest_file = local_dest.join("test_file.txt"); - let dest_content = fs::read(&dest_file).await?; - assert_eq!(expected_content, dest_content); - - let _ = fs::remove_dir_all(&local_dest).await; - Ok(()) -} - -async fn test_download_non_existent_file(_client: StorageClient) -> Result<()> { - let remote_path = TEST_FIXTURE.new_file_path(); - let local_dir = std::env::temp_dir().join(format!("storify-dl-miss-{}", Uuid::new_v4())); - - storify_cmd() - .arg("get") - .arg(&remote_path) - .arg(&local_dir) - .assert() - .failure() - .stderr(predicate::str::contains("Failed to download")); - - assert!(!local_dir.exists()); - Ok(()) -} - -async fn test_download_large_file(client: StorageClient) -> Result<()> { - let staged_file = stage_large_file(&client).await?; - let local_dir = std::env::temp_dir().join(format!("storify-dl-large-{}", Uuid::new_v4())); - - storify_cmd() - .arg("get") - .arg(&staged_file.remote_path) - .arg(&local_dir) - .assert() - .success() - .stdout(predicate::str::contains("Downloaded:")); - - let downloaded_file = local_dir.join(&staged_file.file_name); - let actual_content = fs::read(&downloaded_file).await?; - assert_eq!(staged_file.content.len(), actual_content.len()); - assert_eq!(staged_file.content, actual_content); - - let _ = fs::remove_dir_all(&local_dir).await; - Ok(()) -} - -async fn test_download_with_special_chars(client: StorageClient) -> Result<()> { - let staged_file = stage_special_char_file(&client).await?; - let local_dir = std::env::temp_dir().join(format!("storify-dl-special-{}", Uuid::new_v4())); - - storify_cmd() - .arg("get") - .arg(&staged_file.remote_path) - .arg(&local_dir) - .assert() - .success() - .stdout(predicate::str::contains("Downloaded:")); - - let downloaded_file = local_dir.join(&staged_file.file_name); - let actual_content = fs::read(&downloaded_file).await?; - assert_eq!(staged_file.content, actual_content); - - let _ = fs::remove_dir_all(&local_dir).await; - Ok(()) -} diff --git a/tests/behavior/operations/find.rs b/tests/behavior/operations/find.rs deleted file mode 100644 index 4dd732b..0000000 --- a/tests/behavior/operations/find.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_find_by_name_glob, - test_find_by_regex, - test_find_type_file, - test_find_type_dir, - test_find_type_other_empty - )); -} - -async fn test_find_by_name_glob(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root = TEST_FIXTURE.new_dir_path(); - let sub = format!("{root}sub/"); - env.verifier.operator().create_dir(&root).await?; - env.verifier.operator().create_dir(&sub).await?; - - let f1 = format!("{root}a.log"); - let f2 = format!("{sub}b.log"); - let f3 = format!("{sub}c.txt"); - env.verifier.operator().write(&f1, b"x".to_vec()).await?; - env.verifier.operator().write(&f2, b"y".to_vec()).await?; - env.verifier.operator().write(&f3, b"z".to_vec()).await?; - - storify_cmd() - .arg("find") - .arg(&root) - .arg("--name") - .arg("**/*.log") - .assert() - .success() - .stdout( - predicate::str::contains(&f1) - .and(predicate::str::contains(&f2)) - .and(predicate::str::contains(&f3).not()), - ); - - Ok(()) -} - -async fn test_find_by_regex(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root = TEST_FIXTURE.new_dir_path(); - env.verifier.operator().create_dir(&root).await?; - let f1 = format!("{root}x.csv"); - let f2 = format!("{root}y.parquet"); - let f3 = format!("{root}z.txt"); - env.verifier.operator().write(&f1, b"1".to_vec()).await?; - env.verifier.operator().write(&f2, b"2".to_vec()).await?; - env.verifier.operator().write(&f3, b"3".to_vec()).await?; - - storify_cmd() - .arg("find") - .arg(&root) - .arg("--regex") - .arg(".*\\.(csv|parquet)$") - .assert() - .success() - .stdout( - predicate::str::contains(&f1) - .and(predicate::str::contains(&f2)) - .and(predicate::str::contains(&f3).not()), - ); - - Ok(()) -} - -async fn test_find_type_file(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root = TEST_FIXTURE.new_dir_path(); - let sub = format!("{root}d/"); - env.verifier.operator().create_dir(&root).await?; - env.verifier.operator().create_dir(&sub).await?; - let f = format!("{root}file.bin"); - env.verifier.operator().write(&f, b"data".to_vec()).await?; - - storify_cmd() - .arg("find") - .arg(&root) - .arg("--type") - .arg("f") - .assert() - .success() - .stdout(predicate::str::contains(&f).and(predicate::str::contains(&sub).not())); - - Ok(()) -} - -async fn test_find_type_dir(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root = TEST_FIXTURE.new_dir_path(); - let sub = format!("{root}sub/"); - env.verifier.operator().create_dir(&root).await?; - env.verifier.operator().create_dir(&sub).await?; - let f = format!("{root}f.txt"); - env.verifier.operator().write(&f, b"1".to_vec()).await?; - - storify_cmd() - .arg("find") - .arg(&root) - .arg("--type") - .arg("d") - .assert() - .success() - .stdout(predicate::str::contains(&sub).and(predicate::str::contains(&f).not())); - - Ok(()) -} - -// OpenDAL typically returns only file/dir; other should be empty -async fn test_find_type_other_empty(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root = TEST_FIXTURE.new_dir_path(); - env.verifier.operator().create_dir(&root).await?; - let f = format!("{root}x.txt"); - env.verifier.operator().write(&f, b"1".to_vec()).await?; - - storify_cmd() - .arg("find") - .arg(&root) - .arg("--type") - .arg("o") - .assert() - .success() - .stdout(predicate::str::is_empty()); - - Ok(()) -} diff --git a/tests/behavior/operations/grep.rs b/tests/behavior/operations/grep.rs deleted file mode 100644 index 38433ec..0000000 --- a/tests/behavior/operations/grep.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_grep_basic, - test_grep_ignore_case, - test_grep_line_number, - test_grep_chunk_boundary, - test_grep_recursive_basic, - test_grep_recursive_ignore_case, - test_grep_recursive_line_number, - test_grep_directory_without_recursive_flag - )); -} - -async fn prepare_remote_file(verifier: &StorageClient, content: &[u8]) -> Result { - let path = TEST_FIXTURE.new_file_path(); - verifier.operator().write(&path, content.to_vec()).await?; - Ok(path) -} - -async fn test_grep_basic(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let content = b"alpha\nbeta\ngamma\nAlpha Beta\n"; - let remote_path = prepare_remote_file(&env.verifier, content).await?; - - storify_cmd() - .arg("grep") - .arg("beta") - .arg(&remote_path) - .assert() - .success() - .stdout(predicate::str::contains("beta").and(predicate::str::contains("Alpha Beta").not())); - - Ok(()) -} - -async fn test_grep_ignore_case(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let content = b"alpha\nbeta\ngamma\nAlpha Beta\n"; - let remote_path = prepare_remote_file(&env.verifier, content).await?; - - storify_cmd() - .arg("grep") - .arg("-i") - .arg("beta") - .arg(&remote_path) - .assert() - .success() - .stdout(predicate::str::contains("beta").and(predicate::str::contains("Alpha Beta"))); - - Ok(()) -} - -async fn test_grep_line_number(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let content = b"first\nsecond\nthird\n"; - let remote_path = prepare_remote_file(&env.verifier, content).await?; - - storify_cmd() - .arg("grep") - .arg("-n") - .arg("second") - .arg(&remote_path) - .assert() - .success() - .stdout(predicate::str::contains("2:second")); - - Ok(()) -} - -async fn test_grep_chunk_boundary(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - // Construct content that crosses typical chunk boundary using repeated lines - let mut content = Vec::new(); - for _ in 0..5000 { - content.extend_from_slice(b"lorem ipsum dolor sit amet\n"); - } - - content.extend_from_slice(b"TARGET line here\n"); - for _ in 0..5000 { - content.extend_from_slice(b"lorem ipsum dolor sit amet\n"); - } - - let remote_path = prepare_remote_file(&env.verifier, &content).await?; - - storify_cmd() - .arg("grep") - .arg("TARGET") - .arg(&remote_path) - .assert() - .success() - .stdout(predicate::str::contains("TARGET line here")); - - Ok(()) -} - -async fn test_grep_recursive_basic(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root_dir = TEST_FIXTURE.new_dir_path(); - let sub_dir = format!("{root_dir}sub/"); - env.verifier.operator().create_dir(&root_dir).await?; - env.verifier.operator().create_dir(&sub_dir).await?; - - let root_file = format!("{root_dir}a.txt"); - let sub_file = format!("{sub_dir}b.txt"); - env.verifier - .operator() - .write(&root_file, b"foo\nmatch here\nbar\n".to_vec()) - .await?; - env.verifier - .operator() - .write(&sub_file, b"nope\nTARGET in sub\n".to_vec()) - .await?; - - storify_cmd() - .arg("grep") - .arg("-R") - .arg("TARGET") - .arg(&root_dir) - .assert() - .success() - .stdout( - predicate::str::contains(format!("{}:", sub_file)) - .and(predicate::str::contains("TARGET in sub")), - ); - - Ok(()) -} - -async fn test_grep_recursive_ignore_case(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root_dir = TEST_FIXTURE.new_dir_path(); - let sub_dir = format!("{root_dir}nested/"); - env.verifier.operator().create_dir(&root_dir).await?; - env.verifier.operator().create_dir(&sub_dir).await?; - - let f1 = format!("{root_dir}x.txt"); - let f2 = format!("{sub_dir}y.txt"); - env.verifier - .operator() - .write(&f1, b"Alpha\nBeta\n".to_vec()) - .await?; - env.verifier - .operator() - .write(&f2, b"gamma\nalpha\n".to_vec()) - .await?; - - storify_cmd() - .arg("grep") - .arg("-R") - .arg("-i") - .arg("alpha") - .arg(&root_dir) - .assert() - .success() - .stdout( - predicate::str::contains(format!("{}:", f1)) - .and(predicate::str::contains(format!("{}:", f2))), - ); - - Ok(()) -} - -async fn test_grep_recursive_line_number(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root_dir = TEST_FIXTURE.new_dir_path(); - env.verifier.operator().create_dir(&root_dir).await?; - let f = format!("{root_dir}ln.txt"); - env.verifier - .operator() - .write(&f, b"first\nneedle\nthird\n".to_vec()) - .await?; - - storify_cmd() - .arg("grep") - .arg("-R") - .arg("-n") - .arg("needle") - .arg(&root_dir) - .assert() - .success() - .stdout(predicate::str::contains(format!("{}:2:needle", f))); - - Ok(()) -} - -async fn test_grep_directory_without_recursive_flag(_client: StorageClient) -> Result<()> { - let env = E2eTestEnv::new().await; - let root_dir = TEST_FIXTURE.new_dir_path(); - env.verifier.operator().create_dir(&root_dir).await?; - let f = format!("{root_dir}a.txt"); - env.verifier - .operator() - .write(&f, b"hello\nworld\n".to_vec()) - .await?; - - storify_cmd() - .arg("grep") - .arg("hello") - .arg(&root_dir) - .assert() - .failure() - .stderr(predicate::str::contains("use -R")); - - Ok(()) -} diff --git a/tests/behavior/operations/head.rs b/tests/behavior/operations/head.rs deleted file mode 100644 index dbf20aa..0000000 --- a/tests/behavior/operations/head.rs +++ /dev/null @@ -1,261 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_head_default_10_lines, - test_head_n_lines, - test_head_c_bytes, - test_head_zero_lines_and_zero_bytes, - test_head_nonexistent_file, - test_head_multi_files_with_headers, - test_head_multi_files_quiet, - test_head_multi_files_verbose - )); -} - -/// Create a temporary file under system temp dir with given content and return its path. -fn create_temp_file_with_content(content: &[u8]) -> String { - let path = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string()); - std::fs::write(&path, content).expect("write temp file"); - path.to_string_lossy().to_string() -} - -/// Upload a local file to a generated remote prefix via CLI and return the full remote path. -fn upload_and_remote_path(local_path: &str, dest_prefix: &str) -> String { - storify_cmd() - .arg("put") - .arg(local_path) - .arg(dest_prefix) - .assert() - .success(); - - let file_name = std::path::Path::new(local_path) - .file_name() - .unwrap() - .to_string_lossy() - .to_string(); - join_remote_path(dest_prefix, &file_name) -} - -// Default: first 10 lines -async fn test_head_default_10_lines(_client: StorageClient) -> Result<()> { - // Build a file with >10 lines - let lines: Vec = (1..=20).map(|i| format!("line-{i}")).collect(); - let content = lines.join("\n") + "\n"; // newline-terminated - let local = create_temp_file_with_content(content.as_bytes()); - - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let assert = storify_cmd() - .arg("head") - .arg(&remote_path) - .assert() - .success(); - let output = assert.get_output().stdout.clone(); - let out = String::from_utf8_lossy(&output); - - // Expect exactly 10 lines (if file has >=10) - assert_eq!(out.lines().count(), 10); - // Check first and last emitted line - assert!(out.starts_with("line-1\n")); - assert!(out.contains("line-10\n")); - assert!(!out.contains("line-11\n")); - - Ok(()) -} - -// -n N lines -async fn test_head_n_lines(_client: StorageClient) -> Result<()> { - let content = b"A\nB\nC\nD\nE\n"; // 5 lines - let local = create_temp_file_with_content(content); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let assert = storify_cmd() - .arg("head") - .arg("-n") - .arg("3") - .arg(&remote_path) - .assert() - .success(); - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert_eq!(out.lines().count(), 3); - assert!(out.starts_with("A\n")); - assert!(out.contains("B\n")); - assert!(out.contains("C\n")); - assert!(!out.contains("D\n")); - - Ok(()) -} - -// -c N bytes -async fn test_head_c_bytes(_client: StorageClient) -> Result<()> { - let content = "Hello, World! ".repeat(10); // 150 bytes - let local = create_temp_file_with_content(content.as_bytes()); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let assert = storify_cmd() - .arg("head") - .arg("-c") - .arg("50") - .arg(&remote_path) - .assert() - .success(); - let output = assert.get_output().stdout.clone(); - assert_eq!(output.len(), 50); - - let output_str = String::from_utf8_lossy(&output); - assert!(output_str.starts_with("Hello, World! Hello, World! Hello, World! Hello")); - - Ok(()) -} - -// Zero lines and zero bytes should output nothing -async fn test_head_zero_lines_and_zero_bytes(_client: StorageClient) -> Result<()> { - let content = b"X\nY\nZ\n"; - let local = create_temp_file_with_content(content); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - // -n 0 - let out_n0 = storify_cmd() - .arg("head") - .arg("-n") - .arg("0") - .arg(&remote_path) - .assert() - .success() - .get_output() - .stdout - .clone(); - assert!(out_n0.is_empty()); - - // -c 0 - let out_c0 = storify_cmd() - .arg("head") - .arg("-c") - .arg("0") - .arg(&remote_path) - .assert() - .success() - .get_output() - .stdout - .clone(); - assert!(out_c0.is_empty()); - - Ok(()) -} - -// Non-existent file should fail with not found message -async fn test_head_nonexistent_file(_client: StorageClient) -> Result<()> { - let non_existent_path = "nonexistent_file.txt"; - - let assert = storify_cmd() - .arg("head") - .arg(non_existent_path) - .assert() - .failure(); - - let stderr = assert.get_output().stderr.clone(); - let stderr_str = String::from_utf8_lossy(&stderr); - - assert!( - stderr_str.contains("not found") - || stderr_str.contains("NotFound") - || stderr_str.contains("Path does not exist") - ); - - Ok(()) -} - -// Multiple files: default shows headers when >1 files -async fn test_head_multi_files_with_headers(_client: StorageClient) -> Result<()> { - let src1 = get_test_data_path("small.txt"); - let src2_content = (1..=5) - .map(|i| format!("L{i}")) - .collect::>() - .join("\n") - + "\n"; - let src2 = create_temp_file_with_content(src2_content.as_bytes()); - - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote1 = upload_and_remote_path(&src1.to_string_lossy(), &dest_prefix); - let remote2 = upload_and_remote_path(&src2, &dest_prefix); - - let assert = storify_cmd() - .arg("head") - .arg(&remote1) - .arg(&remote2) - .assert() - .success(); - - let out = String::from_utf8_lossy(&assert.get_output().stdout); - // Expect headers like ==> path <== present twice - assert!(out.contains(&format!("==> {} <==", remote1))); - assert!(out.contains(&format!("==> {} <==", remote2))); - - Ok(()) -} - -// Multiple files with -q: no headers -async fn test_head_multi_files_quiet(_client: StorageClient) -> Result<()> { - let src1 = get_test_data_path("small.txt"); - let src2_content = (1..=3) - .map(|i| format!("q{i}")) - .collect::>() - .join("\n") - + "\n"; - let src2 = create_temp_file_with_content(src2_content.as_bytes()); - - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote1 = upload_and_remote_path(&src1.to_string_lossy(), &dest_prefix); - let remote2 = upload_and_remote_path(&src2, &dest_prefix); - - let assert = storify_cmd() - .arg("head") - .arg("-q") - .arg(&remote1) - .arg(&remote2) - .assert() - .success(); - - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert!(!out.contains("==>")); - - Ok(()) -} - -// Multiple files with -v: always show headers -async fn test_head_multi_files_verbose(_client: StorageClient) -> Result<()> { - let src1 = get_test_data_path("small.txt"); - let src2_content = (1..=2) - .map(|i| format!("v{i}")) - .collect::>() - .join("\n") - + "\n"; - let src2 = create_temp_file_with_content(src2_content.as_bytes()); - - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote1 = upload_and_remote_path(&src1.to_string_lossy(), &dest_prefix); - let remote2 = upload_and_remote_path(&src2, &dest_prefix); - - let assert = storify_cmd() - .arg("head") - .arg("-v") - .arg(&remote1) - .arg(&remote2) - .assert() - .success(); - - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert!(out.contains(&format!("==> {} <==", remote1))); - assert!(out.contains(&format!("==> {} <==", remote2))); - - Ok(()) -} diff --git a/tests/behavior/operations/list.rs b/tests/behavior/operations/list.rs deleted file mode 100644 index fbc4424..0000000 --- a/tests/behavior/operations/list.rs +++ /dev/null @@ -1,202 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use futures::TryStreamExt; -use opendal::EntryMode; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; -use uuid::Uuid; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_list_empty_directory, - test_list_single_file, - test_list_multiple_files, - test_list_nested_directories, - test_list_with_special_chars, - test_list_invalid_path, - test_list_recursive - )); -} - -pub async fn test_list_empty_directory(client: StorageClient) -> Result<()> { - let dir_path = TEST_FIXTURE.new_dir_path(); - - client.operator().create_dir(&dir_path).await?; - - storify_cmd().arg("ls").arg(&dir_path).assert().success(); - - let mut obs = client.operator().lister(&dir_path).await?; - let mut entries = Vec::new(); - while let Some(de) = obs.try_next().await? { - entries.push(de.path().to_string()); - } - - let is_empty = entries.is_empty() - || (entries.len() == 1 && entries[0] == dir_path) - || (entries.len() == 1 && entries[0].ends_with('/')); - - assert!( - is_empty, - "empty directory should have no actual content, found: {entries:?}" - ); - - Ok(()) -} - -pub async fn test_list_single_file(client: StorageClient) -> Result<()> { - let (path, content, size) = TEST_FIXTURE.new_file(client.operator()); - - client.operator().write(&path, content).await?; - - let parent = path.rsplit('/').nth(1).unwrap_or("").to_string(); - let parent_path = if parent.is_empty() { "" } else { &parent }; - - let cli_path = if parent_path.is_empty() { - "/" - } else { - parent_path - }; - storify_cmd() - .arg("ls") - .arg(cli_path) - .assert() - .success() - .stdout(predicate::str::contains(&path)); - - let meta = client.operator().stat(&path).await?; - assert_eq!(meta.mode(), EntryMode::FILE); - assert_eq!(meta.content_length(), size as u64); - - Ok(()) -} - -pub async fn test_list_multiple_files(client: StorageClient) -> Result<()> { - let parent = TEST_FIXTURE.new_dir_path(); - let mut expected_files = Vec::new(); - - for _ in 0..5 { - let file_path = format!("{parent}{}", Uuid::new_v4()); - let (_, content, _) = TEST_FIXTURE.new_file_with_range(&file_path, 100..1000); - client.operator().write(&file_path, content).await?; - expected_files.push(file_path); - } - - let mut assert = storify_cmd(); - assert.arg("ls").arg(&parent); - let mut a = assert.assert(); - a = a.success(); - for expected in &expected_files { - a = a.stdout(predicate::str::contains(expected)); - } - - Ok(()) -} - -pub async fn test_list_nested_directories(client: StorageClient) -> Result<()> { - let root_dir = TEST_FIXTURE.new_dir_path(); - let sub_dir = format!("{root_dir}subdir/"); - let nested_dir = format!("{sub_dir}nested/"); - - client.operator().create_dir(&root_dir).await?; - client.operator().create_dir(&sub_dir).await?; - client.operator().create_dir(&nested_dir).await?; - - let file_path = format!("{nested_dir}test.txt"); - let (_, content, _) = TEST_FIXTURE.new_file_with_range(&file_path, 100..500); - client.operator().write(&file_path, content).await?; - - storify_cmd() - .arg("ls") - .arg(&root_dir) - .assert() - .success() - .stdout(predicate::str::contains(&sub_dir)); - - let meta = client.operator().stat(&sub_dir).await?; - assert_eq!(meta.mode(), EntryMode::DIR); - - Ok(()) -} - -pub async fn test_list_with_special_chars(client: StorageClient) -> Result<()> { - let parent = TEST_FIXTURE.new_dir_path(); - let special_names = vec![ - "file with spaces.txt", - "file-with-dashes.txt", - "file_with_underscores.txt", - "file.with.dots.txt", - "file@#$%^&*().txt", - ]; - - for name in &special_names { - let file_path = format!("{parent}{name}"); - let (_, content, _) = TEST_FIXTURE.new_file_with_range(&file_path, 50..200); - client.operator().write(&file_path, content).await?; - } - - let mut assert = storify_cmd(); - assert.arg("ls").arg(&parent); - let mut a = assert.assert(); - a = a.success(); - for name in &special_names { - let expected_path = format!("{parent}{name}"); - a = a.stdout(predicate::str::contains(&expected_path)); - } - - Ok(()) -} - -pub async fn test_list_invalid_path(client: StorageClient) -> Result<()> { - let invalid_path = format!("{}/non_existent_dir/", Uuid::new_v4()); - - storify_cmd() - .arg("ls") - .arg(&invalid_path) - .assert() - .success(); - - if let Ok(mut obs) = client.operator().lister(&invalid_path).await { - let mut count = 0; - while (obs.try_next().await?).is_some() { - count += 1; - } - assert_eq!(count, 0, "non-existent directory should have no entries"); - } - - Ok(()) -} - -pub async fn test_list_recursive(client: StorageClient) -> Result<()> { - let root_dir = TEST_FIXTURE.new_dir_path(); - let sub_dir = format!("{root_dir}subdir/"); - let nested_dir = format!("{sub_dir}nested/"); - - client.operator().create_dir(&root_dir).await?; - client.operator().create_dir(&sub_dir).await?; - client.operator().create_dir(&nested_dir).await?; - - let root_file = format!("{root_dir}root.txt"); - let sub_file = format!("{sub_dir}sub.txt"); - let nested_file = format!("{nested_dir}nested.txt"); - - for file_path in &[&root_file, &sub_file, &nested_file] { - let (_, content, _) = TEST_FIXTURE.new_file_with_range(*file_path, 100..300); - client.operator().write(file_path, content).await?; - } - - storify_cmd() - .arg("ls") - .arg("-R") - .arg(&root_dir) - .assert() - .success() - .stdout( - predicate::str::contains(&root_file) - .and(predicate::str::contains(&sub_file)) - .and(predicate::str::contains(&nested_file)), - ); - - Ok(()) -} diff --git a/tests/behavior/operations/mkdir.rs b/tests/behavior/operations/mkdir.rs deleted file mode 100644 index 4220b4f..0000000 --- a/tests/behavior/operations/mkdir.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; -use uuid::Uuid; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_create_single_directory, - test_create_directory_with_parents, - test_create_root_directory, - test_create_existing_directory, - test_create_nested_directories - )); -} - -async fn test_create_single_directory(_client: StorageClient) -> Result<()> { - let dir_name = format!("test-dir-{}", Uuid::new_v4()); - - storify_cmd() - .arg("mkdir") - .arg(&dir_name) - .assert() - .success() - .stdout(predicate::str::contains("Created directory:")); - - let _list_result = storify_cmd().arg("ls").arg(&dir_name).assert().success(); - - let _ = storify_cmd().arg("rm").arg("-R").arg(&dir_name).output(); - - Ok(()) -} - -async fn test_create_directory_with_parents(_client: StorageClient) -> Result<()> { - let parent_dir = format!("parent-{}", Uuid::new_v4()); - let nested_path = format!("{}/nested/subdir", parent_dir); - - storify_cmd() - .arg("mkdir") - .arg("-p") - .arg(&nested_path) - .assert() - .success() - .stdout(predicate::str::contains("Created directory:")); - - let _list_result = storify_cmd().arg("ls").arg(&parent_dir).assert().success(); - - let _ = storify_cmd().arg("rm").arg("-R").arg(&parent_dir).output(); - - Ok(()) -} - -async fn test_create_root_directory(_client: StorageClient) -> Result<()> { - storify_cmd() - .arg("mkdir") - .arg("/") - .assert() - .success() - .stdout(predicate::str::contains("already exists")); - - Ok(()) -} - -async fn test_create_existing_directory(_client: StorageClient) -> Result<()> { - let dir_name = format!("existing-dir-{}", Uuid::new_v4()); - - storify_cmd() - .arg("mkdir") - .arg(&dir_name) - .assert() - .success() - .stdout(predicate::str::contains("Created directory:")); - - let result = storify_cmd() - .arg("mkdir") - .arg(&dir_name) - .output() - .expect("Failed to execute command"); - - assert!(result.status.success(), "Second mkdir should succeed"); - - let _ = storify_cmd().arg("rm").arg("-R").arg(&dir_name).output(); - - Ok(()) -} - -async fn test_create_nested_directories(_client: StorageClient) -> Result<()> { - let base_dir = format!("nested-{}", Uuid::new_v4()); - let nested_path = format!("{}/a/b/c/d", base_dir); - - storify_cmd() - .arg("mkdir") - .arg("-p") - .arg(&nested_path) - .assert() - .success() - .stdout(predicate::str::contains("Created directory:")); - - let _list_result = storify_cmd().arg("ls").arg(&nested_path).assert().success(); - - let _ = storify_cmd().arg("rm").arg("-R").arg(&base_dir).output(); - - Ok(()) -} diff --git a/tests/behavior/operations/mv.rs b/tests/behavior/operations/mv.rs deleted file mode 100644 index e5f35bd..0000000 --- a/tests/behavior/operations/mv.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use std::path::Path; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_move_file_to_existing_directory, - test_move_across_directory, - test_move_rename_existing_file, - test_move_to_nonexistent_directory, - test_move_non_existent_file - )); -} - -async fn test_move_file_to_existing_directory(client: StorageClient) -> Result<()> { - let src_dir = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&src_dir).await?; - - let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); - let src_file_path = format!("{}{}", src_dir, src_file); - client - .operator() - .write(&src_file_path, content.clone()) - .await?; - - let dest_dir = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dest_dir).await?; - - storify_cmd() - .arg("mv") - .arg(&src_file_path) - .arg(&dest_dir) - .assert() - .success(); - - let dest_file_path = format!("{}{}", dest_dir, src_file); - let dst_content = client.operator().read(&dest_file_path).await?; - assert_eq!(content, dst_content.to_vec()); - - let src_result = client.operator().read(&src_file_path).await; - assert!( - src_result.is_err(), - "Source file should be deleted after move" - ); - assert!( - matches!(src_result.unwrap_err().kind(), opendal::ErrorKind::NotFound), - "Error should be NotFound for moved source file" - ); - - Ok(()) -} - -async fn test_move_across_directory(client: StorageClient) -> Result<()> { - let (src_path, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&src_path, content.clone()).await?; - - let dest_path = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dest_path).await?; - let final_dest_path = format!( - "{}", - Path::new(&src_path).file_name().unwrap().to_string_lossy() - ); - - let final_dest_path = join_remote_path(&dest_path, &final_dest_path); - - storify_cmd() - .arg("mv") - .arg(&src_path) - .arg(&dest_path) - .assert() - .success(); - - let dest_content = client.operator().read(&final_dest_path).await?; - assert_eq!(content, dest_content.to_vec()); - - let src_result = client.operator().read(&src_path).await; - assert!( - src_result.is_err(), - "Source file should be deleted after move" - ); - assert!( - matches!(src_result.unwrap_err().kind(), opendal::ErrorKind::NotFound), - "Error should be NotFound for moved source file" - ); - - Ok(()) -} - -async fn test_move_rename_existing_file(client: StorageClient) -> Result<()> { - let (src_file_path, src_content, _) = TEST_FIXTURE.new_file(client.operator()); - client - .operator() - .write(&src_file_path, src_content.clone()) - .await?; - - let (dst_file_path, _dst_content, _) = TEST_FIXTURE.new_file(client.operator()); - - storify_cmd() - .arg("mv") - .arg(&src_file_path) - .arg(&dst_file_path) - .assert() - .success(); - - let final_dst_content = client.operator().read(&dst_file_path).await?; - assert_eq!(src_content, final_dst_content.to_vec()); - - let src_result = client.operator().read(&src_file_path).await; - assert!( - src_result.is_err(), - "Source file should be deleted after move" - ); - assert!( - matches!(src_result.unwrap_err().kind(), opendal::ErrorKind::NotFound), - "Error should be NotFound for moved source file" - ); - - Ok(()) -} - -async fn test_move_to_nonexistent_directory(client: StorageClient) -> Result<()> { - let (src_file, content, _) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&src_file, content.clone()).await?; - - let nonexistent_dir = format!("{}/", TEST_FIXTURE.new_dir_path()); - - let final_dest_path = format!( - "{}", - Path::new(&src_file).file_name().unwrap().to_string_lossy() - ); - let final_dest_path = join_remote_path(&nonexistent_dir, &final_dest_path); - - storify_cmd() - .arg("mv") - .arg(&src_file) - .arg(&nonexistent_dir) - .assert() - .success(); - - let dest_content = client.operator().read(&final_dest_path).await?; - assert_eq!(content, dest_content.to_vec()); - - Ok(()) -} - -async fn test_move_non_existent_file(client: StorageClient) -> Result<()> { - let non_existent_src = TEST_FIXTURE.new_dir_path(); - let non_exist_src_file = TEST_FIXTURE.new_file_path(); - client.operator().create_dir(&non_existent_src).await?; - let final_src_file = format!("{}{}", non_existent_src, non_exist_src_file); - - let dest_path = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dest_path).await?; - - storify_cmd() - .arg("mv") - .arg(&final_src_file) - .arg(&dest_path) - .assert() - .failure() - .stderr(predicate::str::contains("Invalid path")); - - Ok(()) -} diff --git a/tests/behavior/operations/stat.rs b/tests/behavior/operations/stat.rs deleted file mode 100644 index c81ccc7..0000000 --- a/tests/behavior/operations/stat.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_stat_file_human, - test_stat_file_json, - test_stat_dir_raw, - test_stat_not_found - )); -} - -pub async fn test_stat_file_human(client: StorageClient) -> Result<()> { - let (path, content, _size) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&path, content).await?; - - storify_cmd() - .arg("stat") - .arg(&path) - .assert() - .success() - .stdout(predicate::str::contains("type=file")) - .stdout(predicate::str::contains("size=")); - Ok(()) -} - -pub async fn test_stat_file_json(client: StorageClient) -> Result<()> { - let (path, content, _size) = TEST_FIXTURE.new_file(client.operator()); - client.operator().write(&path, content).await?; - - storify_cmd() - .arg("stat") - .arg(&path) - .arg("--json") - .assert() - .success() - .stdout(predicate::str::starts_with("{")) - .stdout(predicate::str::contains("\"entry_type\":\"file\"")); - Ok(()) -} - -pub async fn test_stat_dir_raw(client: StorageClient) -> Result<()> { - let dir = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dir).await?; - - storify_cmd() - .arg("stat") - .arg(format!("/{dir}")) - .arg("--raw") - .assert() - .success() - .stdout(predicate::str::contains("type=dir")); - Ok(()) -} - -pub async fn test_stat_not_found(_client: StorageClient) -> Result<()> { - storify_cmd() - .arg("stat") - .arg("/no_such_file") - .assert() - .failure(); - Ok(()) -} diff --git a/tests/behavior/operations/tail.rs b/tests/behavior/operations/tail.rs deleted file mode 100644 index d1266c7..0000000 --- a/tests/behavior/operations/tail.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_tail_default_10_lines, - test_tail_n_lines, - test_tail_c_bytes, - test_tail_zero_lines_and_zero_bytes, - test_tail_nonexistent_file, - test_tail_multi_files_with_headers, - test_tail_multi_files_quiet, - test_tail_multi_files_verbose - )); -} - -fn create_temp_file_with_content(content: &[u8]) -> String { - let path = std::env::temp_dir().join(uuid::Uuid::new_v4().to_string()); - std::fs::write(&path, content).expect("write temp file"); - path.to_string_lossy().to_string() -} - -fn upload_and_remote_path(local_path: &str, dest_prefix: &str) -> String { - storify_cmd() - .arg("put") - .arg(local_path) - .arg(dest_prefix) - .assert() - .success(); - - let file_name = std::path::Path::new(local_path) - .file_name() - .unwrap() - .to_string_lossy() - .to_string(); - join_remote_path(dest_prefix, &file_name) -} - -async fn test_tail_default_10_lines(_client: StorageClient) -> Result<()> { - let lines: Vec = (1..=20).map(|i| format!("line-{i}")).collect(); - let content = lines.join("\n") + "\n"; - let local = create_temp_file_with_content(content.as_bytes()); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let assert = storify_cmd() - .arg("tail") - .arg(&remote_path) - .assert() - .success(); - let output = assert.get_output().stdout.clone(); - let out = String::from_utf8_lossy(&output); - let expected: Vec = (11..=20).map(|i| format!("line-{i}")).collect(); - let tail = expected.join("\n") + "\n"; - assert_eq!(out, tail); - Ok(()) -} - -async fn test_tail_n_lines(_client: StorageClient) -> Result<()> { - let content = b"A\nB\nC\nD\nE\n"; - let local = create_temp_file_with_content(content); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let assert = storify_cmd() - .arg("tail") - .arg("-n") - .arg("3") - .arg(&remote_path) - .assert() - .success(); - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert_eq!(out, "C\nD\nE\n"); - Ok(()) -} - -async fn test_tail_c_bytes(_client: StorageClient) -> Result<()> { - let content = "Hello, World! ".repeat(10); - let local = create_temp_file_with_content(content.as_bytes()); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let assert = storify_cmd() - .arg("tail") - .arg("-c") - .arg("50") - .arg(&remote_path) - .assert() - .success(); - let output = assert.get_output().stdout.clone(); - assert_eq!(output.len(), 50); - Ok(()) -} - -async fn test_tail_zero_lines_and_zero_bytes(_client: StorageClient) -> Result<()> { - let content = b"X\nY\nZ\n"; - let local = create_temp_file_with_content(content); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote_path = upload_and_remote_path(&local, &dest_prefix); - - let out_n0 = storify_cmd() - .arg("tail") - .arg("-n") - .arg("0") - .arg(&remote_path) - .assert() - .success() - .get_output() - .stdout - .clone(); - assert!(out_n0.is_empty()); - - let out_c0 = storify_cmd() - .arg("tail") - .arg("-c") - .arg("0") - .arg(&remote_path) - .assert() - .success() - .get_output() - .stdout - .clone(); - assert!(out_c0.is_empty()); - Ok(()) -} - -async fn test_tail_nonexistent_file(_client: StorageClient) -> Result<()> { - let non_existent_path = "nonexistent_file.txt"; - let assert = storify_cmd() - .arg("tail") - .arg(non_existent_path) - .assert() - .failure(); - let stderr = assert.get_output().stderr.clone(); - let stderr_str = String::from_utf8_lossy(&stderr); - assert!( - stderr_str.contains("not found") - || stderr_str.contains("NotFound") - || stderr_str.contains("Path does not exist") - ); - Ok(()) -} - -async fn test_tail_multi_files_with_headers(_client: StorageClient) -> Result<()> { - let src1 = get_test_data_path("small.txt"); - let src2_content = (1..=5) - .map(|i| format!("L{i}")) - .collect::>() - .join("\n") - + "\n"; - let src2 = create_temp_file_with_content(src2_content.as_bytes()); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote1 = upload_and_remote_path(&src1.to_string_lossy(), &dest_prefix); - let remote2 = upload_and_remote_path(&src2, &dest_prefix); - - let assert = storify_cmd() - .arg("tail") - .arg(&remote1) - .arg(&remote2) - .assert() - .success(); - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert!(out.contains(&format!("==> {} <==", remote1))); - assert!(out.contains(&format!("==> {} <==", remote2))); - Ok(()) -} - -async fn test_tail_multi_files_quiet(_client: StorageClient) -> Result<()> { - let src1 = get_test_data_path("small.txt"); - let src2_content = (1..=3) - .map(|i| format!("q{i}")) - .collect::>() - .join("\n") - + "\n"; - let src2 = create_temp_file_with_content(src2_content.as_bytes()); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote1 = upload_and_remote_path(&src1.to_string_lossy(), &dest_prefix); - let remote2 = upload_and_remote_path(&src2, &dest_prefix); - - let assert = storify_cmd() - .arg("tail") - .arg("-q") - .arg(&remote1) - .arg(&remote2) - .assert() - .success(); - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert!(!out.contains("==>")); - Ok(()) -} - -async fn test_tail_multi_files_verbose(_client: StorageClient) -> Result<()> { - let src1 = get_test_data_path("small.txt"); - let src2_content = (1..=2) - .map(|i| format!("v{i}")) - .collect::>() - .join("\n") - + "\n"; - let src2 = create_temp_file_with_content(src2_content.as_bytes()); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let remote1 = upload_and_remote_path(&src1.to_string_lossy(), &dest_prefix); - let remote2 = upload_and_remote_path(&src2, &dest_prefix); - - let assert = storify_cmd() - .arg("tail") - .arg("-v") - .arg(&remote1) - .arg(&remote2) - .assert() - .success(); - let out = String::from_utf8_lossy(&assert.get_output().stdout); - assert!(out.contains(&format!("==> {} <==", remote1))); - assert!(out.contains(&format!("==> {} <==", remote2))); - Ok(()) -} diff --git a/tests/behavior/operations/touch.rs b/tests/behavior/operations/touch.rs deleted file mode 100644 index cb8abc1..0000000 --- a/tests/behavior/operations/touch.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_touch_create_and_truncate, - test_touch_no_create_is_noop, - test_touch_parents - )); -} - -async fn test_touch_create_and_truncate(_client: StorageClient) -> Result<()> { - let path = TEST_FIXTURE.new_file_path(); - - storify_cmd().arg("touch").arg(&path).assert().success(); - - storify_cmd() - .arg("touch") - .args(["-t", &path]) - .assert() - .success(); - - Ok(()) -} - -async fn test_touch_no_create_is_noop(_client: StorageClient) -> Result<()> { - let path = TEST_FIXTURE.new_file_path(); - storify_cmd() - .arg("touch") - .args(["-c", &path]) - .assert() - .success() - .stdout(predicate::str::contains("Created:").not()) - .stdout(predicate::str::contains("Truncated:").not()); - Ok(()) -} - -async fn test_touch_parents(_client: StorageClient) -> Result<()> { - let dir = TEST_FIXTURE.new_dir_path(); - let nested = format!("{dir}a/b/c.txt"); - storify_cmd() - .arg("touch") - .args(["-p", &nested]) - .assert() - .success(); - Ok(()) -} diff --git a/tests/behavior/operations/tree.rs b/tests/behavior/operations/tree.rs deleted file mode 100644 index 6aad083..0000000 --- a/tests/behavior/operations/tree.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::*; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_tree_empty, - test_tree_nested, - test_tree_depth_limit, - test_tree_dirs_only - )); -} - -pub async fn test_tree_empty(client: StorageClient) -> Result<()> { - let dir = TEST_FIXTURE.new_dir_path(); - client.operator().create_dir(&dir).await?; - - storify_cmd() - .arg("tree") - .arg(&dir) - .assert() - .success() - .stdout(predicate::str::contains("/")); - Ok(()) -} - -pub async fn test_tree_nested(client: StorageClient) -> Result<()> { - let root = TEST_FIXTURE.new_dir_path(); - let d1 = format!("{root}a/"); - let d2 = format!("{d1}b/"); - let f1 = format!("{d2}c.txt"); - - client.operator().create_dir(&root).await?; - client.operator().create_dir(&d1).await?; - client.operator().create_dir(&d2).await?; - client.operator().write(&f1, vec![b'x']).await?; - - storify_cmd() - .arg("tree") - .arg(&root) - .assert() - .success() - .stdout( - predicate::str::contains("/") - .and(predicate::str::contains("a/")) - .and(predicate::str::contains("b/")) - .and(predicate::str::contains("c.txt")), - ); - Ok(()) -} - -pub async fn test_tree_depth_limit(client: StorageClient) -> Result<()> { - let root = TEST_FIXTURE.new_dir_path(); - let d1 = format!("{root}a/"); - let d2 = format!("{d1}b/"); - let f1 = format!("{d2}c.txt"); - - client.operator().create_dir(&root).await?; - client.operator().create_dir(&d1).await?; - client.operator().create_dir(&d2).await?; - client.operator().write(&f1, vec![b'x']).await?; - - storify_cmd() - .arg("tree") - .arg(&root) - .arg("-d") - .arg("1") - .assert() - .success() - .stdout( - predicate::str::contains("a/").and( - predicate::str::is_match(r"(?m)^\s*[├└]── b/$") - .unwrap() - .not(), - ), - ); - Ok(()) -} - -pub async fn test_tree_dirs_only(client: StorageClient) -> Result<()> { - let root = TEST_FIXTURE.new_dir_path(); - let d1 = format!("{root}a/"); - let f1 = format!("{root}file.txt"); - - client.operator().create_dir(&root).await?; - client.operator().create_dir(&d1).await?; - client.operator().write(&f1, vec![b'x']).await?; - - storify_cmd() - .arg("tree") - .arg(&root) - .arg("--dirs-only") - .assert() - .success() - .stdout(predicate::str::contains("a/").and(predicate::str::contains("file.txt").not())); - Ok(()) -} diff --git a/tests/behavior/operations/upload.rs b/tests/behavior/operations/upload.rs deleted file mode 100644 index 4e31360..0000000 --- a/tests/behavior/operations/upload.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::*; -use crate::{get_test_data_path, join_remote_path}; -use assert_cmd::prelude::*; -use predicates::prelude::*; -use storify::error::Result; -use storify::storage::StorageClient; -use tokio::fs; - -pub fn tests(client: &StorageClient, tests: &mut Vec) { - tests.extend(async_trials!( - client, - test_storage_client_write, - test_storage_client_write_from_special_dir - )); - - tests.extend(async_trials!(client, e2e_test_upload_command_succeeds)); -} - -async fn test_storage_client_write(_client: StorageClient) -> Result<()> { - let source_path = get_test_data_path("small.txt"); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let file_name = source_path - .file_name() - .unwrap() - .to_string_lossy() - .to_string(); - let expected_content = fs::read(&source_path).await?; - - storify_cmd() - .arg("put") - .arg(&source_path) - .arg(&dest_prefix) - .assert() - .success() - .stdout(predicate::str::contains("Upload")); - - let env = E2eTestEnv::new().await; - let final_dest_path = join_remote_path(&dest_prefix, &file_name); - let uploaded_content = env.verifier.operator().read(&final_dest_path).await?; - assert_eq!(expected_content, uploaded_content.to_vec()); - Ok(()) -} - -async fn test_storage_client_write_from_special_dir(_client: StorageClient) -> Result<()> { - let source_path = get_test_data_path("special_dir !@#$%^&()_+-=;'/file_in_special_dir.txt"); - let dest_prefix = TEST_FIXTURE.new_file_path(); - let file_name = source_path - .file_name() - .unwrap() - .to_string_lossy() - .to_string(); - let expected_content = fs::read(&source_path).await?; - - storify_cmd() - .arg("put") - .arg(&source_path) - .arg(&dest_prefix) - .assert() - .success() - .stdout(predicate::str::contains("Upload")); - - let env = E2eTestEnv::new().await; - let final_dest_path = join_remote_path(&dest_prefix, &file_name); - let uploaded_content = env.verifier.operator().read(&final_dest_path).await?; - assert_eq!(expected_content, uploaded_content.to_vec()); - Ok(()) -} - -async fn e2e_test_upload_command_succeeds(_client: StorageClient) -> Result<()> { - let source_path = get_test_data_path("small.txt"); - let dest_path = TEST_FIXTURE.new_file_path(); - let final_dest_path = format!("{}", source_path.file_name().unwrap().to_string_lossy()); - let final_dest_path = join_remote_path(&dest_path, &final_dest_path); - - storify_cmd() - .arg("put") - .arg(&source_path) - .arg(&dest_path) - .assert() - .success() - .stdout(predicate::str::contains("Upload")); - - let env = E2eTestEnv::new().await; - let expected_content = fs::read(&source_path).await?; - let actual_content = env.verifier.operator().read(&final_dest_path).await?; - assert_eq!(expected_content, actual_content.to_vec()); - - Ok(()) -} diff --git a/tests/data/small.txt b/tests/data/small.txt deleted file mode 100644 index 2450267..0000000 --- a/tests/data/small.txt +++ /dev/null @@ -1 +0,0 @@ -This is a small test file for storify. \ No newline at end of file diff --git a/tests/data/special_dir !@#$%^&()_+-=;'/file_in_special_dir.txt b/tests/data/special_dir !@#$%^&()_+-=;'/file_in_special_dir.txt deleted file mode 100644 index 8e5849d..0000000 --- a/tests/data/special_dir !@#$%^&()_+-=;'/file_in_special_dir.txt +++ /dev/null @@ -1 +0,0 @@ -content from a file in a special directory \ No newline at end of file