#[path = "../src/activity.rs"] mod activity; #[path = "../src/models.rs"] mod models; #[path = "../src/storage.rs"] mod storage; use models::{ ActivityEntry, ActivityLevel, ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig, Profile, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, SubscriptionCache, SubscriptionServer, Target, TargetKind, }; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use storage::{backup_path, default_config_root, JsonStorage, StoragePaths}; #[test] fn storage_defaults_to_programdata_root() { let expected = PathBuf::from(r"C:\ProgramData\ProxyWarden"); assert_eq!(default_config_root(), expected); assert_eq!(StoragePaths::default().root, expected); } #[test] fn roundtrips_profiles_targets_components_and_activity() { let root = test_root("roundtrip"); let storage = JsonStorage::new(root.clone()); let profiles = vec![sample_profile("discord")]; let targets = vec![sample_target("home-gateway")]; let components = vec![sample_component()]; let activity = vec![sample_activity( "created", "2026-01-01T10:00:00Z", ActivityLevel::Success, )]; storage.write_profiles(&profiles).expect("write profiles"); storage.write_targets(&targets).expect("write targets"); write_json(&storage.paths().components_file, &components); write_json(&storage.paths().activity_file, &activity); assert_eq!(storage.read_profiles().expect("read profiles"), profiles); assert_eq!(storage.read_targets().expect("read targets"), targets); assert_eq!( storage.read_components().expect("read components"), components ); assert_eq!(storage.read_activity().expect("read activity"), activity); cleanup(&root); } #[test] fn roundtrips_local_singbox_config_and_subscription_cache() { let root = test_root("local-singbox"); let storage = JsonStorage::new(root.clone()); let config = LocalSingBoxConfig { subscription_url: Some("https://sub.example.test/path?token=secret".to_string()), device_hwid: Some("hwid-abcdef1234".to_string()), selected_server_tag: Some("nl-1".to_string()), listen_host: "127.0.0.1".to_string(), listen_port: 1080, service_name: "ProxyWardenSingBox".to_string(), install_root: r"C:\Program Files\ProxyWarden\sing-box".to_string(), updated_at: Some("2026-07-07T10:00:00Z".to_string()), }; let cache = sample_subscription_cache(); storage .write_local_singbox_config(&config) .expect("write local sing-box config"); storage .write_singbox_subscription_cache(&cache) .expect("write subscription cache"); assert_eq!( storage .read_local_singbox_config() .expect("read local sing-box config"), config ); assert_eq!( storage .read_singbox_subscription_cache() .expect("read subscription cache"), Some(cache) ); assert_eq!( config.subscription_display_url(), Some("https://sub.example.test/...".to_string()) ); cleanup(&root); } #[test] fn missing_local_singbox_config_defaults_to_optional_empty_state() { let root = test_root("local-singbox-default"); let storage = JsonStorage::new(root.clone()); let config = storage .read_local_singbox_config() .expect("read default local sing-box config"); assert_eq!(config.subscription_url, None); assert_eq!(config.device_hwid, None); assert_eq!(config.selected_server_tag, None); assert_eq!(config.listen_host, "127.0.0.1"); assert_eq!(config.listen_port, 1080); assert_eq!(config.service_name, "ProxyWardenSingBox"); cleanup(&root); } #[test] fn reads_percent_encoded_singbox_tags_as_utf8() { let root = test_root("local-singbox-percent-tags"); let storage = JsonStorage::new(root.clone()); let encoded_tag = "%D0%A3%D0%BC%D0%BD%D1%8B%D0%B9%20%F0%9F%87%B3%F0%9F%87%B1-%3E%F0%9F%87%B7%F0%9F%87%BA"; let decoded_tag = "Умный 🇳🇱->🇷🇺"; storage .write_local_singbox_config(&LocalSingBoxConfig { selected_server_tag: Some(encoded_tag.to_string()), ..LocalSingBoxConfig::default() }) .expect("write local sing-box config"); storage .write_singbox_subscription_cache(&SubscriptionCache { config: serde_json::json!({ "outbounds": [ { "type": "vless", "tag": encoded_tag, "server": "nl.example.test", "server_port": 443 } ] }), servers: vec![SubscriptionServer { tag: encoded_tag.to_string(), server_type: "vless".to_string(), server: "nl.example.test".to_string(), server_port: 443, }], user_info: serde_json::Map::new(), fetched_at: "2026-07-07T10:00:00Z".to_string(), }) .expect("write subscription cache"); let config = storage .read_local_singbox_config() .expect("read local sing-box config"); let cache = storage .read_singbox_subscription_cache() .expect("read subscription cache") .expect("subscription cache"); assert_eq!(config.selected_server_tag, Some(decoded_tag.to_string())); assert_eq!(cache.servers[0].tag, decoded_tag); assert_eq!(cache.config["outbounds"][0]["tag"], decoded_tag); cleanup(&root); } #[test] fn invalid_subscription_cache_falls_back_to_none() { let root = test_root("invalid-subscription-cache"); let storage = JsonStorage::new(root.clone()); fs::create_dir_all(&storage.paths().state_dir).expect("create state dir"); fs::write( &storage.paths().singbox_subscription_cache_file, "{not valid json", ) .expect("write invalid cache"); assert_eq!( storage .read_singbox_subscription_cache() .expect("invalid cache fallback"), None ); cleanup(&root); } #[test] fn invalid_json_falls_back_to_empty_collection() { let root = test_root("invalid-json"); let storage = JsonStorage::new(root.clone()); fs::create_dir_all(&storage.paths().config_dir).expect("create config dir"); fs::write(&storage.paths().profiles_file, "{not valid json").expect("write invalid json"); assert_eq!( storage.read_profiles().expect("invalid profiles fallback"), Vec::::new() ); cleanup(&root); } #[test] fn write_creates_backup_before_overwriting_source_file() { let root = test_root("backup"); let storage = JsonStorage::new(root.clone()); let first = vec![sample_profile("first")]; let second = vec![sample_profile("second")]; storage.write_profiles(&first).expect("first write"); storage.write_profiles(&second).expect("second write"); let backup = backup_path(&storage.paths().profiles_file); assert!(backup.exists(), "backup file should exist"); let backup_contents = fs::read_to_string(backup).expect("read backup"); let backup_profiles: Vec = serde_json::from_str(&backup_contents).expect("backup json"); assert_eq!(backup_profiles, first); assert_eq!(storage.read_profiles().expect("current profiles"), second); cleanup(&root); } #[test] fn activity_entries_are_sorted_and_capped() { let root = test_root("activity"); let storage = JsonStorage::with_activity_limit(root.clone(), 2); storage .append_activity(sample_activity( "old", "2026-01-01T10:00:00Z", ActivityLevel::Info, )) .expect("append old"); storage .append_activity(sample_activity( "new", "2026-01-03T10:00:00Z", ActivityLevel::Success, )) .expect("append new"); storage .append_activity(sample_activity( "middle", "2026-01-02T10:00:00Z", ActivityLevel::Warning, )) .expect("append middle"); let entries = storage.read_activity().expect("read capped activity"); assert_eq!(entries.len(), 2); assert_eq!( entries .iter() .map(|entry| entry.id.as_str()) .collect::>(), vec!["new", "middle"] ); cleanup(&root); } fn test_root(name: &str) -> PathBuf { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system clock before unix epoch") .as_nanos(); std::env::temp_dir().join(format!("proxywarden-storage-{name}-{timestamp}")) } fn cleanup(root: &Path) { let _ = fs::remove_dir_all(root); } fn write_json(path: &Path, value: &T) { if let Some(parent) = path.parent() { fs::create_dir_all(parent).expect("create json parent dir"); } let contents = serde_json::to_vec_pretty(value).expect("serialize json"); fs::write(path, contents).expect("write json"); } fn sample_profile(id: &str) -> Profile { Profile { id: id.to_string(), name: format!("Profile {id}"), enabled: true, target_id: "home-gateway".to_string(), protocols: vec![Protocol::Tcp, Protocol::Udp], items: vec![ProfileItem { item_type: ProfileItemType::Process, value: "Discord".to_string(), recursive: false, }], } } fn sample_target(id: &str) -> Target { Target { id: id.to_string(), name: "Home Gateway".to_string(), kind: TargetKind::External, protocol: ProxyProtocol::Socks5, host: "192.168.50.111".to_string(), port: 8080, requires_component: None, } } fn sample_component() -> ComponentStatus { ComponentStatus { id: ComponentId::Proxyfier, name: "ProxiFyre".to_string(), state: ComponentState::Missing, installed: false, running: false, version: None, path: None, problems: vec!["ProxiFyre не установлен".to_string()], actions: vec!["Установить ProxiFyre".to_string()], } } fn sample_subscription_cache() -> SubscriptionCache { SubscriptionCache { config: serde_json::json!({ "outbounds": [ { "type": "vless", "tag": "nl-1", "server": "nl.example.test", "server_port": 443 } ] }), servers: vec![SubscriptionServer { tag: "nl-1".to_string(), server_type: "vless".to_string(), server: "nl.example.test".to_string(), server_port: 443, }], user_info: serde_json::Map::new(), fetched_at: "2026-07-07T10:00:00Z".to_string(), } } fn sample_activity(id: &str, at: &str, level: ActivityLevel) -> ActivityEntry { ActivityEntry { id: id.to_string(), at: at.to_string(), level, title: format!("Activity {id}"), message: "Storage test activity".to_string(), } }