// 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); } }