use std::process::Command; use tauri::command; use crate::process_ext::HideWindow; /// Generate PowerShell script for Windows Toast Notification fn generate_powershell_toast_script(title: &str, body: &str) -> String { format!( r#" [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null $APP_ID = 'Hikari Desktop' $template = @" {} {} "@ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument $xml.LoadXml($template) $toast = New-Object Windows.UI.Notifications.ToastNotification $xml [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast) "#, title.replace("\"", "`\""), body.replace("\"", "`\"") ) } /// Format simple notification message fn format_simple_notification(title: &str, body: &str) -> String { format!("{}\n\n{}", title, body) } /// Build notify-send command for testing (doesn't execute) #[cfg(test)] fn build_notify_send_command(title: &str, body: &str) -> (String, Vec) { ( "notify-send".to_string(), vec![ title.to_string(), body.to_string(), "--urgency=normal".to_string(), "--app-name=Hikari Desktop".to_string(), ], ) } /// Build Windows PowerShell command for testing (doesn't execute) #[cfg(test)] fn build_windows_powershell_command(title: &str, body: &str) -> (String, Vec) { let script = generate_powershell_toast_script(title, body); ( "pwsh.exe".to_string(), vec![ "-NoProfile".to_string(), "-WindowStyle".to_string(), "Hidden".to_string(), "-Command".to_string(), script, ], ) } /// Build simple notification command for testing (doesn't execute) #[cfg(test)] fn build_simple_notification_command(title: &str, body: &str) -> (String, Vec) { let message = format_simple_notification(title, body); ( "cmd.exe".to_string(), vec!["/c".to_string(), "msg".to_string(), "*".to_string(), message], ) } #[command] pub async fn send_notify_send(title: String, body: String) -> Result<(), String> { // Use notify-send for Linux/WSL let output = Command::new("notify-send") .hide_window() .arg(&title) .arg(&body) .arg("--urgency=normal") .arg("--app-name=Hikari Desktop") .output() .map_err(|e| { format!( "Failed to execute notify-send: {}. Make sure libnotify-bin is installed.", e ) })?; if !output.status.success() { let error = String::from_utf8_lossy(&output.stderr); return Err(format!("notify-send failed: {}", error)); } Ok(()) } #[command] pub async fn send_windows_notification(title: String, body: String) -> Result<(), String> { // Create PowerShell script for Windows Toast Notification let ps_script = generate_powershell_toast_script(&title, &body); // Try PowerShell Core first (pwsh), then fall back to Windows PowerShell let output = Command::new("pwsh.exe") .hide_window() .arg("-NoProfile") .arg("-WindowStyle") .arg("Hidden") .arg("-Command") .arg(&ps_script) .output() .or_else(|_| { Command::new("powershell.exe") .hide_window() .arg("-NoProfile") .arg("-WindowStyle") .arg("Hidden") .arg("-Command") .arg(&ps_script) .output() }) .map_err(|e| format!("Failed to execute PowerShell: {}", e))?; if !output.status.success() { let error = String::from_utf8_lossy(&output.stderr); return Err(format!("PowerShell script failed: {}", error)); } Ok(()) } // Alternative: Use Windows built-in MSG command for simple notifications #[command] pub async fn send_simple_notification(title: String, body: String) -> Result<(), String> { let message = format_simple_notification(&title, &body); Command::new("cmd.exe") .hide_window() .arg("/c") .arg("msg") .arg("*") .arg(&message) .output() .map_err(|e| format!("Failed to send message: {}", e))?; Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_generate_powershell_toast_script_basic() { let script = generate_powershell_toast_script("Title", "Body"); assert!(script.contains("Hikari Desktop")); assert!(script.contains("Title")); assert!(script.contains("Body")); assert!(script.contains("ToastNotification")); } #[test] fn test_generate_powershell_toast_script_escapes_quotes() { let script = generate_powershell_toast_script("Title with \"quotes\"", "Body with \"quotes\""); // Quotes should be escaped as `" in PowerShell assert!(script.contains("Title with `\"quotes`\"")); assert!(script.contains("Body with `\"quotes`\"")); } #[test] fn test_generate_powershell_toast_script_with_special_chars() { let script = generate_powershell_toast_script("Title: Test", "Body\nwith\nnewlines"); assert!(script.contains("Title: Test")); assert!(script.contains("Body\nwith\nnewlines")); } #[test] fn test_generate_powershell_toast_script_unicode() { let script = generate_powershell_toast_script("ę—„ęœ¬čŖž Title", "Unicode: šŸŽ‰"); assert!(script.contains("ę—„ęœ¬čŖž Title")); assert!(script.contains("Unicode: šŸŽ‰")); } #[test] fn test_generate_powershell_toast_script_empty() { let script = generate_powershell_toast_script("", ""); // Should still contain the structure assert!(script.contains("Hikari Desktop")); assert!(script.contains("ToastNotification")); } #[test] fn test_format_simple_notification_basic() { let message = format_simple_notification("Title", "Body"); assert_eq!(message, "Title\n\nBody"); } #[test] fn test_format_simple_notification_with_newlines() { let message = format_simple_notification("Multi\nLine\nTitle", "Multi\nLine\nBody"); assert!(message.contains("Multi\nLine\nTitle")); assert!(message.contains("\n\n")); assert!(message.contains("Multi\nLine\nBody")); } #[test] fn test_format_simple_notification_unicode() { let message = format_simple_notification("ę—„ęœ¬čŖž", "šŸŽ‰ Unicode"); assert_eq!(message, "ę—„ęœ¬čŖž\n\nšŸŽ‰ Unicode"); } #[test] fn test_format_simple_notification_empty() { let message = format_simple_notification("", ""); assert_eq!(message, "\n\n"); } #[test] fn test_format_simple_notification_long_text() { let long_title = "A".repeat(1000); let long_body = "B".repeat(1000); let message = format_simple_notification(&long_title, &long_body); assert!(message.starts_with(&long_title)); assert!(message.ends_with(&long_body)); assert!(message.contains("\n\n")); } #[test] fn test_generate_powershell_toast_script_multiple_quotes() { let script = generate_powershell_toast_script( "\"Quoted\" \"Multiple\" \"Times\"", "\"More\" \"Quotes\" \"Here\"" ); // Each quote should be escaped assert!(script.contains("`\"Quoted`\" `\"Multiple`\" `\"Times`\"")); assert!(script.contains("`\"More`\" `\"Quotes`\" `\"Here`\"")); } // E2E Integration Tests - Command Structure Verification #[test] fn test_e2e_notify_send_command_structure() { let (command, args) = build_notify_send_command("Test Title", "Test Body"); assert_eq!(command, "notify-send"); assert_eq!(args.len(), 4); assert_eq!(args[0], "Test Title"); assert_eq!(args[1], "Test Body"); assert_eq!(args[2], "--urgency=normal"); assert_eq!(args[3], "--app-name=Hikari Desktop"); } #[test] fn test_e2e_notify_send_with_special_chars() { let (command, args) = build_notify_send_command("Title with \"quotes\"", "Body\nwith\nnewlines"); assert_eq!(command, "notify-send"); assert_eq!(args[0], "Title with \"quotes\""); assert_eq!(args[1], "Body\nwith\nnewlines"); // notify-send handles these directly } #[test] fn test_e2e_windows_powershell_command_structure() { let (command, args) = build_windows_powershell_command("Test Title", "Test Body"); assert_eq!(command, "pwsh.exe"); assert_eq!(args.len(), 5); assert_eq!(args[0], "-NoProfile"); assert_eq!(args[1], "-WindowStyle"); assert_eq!(args[2], "Hidden"); assert_eq!(args[3], "-Command"); // Verify the script in args[4] contains expected elements let script = &args[4]; assert!(script.contains("Test Title")); assert!(script.contains("Test Body")); assert!(script.contains("Hikari Desktop")); assert!(script.contains("ToastNotification")); } #[test] fn test_e2e_windows_powershell_quote_escaping() { let (_, args) = build_windows_powershell_command("Title with \"quotes\"", "Body with \"quotes\""); let script = &args[4]; // Verify quotes are properly escaped in the PowerShell script assert!(script.contains("Title with `\"quotes`\"")); assert!(script.contains("Body with `\"quotes`\"")); } #[test] fn test_e2e_simple_notification_command_structure() { let (command, args) = build_simple_notification_command("Test Title", "Test Body"); assert_eq!(command, "cmd.exe"); assert_eq!(args.len(), 4); assert_eq!(args[0], "/c"); assert_eq!(args[1], "msg"); assert_eq!(args[2], "*"); assert_eq!(args[3], "Test Title\n\nTest Body"); } #[test] fn test_e2e_simple_notification_multiline() { let (_, args) = build_simple_notification_command("Multi\nLine\nTitle", "Multi\nLine\nBody"); let message = &args[3]; assert!(message.contains("Multi\nLine\nTitle")); assert!(message.contains("\n\n")); assert!(message.contains("Multi\nLine\nBody")); } #[test] fn test_e2e_command_consistency_across_platforms() { // Test that different platforms use consistent parameters let title = "Consistency Test"; let body = "Testing cross-platform consistency"; // Linux command let (notify_cmd, notify_args) = build_notify_send_command(title, body); assert!(notify_cmd.contains("notify")); assert!(notify_args.iter().any(|arg| arg.contains("Hikari Desktop"))); // Windows PowerShell command let (ps_cmd, ps_args) = build_windows_powershell_command(title, body); assert!(ps_cmd.contains("pwsh") || ps_cmd.contains("powershell")); let ps_script = &ps_args[4]; assert!(ps_script.contains("Hikari Desktop")); // Windows simple command let (msg_cmd, msg_args) = build_simple_notification_command(title, body); assert!(msg_cmd.contains("cmd")); assert!(msg_args[3].contains(title)); assert!(msg_args[3].contains(body)); } #[test] fn test_e2e_unicode_support_across_platforms() { let title = "ę—„ęœ¬čŖž Title"; let body = "Unicode: šŸŽ‰"; // Verify all platforms preserve unicode let (_, notify_args) = build_notify_send_command(title, body); assert_eq!(notify_args[0], title); assert_eq!(notify_args[1], body); let (_, ps_args) = build_windows_powershell_command(title, body); let ps_script = &ps_args[4]; assert!(ps_script.contains(title)); assert!(ps_script.contains(body)); let (_, msg_args) = build_simple_notification_command(title, body); assert!(msg_args[3].contains(title)); assert!(msg_args[3].contains(body)); } #[test] fn test_e2e_empty_input_handling() { // Test that empty inputs are handled gracefully let (_, notify_args) = build_notify_send_command("", ""); assert_eq!(notify_args[0], ""); assert_eq!(notify_args[1], ""); let (_, ps_args) = build_windows_powershell_command("", ""); let ps_script = &ps_args[4]; assert!(ps_script.contains("Hikari Desktop")); // Still has app name let (_, msg_args) = build_simple_notification_command("", ""); assert_eq!(msg_args[3], "\n\n"); } }