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, }; use std::io::{Read, Write}; use std::net::TcpListener; use std::time::Duration; #[test] fn parses_singbox_json_config_servers() { let parsed = parse_subscription_body( r#"{ "outbounds": [ { "type": "direct", "tag": "direct" }, { "type": "vless", "tag": "nl-1", "server": "nl.example.test", "server_port": 443 }, { "type": "trojan", "tag": "de-1", "server": "de.example.test", "server_port": 8443 } ] }"#, ) .expect("json subscription should parse"); assert_eq!(parsed.servers.len(), 2); assert_eq!(parsed.servers[0].tag, "nl-1"); assert_eq!(parsed.servers[0].server_type, "vless"); assert_eq!(parsed.servers[1].server_port, 8443); } #[test] fn parses_base64_vless_link_list() { let link = sample_vless_link("nl-1"); let encoded = general_purpose::STANDARD.encode(format!("{link}\n")); let parsed = parse_subscription_body(&encoded).expect("base64 vless list should parse"); let outbound = &parsed.config["outbounds"][0]; assert_eq!(parsed.servers.len(), 1); assert_eq!(parsed.servers[0].tag, "nl-1"); assert_eq!(outbound["type"], "vless"); assert_eq!(outbound["server"], "nl.example.test"); assert_eq!(outbound["packet_encoding"], "xudp"); } #[test] fn decodes_percent_encoded_vless_fragment_tag() { let link = sample_vless_link( "%D0%A3%D0%BC%D0%BD%D1%8B%D0%B9%20%F0%9F%87%B3%F0%9F%87%B1-%3E%F0%9F%87%B7%F0%9F%87%BA", ); let parsed = parse_subscription_body(&link).expect("vless link should parse"); let outbound = &parsed.config["outbounds"][0]; assert_eq!(parsed.servers[0].tag, "Умный 🇳🇱->🇷🇺"); assert_eq!(outbound["tag"], "Умный 🇳🇱->🇷🇺"); } #[test] fn rejects_body_without_supported_outbounds() { let error = parse_subscription_body(r#"{"outbounds":[{"type":"direct","tag":"direct"}]}"#) .expect_err("unsupported subscription should fail"); assert!(error.message.contains("No supported proxy outbounds found")); } #[test] fn rejects_vless_without_reality_parameters() { let error = parse_subscription_body("vless://uuid@nl.example.test:443#nl-1") .expect_err("missing reality params should fail"); assert!(error.message.contains("pbk and sid")); } #[test] fn parses_subscription_user_info_header() { let user_info = parse_user_info(Some("upload=10; download=20; total=30; expire=bad")); assert_eq!(user_info["upload"], 10); assert_eq!(user_info["download"], 20); assert_eq!(user_info["total"], 30); assert!(!user_info.contains_key("expire")); } #[test] fn rejects_invalid_or_non_http_subscription_url_before_network() { let invalid = subscription::fetch_subscription("not a url") .expect_err("invalid url should fail before request"); let unsupported = subscription::fetch_subscription("file:///C:/subscription.txt") .expect_err("non-http url should fail before request"); assert!(invalid.message.contains("Invalid subscription URL")); assert!(unsupported.message.contains("http or https")); } #[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"); let url = format!("http://{}/subscription", listener.local_addr().unwrap()); let request_thread = std::thread::spawn(move || { let (mut stream, _) = listener.accept().expect("accept test request"); stream .set_read_timeout(Some(Duration::from_secs(2))) .expect("set read timeout"); let mut request = Vec::new(); let mut buffer = [0_u8; 512]; loop { let bytes_read = stream.read(&mut buffer).expect("read request"); if bytes_read == 0 { break; } request.extend_from_slice(&buffer[..bytes_read]); if request.windows(4).any(|window| window == b"\r\n\r\n") { break; } } let body = r#"{"outbounds":[{"type":"vless","tag":"nl-1","server":"nl.example.test","server_port":443}]}"#; let response = format!( "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{}", body.len(), body ); stream .write_all(response.as_bytes()) .expect("write response"); String::from_utf8_lossy(&request).to_ascii_lowercase() }); 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 request = request_thread.join().expect("request thread"); assert_eq!(cache.servers[0].tag, "nl-1"); assert!(request.contains("x-hwid: hwid-abc123")); assert!(request.contains("user-agent: proxywarden/")); assert!(request.contains("x-app-name: proxywarden")); assert!(request.contains("x-device-os:")); assert!(request.contains("x-device-os-version: windows 11 pro | 25h2 | build 26200.8655")); assert!(request.contains("x-device-model: proxywarden")); } #[test] fn redacts_subscription_url_for_display() { assert_eq!( redact_subscription_url("https://sub.example.test/path?token=secret"), "https://sub.example.test/..." ); assert_eq!( redact_subscription_url("vless://uuid@example.test"), "vless://uuid@example.test/..." ); } fn sample_vless_link(tag: &str) -> String { format!( "vless://{}@nl.example.test:443?security=reality&sni=example.test&fp=chrome&pbk=public-key&sid=short-id&flow=xtls-rprx-vision#{}", "11111111-1111-1111-1111-111111111111", tag ) }