#[path = "../src/activity.rs"] mod activity; #[path = "../src/commands.rs"] mod commands; #[path = "../src/component_detection.rs"] mod component_detection; #[path = "../src/models.rs"] mod models; #[path = "../src/process.rs"] mod process; #[path = "../src/adapters/proxifyre.rs"] mod proxifyre; #[path = "../src/adapters/proxy_router.rs"] mod proxy_router; #[path = "../src/adapters/singbox.rs"] mod singbox; #[path = "../src/singbox_service.rs"] mod singbox_service; #[path = "../src/storage.rs"] mod storage; #[path = "../src/subscription.rs"] mod subscription; #[path = "../src/validation.rs"] mod validation; use commands::{ fetch_singbox_subscription_with_fetcher, forget_singbox_subscription_in_storage, generate_singbox_config_with_services, save_singbox_subscription_to_storage, select_singbox_server_in_storage, Clock, SaveSingBoxSubscriptionInputDto, SelectSingBoxServerInputDto, SubscriptionFetcher, }; use models::{ ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache, SubscriptionServer, TargetKind, }; use serde_json::{json, Map}; use singbox::{SingBoxAdapter, SingBoxCheckResult, SingBoxConfigChecker, SingBoxConfigError}; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use storage::JsonStorage; #[test] fn saves_subscription_url_without_exposing_secret_query() { let root = test_root("save-subscription"); let storage = JsonStorage::new(root.clone()); let status = save_singbox_subscription_to_storage( &storage, SaveSingBoxSubscriptionInputDto { subscription_url: " https://sub.example.test/path?token=secret ".to_string(), }, &FixedClock, ) .expect("subscription URL should be saved"); let config = storage .read_local_singbox_config() .expect("read local sing-box config"); assert_eq!( config.subscription_url, Some("https://sub.example.test/path?token=secret".to_string()) ); assert!(status.config.has_subscription); assert_eq!( status.config.subscription_display_url, Some("https://sub.example.test/...".to_string()) ); cleanup(&root); } #[test] fn rejects_non_http_subscription_url() { let root = test_root("invalid-subscription"); let storage = JsonStorage::new(root.clone()); let error = save_singbox_subscription_to_storage( &storage, SaveSingBoxSubscriptionInputDto { subscription_url: "file:///C:/sub.txt".to_string(), }, &FixedClock, ) .expect_err("non-http subscription URL should fail"); assert_eq!(error.code, "singbox_subscription_url_invalid"); cleanup(&root); } #[test] fn fetches_subscription_cache_and_selects_first_server() { let root = test_root("fetch-subscription"); let storage = JsonStorage::new(root.clone()); save_singbox_subscription_to_storage( &storage, SaveSingBoxSubscriptionInputDto { subscription_url: "https://sub.example.test/path?token=secret".to_string(), }, &FixedClock, ) .expect("save subscription URL"); let status = fetch_singbox_subscription_with_fetcher( &storage, &MockFetcher(sample_cache()), &FixedClock, ) .expect("fetch subscription through mock"); let config = storage .read_local_singbox_config() .expect("read local sing-box config"); let cache = storage .read_singbox_subscription_cache() .expect("read cache") .expect("cache should exist"); let activity = storage.read_activity().expect("read activity"); assert_eq!(status.config.selected_server_tag, Some("nl-1".to_string())); assert_eq!(status.cache.expect("status cache").servers.len(), 2); assert_eq!(config.selected_server_tag, Some("nl-1".to_string())); assert_eq!(cache.servers.len(), 2); assert_eq!(activity[0].level, ActivityLevel::Success); assert_eq!(activity[0].title, "Подписка Local sing-box обновлена"); cleanup(&root); } #[test] fn selects_server_from_cached_subscription() { let root = test_root("select-server"); let storage = JsonStorage::new(root.clone()); storage .write_singbox_subscription_cache(&sample_cache()) .expect("write cache"); let status = select_singbox_server_in_storage( &storage, SelectSingBoxServerInputDto { tag: "de-1".to_string(), server: None, server_port: None, }, &FixedClock, ) .expect("server should be selected"); let config = storage .read_local_singbox_config() .expect("read local sing-box config"); assert_eq!(status.config.selected_server_tag, Some("de-1".to_string())); assert_eq!(config.selected_server_tag, Some("de-1".to_string())); cleanup(&root); } #[test] fn selects_server_by_endpoint_when_display_tag_is_sanitized() { let root = test_root("select-server-sanitized-tag"); let storage = JsonStorage::new(root.clone()); storage .write_singbox_subscription_cache(&sample_cache_with_flag_tag()) .expect("write cache"); let status = select_singbox_server_in_storage( &storage, SelectSingBoxServerInputDto { tag: "Умный".to_string(), server: Some("media.example.test".to_string()), server_port: Some(443), }, &FixedClock, ) .expect("server should be selected by endpoint fallback"); let config = storage .read_local_singbox_config() .expect("read local sing-box config"); assert_eq!( status.config.selected_server_tag, Some("Умный 🇳🇱->🇷🇺".to_string()) ); assert_eq!(config.selected_server_tag, Some("Умный 🇳🇱->🇷🇺".to_string())); cleanup(&root); } #[test] fn generate_writes_config_and_local_singbox_target() { let root = test_root("generate-config"); let storage = JsonStorage::new(root.clone()); storage .write_local_singbox_config(&LocalSingBoxConfig { subscription_url: Some("https://sub.example.test/path".to_string()), selected_server_tag: Some("nl-1".to_string()), ..LocalSingBoxConfig::default() }) .expect("write local sing-box config"); storage .write_singbox_subscription_cache(&sample_cache()) .expect("write cache"); let response = generate_singbox_config_with_services( &storage, &SingBoxAdapter::default(), &MockChecker, &FixedClock, Some(Path::new("sing-box.exe")), ) .expect("generate sing-box config"); let generated = fs::read_to_string(&response.generated_config_path).expect("read generated"); let targets = storage.read_targets().expect("read targets"); let target = targets .iter() .find(|target| target.id == "local-singbox") .expect("local sing-box target"); let activity = storage.read_activity().expect("read activity"); assert!(response.success); assert_eq!(response.adapter_id, "singbox"); assert_eq!(response.selected_server_tag, "nl-1"); assert!(response.check.expect("check result").success); assert!(generated.contains("\"type\": \"mixed\"")); assert!(generated.contains("\"tag\": \"vpn\"")); assert_eq!(target.kind, TargetKind::Local); assert_eq!(target.protocol, ProxyProtocol::Socks5); assert_eq!(target.host, "127.0.0.1"); assert_eq!(target.port, 1080); assert_eq!(target.requires_component, Some(ComponentId::Singbox)); assert_eq!(activity[0].title, "Конфиг Local sing-box создан"); cleanup(&root); } #[test] fn generate_requires_cached_subscription() { let root = test_root("generate-missing-cache"); let storage = JsonStorage::new(root.clone()); let error = generate_singbox_config_with_services( &storage, &SingBoxAdapter::default(), &MockChecker, &FixedClock, None, ) .expect_err("missing cache should block generation"); assert_eq!(error.code, "singbox_subscription_cache_missing"); cleanup(&root); } #[test] fn forget_subscription_clears_url_selection_and_cache() { let root = test_root("forget-subscription"); let storage = JsonStorage::new(root.clone()); storage .write_local_singbox_config(&LocalSingBoxConfig { subscription_url: Some("https://sub.example.test/path".to_string()), selected_server_tag: Some("nl-1".to_string()), ..LocalSingBoxConfig::default() }) .expect("write local sing-box config"); storage .write_singbox_subscription_cache(&sample_cache()) .expect("write cache"); let status = forget_singbox_subscription_in_storage(&storage, &FixedClock).expect("forget subscription"); let config = storage .read_local_singbox_config() .expect("read local sing-box config"); let cache = storage .read_singbox_subscription_cache() .expect("read cache"); assert!(!status.config.has_subscription); assert_eq!(config.subscription_url, None); assert_eq!(config.selected_server_tag, None); assert_eq!(cache, None); cleanup(&root); } struct MockFetcher(SubscriptionCache); impl SubscriptionFetcher for MockFetcher { fn fetch_subscription( &self, url: &str, ) -> Result { assert_eq!(url, "https://sub.example.test/path?token=secret"); Ok(self.0.clone()) } } struct MockChecker; impl SingBoxConfigChecker for MockChecker { fn check_config( &self, binary_path: &Path, config_json: &str, ) -> Result { assert_eq!(binary_path, Path::new("sing-box.exe")); assert!(config_json.contains("\"final\": \"vpn\"")); Ok(SingBoxCheckResult { checked: true, success: true, message: "mock check passed".to_string(), }) } } struct FixedClock; impl Clock for FixedClock { fn now(&self) -> String { "2026-07-07T00:00:00Z".to_string() } } fn sample_cache() -> SubscriptionCache { SubscriptionCache { config: json!({ "outbounds": [ { "type": "vless", "tag": "nl-1", "server": "nl.example.test", "server_port": 443, "uuid": "11111111-1111-1111-1111-111111111111" }, { "type": "trojan", "tag": "de-1", "server": "de.example.test", "server_port": 443, "password": "secret" } ] }), servers: vec![ SubscriptionServer { tag: "nl-1".to_string(), server_type: "vless".to_string(), server: "nl.example.test".to_string(), server_port: 443, }, SubscriptionServer { tag: "de-1".to_string(), server_type: "trojan".to_string(), server: "de.example.test".to_string(), server_port: 443, }, ], user_info: Map::new(), fetched_at: "2026-07-07T00:00:00Z".to_string(), } } fn sample_cache_with_flag_tag() -> SubscriptionCache { SubscriptionCache { config: json!({ "outbounds": [ { "type": "vless", "tag": "Умный 🇳🇱->🇷🇺", "server": "media.example.test", "server_port": 443, "uuid": "11111111-1111-1111-1111-111111111111" } ] }), servers: vec![SubscriptionServer { tag: "Умный 🇳🇱->🇷🇺".to_string(), server_type: "vless".to_string(), server: "media.example.test".to_string(), server_port: 443, }], user_info: Map::new(), fetched_at: "2026-07-07T00:00:00Z".to_string(), } } 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-singbox-commands-{name}-{timestamp}")) } fn cleanup(root: &Path) { let _ = fs::remove_dir_all(root); }