212 lines
6.9 KiB
Rust
212 lines
6.9 KiB
Rust
use proxywarden_lib::models::{
|
|
ComponentId, ProfileInput, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol,
|
|
TargetInput, TargetKind,
|
|
};
|
|
use proxywarden_lib::validation::{normalize_profile, normalize_target};
|
|
|
|
#[test]
|
|
fn normalizes_profile_source_items() {
|
|
let profile = normalize_profile(ProfileInput {
|
|
id: Some("Discord + Vesktop".to_string()),
|
|
name: " Discord + Vesktop ".to_string(),
|
|
enabled: true,
|
|
target_id: " home-gateway ".to_string(),
|
|
protocols: vec!["tcp".to_string(), "UDP".to_string(), "TCP".to_string()],
|
|
items: vec![
|
|
ProfileItemInput {
|
|
item_type: "process".to_string(),
|
|
value: "Discord.exe".to_string(),
|
|
recursive: None,
|
|
},
|
|
ProfileItemInput {
|
|
item_type: "folder".to_string(),
|
|
value: "%LOCALAPPDATA%\\Vesktop".to_string(),
|
|
recursive: Some(true),
|
|
},
|
|
ProfileItemInput {
|
|
item_type: "exe".to_string(),
|
|
value: "C:\\Games\\Game\\game.exe".to_string(),
|
|
recursive: Some(true),
|
|
},
|
|
],
|
|
})
|
|
.expect("profile should normalize");
|
|
|
|
assert_eq!(profile.id, "discord-vesktop");
|
|
assert_eq!(profile.name, "Discord + Vesktop");
|
|
assert_eq!(profile.target_id, "home-gateway");
|
|
assert_eq!(profile.protocols, vec![Protocol::Tcp, Protocol::Udp]);
|
|
assert_eq!(profile.items[0].item_type, ProfileItemType::Process);
|
|
assert_eq!(profile.items[0].value, "Discord");
|
|
assert!(!profile.items[0].recursive);
|
|
assert_eq!(profile.items[1].item_type, ProfileItemType::Folder);
|
|
assert!(profile.items[1].recursive);
|
|
assert_eq!(profile.items[2].item_type, ProfileItemType::Exe);
|
|
assert!(!profile.items[2].recursive);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unsupported_profile_protocols() {
|
|
let error = normalize_profile(ProfileInput {
|
|
id: None,
|
|
name: "Bad protocol".to_string(),
|
|
enabled: true,
|
|
target_id: "home-gateway".to_string(),
|
|
protocols: vec!["icmp".to_string()],
|
|
items: vec![ProfileItemInput {
|
|
item_type: "process".to_string(),
|
|
value: "Discord".to_string(),
|
|
recursive: None,
|
|
}],
|
|
})
|
|
.expect_err("unsupported protocol should fail");
|
|
|
|
assert!(error.iter().any(|item| item.field == "protocols"));
|
|
}
|
|
|
|
#[test]
|
|
fn normalizes_external_target_without_local_singbox() {
|
|
let target = normalize_target(TargetInput {
|
|
id: Some("Home Gateway".to_string()),
|
|
name: " Home Gateway ".to_string(),
|
|
kind: "external".to_string(),
|
|
protocol: "socks5".to_string(),
|
|
host: " 192.168.50.111 ".to_string(),
|
|
port: 8080,
|
|
requires_component: None,
|
|
})
|
|
.expect("external target should normalize");
|
|
|
|
assert_eq!(target.id, "home-gateway");
|
|
assert_eq!(target.kind, TargetKind::External);
|
|
assert_eq!(target.protocol, ProxyProtocol::Socks5);
|
|
assert_eq!(target.host, "192.168.50.111");
|
|
assert_eq!(target.port, 8080);
|
|
assert_eq!(target.requires_component, None);
|
|
}
|
|
|
|
#[test]
|
|
fn local_singbox_target_can_exist_before_component_is_installed() {
|
|
let target = normalize_target(TargetInput {
|
|
id: Some("local-singbox".to_string()),
|
|
name: "Local sing-box".to_string(),
|
|
kind: "local".to_string(),
|
|
protocol: "socks5".to_string(),
|
|
host: "127.0.0.1".to_string(),
|
|
port: 1080,
|
|
requires_component: Some("singbox".to_string()),
|
|
})
|
|
.expect("local target definition should not require installed component");
|
|
|
|
assert_eq!(target.kind, TargetKind::Local);
|
|
assert_eq!(target.requires_component, Some(ComponentId::Singbox));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_malformed_target_fields() {
|
|
let error = normalize_target(TargetInput {
|
|
id: None,
|
|
name: "".to_string(),
|
|
kind: "external".to_string(),
|
|
protocol: "ftp".to_string(),
|
|
host: "".to_string(),
|
|
port: 70_000,
|
|
requires_component: Some("unknown".to_string()),
|
|
})
|
|
.expect_err("invalid target should fail");
|
|
|
|
assert!(error.iter().any(|item| item.field == "name"));
|
|
assert!(error.iter().any(|item| item.field == "host"));
|
|
assert!(error.iter().any(|item| item.field == "port"));
|
|
assert!(error.iter().any(|item| item.field == "protocol"));
|
|
assert!(error.iter().any(|item| item.field == "requires_component"));
|
|
}
|
|
|
|
#[test]
|
|
fn unicode_names_receive_distinct_stable_ids() {
|
|
let profile = normalize_profile(ProfileInput {
|
|
id: None,
|
|
name: "Игры".to_string(),
|
|
enabled: true,
|
|
target_id: "main-proxy".to_string(),
|
|
protocols: vec!["TCP".to_string()],
|
|
items: vec![ProfileItemInput {
|
|
item_type: "process".to_string(),
|
|
value: "game.exe".to_string(),
|
|
recursive: None,
|
|
}],
|
|
})
|
|
.expect("unicode profile should normalize");
|
|
let other = normalize_profile(ProfileInput {
|
|
id: None,
|
|
name: "Работа".to_string(),
|
|
enabled: true,
|
|
target_id: "main-proxy".to_string(),
|
|
protocols: vec!["TCP".to_string()],
|
|
items: vec![ProfileItemInput {
|
|
item_type: "process".to_string(),
|
|
value: "work.exe".to_string(),
|
|
recursive: None,
|
|
}],
|
|
})
|
|
.expect("second unicode profile should normalize");
|
|
|
|
assert!(profile.id.starts_with("profile-"));
|
|
assert!(other.id.starts_with("profile-"));
|
|
assert_ne!(profile.id, other.id);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_host_with_scheme_credentials_or_path() {
|
|
for host in [
|
|
"socks5://proxy.example.test",
|
|
"user@proxy.example.test",
|
|
"proxy.example.test/path",
|
|
] {
|
|
let error = normalize_target(TargetInput {
|
|
id: None,
|
|
name: "Invalid host".to_string(),
|
|
kind: "external".to_string(),
|
|
protocol: "socks5".to_string(),
|
|
host: host.to_string(),
|
|
port: 1080,
|
|
requires_component: None,
|
|
})
|
|
.expect_err("host must not contain URL syntax");
|
|
|
|
assert!(error.iter().any(|item| item.field == "host"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_relative_or_non_executable_profile_paths() {
|
|
let error = normalize_profile(ProfileInput {
|
|
id: None,
|
|
name: "Invalid paths".to_string(),
|
|
enabled: true,
|
|
target_id: "main-proxy".to_string(),
|
|
protocols: vec!["TCP".to_string()],
|
|
items: vec![
|
|
ProfileItemInput {
|
|
item_type: "folder".to_string(),
|
|
value: r"relative\folder".to_string(),
|
|
recursive: None,
|
|
},
|
|
ProfileItemInput {
|
|
item_type: "exe".to_string(),
|
|
value: r"C:\Games\game.txt".to_string(),
|
|
recursive: None,
|
|
},
|
|
],
|
|
})
|
|
.expect_err("unsafe path shapes should fail validation");
|
|
|
|
assert_eq!(
|
|
error
|
|
.iter()
|
|
.filter(|item| item.field == "items.value")
|
|
.count(),
|
|
2
|
|
);
|
|
}
|