diff --git a/README.md b/README.md index 017db27..c3a5d95 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,16 @@ status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100% status ⏸ Dire Straits - Tunnel of Love [6% 14:23] 💾 24% 💻 1% 🔌 100% 📶 192.168.1.93 ☀️ 83°F 📅 2025 Jun 07 🕑 09:25PM -07:00 ^C ``` + +# Unit testing + +Use the following command to run unit tests: + +```shell +$ cargo test --package unibar --bin unibar +``` +To run a specific test, use the following: + +```shell +$ cargo test --package unibar --bin unibar -- bar_modules::bar_module_weather::tests::test_get_unicode_symbol_default --exact --show-output +``` diff --git a/src/bar_modules/bar_module_music.rs b/src/bar_modules/bar_module_music.rs index 205ba13..afe8881 100644 --- a/src/bar_modules/bar_module_music.rs +++ b/src/bar_modules/bar_module_music.rs @@ -135,3 +135,7 @@ impl bar_modules::BarModuleDebug for UnibarModuleMusic { fn post_debug(&self) { } } + +#[cfg(test)] +#[path = "../../unit_tests/bar_modules_music.test.rs"] +mod tests; diff --git a/src/bar_modules/bar_module_weather.rs b/src/bar_modules/bar_module_weather.rs index 6bc6efc..5d6fb62 100644 --- a/src/bar_modules/bar_module_weather.rs +++ b/src/bar_modules/bar_module_weather.rs @@ -345,401 +345,5 @@ impl bar_modules::BarModuleDebug for UnibarModuleWeather { } #[cfg(test)] -mod tests { - use super::*; - use crate::bar_modules::BarModuleActions; // Add this line - use crate::common::AppOptions; - use crate::common::TemperatureUnits; - - /// Creates a default AppOptions instance for testing - fn create_test_options(metric: bool) -> AppOptions { - AppOptions { - interval: 1, - weather_units: if metric { TemperatureUnits::Metric } else { TemperatureUnits::Imperial }, - weather_station: "test_station".to_string(), - music_progress: false, - debug: false, - debug_modules: false, - debug_update: false, - } - } - - #[test] - /// Test get_current_temperature returns correct value for metric and imperial units - fn test_get_current_temperature_units() { - let mut metric_module = UnibarModuleWeather::new(create_test_options(true)); - let mut imperial_module = UnibarModuleWeather::new(create_test_options(false)); - - metric_module.weather_info = json!({ - "features": [{ - "properties": { - "temperature": { - "value": 25.0 - } - } - }] - }); - imperial_module.weather_info = metric_module.weather_info.clone(); - - assert_eq!(metric_module.get_current_temperature().unwrap(), 25.0); - assert!((imperial_module.get_current_temperature().unwrap() - 77.0).abs() < 0.01); - } - - #[test] - /// Test get_unicode_symbol returns default for unknown condition - fn test_get_unicode_symbol_default() { - let module = UnibarModuleWeather::new(create_test_options(true)); - assert_eq!(module.get_unicode_symbol("not_a_real_condition"), "🌤️"); - } - - #[test] - /// Test clear resets weather_info to Null - fn test_clear_resets_weather_info() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - module.weather_info = json!({"features": []}); - module.clear(); - assert_eq!(module.weather_info, json!(serde_json::Value::Null)); - } - - #[test] - /// Test get_content returns formatted temperature string - fn test_get_content_format() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - module.weather_info = json!({ - "features": [{ - "properties": { - "temperature": { - "value": 18.7 - } - } - }] - }); - let content = module.get_content(); - assert!(content.starts_with("19°C")); - } - - #[test] - /// Test get_icon returns fallback when icon is missing - fn test_get_icon_missing_icon() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - module.weather_info = json!({ - "features": [{ - "properties": { - "icon": serde_json::Value::Null - } - }] - }); - assert_eq!(module.get_icon(), "⁈"); - } - - #[test] - /// Test get_icon returns fallback when weather_info is Null - fn test_get_icon_null_weather_info() { - let module = UnibarModuleWeather::new(create_test_options(true)); - assert_eq!(module.get_icon(), "⁈"); - } - - #[test] - /// Test weather module initialization - fn test_module_initialization() { - let module = UnibarModuleWeather::new(create_test_options(true)); - assert_eq!(module.update_cnt, 0); - assert_eq!(module.weather_info, json!(serde_json::Value::Null)); - } - - #[test] - /// Test temperature unit display - fn test_temperature_unit_display() { - let metric_module = UnibarModuleWeather::new(create_test_options(true)); - let imperial_module = UnibarModuleWeather::new(create_test_options(false)); - - assert_eq!(metric_module.get_temperature_unit(), "°C"); - assert_eq!(imperial_module.get_temperature_unit(), "°F"); - } - - #[test] - /// Test weather condition to Unicode symbol mapping - fn test_unicode_symbol_mapping() { - let module = UnibarModuleWeather::new(create_test_options(true)); - - assert_eq!(module.get_unicode_symbol("snow"), "❄️"); - assert_eq!(module.get_unicode_symbol("ovc"), "☁️"); - assert_eq!(module.get_unicode_symbol("rain"), "🌧️"); - assert_eq!(module.get_unicode_symbol("unknown"), "🌤️"); - } - - #[test] - /// Test error handling for invalid temperature data - fn test_error_handling() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - // Test null weather info - assert!(module.get_current_temperature().is_ok()); - assert_eq!(module.get_current_temperature().unwrap(), 0.0); - - // Test invalid JSON structure - module.weather_info = json!({ - "features": [{ - "properties": { - "temperature": { - "value": "invalid" - } - } - }] - }); - assert!(module.get_current_temperature().is_err()); - } - - #[test] - /// Test update counter behavior. Update only when update_cnt==0 - fn test_update_counter() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - assert_eq!(module.update_cnt, 0); - assert_eq!(module.should_update(), UpdateResult::Update); - - module.update_cnt = UPDATE_INTERVAL - 1; - assert_eq!(module.should_update(), UpdateResult::Skip); - - module.update_cnt = UPDATE_INTERVAL; - assert_eq!(module.should_update(), UpdateResult::Skip); - } - - #[test] - /// Test JSON parsing with valid weather data - fn test_valid_weather_data_parsing() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - // Simulate valid weather data - module.weather_info = json!({ - "features": [{ - "properties": { - "temperature": { - "value": 20.5 - } - } - }] - }); - - // println!("{:#?}", module.weather_info); - let result = module.get_current_temperature(); - // println!("{:#?}", result); - match result { - Ok(20.5) => { - // This is fine - we expect 20.5 for valid weather_info - assert_eq!(module.weather_info["features"][0]["properties"]["temperature"]["value"], 20.5); - }, - Err(WeatherError::ApiResponseStrErr(_)) => { - assert!(false, "Expected valid temperature but got ApiResponseStrErr"); - }, - other => { - panic!("Unexpected result: {:?}", other); - } - } - } - - #[test] - /// Test icon extraction from weather API URL - fn test_icon_extraction() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - // Simulate weather data with icon URL - module.weather_info = json!({ - "features": [{ - "properties": { - "icon": "https://api.weather.gov/icons/land/night/ovc?size=medium" - } - }] - }); - let icon = module.get_icon(); - // println!("Extracted icon: {}", icon); - // Check if the icon is correctly extracted - assert_eq!(icon, "☁️"); - } - - #[test] - /// Test handling of malformed/incomplete JSON structures - fn test_missing_json_structure() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - // Test cases with increasingly incomplete JSON structures - let test_cases = vec![ - // Missing value field - json!({ - "features": [{ - "properties": { - "temperature": {} - } - }] - }), - // Missing temperature object - json!({ - "features": [{ - "properties": {} - }] - }), - // Missing properties - json!({ - "features": [{}] - }), - // Empty features array - json!({ - "features": [] - }), - // Missing features - json!({}), - // Completely different structure - json!({ - "unexpected": "structure" - }), - // Empty object - json!({}), - // Null value - json!(null), - // Array instead of object - json!([1, 2, 3]), - // Wrong data types - json!({ - "features": [{ - "properties": { - "temperature": { - "value": true // boolean instead of number - } - } - }] - }), - ]; - - for (i, test_case) in test_cases.into_iter().enumerate() { - module.weather_info = test_case; - let result = module.get_current_temperature(); - - match result { - Ok(0.0) => { - // This is fine - we expect 0.0 for null weather_info - assert_eq!(module.weather_info, json!(null), - "Case {}: Should return 0.0 only for null weather_info", i); - }, - Err(WeatherError::ApiResponseStrErr(_)) => { - // This is also fine - we expect ApiResponseStrErr for malformed data - }, - other => { - panic!("Case {}: Unexpected result: {:?}", i, other); - } - } - } - } - - #[test] - /// Test handling of malformed temperature values - fn test_invalid_temperature_values() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - let test_cases = vec![ - // Invalid temperature values - json!({ - "features": [{ - "properties": { - "temperature": { - "value": "not a number" - } - } - }] - }), - json!({ - "features": [{ - "properties": { - "temperature": { - "value": "" - } - } - }] - }), - json!({ - "features": [{ - "properties": { - "temperature": { - "value": null - } - } - }] - }), - json!({ - "features": [{ - "properties": { - "temperature": { - "value": {} - } - } - }] - }), - json!({ - "features": [{ - "properties": { - "temperature": { - "value": [] - } - } - }] - }) - ]; - - for (i, test_case) in test_cases.into_iter().enumerate() { - module.weather_info = test_case; - let result = module.get_current_temperature(); - - assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))), - "Case {}: Expected ApiResponseStrErr for invalid temperature value", i); - } - } - - #[test] - /// Test that deep nested access is safe - fn test_deep_nested_access() { - let mut module = UnibarModuleWeather::new(create_test_options(true)); - - // Test accessing nested fields that don't exist - let test_cases = vec![ - // Deeper nesting than expected - json!({ - "features": [{ - "properties": { - "temperature": { - "value": { - "nested": "too deep" - } - } - } - }] - }), - // Missing intermediate fields - json!({ - "features": [{ - "properties": { - "temperature": { - "missing_value": 20.5 - } - } - }] - }), - // Wrong nesting structure - json!({ - "features": { // Object instead of array - "properties": { - "temperature": { - "value": 20.5 - } - } - } - }) - ]; - - for (i, test_case) in test_cases.into_iter().enumerate() { - module.weather_info = test_case; - let result = module.get_current_temperature(); - - assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))), - "Case {}: Expected ApiResponseStrErr for invalid nested structure", i); - } - } -} +#[path = "../../unit_tests/bar_modules_weather.test.rs"] +mod tests; diff --git a/unit_tests/bar_modules_music.test.rs b/unit_tests/bar_modules_music.test.rs new file mode 100644 index 0000000..7eece6e --- /dev/null +++ b/unit_tests/bar_modules_music.test.rs @@ -0,0 +1,212 @@ +// MIT License +// Copyright (c) 2025 Mahesh @ HeshApps +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! Unit tests for Music module for Unibar + +use super::*; +use std::process::Command; +use crate::bar_modules::BarModuleActions; + +/// Helper function to create test options +fn create_test_options(with_progress: bool) -> common::AppOptions { + common::AppOptions { + interval: 1, + weather_units: common::TemperatureUnits::Metric, + weather_station: "test".to_string(), + music_progress: with_progress, + debug: false, + debug_modules: false, + debug_update: false, + } +} + +/// Test that module handles missing mpc command gracefully. +/// Note: This needs ROOT permissions, hence excluding from normal +/// testing +#[test] +#[ignore] +fn test_missing_mpc_command() { + let mut module = UnibarModuleMusic::new(create_test_options(true)); + + // Temporarily rename mpc command if it exists + let has_mpc = Command::new("which") + .arg("mpc") + .output() + .map(|output| output.status.success()) + .unwrap_or(false); + + let original_mpc_path = "/usr/bin/mpc"; + let backup_mpc_path = "/tmp/mpc.bak"; + + if has_mpc { + // Create a backup + std::fs::copy(original_mpc_path, backup_mpc_path).expect("Failed to backup mpc"); + // Rename the original + std::fs::rename(original_mpc_path, "/tmp/mpc_renamed").expect("Failed to rename mpc"); + } + + // Generate data should not panic + module.generate_data(); + + // Should show appropriate error state + assert_eq!(module.get_icon(), "⏹"); + assert!(module.get_content().contains("No player")); + + // Restore mpc if it existed + if has_mpc { + // Remove the renamed file + std::fs::remove_file("/tmp/mpc_renamed").expect("Failed to remove renamed mpc"); + // Restore from backup + std::fs::rename(backup_mpc_path, original_mpc_path).expect("Failed to restore mpc"); + } +} + +/// Test handling of mpc command failing +#[test] +fn test_mpc_command_failure() { + let mut module = UnibarModuleMusic::new(create_test_options(true)); + + // Mock mpc command to always fail + module.current_stdout = "Error: Command not found".to_string(); + module.progress_stdout = "".to_string(); + module.state_stdout = "stopped".to_string(); + + // Generate data should not panic + // module.generate_data(); + + // Should show appropriate error state + assert_eq!(module.get_icon(), "⏹"); + assert!(module.get_content().contains("Command not found")); +} + +/// Test handling of various mpc output formats +#[test] +fn test_unexpected_mpc_output() { + let mut module = UnibarModuleMusic::new(create_test_options(true)); + + // Test cases for current_stdout + let test_cases = vec![ + "", // Empty output + "\n", // Just newline + //"invalid utf8 \x80", // Invalid UTF-8 + "very\nlong\noutput\n", // Multiple lines + " ", // Just whitespace + "🎵 unicode 🎶", // Unicode characters + ]; + + for test_case in test_cases { + module.current_stdout = test_case.to_string(); + // get_content should not panic + let content = module.get_content(); + assert!(!content.is_empty(), "Content should not be empty"); + } +} + +/// Test state handling and icon selection +#[test] +fn test_player_states() { + let mut module = UnibarModuleMusic::new(create_test_options(false)); + + // Test valid states + let states = vec![ + ("playing", "𝄞"), + ("paused", "⏸"), + ("stopped", "⏹"), + ]; + + for (state, expected_icon) in states { + module.state_stdout = state.to_string(); + assert_eq!(module.get_icon(), expected_icon); + } + + // Test invalid states + let invalid_states = vec![ + "", + "invalid", + "PLAYING", + "unknown", + "\n", + "playing\n", + ]; + + for state in invalid_states { + module.state_stdout = state.to_string(); + assert_eq!(module.get_icon(), "𝄞", "Invalid state should default to playing icon"); + } +} + +/// Test progress display formatting +#[test] +fn test_progress_display() { + let mut module = UnibarModuleMusic::new(create_test_options(true)); + + // Test various progress formats + let test_cases = vec![ + ("50% 3:30", "[50% 3:30]"), + ("", "[]"), + ("invalid", "[invalid]"), + ("100% 0:00", "[100% 0:00]"), + (" 50% 3:30 ", "[50% 3:30]"), // Extra whitespace + ]; + + for (progress, expected) in test_cases { + module.progress_stdout = progress.to_string(); + module.current_stdout = "Test Song".to_string(); + let content = module.get_content(); + assert!(content.contains(expected)); + } + + // Test with progress disabled + let mut module = UnibarModuleMusic::new(create_test_options(false)); + module.progress_stdout = "50% 3:30".to_string(); + module.current_stdout = "Test Song".to_string(); + assert_eq!(module.get_content(), "Test Song"); +} + +/// Test module initialization +#[test] +fn test_module_initialization() { + let module = UnibarModuleMusic::new(create_test_options(true)); + + assert_eq!(module.current_stdout, ""); + assert_eq!(module.progress_stdout, ""); + assert_eq!(module.state_stdout, "stopped"); + assert_eq!(module.get_icon(), "⏹"); +} + +/// Test clear functionality +#[test] +fn test_clear() { + let mut module = UnibarModuleMusic::new(create_test_options(true)); + + // Set some data + module.current_stdout = "Test Song".to_string(); + module.progress_stdout = "50% 3:30".to_string(); + module.state_stdout = "playing".to_string(); + + // Clear the module + module.clear(); + + // Verify cleared state + assert_eq!(module.current_stdout, ""); + assert_eq!(module.progress_stdout, ""); + assert_eq!(module.state_stdout, "stopped"); +} diff --git a/unit_tests/bar_modules_weather.test.rs b/unit_tests/bar_modules_weather.test.rs new file mode 100644 index 0000000..651295d --- /dev/null +++ b/unit_tests/bar_modules_weather.test.rs @@ -0,0 +1,418 @@ +// MIT License +// Copyright (c) 2025 Mahesh @ HeshApps +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! Unit tests for Weather module for Unibar +use super::*; +use crate::bar_modules::BarModuleActions; // Add this line +use crate::common::AppOptions; +use crate::common::TemperatureUnits; + +/// Creates a default AppOptions instance for testing +fn create_test_options(metric: bool) -> AppOptions { + AppOptions { + interval: 1, + weather_units: if metric { TemperatureUnits::Metric } else { TemperatureUnits::Imperial }, + weather_station: "test_station".to_string(), + music_progress: false, + debug: false, + debug_modules: false, + debug_update: false, + } +} + +#[test] +/// Test get_current_temperature returns correct value for metric and imperial units +fn test_get_current_temperature_units() { + let mut metric_module = UnibarModuleWeather::new(create_test_options(true)); + let mut imperial_module = UnibarModuleWeather::new(create_test_options(false)); + + metric_module.weather_info = json!({ + "features": [{ + "properties": { + "temperature": { + "value": 25.0 + } + } + }] + }); + imperial_module.weather_info = metric_module.weather_info.clone(); + + assert_eq!(metric_module.get_current_temperature().unwrap(), 25.0); + assert!((imperial_module.get_current_temperature().unwrap() - 77.0).abs() < 0.01); +} + +#[test] +/// Test get_unicode_symbol returns default for unknown condition +fn test_get_unicode_symbol_default() { + let module = UnibarModuleWeather::new(create_test_options(true)); + assert_eq!(module.get_unicode_symbol("not_a_real_condition"), "🌤️"); +} + +#[test] +/// Test clear resets weather_info to Null +fn test_clear_resets_weather_info() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + module.weather_info = json!({"features": []}); + module.clear(); + assert_eq!(module.weather_info, json!(serde_json::Value::Null)); +} + +#[test] +/// Test get_content returns formatted temperature string +fn test_get_content_format() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + module.weather_info = json!({ + "features": [{ + "properties": { + "temperature": { + "value": 18.7 + } + } + }] + }); + let content = module.get_content(); + assert!(content.starts_with("19°C")); +} + +#[test] +/// Test get_icon returns fallback when icon is missing +fn test_get_icon_missing_icon() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + module.weather_info = json!({ + "features": [{ + "properties": { + "icon": serde_json::Value::Null + } + }] + }); + assert_eq!(module.get_icon(), "⁈"); +} + +#[test] +/// Test get_icon returns fallback when weather_info is Null +fn test_get_icon_null_weather_info() { + let module = UnibarModuleWeather::new(create_test_options(true)); + assert_eq!(module.get_icon(), "⁈"); +} + +#[test] +/// Test weather module initialization +fn test_module_initialization() { + let module = UnibarModuleWeather::new(create_test_options(true)); + assert_eq!(module.update_cnt, 0); + assert_eq!(module.weather_info, json!(serde_json::Value::Null)); +} + +#[test] +/// Test temperature unit display +fn test_temperature_unit_display() { + let metric_module = UnibarModuleWeather::new(create_test_options(true)); + let imperial_module = UnibarModuleWeather::new(create_test_options(false)); + + assert_eq!(metric_module.get_temperature_unit(), "°C"); + assert_eq!(imperial_module.get_temperature_unit(), "°F"); +} + +#[test] +/// Test weather condition to Unicode symbol mapping +fn test_unicode_symbol_mapping() { + let module = UnibarModuleWeather::new(create_test_options(true)); + + assert_eq!(module.get_unicode_symbol("snow"), "❄️"); + assert_eq!(module.get_unicode_symbol("ovc"), "☁️"); + assert_eq!(module.get_unicode_symbol("rain"), "🌧️"); + assert_eq!(module.get_unicode_symbol("unknown"), "🌤️"); +} + +#[test] +/// Test error handling for invalid temperature data +fn test_error_handling() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + // Test null weather info + assert!(module.get_current_temperature().is_ok()); + assert_eq!(module.get_current_temperature().unwrap(), 0.0); + + // Test invalid JSON structure + module.weather_info = json!({ + "features": [{ + "properties": { + "temperature": { + "value": "invalid" + } + } + }] + }); + assert!(module.get_current_temperature().is_err()); +} + +#[test] +/// Test update counter behavior. Update only when update_cnt==0 +fn test_update_counter() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + assert_eq!(module.update_cnt, 0); + assert_eq!(module.should_update(), UpdateResult::Update); + + module.update_cnt = UPDATE_INTERVAL - 1; + assert_eq!(module.should_update(), UpdateResult::Skip); + + module.update_cnt = UPDATE_INTERVAL; + assert_eq!(module.should_update(), UpdateResult::Skip); +} + +#[test] +/// Test JSON parsing with valid weather data +fn test_valid_weather_data_parsing() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + // Simulate valid weather data + module.weather_info = json!({ + "features": [{ + "properties": { + "temperature": { + "value": 20.5 + } + } + }] + }); + + // println!("{:#?}", module.weather_info); + let result = module.get_current_temperature(); + // println!("{:#?}", result); + match result { + Ok(20.5) => { + // This is fine - we expect 20.5 for valid weather_info + assert_eq!(module.weather_info["features"][0]["properties"]["temperature"]["value"], 20.5); + }, + Err(WeatherError::ApiResponseStrErr(_)) => { + assert!(false, "Expected valid temperature but got ApiResponseStrErr"); + }, + other => { + panic!("Unexpected result: {:?}", other); + } + } +} + +#[test] +/// Test icon extraction from weather API URL +fn test_icon_extraction() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + // Simulate weather data with icon URL + module.weather_info = json!({ + "features": [{ + "properties": { + "icon": "https://api.weather.gov/icons/land/night/ovc?size=medium" + } + }] + }); + let icon = module.get_icon(); + // println!("Extracted icon: {}", icon); + // Check if the icon is correctly extracted + assert_eq!(icon, "☁️"); +} + +#[test] +/// Test handling of malformed/incomplete JSON structures +fn test_missing_json_structure() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + // Test cases with increasingly incomplete JSON structures + let test_cases = vec![ + // Missing value field + json!({ + "features": [{ + "properties": { + "temperature": {} + } + }] + }), + // Missing temperature object + json!({ + "features": [{ + "properties": {} + }] + }), + // Missing properties + json!({ + "features": [{}] + }), + // Empty features array + json!({ + "features": [] + }), + // Missing features + json!({}), + // Completely different structure + json!({ + "unexpected": "structure" + }), + // Empty object + json!({}), + // Null value + json!(null), + // Array instead of object + json!([1, 2, 3]), + // Wrong data types + json!({ + "features": [{ + "properties": { + "temperature": { + "value": true // boolean instead of number + } + } + }] + }), + ]; + + for (i, test_case) in test_cases.into_iter().enumerate() { + module.weather_info = test_case; + let result = module.get_current_temperature(); + + match result { + Ok(0.0) => { + // This is fine - we expect 0.0 for null weather_info + assert_eq!(module.weather_info, json!(null), + "Case {}: Should return 0.0 only for null weather_info", i); + }, + Err(WeatherError::ApiResponseStrErr(_)) => { + // This is also fine - we expect ApiResponseStrErr for malformed data + }, + other => { + panic!("Case {}: Unexpected result: {:?}", i, other); + } + } + } +} + +#[test] +/// Test handling of malformed temperature values +fn test_invalid_temperature_values() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + let test_cases = vec![ + // Invalid temperature values + json!({ + "features": [{ + "properties": { + "temperature": { + "value": "not a number" + } + } + }] + }), + json!({ + "features": [{ + "properties": { + "temperature": { + "value": "" + } + } + }] + }), + json!({ + "features": [{ + "properties": { + "temperature": { + "value": null + } + } + }] + }), + json!({ + "features": [{ + "properties": { + "temperature": { + "value": {} + } + } + }] + }), + json!({ + "features": [{ + "properties": { + "temperature": { + "value": [] + } + } + }] + }) + ]; + + for (i, test_case) in test_cases.into_iter().enumerate() { + module.weather_info = test_case; + let result = module.get_current_temperature(); + + assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))), + "Case {}: Expected ApiResponseStrErr for invalid temperature value", i); + } +} + +#[test] +/// Test that deep nested access is safe +fn test_deep_nested_access() { + let mut module = UnibarModuleWeather::new(create_test_options(true)); + + // Test accessing nested fields that don't exist + let test_cases = vec![ + // Deeper nesting than expected + json!({ + "features": [{ + "properties": { + "temperature": { + "value": { + "nested": "too deep" + } + } + } + }] + }), + // Missing intermediate fields + json!({ + "features": [{ + "properties": { + "temperature": { + "missing_value": 20.5 + } + } + }] + }), + // Wrong nesting structure + json!({ + "features": { // Object instead of array + "properties": { + "temperature": { + "value": 20.5 + } + } + } + }) + ]; + + for (i, test_case) in test_cases.into_iter().enumerate() { + module.weather_info = test_case; + let result = module.get_current_temperature(); + + assert!(matches!(result, Err(WeatherError::ApiResponseStrErr(_))), + "Case {}: Expected ApiResponseStrErr for invalid nested structure", i); + } +}