#[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\VpnProxy"); 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"); storage .write_components(&components) .expect("write components"); storage.write_activity(&activity).expect("write 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()), selected_server_tag: Some("nl-1".to_string()), listen_host: "127.0.0.1".to_string(), listen_port: 1080, service_name: "VpnProxySingBox".to_string(), install_root: r"C:\Program Files\VpnProxy\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.selected_server_tag, None); assert_eq!(config.listen_host, "127.0.0.1"); assert_eq!(config.listen_port, 1080); assert_eq!(config.service_name, "VpnProxySingBox"); cleanup(&root); } #[test] fn invalid_subscription_cache_falls_back_to_none() { let root = test_root("invalid-subscription-cache"); let storage = JsonStorage::new(root.clone()); storage.ensure_dirs().expect("create storage dirs"); 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()); storage.ensure_dirs().expect("create storage dirs"); 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!("vpn-proxy-storage-{name}-{timestamp}")) } fn cleanup(root: &Path) { let _ = fs::remove_dir_all(root); } 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(), } }