Files
ProxyWarden/src-tauri/tests/singbox_command_tests.rs

510 lines
16 KiB
Rust

#[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_valid_generated_hwid(config.device_hwid.as_deref());
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 fetches_subscription_with_saved_device_hwid() {
let root = test_root("fetch-subscription-hwid");
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 and generated HWID");
let expected_hwid = storage
.read_local_singbox_config()
.expect("read local sing-box config")
.device_hwid
.expect("generated HWID");
fetch_singbox_subscription_with_fetcher(
&storage,
&HwidAssertingFetcher {
cache: sample_cache(),
expected_hwid,
},
&FixedClock,
)
.expect("fetch subscription through mock");
cleanup(&root);
}
#[test]
fn fetch_generates_device_hwid_for_existing_subscription_without_one() {
let root = test_root("fetch-generates-hwid");
let storage = JsonStorage::new(root.clone());
storage
.write_local_singbox_config(&LocalSingBoxConfig {
subscription_url: Some("https://sub.example.test/path?token=secret".to_string()),
device_hwid: None,
..LocalSingBoxConfig::default()
})
.expect("write local sing-box config");
fetch_singbox_subscription_with_fetcher(
&storage,
&GeneratedHwidAssertingFetcher(sample_cache()),
&FixedClock,
)
.expect("fetch subscription through mock");
let config = storage
.read_local_singbox_config()
.expect("read local sing-box config");
assert_valid_generated_hwid(config.device_hwid.as_deref());
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()),
device_hwid: Some("C34C14C9-94BC-4918-B053-A249BC117A91".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());
let existing_hwid = "C34C14C9-94BC-4918-B053-A249BC117A91".to_string();
storage
.write_local_singbox_config(&LocalSingBoxConfig {
subscription_url: Some("https://sub.example.test/path".to_string()),
device_hwid: Some(existing_hwid.clone()),
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.device_hwid, Some(existing_hwid));
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,
_identity: &subscription::SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
assert_eq!(url, "https://sub.example.test/path?token=secret");
Ok(self.0.clone())
}
}
struct HwidAssertingFetcher {
cache: SubscriptionCache,
expected_hwid: String,
}
impl SubscriptionFetcher for HwidAssertingFetcher {
fn fetch_subscription(
&self,
url: &str,
identity: &subscription::SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
assert_eq!(url, "https://sub.example.test/path?token=secret");
assert_eq!(
identity.device_hwid.as_deref(),
Some(self.expected_hwid.as_str())
);
assert_eq!(identity.app_name, "ProxyWarden");
assert!(identity.user_agent.starts_with("ProxyWarden/"));
assert_eq!(identity.device_os, std::env::consts::OS);
assert_eq!(identity.device_model, "ProxyWarden");
Ok(self.cache.clone())
}
}
struct GeneratedHwidAssertingFetcher(SubscriptionCache);
impl SubscriptionFetcher for GeneratedHwidAssertingFetcher {
fn fetch_subscription(
&self,
url: &str,
identity: &subscription::SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
assert_eq!(url, "https://sub.example.test/path?token=secret");
assert_valid_generated_hwid(identity.device_hwid.as_deref());
assert_eq!(identity.app_name, "ProxyWarden");
assert!(identity.user_agent.starts_with("ProxyWarden/"));
Ok(self.0.clone())
}
}
fn assert_valid_generated_hwid(value: Option<&str>) {
let value = value.expect("generated HWID");
uuid::Uuid::parse_str(value).expect("HWID should be a UUID");
assert_eq!(value, value.to_ascii_uppercase());
}
struct MockChecker;
impl SingBoxConfigChecker for MockChecker {
fn check_config(
&self,
binary_path: &Path,
config_json: &str,
) -> Result<SingBoxCheckResult, SingBoxConfigError> {
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);
}