From 19f5f987f870d5791a0a9d86575494ea4e30b37f Mon Sep 17 00:00:00 2001 From: Safronov Pavel Date: Tue, 29 Sep 2026 14:10:31 +0530 Subject: [PATCH 1/3] Move screen add, delete and the token check off v3 The v3 API is being retired with the legacy app server, and these were the last three calls the CLI made to it. Adding a screen now posts to v4.1, which answers with the new id alone, so the screen is read back to keep the command's output the same. Deleting filters on the id the way the other v4 calls do. Verifying a token asked v3 for a group that does not exist and took the 404 as proof the token was good. It now reads a single screen and takes 200 as proof, which also means an invalid token is rejected rather than reported as valid. --- src/authentication.rs | 22 ++++++++++----------- src/commands/screen.rs | 43 +++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/authentication.rs b/src/authentication.rs index ae14e673..c204fdfb 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -129,8 +129,7 @@ pub fn verify_and_store_token( } fn verify_token(token: &str, api_url: &str) -> anyhow::Result<(), AuthenticationError> { - // Using uuid of non existing playlist. If we get 404 it means we authenticated successfully. - let url = format!("{api_url}/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/"); + let url = format!("{api_url}/v4/screens?limit=1"); let secret = format!("Token {token}"); let client = reqwest::blocking::Client::builder().build()?; @@ -140,8 +139,8 @@ fn verify_token(token: &str, api_url: &str) -> anyhow::Result<(), Authentication .send()?; match res.status() { + StatusCode::OK => Ok(()), StatusCode::UNAUTHORIZED => Err(AuthenticationError::WrongCredentials), - StatusCode::NOT_FOUND => Ok(()), _ => Err(AuthenticationError::Unknown), } } @@ -173,9 +172,9 @@ mod tests { let mock_server = MockServer::start(); mock_server.mock(|when, then| { when.method(GET) - .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/") - .header("Authorization", "Token token"); - then.status(404); + .path("/v4/screens") + .header("Authorization", "Token correct_token"); + then.status(200).body("[]"); }); let config = Config::new(mock_server.base_url()); @@ -196,8 +195,7 @@ mod tests { let mock_server = MockServer::start(); mock_server.mock(|when, then| { - when.method(GET) - .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/"); + when.method(GET).path("/v4/screens"); then.status(401); }); @@ -248,11 +246,11 @@ mod tests { let _test = set_env(OsString::from("HOME"), tmp_dir.path().to_str().unwrap()); let mock_server = MockServer::start(); - let group_call_mock = mock_server.mock(|when, then| { + let screens_call_mock = mock_server.mock(|when, then| { when.method(GET) - .path("/v3/groups/11CF9Z3GZR0005XXKH00F8V20R/") + .path("/v4/screens") .header("Authorization", "Token correct_token"); - then.status(404); + then.status(200).body("[]"); }); let config = Config::new(mock_server.base_url()); @@ -261,7 +259,7 @@ mod tests { let path = tmp_dir.path().join(".screenly"); assert!(path.exists()); let contents = fs::read_to_string(path).unwrap(); - group_call_mock.assert(); + screens_call_mock.assert(); assert!(contents.eq("correct_token")); } } diff --git a/src/commands/screen.rs b/src/commands/screen.rs index 15f32c2c..5f770fbc 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -1,7 +1,5 @@ use std::collections::HashMap; -use reqwest::StatusCode; - use crate::authentication::Authentication; use crate::commands; use crate::commands::{CommandError, Screens}; @@ -36,32 +34,22 @@ impl ScreenCommand { pin: &str, maybe_name: Option, ) -> anyhow::Result { - let url = format!("{}/v3/screens/", &self.authentication.config.url); let mut payload = HashMap::new(); payload.insert("pin".to_string(), pin.to_string()); if let Some(name) = maybe_name { payload.insert("name".to_string(), name); } - let response = self - .authentication - .build_client()? - .post(url) - .json(&payload) - .send()?; - if response.status() != StatusCode::CREATED { - return Err(CommandError::WrongResponseStatus( - response.status().as_u16(), - )); - } - // Our newer endpoints all return arrays so let's just convert the output from v3 to be the same - let mut array: Vec = Vec::new(); - array.insert(0, serde_json::from_str(&response.text()?)?); - Ok(Screens::new(serde_json::Value::Array(array))) + // Creating a screen answers with the new id alone, so the screen is read back to + // return the same shape as the other screen commands. + let created = commands::post(&self.authentication, "v4.1/screens", &payload)?; + let id = created["id"].as_str().ok_or(CommandError::MissingField)?; + + self.get(id) } pub fn delete(&self, id: &str) -> anyhow::Result<(), CommandError> { - let endpoint = format!("v3/screens/{id}/"); + let endpoint = format!("v4.1/screens?id=eq.{id}"); commands::delete(&self.authentication, &endpoint) } } @@ -108,7 +96,7 @@ mod tests { let mock_server = MockServer::start(); let post_mock = mock_server.mock(|when, then| { when.method(POST) - .path("/v3/screens/") + .path("/v4.1/screens") .header("Authorization", "Token token") .header("content-type", "application/json") .header( @@ -116,7 +104,16 @@ mod tests { format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), ) .json_body(json!({"pin": "test-pin", "name": "test"})); - then.status(201).json_body(new_screen.clone()); + then.status(200) + .json_body(json!({"id": "017a5104-524b-33d8-8026-9087b59e7eb5"})); + }); + + let get_mock = mock_server.mock(|when, then| { + when.method(GET) + .path("/v4/screens") + .query_param("id", "eq.017a5104-524b-33d8-8026-9087b59e7eb5") + .header("Authorization", "Token token"); + then.status(200).json_body(json!([new_screen.clone()])); }); let config = Config::new(mock_server.base_url()); @@ -124,6 +121,7 @@ mod tests { let screen_command = ScreenCommand::new(authentication); let v = screen_command.add("test-pin", Some("test".to_string())); post_mock.assert(); + get_mock.assert(); assert!(v.is_ok()); assert_eq!(v.unwrap().value.as_array().unwrap()[0], new_screen); } @@ -157,7 +155,8 @@ mod tests { let mock_server = MockServer::start(); mock_server.mock(|when, then| { when.method(DELETE) - .path("/v3/screens/test-id/") + .path("/v4.1/screens") + .query_param("id", "eq.test-id") .header( "user-agent", format!("screenly-cli {}", env!("CARGO_PKG_VERSION")), From 8d42cf7d9ebc67325e2d2708441180b23c54938f Mon Sep 17 00:00:00 2001 From: Safronov Pavel Date: Tue, 29 Sep 2026 14:15:15 +0530 Subject: [PATCH 2/3] Use the repo's naming for the create result, a ULID fixture, and drop a redundant comment --- src/commands/screen.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commands/screen.rs b/src/commands/screen.rs index 5f770fbc..29f5d7b1 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -40,10 +40,8 @@ impl ScreenCommand { payload.insert("name".to_string(), name); } - // Creating a screen answers with the new id alone, so the screen is read back to - // return the same shape as the other screen commands. - let created = commands::post(&self.authentication, "v4.1/screens", &payload)?; - let id = created["id"].as_str().ok_or(CommandError::MissingField)?; + let result = commands::post(&self.authentication, "v4.1/screens", &payload)?; + let id = result["id"].as_str().ok_or(CommandError::MissingField)?; self.get(id) } @@ -92,7 +90,7 @@ mod tests { #[test] fn test_add_screen_should_send_correct_request() { - 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 new_screen = serde_json::from_str::("{\"id\":\"01HBDXC6ZRGC2PKDZC7X5ZFGE0\",\"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(); let post_mock = mock_server.mock(|when, then| { when.method(POST) @@ -105,13 +103,13 @@ mod tests { ) .json_body(json!({"pin": "test-pin", "name": "test"})); then.status(200) - .json_body(json!({"id": "017a5104-524b-33d8-8026-9087b59e7eb5"})); + .json_body(json!({"id": "01HBDXC6ZRGC2PKDZC7X5ZFGE0"})); }); let get_mock = mock_server.mock(|when, then| { when.method(GET) .path("/v4/screens") - .query_param("id", "eq.017a5104-524b-33d8-8026-9087b59e7eb5") + .query_param("id", "eq.01HBDXC6ZRGC2PKDZC7X5ZFGE0") .header("Authorization", "Token token"); then.status(200).json_body(json!([new_screen.clone()])); }); From 9769931ca40d5d8d5e82eada9104d8ae070f22cc Mon Sep 17 00:00:00 2001 From: Safronov Pavel Date: Tue, 29 Sep 2026 14:16:00 +0530 Subject: [PATCH 3/3] Name the create result created --- src/commands/screen.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/screen.rs b/src/commands/screen.rs index 29f5d7b1..b8361fc4 100644 --- a/src/commands/screen.rs +++ b/src/commands/screen.rs @@ -40,8 +40,8 @@ impl ScreenCommand { payload.insert("name".to_string(), name); } - let result = commands::post(&self.authentication, "v4.1/screens", &payload)?; - let id = result["id"].as_str().ok_or(CommandError::MissingField)?; + let created = commands::post(&self.authentication, "v4.1/screens", &payload)?; + let id = created["id"].as_str().ok_or(CommandError::MissingField)?; self.get(id) }