From 54e27314e9bbb76b2b883db13adbb7fa1139fff8 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Tue, 25 Oct 2022 16:41:18 +0400 Subject: [PATCH 1/5] Initial commit. Login is implemented. Token is verified as working when making a request to get a non existing group. There are tests that check that response for correct and incorrect token is handled. In case of correct token path $HOME/.screenly is checked to have the token stored upon successful login. --- .gitignore | 5 + .idea/.gitignore | 8 + .idea/cli.iml | 11 ++ .idea/inspectionProfiles/Project_Default.xml | 6 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/sonarlint/issuestore/index.pb | 0 .idea/vcs.xml | 6 + Cargo.toml | 19 +++ src/authentication.rs | 146 ++++++++++++++++++ src/commands.rs | 21 +++ src/main.rs | 74 +++++++++ 13 files changed, 314 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/cli.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/sonarlint/issuestore/index.pb create mode 100644 .idea/vcs.xml create mode 100644 Cargo.toml create mode 100644 src/authentication.rs create mode 100644 src/commands.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 088ba6ba..22d35163 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + + +# Added by cargo + +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cli.iml b/.idea/cli.iml new file mode 100644 index 00000000..d900b784 --- /dev/null +++ b/.idea/cli.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..cff13543 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..dd602185 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..6fcc0dcf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb new file mode 100644 index 00000000..e69de29b diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..258038d4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.65" +clap = { version = "4.0.17", features = ["derive"] } +hyper = "0.14.20" +log = { version = "0.4.17", features = ["release_max_level_info", "max_level_trace"] } +reqwest = { version = "0.11.12", features = ["json", "blocking"] } +simple_logger = "3.0.0" +tempdir = "0.3.7" +thiserror = "1.0.37" + +[dev-dependencies] +httpmock = "0.6" diff --git a/src/authentication.rs b/src/authentication.rs new file mode 100644 index 00000000..f0f512cd --- /dev/null +++ b/src/authentication.rs @@ -0,0 +1,146 @@ + +use reqwest::{header}; +use std::{env, fs}; +use thiserror::Error; + +const API_BASE_URL: &str = "https://api.screenlyapp.com/api"; + +pub struct Config { + url: String, +} + +#[derive(Error, Debug)] +pub enum AuthenticationError { + #[error("wrong credentials error")] + WrongCredentialsError, + #[error("request error")] + RequestError(#[from] reqwest::Error), + #[error("i/o error")] + IoError(#[from] std::io::Error), + #[error("env error")] + EnvError(#[from] env::VarError), + #[error("unknown error")] + Unknown, +} + +pub struct Authentication { + config: Config, +} + +impl Config { + pub fn default() -> Self { + Self { + url: API_BASE_URL.to_string(), + } + } + + #[cfg(test)] + pub fn new(url: String) -> Self { + Self { + url: url.to_string(), + } + } +} + +impl Authentication { + pub fn new() -> Self { + Self { + config: Config::default(), + } + } + + #[cfg(test)] + pub fn new_with_config(config: Config) -> Self { + Self { config } + } + + pub fn verify_and_store_token(&self, token: &str) -> anyhow::Result<(), AuthenticationError> { + self.verify_token(token)?; + + match std::env::var("HOME") { + Ok(home) => { + fs::write(home + "/.screenly", &token)?; + Ok(()) + } + Err(e) => Err(AuthenticationError::EnvError(e)), + } + } + + fn verify_token(&self, token: &str) -> anyhow::Result<(), AuthenticationError> { + // Using uuid of non existing playlist. If we get 404 it means we authenticated successfully. + let url = self.config.url.clone() + "/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/"; + let secret = "Token ".to_owned() + token; + let client = reqwest::blocking::Client::builder().build()?; + + let res = client + .get(url) + .header(header::AUTHORIZATION, &secret) + .send()?; + + return match res.status().as_u16() { + 401 => Err(AuthenticationError::WrongCredentialsError), + 404 => Ok(()), + _ => Err(AuthenticationError::Unknown), + }; + } +} + +#[cfg(test)] +mod tests { + use crate::authentication::Config; + use crate::Authentication; + use httpmock::{Method::GET, MockServer}; + use simple_logger::SimpleLogger; + + + + use std::{env, fs}; + use tempdir::TempDir; + + #[test] + fn test_verify_and_store_token_when_token_is_valid() { + SimpleLogger::new() + .with_level(log::LevelFilter::Debug) + .init() + .unwrap(); + let tmp_dir = TempDir::new("test").unwrap(); + env::set_var("HOME", tmp_dir.path().to_str().unwrap()); + let mock_server = MockServer::start(); + mock_server.mock(|when, then| { + when.method(GET) + .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/"); + then.status(404); + }); + + let config = Config::new(mock_server.base_url()); + let authentication = Authentication::new_with_config(config); + assert!(authentication + .verify_and_store_token("correct_token") + .is_ok()); + let path = tmp_dir.path().join(".screenly"); + assert!(path.exists()); + let contents = fs::read_to_string(path).unwrap(); + assert!(contents.eq("correct_token")); + tmp_dir.close().unwrap(); + } + + #[test] + fn test_verify_and_store_token_when_token_is_invalid() { + let tmp_dir = TempDir::new("invalid").unwrap(); + env::set_var("HOME", tmp_dir.path().to_str().unwrap()); + let mock_server = MockServer::start(); + mock_server.mock(|when, then| { + when.method(GET) + .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/"); + then.status(401); + }); + + let config = Config::new(mock_server.base_url()); + let authentication = Authentication::new_with_config(config); + assert!(authentication + .verify_and_store_token("wrong_token") + .is_err()); + let path = tmp_dir.path().join(".screenly"); + assert!(!path.exists()); + } +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 00000000..b9c10930 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,21 @@ +use crate::Authentication; + + + + + + + +pub struct ScreenCommand { + authentication: Authentication, +} + +impl ScreenCommand { + fn new(authentication: Authentication) -> Self { + Self { authentication } + } + + fn list() {} + + fn get(_id: &str) {} +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..858ba045 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,74 @@ +mod authentication; +mod commands; + +use crate::authentication::{Authentication, AuthenticationError}; +use clap::{command, Parser, Subcommand}; +use log::{warn}; +use simple_logger::SimpleLogger; + + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Logins with the token and stores it for further use if it's valid + Login { token: String }, + /// Screen related commands + #[command(subcommand)] + Screen(ScreenCommands), +} + +#[derive(Subcommand, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum ScreenCommands { + /// Lists your screens + List, + /// Gets a single screen by id + Get { id: String }, +} + +fn main() { + SimpleLogger::new() + .with_level(log::LevelFilter::Info) + .init() + .unwrap(); + let cli = Cli::parse(); + let authentication = Authentication::new(); + match &cli.command { + Commands::Login { token } => match authentication.verify_and_store_token(token) { + Ok(()) => { + println!("Login credentials have been saved."); + std::process::exit(0); + } + + Err(e) => match e { + AuthenticationError::ConnectionError => { + println!("Connection error. Try again later."); + std::process::exit(1); + } + AuthenticationError::WrongCredentialsError => { + println!("Token verification failed."); + std::process::exit(2); + } + _ => { + println!("Unknown error"); + std::process::exit(2); + } + }, + }, + Commands::Screen(command) => match command { + ScreenCommands::List => { + warn!("List"); + } + ScreenCommands::Get { id: _ } => { + warn!("GET"); + } + }, + } + warn!("Hello, world!"); +} From 16b61fa8bbac7efa3acf4a87031d7df0d9cd12d4 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Wed, 26 Oct 2022 16:58:34 +0400 Subject: [PATCH 2/5] Remove some debugging code. --- Cargo.toml | 1 - src/main.rs | 11 +++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 258038d4..690006b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] anyhow = "1.0.65" clap = { version = "4.0.17", features = ["derive"] } -hyper = "0.14.20" log = { version = "0.4.17", features = ["release_max_level_info", "max_level_trace"] } reqwest = { version = "0.11.12", features = ["json", "blocking"] } simple_logger = "3.0.0" diff --git a/src/main.rs b/src/main.rs index 858ba045..abbb6e42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,13 +47,9 @@ fn main() { } Err(e) => match e { - AuthenticationError::ConnectionError => { - println!("Connection error. Try again later."); - std::process::exit(1); - } AuthenticationError::WrongCredentialsError => { println!("Token verification failed."); - std::process::exit(2); + std::process::exit(1); } _ => { println!("Unknown error"); @@ -63,12 +59,11 @@ fn main() { }, Commands::Screen(command) => match command { ScreenCommands::List => { - warn!("List"); + warn!("List: to be implemented"); } ScreenCommands::Get { id: _ } => { - warn!("GET"); + warn!("Get: to be implemented"); } }, } - warn!("Hello, world!"); } From e0959412d1dad8b9e698375e826628959e04e10d Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Wed, 26 Oct 2022 17:02:19 +0400 Subject: [PATCH 3/5] Set the binary name to "screenly" --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 690006b4..025ea7a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "cli" version = "0.1.0" edition = "2021" +[[bin]] +name = "screenly" +path = "src/main.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] From 6aa25458028d82d229187862a67d67bee8670475 Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Fri, 28 Oct 2022 13:19:55 +0400 Subject: [PATCH 4/5] cargo fmt --- src/authentication.rs | 7 ++----- src/commands.rs | 6 ------ src/main.rs | 3 +-- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/authentication.rs b/src/authentication.rs index f0f512cd..5126a5e8 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -1,5 +1,4 @@ - -use reqwest::{header}; +use reqwest::header; use std::{env, fs}; use thiserror::Error; @@ -91,9 +90,7 @@ mod tests { use crate::Authentication; use httpmock::{Method::GET, MockServer}; use simple_logger::SimpleLogger; - - - + use std::{env, fs}; use tempdir::TempDir; diff --git a/src/commands.rs b/src/commands.rs index b9c10930..384daf04 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,11 +1,5 @@ use crate::Authentication; - - - - - - pub struct ScreenCommand { authentication: Authentication, } diff --git a/src/main.rs b/src/main.rs index abbb6e42..9b84d2a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,9 @@ mod commands; use crate::authentication::{Authentication, AuthenticationError}; use clap::{command, Parser, Subcommand}; -use log::{warn}; +use log::warn; use simple_logger::SimpleLogger; - #[derive(Parser)] #[command(author, version, about, long_about = None)] #[command(propagate_version = true)] From ed765bca306dc3f8e93640a93ce49e4f7e13cc9b Mon Sep 17 00:00:00 2001 From: Sergey Borovkov Date: Fri, 28 Oct 2022 13:24:31 +0400 Subject: [PATCH 5/5] Ref. code review regarding setting env variables --- Cargo.toml | 1 + src/authentication.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 025ea7a9..f11b327d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.65" clap = { version = "4.0.17", features = ["derive"] } +envtestkit = "1.1.2" log = { version = "0.4.17", features = ["release_max_level_info", "max_level_trace"] } reqwest = { version = "0.11.12", features = ["json", "blocking"] } simple_logger = "3.0.0" diff --git a/src/authentication.rs b/src/authentication.rs index 5126a5e8..59e6c45e 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -86,14 +86,18 @@ impl Authentication { #[cfg(test)] mod tests { + use std::ffi::OsString; use crate::authentication::Config; use crate::Authentication; use httpmock::{Method::GET, MockServer}; use simple_logger::SimpleLogger; - use std::{env, fs}; + use std::fs; use tempdir::TempDir; + use envtestkit::lock::lock_test; + use envtestkit::set_env; + #[test] fn test_verify_and_store_token_when_token_is_valid() { SimpleLogger::new() @@ -101,7 +105,8 @@ mod tests { .init() .unwrap(); let tmp_dir = TempDir::new("test").unwrap(); - env::set_var("HOME", tmp_dir.path().to_str().unwrap()); + let _lock = lock_test(); + let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); let mock_server = MockServer::start(); mock_server.mock(|when, then| { when.method(GET) @@ -124,7 +129,8 @@ mod tests { #[test] fn test_verify_and_store_token_when_token_is_invalid() { let tmp_dir = TempDir::new("invalid").unwrap(); - env::set_var("HOME", tmp_dir.path().to_str().unwrap()); + let _lock = lock_test(); + let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); let mock_server = MockServer::start(); mock_server.mock(|when, then| { when.method(GET)