Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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"] }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"
simple_logger = "3.0.0"
tempdir = "0.3.7"
thiserror = "1.0.37"
Expand Down
45 changes: 38 additions & 7 deletions src/authentication.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
use reqwest::header;
use std::{env, fs};

use reqwest::header::{HeaderMap, InvalidHeaderValue};
use thiserror::Error;

const API_BASE_URL: &str = "https://api.screenlyapp.com/api";

pub struct Config {
url: String,
pub url: String,
}

#[derive(Error, Debug)]
pub enum AuthenticationError {
#[error("wrong credentials error")]
WrongCredentialsError,
#[error("no credentials error")]
NoCredentialsError,
#[error("request error")]
RequestError(#[from] reqwest::Error),
#[error("i/o error")]
IoError(#[from] std::io::Error),
#[error("env error")]
EnvError(#[from] env::VarError),
#[error("invalid header error")]
InvalidHeaderError(#[from] InvalidHeaderValue),
#[error("unknown error")]
Unknown,
}

pub struct Authentication {
config: Config,
pub config: Config,
}

impl Config {
Expand All @@ -48,6 +54,15 @@ impl Authentication {
}
}

fn read_token() -> Result<String, AuthenticationError> {
match env::var("HOME") {
Ok(path) => {
std::fs::read_to_string(path + "/.screenly").map_err(AuthenticationError::IoError)
}
Err(_) => Err(AuthenticationError::NoCredentialsError),
}
}

#[cfg(test)]
pub fn new_with_config(config: Config) -> Self {
Self { config }
Expand All @@ -68,29 +83,44 @@ impl Authentication {
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 secret = format!("Token {}", token);
let client = reqwest::blocking::Client::builder().build()?;

let res = client
.get(url)
.header(header::AUTHORIZATION, &secret)
.send()?;

return match res.status().as_u16() {
match res.status().as_u16() {
401 => Err(AuthenticationError::WrongCredentialsError),
404 => Ok(()),
_ => Err(AuthenticationError::Unknown),
};
}
}

pub fn build_client(&self) -> Result<reqwest::blocking::Client, AuthenticationError> {
let token = Authentication::read_token()?;
let secret = format!("Token {}", token.as_str());
let mut default_headers = HeaderMap::new();
default_headers.insert(header::AUTHORIZATION, secret.parse()?);
default_headers.insert(
header::USER_AGENT,
"screenly-cli 0.1.0".to_string().parse()?,
);
reqwest::blocking::Client::builder()
.default_headers(default_headers)
.build()
.map_err(AuthenticationError::RequestError)
}
}

#[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::ffi::OsString;

use std::fs;
use tempdir::TempDir;
Expand All @@ -110,7 +140,8 @@ mod tests {
let mock_server = MockServer::start();
mock_server.mock(|when, then| {
when.method(GET)
.path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/");
.path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/")
.header("Authorization", "Token token");
then.status(404);
});

Expand Down
194 changes: 189 additions & 5 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,199 @@
use crate::Authentication;
use crate::{Authentication, AuthenticationError};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use serde_json::Value;

#[derive(Error, Debug)]
pub enum CommandError {
#[error("auth error")]
AuthenticationError(#[from] AuthenticationError),
#[error("request error")]
RequestError(#[from] reqwest::Error),
#[error("parse error")]
ParseError(#[from] serde_json::Error),
#[error("unknown error #[0]")]
WrongResponseStatus(u16),
}
// TODO: Use in the future no need to deserialize for current features
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct ScreenConfig {}
// TODO: Use in the future no need to deserialize for current features
#[derive(Deserialize, Serialize, Debug, Default)]
pub struct Screen {
Comment thread
sergey-borovkov marked this conversation as resolved.
Comment thread
sergey-borovkov marked this conversation as resolved.
#[serde(default)]
pub id: String,
#[serde(default)]
pub team_id: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub is_enabled: bool,
#[serde(default)]
pub last_ping: Option<String>,
#[serde(default)]
pub last_ip: Option<String>,
#[serde(default)]
pub local_ip: Option<String>,
#[serde(default)]
pub mac: Option<String>,
#[serde(default)]
pub last_screenshot_time: Option<String>,
#[serde(default)]
pub uptime: Option<String>,
#[serde(default)]
pub load_avg: Option<String>,
#[serde(default)]
pub signal_strength: Option<i32>,
#[serde(default)]
pub interface: Option<String>,
#[serde(default)]
pub debug: bool,
#[serde(default)]
pub location: Option<String>,
#[serde(default)]
pub team: Option<String>,
#[serde(default)]
pub timezone: Option<String>,
#[serde(default)]
pub hostname: Option<String>,
#[serde(default)]
pub ws_open: bool,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub last_screenshot: Option<String>,
#[serde(default)]
pub in_sync: bool,
#[serde(default)]
pub software_version: Option<String>,
#[serde(default)]
pub hardware_version: Option<String>,
#[serde(default)]
pub config: ScreenConfig,
}
pub struct ScreenCommand {
authentication: Authentication,
}

impl ScreenCommand {
fn new(authentication: Authentication) -> Self {
pub fn new(authentication: Authentication) -> Self {
Self { authentication }
}
pub fn list(&self) -> anyhow::Result<Value, CommandError> {
Comment thread
sergey-borovkov marked this conversation as resolved.
let url = self.authentication.config.url.clone() + "/v4/screens";
let response = self.authentication.build_client()?.get(url).send()?;
if response.status().as_u16() != 200 {
return Err(CommandError::WrongResponseStatus(
response.status().as_u16(),
));
}
serde_json::from_str(&response.text().unwrap_or_else(|_| "{}".to_owned()))
.map_err(CommandError::ParseError)
}
pub fn get(&self, id: &str) -> anyhow::Result<Value, CommandError> {
let url = self.authentication.config.url.clone() + "/v4/screens?id=eq." + id;
let response = self.authentication.build_client()?.get(url).send()?;
if response.status().as_u16() != 200 {
return Err(CommandError::WrongResponseStatus(
response.status().as_u16(),
));
}

serde_json::from_str(&response.text().unwrap_or_else(|_| "{}".to_owned()))
Comment thread
sergey-borovkov marked this conversation as resolved.
.map_err(CommandError::ParseError)
}
}

#[cfg(test)]
mod tests {
use crate::authentication::Config;
use crate::Authentication;
use httpmock::{Method::GET, MockServer};

use envtestkit::lock::lock_test;
use envtestkit::set_env;
use std::ffi::OsString;
use std::fs;

use crate::commands::ScreenCommand;
use serde_json::Value;
use tempdir::TempDir;

#[test]
fn test_list_screens_should_return_correct_screen_list() {
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();
Comment thread
sergey-borovkov marked this conversation as resolved.
let mock_server = MockServer::start();
mock_server.mock(|when, then| {
when.method(GET)
.path("/v4/screens")
.header("Authorization", "Token token")
.header("user-agent", "screenly-cli 0.1.0");
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}}]");
});

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::<Value>("[{\"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, expected);
}

fn list() {}
#[test]
fn test_list_screens_when_not_authenticated_should_return_error() {
let tmp_dir = TempDir::new("test").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)
.path("/v4/screens")
.header("Authorization", "Token token")
.header("user-agent", "screenly-cli 0.1.0");
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}}]");
});

fn get(_id: &str) {}
let config = Config::new(mock_server.base_url());
let authentication = Authentication::new_with_config(config);
let screen_command = ScreenCommand::new(authentication);
let v = screen_command.list();
assert!(v.is_err());
}

#[test]
fn test_get_screen_should_return_screen() {
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(GET)
.path("/v4/screens")
.query_param("id", "eq.017a5104-524b-33d8-8026-9087b59e7eb5")
.header("user-agent", "screenly-cli 0.1.0")
.header("Authorization", "Token token");
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}}]");
});

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::<Value>("[{\"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
.get("017a5104-524b-33d8-8026-9087b59e7eb5")
.unwrap();
assert_eq!(v, expected);
}
}
Loading