#[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, Profile, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, 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 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_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(), } }