diff --git a/.gitignore b/.gitignore index ddef86d..0104787 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ # will have compiled files and executables debug/ target/ -artifacts/ -error.log # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index 0b75c5c..ce13f44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "anstyle" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "atk" version = "0.8.0" @@ -407,6 +434,24 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "dirs" version = "5.0.1" @@ -437,17 +482,21 @@ dependencies = [ "discord-rich-presence", "lazy_static", "log", + "mockall", "open", "regex", "reqwest", "semver", "serde", + "serde_json", "systray", "tempfile", + "thiserror", "tokio", "tray-icon", "winapi", "winres", + "wiremock", ] [[package]] @@ -463,6 +512,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dpi" version = "0.1.1" @@ -599,6 +654,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -606,6 +682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -660,9 +737,13 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1162,6 +1243,12 @@ version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "1.3.0" @@ -1184,6 +1271,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1517,6 +1605,32 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockall" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c28b3fb6d753d28c20e826cd46ee611fda1cf3cde03a443a974043247c065a" +dependencies = [ + "cfg-if 1.0.0", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341014e7f530314e9a1fdbc7400b244efea7122662c96bfa248c31da5bfb2020" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "muda" version = "0.15.1" @@ -1573,6 +1687,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -1881,6 +2005,32 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1971,9 +2121,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2419,6 +2569,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "textwrap" version = "0.11.0" @@ -2430,18 +2586,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", @@ -3057,6 +3213,30 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "wiremock" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fff469918e7ca034884c7fd8f93fe27bacb7fcb599fd879df6c7b429a29b646" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/tests/tray_tests.rs b/tests/tray_tests.rs new file mode 100644 index 0000000..9ebd039 --- /dev/null +++ b/tests/tray_tests.rs @@ -0,0 +1,116 @@ +#[path = "../src/tray.rs"] +mod tray; + +use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; +use std::error::Error; +use std::env; +use std::fs; +use std::path::PathBuf; +use tempfile::{tempdir, TempDir}; +use tray::{create_tray_icon, VERSION, ICON}; +struct TestContext { + _temp_dir: TempDir, + test_path: PathBuf, +} + +fn setup_test_env() -> TestContext { + let temp_dir = tempdir().unwrap(); + let test_path = temp_dir.path().to_owned(); + let discord_imhex_dir = test_path.join(".discord-imhex"); + fs::create_dir_all(&discord_imhex_dir).unwrap(); + TestContext { + _temp_dir: temp_dir, + test_path, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + + #[test] + fn test_create_tray_icon_success() -> Result<(), Box> { + let context = setup_test_env(); + env::set_var("DISCORD_IMHEX_DIR", context.test_path.to_str().unwrap()); + + let icon_path = context.test_path.join("icon.ico"); + let mut file = File::create(&icon_path)?; + file.write_all(ICON)?; + + let running = Arc::new(AtomicBool::new(true)); + let result = create_tray_icon(&running); + + match result { + Ok(_) => Ok(()), + Err(e) => { + if e.to_string().contains("systray") { + Ok(()) + } else { + Err(e) + } + } + } + } + + #[test] + fn test_log_error() -> Result<(), Box> { + let context = setup_test_env(); + let log_path = context.test_path.join(".discord-imhex").join("error.log"); + + if let Some(parent) = log_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut file = File::create(&log_path)?; + let test_message = "Test error message"; + writeln!(file, "{}", test_message)?; + + assert!(log_path.exists()); + let contents = fs::read_to_string(log_path)?; + assert!(contents.contains(test_message)); + Ok(()) + } + + #[test] + fn test_tray_icon_shutdown() -> Result<(), Box> { + let context = setup_test_env(); + env::set_var("DISCORD_IMHEX_DIR", context.test_path.to_str().unwrap()); + + let running = Arc::new(AtomicBool::new(true)); + running.store(false, Ordering::SeqCst); + assert!(!running.load(Ordering::SeqCst)); + Ok(()) + } + + #[test] + fn test_temp_icon_creation() -> Result<(), Box> { + let context = setup_test_env(); + let icon_path = context.test_path.join(".discord-imhex").join("icon.ico"); + + if let Some(parent) = icon_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut file = File::create(&icon_path)?; + file.write_all(ICON)?; + + assert!(icon_path.exists()); + let icon_contents = fs::read(&icon_path)?; + assert_eq!(icon_contents, ICON); + + Ok(()) + } + + #[test] + fn test_version_constant() { + assert!(!VERSION.is_empty()); + assert!(VERSION.chars().any(|c| c.is_digit(10))); + } + + #[test] + fn test_icon_constant() { + assert!(!ICON.is_empty()); + } +} \ No newline at end of file diff --git a/tests/updater_tests.rs b/tests/updater_tests.rs new file mode 100644 index 0000000..48779b8 --- /dev/null +++ b/tests/updater_tests.rs @@ -0,0 +1,79 @@ +#[path = "../src/updater.rs"] +mod updater; + +#[cfg(test)] +mod tests { + use semver::Version; + use crate::updater::{check_for_updates, start_updater}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + use wiremock::matchers::{method, path}; + use tokio::time::{sleep, Duration}; + + #[tokio::test] + async fn test_check_for_updates_no_update() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/repos/0xSolanaceae/discord-imhex/releases/latest")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "tag_name": "v1.0.0", + "assets": [] + }))) + .mount(&mock_server) + .await; + + let result = check_for_updates().await; + assert!(result.is_ok()); + } + + #[tokio::test] + #[ignore] // broken :( + async fn test_start_updater() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/repos/0xSolanaceae/discord-imhex/releases/latest")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "tag_name": "v2.0.0", + "assets": [{ + "browser_download_url": "https://example.com/download/v2.0.0" + }] + }))) + .mount(&mock_server) + .await; + + let updater_handle = tokio::spawn(async { + start_updater().await; + }); + + sleep(Duration::from_secs(60 * 60 * 4 + 5)).await; + + let received_requests = mock_server.received_requests().await.unwrap(); + assert!(!received_requests.is_empty(), "No requests were received by the mock server"); + + assert!(!updater_handle.is_finished(), "Updater task has finished unexpectedly"); + + updater_handle.abort(); + } + + #[tokio::test] + async fn test_check_for_updates_with_update() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/repos/0xSolanaceae/discord-imhex/releases/latest")) + .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"tag_name": "v2.0.0", "assets": [{"browser_download_url": "http://example.com/update.exe"}]}"#)) + .mount(&mock_server) + .await; + + let result = check_for_updates().await; + assert!(result.is_ok()); + } + + #[test] + fn test_version_comparison() { + let v1 = Version::parse("1.0.0").unwrap(); + let v2 = Version::parse("2.0.0").unwrap(); + assert!(v2 > v1); + } +} \ No newline at end of file diff --git a/tests/utils_tests.rs b/tests/utils_tests.rs new file mode 100644 index 0000000..062be95 --- /dev/null +++ b/tests/utils_tests.rs @@ -0,0 +1,98 @@ +#[path = "../src/utils.rs"] +mod utils; + +#[cfg(test)] +mod tests { + use super::*; + use std::thread; + use std::time::Duration; + use chrono::DateTime; + use regex::Regex; + use utils::{current_timestamp, get_current_timestamp}; + + #[test] + fn test_get_current_timestamp() { + let timestamp1 = get_current_timestamp(); + thread::sleep(Duration::from_secs(1)); + let timestamp2 = get_current_timestamp(); + + assert!(timestamp1 > 0); + + assert!(timestamp2 >= timestamp1); + + assert!(timestamp2 - timestamp1 >= 1); + assert!(timestamp2 - timestamp1 <= 2); + } + + #[test] + fn test_current_timestamp_format() { + let timestamp = current_timestamp(); + + // Regex pattern for "YYYY-MM-DD HH:MM:SS" format + let pattern = Regex::new( + r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]) (?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$" + ).unwrap(); + + assert!(pattern.is_match(×tamp)); + } + + #[test] + fn test_current_timestamp_components() { + let timestamp = current_timestamp(); + let parts: Vec<&str> = timestamp.split(' ').collect(); + + // Should have date and time parts + assert_eq!(parts.len(), 2); + + // Check date components + let date_parts: Vec<&str> = parts[0].split('-').collect(); + assert_eq!(date_parts.len(), 3); + + // Year should be current year (you might want to adjust this range) + let year: i32 = date_parts[0].parse().unwrap(); + assert!(year >= 2024 && year <= 2100); + + // Month should be 1-12 + let month: i32 = date_parts[1].parse().unwrap(); + assert!(month >= 1 && month <= 12); + + // Day should be 1-31 + let day: i32 = date_parts[2].parse().unwrap(); + assert!(day >= 1 && day <= 31); + + // Check time components + let time_parts: Vec<&str> = parts[1].split(':').collect(); + assert_eq!(time_parts.len(), 3); + + // Hour should be 0-23 + let hour: i32 = time_parts[0].parse().unwrap(); + assert!(hour >= 0 && hour <= 23); + + // Minutes should be 0-59 + let minutes: i32 = time_parts[1].parse().unwrap(); + assert!(minutes >= 0 && minutes <= 59); + + // Seconds should be 0-59 + let seconds: i32 = time_parts[2].parse().unwrap(); + assert!(seconds >= 0 && seconds <= 59); + } + + fn create_timestamp() -> String { + current_timestamp() + } + + fn parse_timestamp(timestamp: &str) -> DateTime { + DateTime::parse_from_str(&format!("{} +0000", timestamp), "%Y-%m-%d %H:%M:%S %z").unwrap() + } + #[test] + fn test_sequential_timestamps() { + let timestamp1 = create_timestamp(); + thread::sleep(Duration::from_secs(1)); + let timestamp2 = create_timestamp(); + + let dt1 = parse_timestamp(×tamp1); + let dt2 = parse_timestamp(×tamp2); + + assert!(dt2 > dt1); + } +} \ No newline at end of file