Refactor ProxyWarden routing and settings flow
This commit is contained in:
@@ -400,11 +400,14 @@ fn detected_proxy_apply_helper_writes_proxifyre_app_config() {
|
||||
|
||||
let applied =
|
||||
fs::read_to_string(install_dir.join("app-config.json")).expect("read applied app-config");
|
||||
let backup =
|
||||
fs::read_to_string(install_dir.join("app-config.json.bak")).expect("read backup config");
|
||||
|
||||
assert!(result.success);
|
||||
assert!(result.changed);
|
||||
assert_eq!(result.action, "proxifyre.apply-detected-config");
|
||||
assert_eq!(applied, r#"{"proxies":[]}"#);
|
||||
assert_eq!(backup, "{}");
|
||||
assert!(install_dir.join("app-config.json.bak").exists());
|
||||
|
||||
cleanup(&root);
|
||||
|
||||
@@ -159,7 +159,7 @@ fn reads_percent_encoded_singbox_tags_as_utf8() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_subscription_cache_falls_back_to_none() {
|
||||
fn invalid_subscription_cache_without_backup_returns_error_and_moves_corrupt_file() {
|
||||
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");
|
||||
@@ -169,27 +169,62 @@ fn invalid_subscription_cache_falls_back_to_none() {
|
||||
)
|
||||
.expect("write invalid cache");
|
||||
|
||||
assert_eq!(
|
||||
storage
|
||||
.read_singbox_subscription_cache()
|
||||
.expect("invalid cache fallback"),
|
||||
None
|
||||
);
|
||||
let error = storage
|
||||
.read_singbox_subscription_cache()
|
||||
.expect_err("invalid cache should not silently fallback");
|
||||
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
|
||||
assert!(!storage.paths().singbox_subscription_cache_file.exists());
|
||||
assert!(has_corrupt_sibling(
|
||||
&storage.paths().singbox_subscription_cache_file
|
||||
));
|
||||
|
||||
cleanup(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_json_falls_back_to_empty_collection() {
|
||||
let root = test_root("invalid-json");
|
||||
fn invalid_json_without_backup_returns_error_and_moves_corrupt_file() {
|
||||
let root = test_root("invalid-json-no-backup");
|
||||
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");
|
||||
|
||||
let error = storage
|
||||
.read_profiles()
|
||||
.expect_err("invalid profiles should not silently fallback");
|
||||
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
|
||||
assert!(!storage.paths().profiles_file.exists());
|
||||
assert!(has_corrupt_sibling(&storage.paths().profiles_file));
|
||||
|
||||
cleanup(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_json_recovers_from_valid_backup() {
|
||||
let root = test_root("invalid-json-valid-backup");
|
||||
let storage = JsonStorage::new(root.clone());
|
||||
let backup_profiles = vec![sample_profile("backup")];
|
||||
let current_profiles = vec![sample_profile("current")];
|
||||
|
||||
storage
|
||||
.write_profiles(&backup_profiles)
|
||||
.expect("write first profiles");
|
||||
storage
|
||||
.write_profiles(¤t_profiles)
|
||||
.expect("write second profiles");
|
||||
fs::write(&storage.paths().profiles_file, "{not valid json").expect("corrupt live json");
|
||||
|
||||
let recovered = storage
|
||||
.read_profiles()
|
||||
.expect("invalid profiles should recover from valid backup");
|
||||
|
||||
assert_eq!(recovered, backup_profiles);
|
||||
assert_eq!(
|
||||
storage.read_profiles().expect("invalid profiles fallback"),
|
||||
Vec::<Profile>::new()
|
||||
storage.read_profiles().expect("restored live profiles"),
|
||||
backup_profiles
|
||||
);
|
||||
assert!(has_corrupt_sibling(&storage.paths().profiles_file));
|
||||
|
||||
cleanup(&root);
|
||||
}
|
||||
@@ -279,6 +314,26 @@ fn write_json<T: serde::Serialize + ?Sized>(path: &Path, value: &T) {
|
||||
fs::write(path, contents).expect("write json");
|
||||
}
|
||||
|
||||
fn has_corrupt_sibling(path: &Path) -> bool {
|
||||
let Some(parent) = path.parent() else {
|
||||
return false;
|
||||
};
|
||||
let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
|
||||
return false;
|
||||
};
|
||||
let prefix = format!("{file_name}.corrupt.");
|
||||
|
||||
fs::read_dir(parent)
|
||||
.expect("read sibling dir")
|
||||
.filter_map(Result::ok)
|
||||
.any(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|name| name.starts_with(&prefix))
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_profile(id: &str) -> Profile {
|
||||
Profile {
|
||||
id: id.to_string(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use base64::{engine::general_purpose, Engine};
|
||||
use proxywarden_lib::models::redact_subscription_url;
|
||||
use proxywarden_lib::subscription::{
|
||||
self, parse_subscription_body, parse_user_info, SubscriptionFetchIdentity,
|
||||
SubscriptionFetchPolicy,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
@@ -91,6 +92,25 @@ fn rejects_invalid_or_non_http_subscription_url_before_network() {
|
||||
assert!(unsupported.message.contains("http or https"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsafe_local_subscription_urls_before_network() {
|
||||
for url in [
|
||||
"http://127.0.0.1:9/subscription",
|
||||
"http://localhost/subscription",
|
||||
"http://169.254.169.254/latest/meta-data",
|
||||
"http://192.168.0.1/subscription",
|
||||
"http://[::1]/subscription",
|
||||
] {
|
||||
let error = subscription::fetch_subscription(url)
|
||||
.expect_err("unsafe local URL should fail before request");
|
||||
assert!(
|
||||
error.message.contains("local, private"),
|
||||
"unexpected error for {url}: {}",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fetch_subscription_sends_device_hwid_header_when_identity_is_set() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind local test listener");
|
||||
@@ -129,8 +149,14 @@ fn fetch_subscription_sends_device_hwid_header_when_identity_is_set() {
|
||||
|
||||
let mut identity = SubscriptionFetchIdentity::with_device_hwid(Some("hwid-abc123"));
|
||||
identity.device_os_version = Some("Windows 11 Pro | 25H2 | build 26200.8655".to_string());
|
||||
let cache = subscription::fetch_subscription_with_identity(&url, &identity)
|
||||
.expect("fetch subscription through local test server");
|
||||
let cache = subscription::fetch_subscription_with_identity_and_policy(
|
||||
&url,
|
||||
&identity,
|
||||
SubscriptionFetchPolicy {
|
||||
allow_unsafe_local_urls: true,
|
||||
},
|
||||
)
|
||||
.expect("fetch subscription through local test server");
|
||||
let request = request_thread.join().expect("request thread");
|
||||
|
||||
assert_eq!(cache.servers[0].tag, "nl-1");
|
||||
@@ -151,7 +177,13 @@ fn redacts_subscription_url_for_display() {
|
||||
);
|
||||
assert_eq!(
|
||||
redact_subscription_url("vless://uuid@example.test"),
|
||||
"vless://uuid@example.test/..."
|
||||
"vless://example.test/..."
|
||||
);
|
||||
assert_eq!(
|
||||
redact_subscription_url(
|
||||
"https://user:password@sub.example.test:8443/path?token=secret#frag"
|
||||
),
|
||||
"https://sub.example.test:8443/..."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user