From eb1314808fa4ed35b263f4529ccd81c3985e60c3 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Tue, 20 Dec 2022 19:12:06 +0400 Subject: [PATCH 01/10] Improve error names --- .idea/codeStyles/codeStyleConfig.xml | 5 ++++ .../68bbba69fbc9469ed10568ad6d2fda4fa93abffb | 0 src/authentication.rs | 26 +++++++++---------- src/commands.rs | 8 +++--- src/main.rs | 6 ++--- 5 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/sonarlint/issuestore/6/8/68bbba69fbc9469ed10568ad6d2fda4fa93abffb diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..a55e7a17 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/6/8/68bbba69fbc9469ed10568ad6d2fda4fa93abffb b/.idea/sonarlint/issuestore/6/8/68bbba69fbc9469ed10568ad6d2fda4fa93abffb new file mode 100644 index 00000000..e69de29b diff --git a/src/authentication.rs b/src/authentication.rs index 004ed1a6..b975a918 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -13,19 +13,19 @@ pub struct Config { #[derive(Error, Debug)] pub enum AuthenticationError { #[error("wrong credentials error")] - WrongCredentialsError, + WrongCredentials, #[error("no credentials error")] - NoCredentialsError, + NoCredentials, #[error("request error")] - RequestError(#[from] reqwest::Error), + Request(#[from] reqwest::Error), #[error("i/o error")] - IoError(#[from] std::io::Error), + Io(#[from] std::io::Error), #[error("env error")] - EnvError(#[from] env::VarError), + Env(#[from] env::VarError), #[error("missing home dir error")] - MissingHomeDirError(), + MissingHomeDir(), #[error("invalid header error")] - InvalidHeaderError(#[from] InvalidHeaderValue), + InvalidHeader(#[from] InvalidHeaderValue), #[error("unknown error")] Unknown, } @@ -61,9 +61,9 @@ impl Authentication { match dirs::home_dir() { Some(path) => { - fs::read_to_string(path.join(".screenly")).map_err(AuthenticationError::IoError) + fs::read_to_string(path.join(".screenly")).map_err(AuthenticationError::Io) } - None => Err(AuthenticationError::NoCredentialsError), + None => Err(AuthenticationError::NoCredentials), } } @@ -80,7 +80,7 @@ impl Authentication { fs::write(home.join(".screenly"), token)?; Ok(()) } - None => Err(AuthenticationError::MissingHomeDirError()), + None => Err(AuthenticationError::MissingHomeDir()), } } @@ -96,7 +96,7 @@ impl Authentication { .send()?; match res.status().as_u16() { - 401 => Err(AuthenticationError::WrongCredentialsError), + 401 => Err(AuthenticationError::WrongCredentials), 404 => Ok(()), _ => Err(AuthenticationError::Unknown), } @@ -115,7 +115,7 @@ impl Authentication { reqwest::blocking::Client::builder() .default_headers(default_headers) .build() - .map_err(AuthenticationError::RequestError) + .map_err(AuthenticationError::Request) } } @@ -184,7 +184,7 @@ mod tests { } #[test] - fn test_read_token_when_token_is_overriden_with_env_variable_correct_token_is_returned() { + fn test_read_token_when_token_is_overridden_with_env_variable_correct_token_is_returned() { let tmp_dir = TempDir::new("test").unwrap(); let _lock = lock_test(); let _token = set_env(OsString::from("API_TOKEN"), "env_token"); diff --git a/src/commands.rs b/src/commands.rs index 01a903a4..f38b35c0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -23,17 +23,17 @@ pub trait Formatter { #[derive(Error, Debug)] pub enum CommandError { #[error("auth error")] - AuthenticationError(#[from] AuthenticationError), + Authentication(#[from] AuthenticationError), #[error("request error")] - RequestError(#[from] reqwest::Error), + Request(#[from] reqwest::Error), #[error("parse error")] - ParseError(#[from] serde_json::Error), + Parse(#[from] serde_json::Error), #[error("unknown error #[0]")] WrongResponseStatus(u16), #[error("Required field is missing in the response")] MissingField, #[error("I/O error #[0]")] - IoError(#[from] std::io::Error), + Io(#[from] std::io::Error), #[error("Invalid header value")] InvalidHeaderValue(#[from] InvalidHeaderValue), } diff --git a/src/main.rs b/src/main.rs index bcea9475..1277bdb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -108,7 +108,7 @@ enum AssetCommands { }, } -fn handle_command_execution_result( +fn handle_command_execution_result( result: anyhow::Result, json: &Option, ) { @@ -123,7 +123,7 @@ fn handle_command_execution_result( } Err(e) => { match e { - CommandError::AuthenticationError(_) => { + CommandError::Authentication(_) => { error!( "Authentication error occurred. Please use login command to authenticate." ) @@ -196,7 +196,7 @@ fn main() { } Err(e) => match e { - AuthenticationError::WrongCredentialsError => { + AuthenticationError::WrongCredentials => { error!("Token verification failed."); std::process::exit(1); } From 2dd69eecf2da51a98efa6dd311776fd56cb508cf Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Tue, 20 Dec 2022 19:21:47 +0400 Subject: [PATCH 02/10] Properly use &str for function arguments --- src/commands.rs | 19 +++++++------------ src/main.rs | 5 +---- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index f38b35c0..03f38b28 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -256,7 +256,7 @@ impl AssetCommand { &self, url: &str, headers: &HeaderMap, - payload: &HashMap<&str, &String>, + payload: &HashMap<&str, &str>, ) -> anyhow::Result { let response = self .authentication @@ -274,7 +274,7 @@ impl AssetCommand { Ok(Assets::new(serde_json::from_str(&response.text()?)?)) } - pub fn add(&self, path: String, title: String) -> anyhow::Result { + pub fn add(&self, path: &str, title: &str) -> anyhow::Result { let url = format!("{}/v4/assets", &self.authentication.config.url); let mut headers = HeaderMap::new(); @@ -282,8 +282,8 @@ impl AssetCommand { if path.starts_with("http://") || path.starts_with("https://") { let mut payload = HashMap::new(); - payload.insert("title", &title); - payload.insert("source_url", &path); + payload.insert("title", title.clone()); + payload.insert("source_url", path); return self.add_web_asset(&url, &headers, &payload); } @@ -299,7 +299,7 @@ impl AssetCommand { let part = reqwest::blocking::multipart::Part::reader(pb.wrap_read(file)).file_name("file"); let form = reqwest::blocking::multipart::Form::new() - .text("title", title) + .text("title", title.to_owned()) .part("file", part); let response = self @@ -506,10 +506,7 @@ mod tests { let authentication = Authentication::new_with_config(config); let asset_command = AssetCommand::new(authentication); let v = asset_command - .add( - tmp_dir.path().join("1.html").to_str().unwrap().to_string(), - "test".to_owned(), - ) + .add(tmp_dir.path().join("1.html").to_str().unwrap(), "test") .unwrap(); assert_eq!(v.value, new_asset); } @@ -560,9 +557,7 @@ mod tests { let config = Config::new(mock_server.base_url()); let authentication = Authentication::new_with_config(config); let asset_command = AssetCommand::new(authentication); - let v = asset_command - .add("https://google.com".to_owned(), "test".to_owned()) - .unwrap(); + let v = asset_command.add("https://google.com", "test").unwrap(); assert_eq!(v.value, new_asset); } diff --git a/src/main.rs b/src/main.rs index 1277bdb3..9c426962 100644 --- a/src/main.rs +++ b/src/main.rs @@ -271,10 +271,7 @@ fn main() { } AssetCommands::Add { path, title, json } => { let asset_command = commands::AssetCommand::new(authentication); - handle_command_execution_result( - asset_command.add(path.clone(), title.clone()), - json, - ); + handle_command_execution_result(asset_command.add(path, title), json); } AssetCommands::Delete { uuid } => { let asset_command = commands::AssetCommand::new(authentication); From 9798dce38f0c899c325709babd8b04c2399a6498 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Mon, 26 Dec 2022 13:31:48 +0400 Subject: [PATCH 03/10] More code cleanup --- src/authentication.rs | 3 +- src/commands.rs | 66 +++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/authentication.rs b/src/authentication.rs index b975a918..fea7bb0b 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -130,8 +130,7 @@ mod tests { use simple_logger::SimpleLogger; use tempdir::TempDir; - use crate::authentication::Config; - use crate::Authentication; + use super::*; #[test] fn test_verify_and_store_token_when_token_is_valid() { diff --git a/src/commands.rs b/src/commands.rs index 03f38b28..8c0b2c77 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -326,8 +326,7 @@ impl AssetCommand { #[cfg(test)] mod tests { - use crate::authentication::Config; - use crate::{Authentication, Formatter, OutputType}; + use super::*; use httpmock::{Method::GET, MockServer}; use envtestkit::lock::lock_test; @@ -336,7 +335,7 @@ mod tests { use std::ffi::OsString; use std::fs; - use crate::commands::{AssetCommand, Assets, ScreenCommand, Screens}; + use crate::authentication::Config; use serde_json::{json, Value}; use tempdir::TempDir; @@ -346,23 +345,26 @@ mod tests { let _lock = lock_test(); let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); fs::write(tmp_dir.path().join(".screenly").to_str().unwrap(), "token").unwrap(); + + let screens = serde_json::from_str::("[{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Renat's integrated wired NM\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}]").unwrap(); + let mock_server = MockServer::start(); mock_server.mock(|when, then| { when.method(GET) .path("/v4/screens") .header("Authorization", "Token token") - .header("user-agent", format!("screenly-cli {}", env!("CARGO_PKG_VERSION"))); - then - .status(200) - .body(b"[{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Renat's integrated wired NM\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}]"); + .header( + "user-agent", + format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), + ); + then.status(200).json_body(screens.clone()); }); let config = Config::new(mock_server.base_url()); let authentication = Authentication::new_with_config(config); let screen_command = ScreenCommand::new(authentication); - let expected = serde_json::from_str::("[{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Renat's integrated wired NM\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}]").unwrap(); let v = screen_command.list().unwrap(); - assert_eq!(v.value, expected); + assert_eq!(v.value, screens); } #[test] @@ -435,27 +437,29 @@ mod tests { let _lock = lock_test(); let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); fs::write(tmp_dir.path().join(".screenly").to_str().unwrap(), "token").unwrap(); + let new_screen = serde_json::from_str::("{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Test\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}").unwrap(); + let mock_server = MockServer::start(); mock_server.mock(|when, then| { when.method(POST) .path("/v3/screens/") .header("Authorization", "Token token") .header("content-type", "application/json") - .header("user-agent", format!("screenly-cli {}", env!("CARGO_PKG_VERSION"))) + .header( + "user-agent", + format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), + ) .json_body(json!({"pin": "test-pin", "name": "test"})); - then - .status(201) - .body(b"{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Test\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}"); + then.status(201).json_body(new_screen.clone()); }); let config = Config::new(mock_server.base_url()); let authentication = Authentication::new_with_config(config); let screen_command = ScreenCommand::new(authentication); - let expected = serde_json::from_str::("[{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Test\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}]").unwrap(); let v = screen_command .add("test-pin", Some("test".to_string())) .unwrap(); - assert_eq!(v.value, expected); + assert_eq!(v.value.as_array().unwrap()[0], new_screen); } #[test] @@ -716,15 +720,15 @@ mod tests { { let screen = Screens::new(serde_json::from_str("[{\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb5\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"created_at\":\"2021-06-28T05:07:55+00:00\",\"name\":\"Renat's integrated wired NM\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2021-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}, {\"id\":\"017a5104-524b-33d8-8026-9087b59e7eb6\",\"team_id\":\"016343c2-82b8-0000-a121-e30f1035875d\",\"created_at\":\"2020-06-28T05:07:55+00:00\",\"name\":\"Not Renat's integrated wired NM\",\"is_enabled\":true,\"coords\":[55.22931, 48.90429],\"last_ping\":\"2020-08-25T06:17:20.728+00:00\",\"last_ip\":null,\"local_ip\":\"192.168.1.146\",\"mac\":\"b8:27:eb:d6:83:6f\",\"last_screenshot_time\":\"2021-08-25T06:09:04.399+00:00\",\"uptime\":\"230728.38\",\"load_avg\":\"0.14\",\"signal_strength\":null,\"interface\":\"eth0\",\"debug\":false,\"location\":\"Kamsko-Ust'inskiy rayon, Russia\",\"team\":\"016343c2-82b8-0000-a121-e30f1035875e\",\"timezone\":\"Europe/Moscow\",\"type\":\"hardware\",\"hostname\":\"srly-4shnfrdc5cd2p0p\",\"ws_open\":false,\"status\":\"Offline\",\"last_screenshot\":\"https://us-assets.screenlyapp.com/01CD1W50NR000A28F31W83B1TY/screenshots/01F98G8MJB6FC809MGGYTSWZNN/5267668e6db35498e61b83d4c702dbe8\",\"in_sync\":false,\"software_version\":\"Screenly 2 Player\",\"hardware_version\":\"Raspberry Pi 3B\",\"config\":{\"hdmi_mode\": 34, \"hdmi_boost\": 2, \"hdmi_drive\": 0, \"hdmi_group\": 0, \"verify_ssl\": true, \"audio_output\": \"hdmi\", \"hdmi_timings\": \"\", \"overscan_top\": 0, \"overscan_left\": 0, \"use_composite\": false, \"display_rotate\": 0, \"overscan_right\": 0, \"overscan_scale\": 0, \"overscan_bottom\": 0, \"disable_overscan\": 0, \"shuffle_playlist\": false, \"framebuffer_width\": 0, \"use_composite_pal\": false, \"framebuffer_height\": 0, \"hdmi_force_hotplug\": true, \"use_composite_ntsc\": false, \"hdmi_pixel_encoding\": 0, \"play_history_enabled\": false}}]").unwrap()); println!("{}", screen.format(OutputType::HumanReadable)); - let expected_output = concat!( -"+--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n", -"| Id | Name | Hardware Version | In Sync | Last Ping | Uptime |\n", -"+--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n", -"| 017a5104-524b-33d8-8026-9087b59e7eb5 | Renat's integrated wired NM | Raspberry Pi 3B | ❌ | 2021-08-25T06:17:20.728+00:00 | 2days 16h 5m 28s |\n", -"+--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n", -"| 017a5104-524b-33d8-8026-9087b59e7eb6 | Not Renat's integrated wired NM | Raspberry Pi 3B | ❌ | 2020-08-25T06:17:20.728+00:00 | 2days 16h 5m 28s |\n", -"+--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n" -); + let expected_output = +"+--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n\ +| Id | Name | Hardware Version | In Sync | Last Ping | Uptime |\n\ ++--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n\ +| 017a5104-524b-33d8-8026-9087b59e7eb5 | Renat's integrated wired NM | Raspberry Pi 3B | ❌ | 2021-08-25T06:17:20.728+00:00 | 2days 16h 5m 28s |\n\ ++--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n\ +| 017a5104-524b-33d8-8026-9087b59e7eb6 | Not Renat's integrated wired NM | Raspberry Pi 3B | ❌ | 2020-08-25T06:17:20.728+00:00 | 2days 16h 5m 28s |\n\ ++--------------------------------------+---------------------------------+------------------+---------+-------------------------------+------------------+\n"; + assert_eq!(screen.format(OutputType::HumanReadable), expected_output); } @@ -754,13 +758,13 @@ mod tests { ])); println!("{}", asset.format(OutputType::HumanReadable)); - let expected_output = concat!( - "+--------------------------------------+------------+------+--------+\n", - "| Id | Title | Type | Status |\n", - "+--------------------------------------+------------+------+--------+\n", - "| 0184f162-585e-6334-8dae-38a80062a6c2 | test3.html | N/A | none |\n", - "+--------------------------------------+------------+------+--------+\n" - ); + let expected_output = + "+--------------------------------------+------------+------+--------+\n\ + | Id | Title | Type | Status |\n\ + +--------------------------------------+------------+------+--------+\n\ + | 0184f162-585e-6334-8dae-38a80062a6c2 | test3.html | N/A | none |\n\ + +--------------------------------------+------------+------+--------+\n"; + assert_eq!(asset.format(OutputType::HumanReadable), expected_output); } } From 46378279ec7635671888db2a3c67b9f2a5b7fb6c Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Wed, 28 Dec 2022 16:22:29 +0400 Subject: [PATCH 04/10] Support inject-js and set-headers asset commands. --- Cargo.toml | 2 +- src/commands.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 75 +++++++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 93616eed..1d14fac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ path = "src/main.rs" [dependencies] openssl = { version = '0.10', features = ["vendored"] } anyhow = "1.0.65" -clap = { version = "4.0.17", features = ["derive"] } +clap = { version = "4.0.17", features = ["derive", "cargo"] } envtestkit = "1.1.2" humantime = "2.1.0" indicatif = "0.17.2" diff --git a/src/commands.rs b/src/commands.rs index 8c0b2c77..fb5ec203 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -10,6 +10,7 @@ use thiserror::Error; use prettytable::{row, Table}; use reqwest::header::{HeaderMap, InvalidHeaderValue}; +use serde_json::json; pub enum OutputType { HumanReadable, @@ -187,6 +188,31 @@ fn delete(authentication: &Authentication, endpoint: &str) -> anyhow::Result<(), Ok(()) } +fn patch( + authentication: &Authentication, + endpoint: &str, + payload: &serde_json::Value, +) -> anyhow::Result<(), CommandError> { + let url = format!("{}/{}", &authentication.config.url, endpoint); + let mut headers = HeaderMap::new(); + headers.insert("Prefer", "return=representation".parse()?); + + let response = authentication + .build_client()? + .patch(url) + .json(&payload) + .headers(headers) + .send()?; + + if response.status().as_u16() != 200 { + return Err(CommandError::WrongResponseStatus( + response.status().as_u16(), + )); + } + + Ok(()) +} + impl ScreenCommand { pub fn new(authentication: Authentication) -> Self { Self { authentication } @@ -318,6 +344,28 @@ impl AssetCommand { Ok(Assets::new(serde_json::from_str(&response.text()?)?)) } + pub fn set_headers( + &self, + id: &str, + headers: HashMap<&str, &str>, + ) -> anyhow::Result<(), CommandError> { + let endpoint = format!("v4/assets?id=eq.{}", id); + patch( + &self.authentication, + &endpoint, + &json!({ "headers": headers }), + ) + } + + pub fn inject_js(&self, id: &str, js_code: &str) -> anyhow::Result<(), CommandError> { + let endpoint = format!("v4/assets?id=eq.{}", id); + patch( + &self.authentication, + &endpoint, + &json!({ "js-injection": js_code }), + ) + } + pub fn delete(&self, id: &str) -> anyhow::Result<(), CommandError> { let endpoint = format!("v4/assets?id=eq.{}", id); delete(&self.authentication, &endpoint) @@ -331,7 +379,7 @@ mod tests { use envtestkit::lock::lock_test; use envtestkit::set_env; - use httpmock::Method::{DELETE, POST}; + use httpmock::Method::{DELETE, PATCH, POST}; use std::ffi::OsString; use std::fs; @@ -715,6 +763,61 @@ mod tests { assert!(asset_command.delete("test-id").is_ok()); } + #[test] + fn test_inject_js_should_send_correct_request() { + let tmp_dir = TempDir::new("test").unwrap(); + let _lock = lock_test(); + let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); + fs::write(tmp_dir.path().join(".screenly").to_str().unwrap(), "token").unwrap(); + let mock_server = MockServer::start(); + mock_server.mock(|when, then| { + when.method(PATCH) + .path("/v4/assets") + .query_param("id", "eq.test-id") + .header( + "user-agent", + format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), + ) + .json_body(json!({"js-injection": "console.log(1)"})) + .header("Authorization", "Token token"); + then.status(200); + }); + + let config = Config::new(mock_server.base_url()); + let authentication = Authentication::new_with_config(config); + let asset_command = AssetCommand::new(authentication); + assert!(asset_command.inject_js("test-id", "console.log(1)").is_ok()); + } + + #[test] + fn test_set_headers_should_send_correct_request() { + let tmp_dir = TempDir::new("test").unwrap(); + let _lock = lock_test(); + let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); + fs::write(tmp_dir.path().join(".screenly").to_str().unwrap(), "token").unwrap(); + let mock_server = MockServer::start(); + mock_server.mock(|when, then| { + when.method(PATCH) + .path("/v4/assets") + .query_param("id", "eq.test-id") + .header( + "user-agent", + format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), + ) + .json_body(json!({"headers": {"k": "v"}})) + .header("Authorization", "Token token"); + then.status(200); + }); + + let config = Config::new(mock_server.base_url()); + let authentication = Authentication::new_with_config(config); + let asset_command = AssetCommand::new(authentication); + let mut headers = HashMap::new(); + headers.insert("k", "v"); + + assert!(asset_command.set_headers("test-id", headers).is_ok()); + } + #[test] fn test_format_screen_when_human_readable_output_is_set_should_return_correct_formatted_string() { diff --git a/src/main.rs b/src/main.rs index 9c426962..2b555c90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,13 @@ mod commands; extern crate prettytable; use crate::authentication::{Authentication, AuthenticationError}; -#[allow(unused_imports)] use crate::commands::{CommandError, Formatter, OutputType}; use clap::{command, Parser, Subcommand}; use log::{error, info}; use simple_logger::SimpleLogger; -use std::io; +use std::collections::HashMap; use std::io::Write; +use std::{fs, io}; #[derive(Parser)] #[command( @@ -106,6 +106,24 @@ enum AssetCommands { /// UUID of the asset to be deleted. uuid: String, }, + + /// Injects javascript code inside of the web asset. It will be executed once the asset loads during playback. + InjectJs { + /// UUID of the web asset to inject with JavaScript. + uuid: String, + + /// Path to local file or URL for remote file. + path: String, + }, + + /// Sets http headers for web asset. + SetHeaders { + /// UUID of the web asset to set http headers. + uuid: String, + + /// HTTP headers in the dictionary form: {header1=value, header2=value2}. + headers: String, + }, } fn handle_command_execution_result( @@ -312,6 +330,59 @@ fn main() { } } } + AssetCommands::InjectJs { uuid, path } => { + let asset_command = commands::AssetCommand::new(authentication); + let js_code = if path.starts_with("http://") || path.starts_with("https://") { + match reqwest::blocking::get(path) { + Ok(response) => match response.status().as_u16() { + 200 => response.text().unwrap_or_default(), + status => { + error!("Failed to retrieve JS injection code. Wrong response status: {}", status); + std::process::exit(1); + } + }, + Err(e) => { + error!("Failed to retrieve JS injection code. Error: {}", e); + std::process::exit(1); + } + } + } else { + match fs::read_to_string(&path) { + Ok(text) => text, + Err(e) => { + error!("Failed to read file with JS injection code. Error: {}", e); + std::process::exit(1); + } + } + }; + + match asset_command.inject_js(uuid, &js_code) { + Ok(()) => { + info!("Asset updated successfully."); + } + Err(e) => { + error!("Error occurred: {:?}", e); + std::process::exit(1); + } + } + } + AssetCommands::SetHeaders { uuid, headers } => { + if let Ok(headers) = serde_json::from_str::>(headers) { + let asset_command = commands::AssetCommand::new(authentication); + match asset_command.set_headers(uuid, headers) { + Ok(()) => { + info!("Asset updated successfully."); + } + Err(e) => { + error!("Error occurred: {:?}", e); + std::process::exit(1); + } + } + } else { + error!("Failed to parse provided headers."); + std::process::exit(1); + } + } }, } } From c49cb6ac1fee25bbb8259ca5d06634583f97157c Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Wed, 28 Dec 2022 16:25:00 +0400 Subject: [PATCH 05/10] Cargo clippy run --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 2b555c90..9b012b18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -347,7 +347,7 @@ fn main() { } } } else { - match fs::read_to_string(&path) { + match fs::read_to_string(path) { Ok(text) => text, Err(e) => { error!("Failed to read file with JS injection code. Error: {}", e); From 97370cd6ec74472b281e04031aa29aca34a0c17e Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Wed, 28 Dec 2022 16:29:18 +0400 Subject: [PATCH 06/10] Fix clippy warning --- src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index fb5ec203..384e2a58 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -308,7 +308,7 @@ impl AssetCommand { if path.starts_with("http://") || path.starts_with("https://") { let mut payload = HashMap::new(); - payload.insert("title", title.clone()); + payload.insert("title", title); payload.insert("source_url", path); return self.add_web_asset(&url, &headers, &payload); } From d71e97bd5dc818a3c285d3a87510a3b18abc5a61 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Thu, 29 Dec 2022 17:22:23 +0400 Subject: [PATCH 07/10] New syntax for passing headers. Pass header with --header k=v. This command can be used more than once to pass multiple headers. --- src/commands.rs | 13 ++++--------- src/main.rs | 44 +++++++++++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 384e2a58..e4bf2c0c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -347,14 +347,11 @@ impl AssetCommand { pub fn set_headers( &self, id: &str, - headers: HashMap<&str, &str>, + headers: Vec<(String, String)>, ) -> anyhow::Result<(), CommandError> { let endpoint = format!("v4/assets?id=eq.{}", id); - patch( - &self.authentication, - &endpoint, - &json!({ "headers": headers }), - ) + let map: HashMap<_, _> = headers.into_iter().collect(); + patch(&self.authentication, &endpoint, &json!({ "headers": map })) } pub fn inject_js(&self, id: &str, js_code: &str) -> anyhow::Result<(), CommandError> { @@ -812,9 +809,7 @@ mod tests { let config = Config::new(mock_server.base_url()); let authentication = Authentication::new_with_config(config); let asset_command = AssetCommand::new(authentication); - let mut headers = HashMap::new(); - headers.insert("k", "v"); - + let headers = vec![("k".to_owned(), "v".to_owned())]; assert!(asset_command.set_headers("test-id", headers).is_ok()); } diff --git a/src/main.rs b/src/main.rs index 9b012b18..8fe64d68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,15 +6,28 @@ extern crate prettytable; use crate::authentication::{Authentication, AuthenticationError}; use crate::commands::{CommandError, Formatter, OutputType}; use clap::{command, Parser, Subcommand}; + use log::{error, info}; use simple_logger::SimpleLogger; -use std::collections::HashMap; use std::io::Write; use std::{fs, io}; +use thiserror::Error; + +#[derive(Error, Debug)] +enum ParseError { + #[error("missing \"=\" symbol")] + MissingSymbol(), +} +fn parse_key_val(s: &str) -> Result<(String, String), ParseError> { + let pos = s + .find('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s)) + .map_err(|_| ParseError::MissingSymbol())?; + Ok((s[..pos].to_string(), s[pos + 1..].to_string())) +} #[derive(Parser)] #[command( - author, version, about, long_about = "Command line interface is intended for quick interaction with Screenly through terminal. Moreover, this CLI is built such that it can be used for automating tasks." @@ -121,8 +134,9 @@ enum AssetCommands { /// UUID of the web asset to set http headers. uuid: String, - /// HTTP headers in the dictionary form: {header1=value, header2=value2}. - headers: String, + /// HTTP header in the following form "k=v". This command can be used more than once to pass multiple headers. + #[arg(long="header", action = clap::ArgAction::Append, value_parser = parse_key_val)] + headers: Vec<(String, String)>, }, } @@ -205,6 +219,7 @@ fn main() { .init() .unwrap(); let cli = Cli::parse(); + let authentication = Authentication::new(); match &cli.command { Commands::Login { token } => match authentication.verify_and_store_token(token) { @@ -367,20 +382,15 @@ fn main() { } } AssetCommands::SetHeaders { uuid, headers } => { - if let Ok(headers) = serde_json::from_str::>(headers) { - let asset_command = commands::AssetCommand::new(authentication); - match asset_command.set_headers(uuid, headers) { - Ok(()) => { - info!("Asset updated successfully."); - } - Err(e) => { - error!("Error occurred: {:?}", e); - std::process::exit(1); - } + let asset_command = commands::AssetCommand::new(authentication); + match asset_command.set_headers(uuid, headers.clone()) { + Ok(()) => { + info!("Asset updated successfully."); + } + Err(e) => { + error!("Error occurred: {:?}", e); + std::process::exit(1); } - } else { - error!("Failed to parse provided headers."); - std::process::exit(1); } } }, From 26c7d4df6305908122aa4460fc2f36c60c9acb21 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Thu, 29 Dec 2022 17:25:23 +0400 Subject: [PATCH 08/10] Simplify parsing --- src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8fe64d68..9e9dba1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,10 +19,7 @@ enum ParseError { MissingSymbol(), } fn parse_key_val(s: &str) -> Result<(String, String), ParseError> { - let pos = s - .find('=') - .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s)) - .map_err(|_| ParseError::MissingSymbol())?; + let pos = s.find('=').ok_or(ParseError::MissingSymbol())?; Ok((s[..pos].to_string(), s[pos + 1..].to_string())) } From 3abb9437c6b2b1f9bdc3f3af46d673be1189a794 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Thu, 29 Dec 2022 17:47:17 +0400 Subject: [PATCH 09/10] Implement command for setting basic auth headers --- Cargo.toml | 1 + src/main.rs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1d14fac5..fbccfa7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ tempdir = "0.3.7" term = "0.7.0" thiserror = "1.0.37" dirs = "4.0.0" +http-auth-basic = "0.3.3" [dev-dependencies] httpmock = "0.6" diff --git a/src/main.rs b/src/main.rs index 9e9dba1b..aef07f33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use crate::authentication::{Authentication, AuthenticationError}; use crate::commands::{CommandError, Formatter, OutputType}; use clap::{command, Parser, Subcommand}; +use http_auth_basic::Credentials; use log::{error, info}; use simple_logger::SimpleLogger; use std::io::Write; @@ -135,6 +136,14 @@ enum AssetCommands { #[arg(long="header", action = clap::ArgAction::Append, value_parser = parse_key_val)] headers: Vec<(String, String)>, }, + + BasicAuth { + /// UUID of the web asset to set up basic authentication for. + uuid: String, + /// Shortcut for setting up basic authentication headers. Accepts login in password in form user=password. + #[arg(value_parser = parse_key_val)] + credentials: (String, String), + }, } fn handle_command_execution_result( @@ -390,6 +399,22 @@ fn main() { } } } + AssetCommands::BasicAuth { uuid, credentials } => { + let asset_command = commands::AssetCommand::new(authentication); + let basic_auth = Credentials::new(&credentials.0, &credentials.1); + match asset_command.set_headers( + uuid, + vec![("Authorization".to_owned(), basic_auth.as_http_header())], + ) { + Ok(()) => { + info!("Asset updated successfully."); + } + Err(e) => { + error!("Error occurred: {:?}", e); + std::process::exit(1); + } + } + } }, } } From 67a333eeafd7290fdd00a106d599d5d403a37284 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Thu, 29 Dec 2022 17:52:54 +0400 Subject: [PATCH 10/10] Ref. code review. --- src/commands.rs | 6 ++++-- src/main.rs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index e4bf2c0c..c8de4329 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -344,7 +344,7 @@ impl AssetCommand { Ok(Assets::new(serde_json::from_str(&response.text()?)?)) } - pub fn set_headers( + pub fn set_web_asset_headers( &self, id: &str, headers: Vec<(String, String)>, @@ -810,7 +810,9 @@ mod tests { let authentication = Authentication::new_with_config(config); let asset_command = AssetCommand::new(authentication); let headers = vec![("k".to_owned(), "v".to_owned())]; - assert!(asset_command.set_headers("test-id", headers).is_ok()); + assert!(asset_command + .set_web_asset_headers("test-id", headers) + .is_ok()); } #[test] diff --git a/src/main.rs b/src/main.rs index aef07f33..72ab30bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -389,7 +389,7 @@ fn main() { } AssetCommands::SetHeaders { uuid, headers } => { let asset_command = commands::AssetCommand::new(authentication); - match asset_command.set_headers(uuid, headers.clone()) { + match asset_command.set_web_asset_headers(uuid, headers.clone()) { Ok(()) => { info!("Asset updated successfully."); } @@ -402,7 +402,7 @@ fn main() { AssetCommands::BasicAuth { uuid, credentials } => { let asset_command = commands::AssetCommand::new(authentication); let basic_auth = Credentials::new(&credentials.0, &credentials.1); - match asset_command.set_headers( + match asset_command.set_web_asset_headers( uuid, vec![("Authorization".to_owned(), basic_auth.as_http_header())], ) {