From 32150f0956becb6d4f6d813a824e91c5ea4ed560 Mon Sep 17 00:00:00 2001 From: Chip Senkbeil Date: Sun, 29 Aug 2021 16:47:00 -0500 Subject: [PATCH] Fix file-write and file-append not parsing input correctly --- core/Cargo.toml | 2 +- core/src/data.rs | 16 ++- tests/cli/action/file_append.rs | 143 +++++++++++++++++++++++++++ tests/cli/action/file_append_text.rs | 143 +++++++++++++++++++++++++++ tests/cli/action/file_read.rs | 35 +++++-- tests/cli/action/file_read_text.rs | 35 +++++-- tests/cli/action/file_write.rs | 126 +++++++++++++++++++++++ tests/cli/action/file_write_text.rs | 136 +++++++++++++++++++++++++ tests/cli/action/mod.rs | 4 + tests/cli/utils.rs | 5 + 10 files changed, 628 insertions(+), 17 deletions(-) create mode 100644 tests/cli/action/file_append.rs create mode 100644 tests/cli/action/file_append_text.rs create mode 100644 tests/cli/action/file_write.rs create mode 100644 tests/cli/action/file_write_text.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 767fd5d..dfc5763 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" license = "MIT OR Apache-2.0" [dependencies] -bytes = "1.0.1" +bytes = "1.1.0" derive_more = { version = "0.99.16", default-features = false, features = ["display", "from", "error", "is_variant"] } futures = "0.3.16" hex = "0.4.3" diff --git a/core/src/data.rs b/core/src/data.rs index f0271ba..4dc54b3 100644 --- a/core/src/data.rs +++ b/core/src/data.rs @@ -3,6 +3,16 @@ use serde::{Deserialize, Serialize}; use std::{io, path::PathBuf}; use strum::AsRefStr; +/// Type alias for a vec of bytes +/// +/// NOTE: This only exists to support properly parsing a Vec from an entire string +/// with structopt rather than trying to parse a string as a singular u8 +pub type ByteVec = Vec; + +fn parse_byte_vec(src: &str) -> ByteVec { + src.as_bytes().to_vec() +} + /// Represents the request to be performed on the remote machine #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields)] @@ -70,7 +80,8 @@ pub enum RequestData { path: PathBuf, /// Data for server-side writing of content - data: Vec, + #[cfg_attr(feature = "structopt", structopt(parse(from_str = parse_byte_vec)))] + data: ByteVec, }, /// Writes a file using text instead of bytes, creating it if it does not exist, @@ -89,7 +100,8 @@ pub enum RequestData { path: PathBuf, /// Data for server-side writing of content - data: Vec, + #[cfg_attr(feature = "structopt", structopt(parse(from_str = parse_byte_vec)))] + data: ByteVec, }, /// Appends text to a file, creating it if it does not exist, on the remote machine diff --git a/tests/cli/action/file_append.rs b/tests/cli/action/file_append.rs new file mode 100644 index 0000000..801280c --- /dev/null +++ b/tests/cli/action/file_append.rs @@ -0,0 +1,143 @@ +use crate::cli::{ + fixtures::*, + utils::{random_tenant, FAILURE_LINE}, +}; +use assert_cmd::Command; +use assert_fs::prelude::*; +use distant::ExitCode; +use distant_core::{ + data::{Error, ErrorKind}, + Request, RequestData, Response, ResponseData, +}; +use rstest::*; + +const FILE_CONTENTS: &str = r#" +some text +on multiple lines +that is a file's contents +"#; + +const APPENDED_FILE_CONTENTS: &str = r#" +even more +file contents +"#; + +#[rstest] +fn should_report_ok_when_done(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str(FILE_CONTENTS).unwrap(); + + // distant action file-append {path} -- {contents} + action_cmd + .args(&[ + "file-append", + file.to_str().unwrap(), + "--", + APPENDED_FILE_CONTENTS, + ]) + .assert() + .success() + .stdout("") + .stderr(""); + + // Because we're talking to a local server, we can verify locally + file.assert(format!("{}{}", FILE_CONTENTS, APPENDED_FILE_CONTENTS)); +} + +#[rstest] +fn should_support_json_output(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str(FILE_CONTENTS).unwrap(); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileAppend { + path: file.to_path_buf(), + data: APPENDED_FILE_CONTENTS.as_bytes().to_vec(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!(res.payload[0], ResponseData::Ok), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(format!("{}{}", FILE_CONTENTS, APPENDED_FILE_CONTENTS)); +} + +#[rstest] +fn yield_an_error_when_fails(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + // distant action file-append {path} -- {contents} + action_cmd + .args(&[ + "file-append", + file.to_str().unwrap(), + "--", + APPENDED_FILE_CONTENTS, + ]) + .assert() + .code(ExitCode::Software.to_i32()) + .stdout("") + .stderr(FAILURE_LINE.clone()); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} + +#[rstest] +fn should_support_json_output_for_error(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileAppend { + path: file.to_path_buf(), + data: APPENDED_FILE_CONTENTS.as_bytes().to_vec(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!( + res.payload[0], + ResponseData::Error(Error { + kind: ErrorKind::NotFound, + .. + }) + ), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} diff --git a/tests/cli/action/file_append_text.rs b/tests/cli/action/file_append_text.rs new file mode 100644 index 0000000..1496ae7 --- /dev/null +++ b/tests/cli/action/file_append_text.rs @@ -0,0 +1,143 @@ +use crate::cli::{ + fixtures::*, + utils::{random_tenant, FAILURE_LINE}, +}; +use assert_cmd::Command; +use assert_fs::prelude::*; +use distant::ExitCode; +use distant_core::{ + data::{Error, ErrorKind}, + Request, RequestData, Response, ResponseData, +}; +use rstest::*; + +const FILE_CONTENTS: &str = r#" +some text +on multiple lines +that is a file's contents +"#; + +const APPENDED_FILE_CONTENTS: &str = r#" +even more +file contents +"#; + +#[rstest] +fn should_report_ok_when_done(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str(FILE_CONTENTS).unwrap(); + + // distant action file-append-text {path} -- {contents} + action_cmd + .args(&[ + "file-append-text", + file.to_str().unwrap(), + "--", + APPENDED_FILE_CONTENTS, + ]) + .assert() + .success() + .stdout("") + .stderr(""); + + // Because we're talking to a local server, we can verify locally + file.assert(format!("{}{}", FILE_CONTENTS, APPENDED_FILE_CONTENTS)); +} + +#[rstest] +fn should_support_json_output(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str(FILE_CONTENTS).unwrap(); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileAppendText { + path: file.to_path_buf(), + text: APPENDED_FILE_CONTENTS.to_string(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!(res.payload[0], ResponseData::Ok), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(format!("{}{}", FILE_CONTENTS, APPENDED_FILE_CONTENTS)); +} + +#[rstest] +fn yield_an_error_when_fails(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + // distant action file-append-text {path} -- {contents} + action_cmd + .args(&[ + "file-append-text", + file.to_str().unwrap(), + "--", + APPENDED_FILE_CONTENTS, + ]) + .assert() + .code(ExitCode::Software.to_i32()) + .stdout("") + .stderr(FAILURE_LINE.clone()); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} + +#[rstest] +fn should_support_json_output_for_error(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileAppendText { + path: file.to_path_buf(), + text: APPENDED_FILE_CONTENTS.to_string(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!( + res.payload[0], + ResponseData::Error(Error { + kind: ErrorKind::NotFound, + .. + }) + ), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} diff --git a/tests/cli/action/file_read.rs b/tests/cli/action/file_read.rs index 66a0446..b9242c4 100644 --- a/tests/cli/action/file_read.rs +++ b/tests/cli/action/file_read.rs @@ -1,10 +1,13 @@ -use crate::cli::{fixtures::*, utils::FAILURE_LINE}; +use crate::cli::{ + fixtures::*, + utils::{random_tenant, FAILURE_LINE}, +}; use assert_cmd::Command; use assert_fs::prelude::*; use distant::ExitCode; use distant_core::{ data::{Error, ErrorKind}, - Response, ResponseData, + Request, RequestData, Response, ResponseData, }; use rstest::*; @@ -35,10 +38,19 @@ fn should_support_json_output(mut action_cmd: Command) { let file = temp.child("test-file"); file.write_str(FILE_CONTENTS).unwrap(); - // distant action --format json file-read {path} + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileRead { + path: file.to_path_buf(), + }], + }; + + // distant action --format json --interactive let cmd = action_cmd .args(&["--format", "json"]) - .args(&["file-read", file.to_str().unwrap()]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) .assert() .success() .stderr(""); @@ -71,12 +83,21 @@ fn should_support_json_output_for_error(mut action_cmd: Command) { let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("missing-file"); - // distant action --format json file-read {path} + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileRead { + path: file.to_path_buf(), + }], + }; + + // distant action --format json --interactive let cmd = action_cmd .args(&["--format", "json"]) - .args(&["file-read", file.to_str().unwrap()]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) .assert() - .code(ExitCode::Software.to_i32()) + .success() .stderr(""); let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); diff --git a/tests/cli/action/file_read_text.rs b/tests/cli/action/file_read_text.rs index 87d0fbd..a084f87 100644 --- a/tests/cli/action/file_read_text.rs +++ b/tests/cli/action/file_read_text.rs @@ -1,10 +1,13 @@ -use crate::cli::{fixtures::*, utils::FAILURE_LINE}; +use crate::cli::{ + fixtures::*, + utils::{random_tenant, FAILURE_LINE}, +}; use assert_cmd::Command; use assert_fs::prelude::*; use distant::ExitCode; use distant_core::{ data::{Error, ErrorKind}, - Response, ResponseData, + Request, RequestData, Response, ResponseData, }; use rstest::*; @@ -35,10 +38,19 @@ fn should_support_json_output(mut action_cmd: Command) { let file = temp.child("test-file"); file.write_str(FILE_CONTENTS).unwrap(); - // distant action --format json file-read-text {path} + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileReadText { + path: file.to_path_buf(), + }], + }; + + // distant action --format json --interactive let cmd = action_cmd .args(&["--format", "json"]) - .args(&["file-read-text", file.to_str().unwrap()]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) .assert() .success() .stderr(""); @@ -71,12 +83,21 @@ fn should_support_json_output_for_error(mut action_cmd: Command) { let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("missing-file"); - // distant action --format json file-read-text {path} + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileReadText { + path: file.to_path_buf(), + }], + }; + + // distant action --format json --interactive let cmd = action_cmd .args(&["--format", "json"]) - .args(&["file-read-text", file.to_str().unwrap()]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) .assert() - .code(ExitCode::Software.to_i32()) + .success() .stderr(""); let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); diff --git a/tests/cli/action/file_write.rs b/tests/cli/action/file_write.rs new file mode 100644 index 0000000..a315e2b --- /dev/null +++ b/tests/cli/action/file_write.rs @@ -0,0 +1,126 @@ +use crate::cli::{ + fixtures::*, + utils::{random_tenant, FAILURE_LINE}, +}; +use assert_cmd::Command; +use assert_fs::prelude::*; +use distant::ExitCode; +use distant_core::{ + data::{Error, ErrorKind}, + Request, RequestData, Response, ResponseData, +}; +use rstest::*; + +const FILE_CONTENTS: &str = r#" +some text +on multiple lines +that is a file's contents +"#; + +#[rstest] +fn should_report_ok_when_done(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + + // distant action file-write {path} -- {contents} + action_cmd + .args(&["file-write", file.to_str().unwrap(), "--", FILE_CONTENTS]) + .assert() + .success() + .stdout("") + .stderr(""); + + // Because we're talking to a local server, we can verify locally + file.assert(FILE_CONTENTS); +} + +#[rstest] +fn should_support_json_output(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileWrite { + path: file.to_path_buf(), + data: FILE_CONTENTS.as_bytes().to_vec(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!(res.payload[0], ResponseData::Ok), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(FILE_CONTENTS); +} + +#[rstest] +fn yield_an_error_when_fails(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + // distant action file-write {path} -- {contents} + action_cmd + .args(&["file-write", file.to_str().unwrap(), "--", FILE_CONTENTS]) + .assert() + .code(ExitCode::Software.to_i32()) + .stdout("") + .stderr(FAILURE_LINE.clone()); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} + +#[rstest] +fn should_support_json_output_for_error(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileWrite { + path: file.to_path_buf(), + data: FILE_CONTENTS.as_bytes().to_vec(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!( + res.payload[0], + ResponseData::Error(Error { + kind: ErrorKind::NotFound, + .. + }) + ), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} diff --git a/tests/cli/action/file_write_text.rs b/tests/cli/action/file_write_text.rs new file mode 100644 index 0000000..12f9546 --- /dev/null +++ b/tests/cli/action/file_write_text.rs @@ -0,0 +1,136 @@ +use crate::cli::{ + fixtures::*, + utils::{random_tenant, FAILURE_LINE}, +}; +use assert_cmd::Command; +use assert_fs::prelude::*; +use distant::ExitCode; +use distant_core::{ + data::{Error, ErrorKind}, + Request, RequestData, Response, ResponseData, +}; +use rstest::*; + +const FILE_CONTENTS: &str = r#" +some text +on multiple lines +that is a file's contents +"#; + +#[rstest] +fn should_report_ok_when_done(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + + // distant action file-write-text {path} -- {contents} + action_cmd + .args(&[ + "file-write-text", + file.to_str().unwrap(), + "--", + FILE_CONTENTS, + ]) + .assert() + .success() + .stdout("") + .stderr(""); + + // Because we're talking to a local server, we can verify locally + file.assert(FILE_CONTENTS); +} + +#[rstest] +fn should_support_json_output(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("test-file"); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileWriteText { + path: file.to_path_buf(), + text: FILE_CONTENTS.to_string(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!(res.payload[0], ResponseData::Ok), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(FILE_CONTENTS); +} + +#[rstest] +fn yield_an_error_when_fails(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + // distant action file-write {path} -- {contents} + action_cmd + .args(&[ + "file-write-text", + file.to_str().unwrap(), + "--", + FILE_CONTENTS, + ]) + .assert() + .code(ExitCode::Software.to_i32()) + .stdout("") + .stderr(FAILURE_LINE.clone()); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} + +#[rstest] +fn should_support_json_output_for_error(mut action_cmd: Command) { + let temp = assert_fs::TempDir::new().unwrap(); + let file = temp.child("missing-dir").child("missing-file"); + + let req = Request { + id: rand::random(), + tenant: random_tenant(), + payload: vec![RequestData::FileWriteText { + path: file.to_path_buf(), + text: FILE_CONTENTS.to_string(), + }], + }; + + // distant action --format json --interactive + let cmd = action_cmd + .args(&["--format", "json"]) + .arg("--interactive") + .write_stdin(format!("{}\n", serde_json::to_string(&req).unwrap())) + .assert() + .success() + .stderr(""); + + let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); + assert!( + matches!( + res.payload[0], + ResponseData::Error(Error { + kind: ErrorKind::NotFound, + .. + }) + ), + "Unexpected response: {:?}", + res.payload[0] + ); + + // Because we're talking to a local server, we can verify locally + file.assert(predicates::path::missing()); +} diff --git a/tests/cli/action/mod.rs b/tests/cli/action/mod.rs index 262c2e1..23c30e0 100644 --- a/tests/cli/action/mod.rs +++ b/tests/cli/action/mod.rs @@ -1,2 +1,6 @@ +mod file_append; +mod file_append_text; mod file_read; mod file_read_text; +mod file_write; +mod file_write_text; diff --git a/tests/cli/utils.rs b/tests/cli/utils.rs index af69fc9..c707425 100644 --- a/tests/cli/utils.rs +++ b/tests/cli/utils.rs @@ -5,3 +5,8 @@ lazy_static::lazy_static! { pub static ref FAILURE_LINE: predicates::str::RegexPredicate = predicate::str::is_match(r"^Failed \(.*\): '.*'\.\n$").unwrap(); } + +/// Creates a random tenant name +pub fn random_tenant() -> String { + format!("test-tenant-{}", rand::random::()) +}