package proxy import ( "encoding/base64" "net/url" "reflect" "strings" "testing" "github.com/peterqiu0516/sub-store/internal/model" ) // ptrBool is a helper to take the address of a bool literal. func ptrBool(b bool) *bool { return &b } // nodeType returns the node's type, asserting string. func nodeType(t *testing.T, n model.ProxyNode, want string) { t.Helper() got, _ := n["type"].(string) if got != want { t.Errorf("type = %q, want %q", got, want) } } // -------------------------------------------------------------------------------- // NormalizeClientProxyKind // -------------------------------------------------------------------------------- func TestNormalizeClientProxyKind(t *testing.T) { cases := []struct { in string want string }{ // aliases {"shadowsocks", "ss"}, {"Shadowsocks", "ss"}, {" shadowsocks ", "ss"}, {"socks5-tls", "socks5"}, {"https", "http"}, {"hysteria2", "hysteria2"}, {"hysteria 2", "hysteria2"}, {"tuic-v5", "tuic"}, // canonical names {"ss", "ss"}, {"ssr", "ssr"}, {"vmess", "vmess"}, {"vless", "vless"}, {"trojan", "trojan"}, {"http", "http"}, {"socks5", "socks5"}, {"tuic", "tuic"}, {"anytls", "anytls"}, {"snell", "snell"}, {"ssh", "ssh"}, {"h2-connect", "h2-connect"}, // unknown {"unknown", ""}, {"", ""}, {"xyz", ""}, // case-insensitivity {"VMess", "vmess"}, {"TuIC-V5", "tuic"}, } for _, c := range cases { if got := NormalizeClientProxyKind(c.in); got != c.want { t.Errorf("NormalizeClientProxyKind(%q) = %q, want %q", c.in, got, c.want) } } } // -------------------------------------------------------------------------------- // SplitClientCsv // -------------------------------------------------------------------------------- func TestSplitClientCsv(t *testing.T) { t.Run("basic", func(t *testing.T) { got := SplitClientCsv("a, b, c") want := []string{"a", "b", "c"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } }) t.Run("double_quotes", func(t *testing.T) { got := SplitClientCsv(`a, "b,c", d`) want := []string{"a", "b,c", "d"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } }) t.Run("single_quotes", func(t *testing.T) { got := SplitClientCsv(`a, 'b,c', d`) want := []string{"a", "b,c", "d"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } }) t.Run("empty_parts_filtered", func(t *testing.T) { got := SplitClientCsv("a, , b") want := []string{"a", "b"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } }) t.Run("all_empty", func(t *testing.T) { got := SplitClientCsv(" , , ") if len(got) != 0 { t.Errorf("got %v, want empty", got) } }) t.Run("empty_input", func(t *testing.T) { got := SplitClientCsv("") if len(got) != 0 { t.Errorf("got %v, want empty", got) } }) } // -------------------------------------------------------------------------------- // ParseClientOptions // -------------------------------------------------------------------------------- func TestParseClientOptions(t *testing.T) { t.Run("basic_kv", func(t *testing.T) { got := ParseClientOptions([]string{"foo=bar", "Baz=Qux"}) want := map[string]string{"foo": "bar", "baz": "Qux"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } }) t.Run("no_key_skipped", func(t *testing.T) { // "=value" has equalIndex==0, skipped got := ParseClientOptions([]string{"=value", "key=val"}) if len(got) != 1 || got["key"] != "val" { t.Errorf("got %v, want only key=val", got) } }) t.Run("no_equal_skipped", func(t *testing.T) { got := ParseClientOptions([]string{"novalue", "key=val"}) if len(got) != 1 || got["key"] != "val" { t.Errorf("got %v, want only key=val", got) } }) t.Run("unquoted", func(t *testing.T) { got := ParseClientOptions([]string{`key="quoted value"`}) if got["key"] != "quoted value" { t.Errorf("got %q, want %q", got["key"], "quoted value") } }) t.Run("trimmed", func(t *testing.T) { got := ParseClientOptions([]string{" key = value "}) if got["key"] != "value" { t.Errorf("got %q, want %q", got["key"], "value") } }) t.Run("empty_input", func(t *testing.T) { got := ParseClientOptions(nil) if len(got) != 0 { t.Errorf("got %v, want empty", got) } }) } // -------------------------------------------------------------------------------- // ClientOption // -------------------------------------------------------------------------------- func TestClientOption(t *testing.T) { opts := map[string]string{"foo": "bar"} if got := ClientOption(opts, "foo"); got != "bar" { t.Errorf("got %q, want bar", got) } if got := ClientOption(opts, "FOO"); got != "bar" { t.Errorf("case-insensitive lookup failed: got %q", got) } if got := ClientOption(opts, "missing"); got != "" { t.Errorf("got %q, want empty", got) } } // -------------------------------------------------------------------------------- // OptionBoolean // -------------------------------------------------------------------------------- func TestOptionBoolean(t *testing.T) { trueVals := []string{"1", "true", "yes", "on", "enabled", "TRUE", " Yes "} falseVals := []string{"0", "false", "no", "off", "disabled", "False", " NO "} unknownVals := []string{"", "maybe", "2", "yesno", "y"} for _, v := range trueVals { got := OptionBoolean(v) if got == nil || !*got { t.Errorf("OptionBoolean(%q) = %v, want true", v, got) } } for _, v := range falseVals { got := OptionBoolean(v) if got == nil || *got { t.Errorf("OptionBoolean(%q) = %v, want false", v, got) } } for _, v := range unknownVals { got := OptionBoolean(v) if got != nil { t.Errorf("OptionBoolean(%q) = %v, want nil", v, got) } } } // -------------------------------------------------------------------------------- // OptionBooleanInverted // -------------------------------------------------------------------------------- func TestOptionBooleanInverted(t *testing.T) { // true-ish inputs → false trueVals := []string{"1", "true", "yes", "on", "enabled"} for _, v := range trueVals { got := OptionBooleanInverted(v) if got == nil || *got { t.Errorf("OptionBooleanInverted(%q) = %v, want false", v, got) } } // false-ish inputs → true falseVals := []string{"0", "false", "no", "off", "disabled"} for _, v := range falseVals { got := OptionBooleanInverted(v) if got == nil || !*got { t.Errorf("OptionBooleanInverted(%q) = %v, want true", v, got) } } // unknown → nil for _, v := range []string{"", "maybe", "2"} { if got := OptionBooleanInverted(v); got != nil { t.Errorf("OptionBooleanInverted(%q) = %v, want nil", v, got) } } } // -------------------------------------------------------------------------------- // ClientCommonOptions // -------------------------------------------------------------------------------- func TestClientCommonOptions(t *testing.T) { t.Run("skip_cert_verify_true", func(t *testing.T) { opts := map[string]string{"skip-cert-verify": "true"} got := ClientCommonOptions(opts) if v, ok := got["skip-cert-verify"].(bool); !ok || !v { t.Errorf("expected skip-cert-verify=true, got %v", got["skip-cert-verify"]) } }) t.Run("skip_cert_verify_false", func(t *testing.T) { opts := map[string]string{"skip-cert-verify": "false"} got := ClientCommonOptions(opts) if v, ok := got["skip-cert-verify"].(bool); !ok || v { t.Errorf("expected skip-cert-verify=false, got %v", got["skip-cert-verify"]) } }) t.Run("tls_verification_inverted", func(t *testing.T) { // tls-verification=false → skip=true opts := map[string]string{"tls-verification": "false"} got := ClientCommonOptions(opts) if v, ok := got["skip-cert-verify"].(bool); !ok || !v { t.Errorf("expected skip-cert-verify=true via inverted, got %v", got["skip-cert-verify"]) } }) t.Run("tls_verification_true", func(t *testing.T) { // tls-verification=true → skip=false opts := map[string]string{"tls-verification": "true"} got := ClientCommonOptions(opts) if v, ok := got["skip-cert-verify"].(bool); !ok || v { t.Errorf("expected skip-cert-verify=false via inverted, got %v", got["skip-cert-verify"]) } }) t.Run("skip_cert_verify_precedence", func(t *testing.T) { // skip-cert-verify takes precedence over tls-verification opts := map[string]string{ "skip-cert-verify": "true", "tls-verification": "true", } got := ClientCommonOptions(opts) if v, ok := got["skip-cert-verify"].(bool); !ok || !v { t.Errorf("expected precedence skip-cert-verify=true, got %v", got["skip-cert-verify"]) } }) t.Run("fingerprint", func(t *testing.T) { opts := map[string]string{"client-fingerprint": "chrome"} got := ClientCommonOptions(opts) if got["client-fingerprint"] != "chrome" { t.Errorf("expected client-fingerprint=chrome, got %v", got["client-fingerprint"]) } }) t.Run("fingerprint_alias", func(t *testing.T) { opts := map[string]string{"fingerprint": "firefox"} got := ClientCommonOptions(opts) if got["client-fingerprint"] != "firefox" { t.Errorf("expected client-fingerprint=firefox, got %v", got["client-fingerprint"]) } }) t.Run("empty", func(t *testing.T) { got := ClientCommonOptions(map[string]string{}) if len(got) != 0 { t.Errorf("expected empty map, got %v", got) } }) } // -------------------------------------------------------------------------------- // unquoteClientValue // -------------------------------------------------------------------------------- func TestUnquoteClientValue(t *testing.T) { cases := []struct { in string want string }{ {`"hello"`, "hello"}, {`'hello'`, "hello"}, {`"hello`, `"hello`}, // unbalanced {`hello`, "hello"}, // no quotes {`""`, ""}, // empty quoted {`''`, ""}, // empty quoted {`h`, "h"}, // single char {``, ""}, // empty {` "trimmed" `, "trimmed"}, // trimmed } for _, c := range cases { if got := unquoteClientValue(c.in); got != c.want { t.Errorf("unquoteClientValue(%q) = %q, want %q", c.in, got, c.want) } } } // -------------------------------------------------------------------------------- // qxTlsEnabled // -------------------------------------------------------------------------------- func TestQxTlsEnabled(t *testing.T) { cases := []struct { name string options map[string]string want bool }{ {"obfs_tls", map[string]string{"obfs": "tls"}, true}, {"obfs_wss", map[string]string{"obfs": "wss"}, true}, {"obfs_over_tls", map[string]string{"obfs": "over-tls"}, true}, {"over-tls_bool_true", map[string]string{"over-tls": "true"}, true}, {"tls_bool_true", map[string]string{"tls": "1"}, true}, {"obfs_ws", map[string]string{"obfs": "ws"}, false}, {"empty", map[string]string{}, false}, {"over-tls_false", map[string]string{"over-tls": "false"}, false}, {"tls_invalid", map[string]string{"tls": "maybe"}, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := qxTlsEnabled(c.options); got != c.want { t.Errorf("qxTlsEnabled(%v) = %v, want %v", c.options, got, c.want) } }) } } // -------------------------------------------------------------------------------- // qxNetwork // -------------------------------------------------------------------------------- func TestQxNetwork(t *testing.T) { cases := []struct { options map[string]string want string }{ {map[string]string{"obfs": "ws"}, "ws"}, {map[string]string{"obfs": "wss"}, "ws"}, {map[string]string{"obfs": "tls"}, "tcp"}, {map[string]string{}, "tcp"}, } for _, c := range cases { if got := qxNetwork(c.options); got != c.want { t.Errorf("qxNetwork(%v) = %q, want %q", c.options, got, c.want) } } } // -------------------------------------------------------------------------------- // qxWsOptions // -------------------------------------------------------------------------------- func TestQxWsOptions(t *testing.T) { t.Run("not_ws_returns_nil", func(t *testing.T) { if got := qxWsOptions(map[string]string{"obfs": "tls"}); got != nil { t.Errorf("expected nil, got %v", got) } }) t.Run("ws_with_host_and_path", func(t *testing.T) { got := qxWsOptions(map[string]string{"obfs": "ws", "obfs-host": "example.com", "obfs-uri": "/ws"}) m, ok := got.(map[string]any) if !ok { t.Fatalf("expected map, got %T", got) } if m["path"] != "/ws" { t.Errorf("path = %v, want /ws", m["path"]) } headers, ok := m["headers"].(map[string]any) if !ok { t.Fatalf("expected headers map, got %T", m["headers"]) } if headers["Host"] != "example.com" { t.Errorf("Host = %v, want example.com", headers["Host"]) } }) t.Run("ws_default_path", func(t *testing.T) { got := qxWsOptions(map[string]string{"obfs": "ws"}).(map[string]any) if got["path"] != "/" { t.Errorf("path = %v, want /", got["path"]) } if _, ok := got["headers"]; ok { t.Errorf("expected no headers, got %v", got["headers"]) } }) } // -------------------------------------------------------------------------------- // qxPlugin / qxPluginOptions // -------------------------------------------------------------------------------- func TestQxPlugin(t *testing.T) { cases := []struct { options map[string]string want any }{ {map[string]string{"obfs": "http"}, "obfs"}, {map[string]string{"obfs": "shadowsocks-http"}, "obfs"}, {map[string]string{"obfs": "tls"}, nil}, {map[string]string{}, nil}, } for _, c := range cases { if got := qxPlugin(c.options); got != c.want { t.Errorf("qxPlugin(%v) = %v, want %v", c.options, got, c.want) } } } func TestQxPluginOptions(t *testing.T) { t.Run("nil_when_no_plugin", func(t *testing.T) { if got := qxPluginOptions(map[string]string{"obfs": "tls"}); got != nil { t.Errorf("expected nil, got %v", got) } }) t.Run("with_plugin", func(t *testing.T) { got := qxPluginOptions(map[string]string{"obfs": "http", "obfs-host": "h", "obfs-uri": "/p"}).(map[string]any) if got["mode"] != "http" { t.Errorf("mode = %v, want http", got["mode"]) } if got["host"] != "h" { t.Errorf("host = %v, want h", got["host"]) } if got["path"] != "/p" { t.Errorf("path = %v, want /p", got["path"]) } }) } // -------------------------------------------------------------------------------- // namedClientNetwork // -------------------------------------------------------------------------------- func TestNamedClientNetwork(t *testing.T) { cases := []struct { name string options map[string]string want string }{ {"ws_true", map[string]string{"ws": "true"}, "ws"}, {"transport", map[string]string{"transport": "grpc"}, "grpc"}, {"network", map[string]string{"network": "h2"}, "h2"}, {"transport_over_network", map[string]string{"transport": "grpc", "network": "h2"}, "grpc"}, {"default_tcp", map[string]string{}, "tcp"}, {"ws_false_uses_tcp", map[string]string{"ws": "false"}, "tcp"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := namedClientNetwork(c.options); got != c.want { t.Errorf("namedClientNetwork(%v) = %q, want %q", c.options, got, c.want) } }) } } // -------------------------------------------------------------------------------- // namedClientWsOptions // -------------------------------------------------------------------------------- func TestNamedClientWsOptions(t *testing.T) { t.Run("not_ws_returns_nil", func(t *testing.T) { if got := namedClientWsOptions(map[string]string{}); got != nil { t.Errorf("expected nil, got %v", got) } }) t.Run("ws_path_from_ws_path", func(t *testing.T) { got := namedClientWsOptions(map[string]string{"ws": "true", "ws-path": "/p"}).(map[string]any) if got["path"] != "/p" { t.Errorf("path = %v, want /p", got["path"]) } }) t.Run("ws_path_from_path", func(t *testing.T) { got := namedClientWsOptions(map[string]string{"ws": "true", "path": "/x"}).(map[string]any) if got["path"] != "/x" { t.Errorf("path = %v, want /x", got["path"]) } }) t.Run("ws_default_path", func(t *testing.T) { got := namedClientWsOptions(map[string]string{"ws": "true"}).(map[string]any) if got["path"] != "/" { t.Errorf("path = %v, want /", got["path"]) } }) t.Run("ws_host_from_ws_headers", func(t *testing.T) { got := namedClientWsOptions(map[string]string{"ws": "true", "ws-headers": "Host: example.com"}).(map[string]any) headers := got["headers"].(map[string]any) if headers["Host"] != "example.com" { t.Errorf("Host = %v, want example.com", headers["Host"]) } }) t.Run("ws_host_from_ws_host", func(t *testing.T) { got := namedClientWsOptions(map[string]string{"ws": "true", "ws-host": "h.example.com"}).(map[string]any) headers := got["headers"].(map[string]any) if headers["Host"] != "h.example.com" { t.Errorf("Host = %v, want h.example.com", headers["Host"]) } }) t.Run("ws_host_from_host", func(t *testing.T) { got := namedClientWsOptions(map[string]string{"ws": "true", "host": "hh.example.com"}).(map[string]any) headers := got["headers"].(map[string]any) if headers["Host"] != "hh.example.com" { t.Errorf("Host = %v, want hh.example.com", headers["Host"]) } }) } // -------------------------------------------------------------------------------- // parseRealityOptions // -------------------------------------------------------------------------------- func TestParseRealityOptions(t *testing.T) { t.Run("nil_when_no_pubkey", func(t *testing.T) { if got := parseRealityOptions(map[string]string{}); got != nil { t.Errorf("expected nil, got %v", got) } }) t.Run("reality-base64-pubkey", func(t *testing.T) { got := parseRealityOptions(map[string]string{ "reality-base64-pubkey": "PUB", "reality-hex-shortid": "abcd", }).(map[string]any) if got["public-key"] != "PUB" { t.Errorf("public-key = %v", got["public-key"]) } if got["short-id"] != "abcd" { t.Errorf("short-id = %v", got["short-id"]) } }) t.Run("public-key_alias", func(t *testing.T) { got := parseRealityOptions(map[string]string{ "public-key": "PK", "short-id": "sid", }).(map[string]any) if got["public-key"] != "PK" { t.Errorf("public-key = %v", got["public-key"]) } if got["short-id"] != "sid" { t.Errorf("short-id = %v", got["short-id"]) } }) } // -------------------------------------------------------------------------------- // toFloat, getAt, ternary // -------------------------------------------------------------------------------- func TestToFloat(t *testing.T) { cases := []struct { in string want float64 }{ {"443", 443}, {" 8080 ", 8080}, {"", 0}, {"abc", 0}, {"3.14", 3.14}, {"-1", -1}, } for _, c := range cases { if got := toFloat(c.in); got != c.want { t.Errorf("toFloat(%q) = %v, want %v", c.in, got, c.want) } } } func TestGetAt(t *testing.T) { s := []string{"a", "b", "c"} cases := []struct { i int want string }{ {0, "a"}, {2, "c"}, {-1, ""}, {3, ""}, {5, ""}, } for _, c := range cases { if got := getAt(s, c.i); got != c.want { t.Errorf("getAt(_, %d) = %q, want %q", c.i, got, c.want) } } if got := getAt(nil, 0); got != "" { t.Errorf("getAt(nil, 0) = %q, want empty", got) } } func TestTernary(t *testing.T) { if got := ternary(true, "yes", "no"); got != "yes" { t.Errorf("ternary(true) = %q", got) } if got := ternary(false, "yes", "no"); got != "no" { t.Errorf("ternary(false) = %q", got) } } // -------------------------------------------------------------------------------- // ParseQxProxyLine // -------------------------------------------------------------------------------- func TestParseQxProxyLine(t *testing.T) { boolVal := func(m map[string]any, key string) *bool { v, _ := m[key].(*bool) return v } t.Run("shadowsocks", func(t *testing.T) { line := "shadowsocks = 1.2.3.4:8388, tag=SS, method=aes-256-gcm, password=pass, obfs=http, obfs-host=h.com, udp-relay=true, fast-open=1, skip-cert-verify=true, client-fingerprint=chrome" n := ParseQxProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ss") if n["name"] != "SS" { t.Errorf("name = %v", n["name"]) } if n["cipher"] != "aes-256-gcm" { t.Errorf("cipher = %v", n["cipher"]) } if n["plugin"] != "obfs" { t.Errorf("plugin = %v", n["plugin"]) } if b := boolVal(n, "udp"); b == nil || !*b { t.Errorf("udp = %v, want true", n["udp"]) } if b := boolVal(n, "tfo"); b == nil || !*b { t.Errorf("tfo = %v, want true", n["tfo"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } port, _ := n["port"].(float64) if port != 8388 { t.Errorf("port = %v", port) } }) t.Run("shadowsocks_default_port", func(t *testing.T) { line := "shadowsocks = 1.2.3.4, tag=S" n := ParseQxProxyLine(line, 0) port, _ := n["port"].(float64) if port != 443 { t.Errorf("default port = %v, want 443", port) } }) t.Run("shadowsocks_port_from_option", func(t *testing.T) { line := "shadowsocks = 1.2.3.4, tag=S, port=8388" n := ParseQxProxyLine(line, 0) port, _ := n["port"].(float64) if port != 8388 { t.Errorf("port = %v, want 8388", port) } }) t.Run("vmess_ws", func(t *testing.T) { line := "vmess = 1.2.3.4:443, tag=VM, password=uuid-1234, method=aes-128-gcm, alterId=64, obfs=ws, obfs-host=h.com, obfs-uri=/ws, over-tls=true" n := ParseQxProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "vmess") if n["uuid"] != "uuid-1234" { t.Errorf("uuid = %v", n["uuid"]) } if n["cipher"] != "aes-128-gcm" { t.Errorf("cipher = %v", n["cipher"]) } if n["network"] != "ws" { t.Errorf("network = %v", n["network"]) } if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } ws, ok := n["ws-opts"].(map[string]any) if !ok { t.Fatalf("expected ws-opts map, got %T", n["ws-opts"]) } if ws["path"] != "/ws" { t.Errorf("ws path = %v", ws["path"]) } aid, _ := n["alterId"].(float64) if aid != 64 { t.Errorf("alterId = %v", aid) } }) t.Run("vmess_default_cipher", func(t *testing.T) { line := "vmess = 1.2.3.4:443, tag=VM, password=uuid" n := ParseQxProxyLine(line, 0) if n["cipher"] != "auto" { t.Errorf("cipher = %v, want auto", n["cipher"]) } }) t.Run("vless_reality", func(t *testing.T) { line := "vless = 1.2.3.4:443, tag=VL, password=uuid, flow=xtls-rprx-vision, reality-base64-pubkey=PUB, reality-hex-shortid=ab, encryption=none" n := ParseQxProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "vless") if n["flow"] != "xtls-rprx-vision" { t.Errorf("flow = %v", n["flow"]) } if n["encryption"] != "none" { t.Errorf("encryption = %v", n["encryption"]) } ro, ok := n["reality-opts"].(map[string]any) if !ok { t.Fatalf("expected reality-opts map, got %T", n["reality-opts"]) } if ro["public-key"] != "PUB" { t.Errorf("public-key = %v", ro["public-key"]) } }) t.Run("vless_default_encryption", func(t *testing.T) { line := "vless = 1.2.3.4:443, tag=VL, password=uuid" n := ParseQxProxyLine(line, 0) if n["encryption"] != "none" { t.Errorf("encryption = %v, want none", n["encryption"]) } }) t.Run("trojan", func(t *testing.T) { line := "trojan = 1.2.3.4:443, tag=TJ, password=pass, tls-host=sni.com, skip-cert-verify=false" n := ParseQxProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "trojan") if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if n["sni"] != "sni.com" { t.Errorf("sni = %v", n["sni"]) } if n["skip-cert-verify"] != false { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } }) t.Run("anytls", func(t *testing.T) { line := "anytls = 1.2.3.4:443, tag=AT, password=pass, tls-host=sni.com" n := ParseQxProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "anytls") if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } }) t.Run("http_default_port", func(t *testing.T) { line := "http = 1.2.3.4, tag=HTTP, username=user, password=pass" n := ParseQxProxyLine(line, 0) nodeType(t, n, "http") port, _ := n["port"].(float64) if port != 80 { t.Errorf("port = %v, want 80", port) } if n["username"] != "user" { t.Errorf("username = %v", n["username"]) } }) t.Run("http_with_tls", func(t *testing.T) { line := "http = 1.2.3.4:8080, tag=HTTPS, over-tls=true" n := ParseQxProxyLine(line, 0) if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } }) t.Run("socks5_default_port", func(t *testing.T) { line := "socks5 = 1.2.3.4, tag=SK, username=u, password=p" n := ParseQxProxyLine(line, 0) nodeType(t, n, "socks5") port, _ := n["port"].(float64) if port != 80 { t.Errorf("port = %v, want 80", port) } }) t.Run("invalid_no_equal", func(t *testing.T) { if n := ParseQxProxyLine("shadowsocks no equal here", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) t.Run("invalid_empty_parts", func(t *testing.T) { if n := ParseQxProxyLine("shadowsocks = ", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) t.Run("unknown_kind", func(t *testing.T) { // QX regex only matches known kinds; this won't match but ParseQxProxyLine // called directly with an unknown kind returns nil if n := ParseQxProxyLine("unknown = 1.2.3.4:443, tag=X", 0); n != nil { t.Errorf("expected nil for unknown kind, got %v", n) } }) t.Run("tag_default", func(t *testing.T) { line := "shadowsocks = 1.2.3.4:8388, method=aes-256-gcm" n := ParseQxProxyLine(line, 5) if n["name"] != "shadowsocks-6" { t.Errorf("default name = %v, want shadowsocks-6", n["name"]) } }) } // -------------------------------------------------------------------------------- // ParseNamedClientProxyLine // -------------------------------------------------------------------------------- func TestParseNamedClientProxyLine(t *testing.T) { t.Run("ss", func(t *testing.T) { line := "MySS = ss, 1.2.3.4, 8388, aes-256-gcm, pass, udp=true, skip-cert-verify=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ss") if n["name"] != "MySS" { t.Errorf("name = %v", n["name"]) } if n["cipher"] != "aes-256-gcm" { t.Errorf("cipher = %v", n["cipher"]) } if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if b, ok := n["udp"].(*bool); !ok || !*b { t.Errorf("udp = %v, want *bool(true)", n["udp"]) } }) t.Run("ss_with_obfs", func(t *testing.T) { line := "MySS = ss, 1.2.3.4, 8388, aes-256-gcm, pass, obfs=http, obfs-host=h.com" n := ParseNamedClientProxyLine(line, 0) if n["plugin"] != "obfs" { t.Errorf("plugin = %v", n["plugin"]) } po, ok := n["plugin-opts"].(map[string]any) if !ok { t.Fatalf("expected plugin-opts map, got %T", n["plugin-opts"]) } if po["mode"] != "http" { t.Errorf("mode = %v", po["mode"]) } }) t.Run("ssr", func(t *testing.T) { line := "MySSR = ssr, 1.2.3.4, 8388, aes-256-cfb, pass, protocol=auth_aes128_sha1, obfs=http_simple, protocol-param=pp, obfs-param=op, udp-relay=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ssr") if n["cipher"] != "aes-256-cfb" { t.Errorf("cipher = %v", n["cipher"]) } if n["protocol"] != "auth_aes128_sha1" { t.Errorf("protocol = %v", n["protocol"]) } if n["obfs"] != "http_simple" { t.Errorf("obfs = %v", n["obfs"]) } if n["protocol-param"] != "pp" { t.Errorf("protocol-param = %v", n["protocol-param"]) } if n["obfs-param"] != "op" { t.Errorf("obfs-param = %v", n["obfs-param"]) } }) t.Run("ssr_defaults", func(t *testing.T) { line := "MySSR = ssr, 1.2.3.4, 8388, aes-256-cfb, pass" n := ParseNamedClientProxyLine(line, 0) if n["protocol"] != "origin" { t.Errorf("default protocol = %v, want origin", n["protocol"]) } if n["obfs"] != "plain" { t.Errorf("default obfs = %v, want plain", n["obfs"]) } }) t.Run("vmess_ws", func(t *testing.T) { line := "MyVM = vmess, 1.2.3.4, 443, aes-128-gcm, uuid-1234, ws=true, ws-path=/p, ws-host=h.com, alterId=64, tls=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "vmess") if n["uuid"] != "uuid-1234" { t.Errorf("uuid = %v", n["uuid"]) } if n["network"] != "ws" { t.Errorf("network = %v", n["network"]) } if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } ws, ok := n["ws-opts"].(map[string]any) if !ok { t.Fatalf("expected ws-opts map, got %T", n["ws-opts"]) } if ws["path"] != "/p" { t.Errorf("ws path = %v", ws["path"]) } aid, _ := n["alterId"].(float64) if aid != 64 { t.Errorf("alterId = %v", aid) } }) t.Run("vmess_default_cipher", func(t *testing.T) { // No positional values; cipher falls to "auto" via firstNonEmpty default line := "MyVM = vmess, 1.2.3.4, 443, password=uuid-1234" n := ParseNamedClientProxyLine(line, 0) if n["cipher"] != "auto" { t.Errorf("cipher = %v, want auto", n["cipher"]) } if n["uuid"] != "uuid-1234" { t.Errorf("uuid = %v", n["uuid"]) } }) t.Run("vless_reality", func(t *testing.T) { line := "MyVL = vless, 1.2.3.4, 443, uuid-1234, flow=xtls-rprx-vision, reality-base64-pubkey=PUB, reality-hex-shortid=ab" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "vless") if n["flow"] != "xtls-rprx-vision" { t.Errorf("flow = %v", n["flow"]) } if n["encryption"] != "none" { t.Errorf("encryption = %v", n["encryption"]) } ro, ok := n["reality-opts"].(map[string]any) if !ok { t.Fatalf("expected reality-opts map, got %T", n["reality-opts"]) } if ro["public-key"] != "PUB" { t.Errorf("public-key = %v", ro["public-key"]) } }) t.Run("trojan", func(t *testing.T) { line := "MyTJ = trojan, 1.2.3.4, 443, pass, sni=sni.com, skip-cert-verify=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "trojan") if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if n["sni"] != "sni.com" { t.Errorf("sni = %v", n["sni"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } }) t.Run("anytls", func(t *testing.T) { line := "MyAT = anytls, 1.2.3.4, 443, pass, sni=sni.com" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "anytls") if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } }) t.Run("http_https_kind", func(t *testing.T) { line := "MyHTTP = https, 1.2.3.4, 443, username=user, password=pass, tls=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "http") if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } if n["username"] != "user" { t.Errorf("username = %v", n["username"]) } }) t.Run("http_https_kind_implicit_tls", func(t *testing.T) { // https kind → tls=true by default line := "MyHTTP = https, 1.2.3.4, 443" n := ParseNamedClientProxyLine(line, 0) if n["tls"] != true { t.Errorf("implicit tls = %v, want true", n["tls"]) } }) t.Run("http_kind", func(t *testing.T) { line := "MyHTTP = http, 1.2.3.4, 80, username=u, password=p" n := ParseNamedClientProxyLine(line, 0) nodeType(t, n, "http") if n["tls"] != false { t.Errorf("tls = %v, want false", n["tls"]) } }) t.Run("socks5_tls_kind", func(t *testing.T) { line := "MySK = socks5-tls, 1.2.3.4, 443, username=u, password=p" n := ParseNamedClientProxyLine(line, 0) nodeType(t, n, "socks5") if n["tls"] != true { t.Errorf("tls = %v, want true", n["tls"]) } }) t.Run("socks5_kind", func(t *testing.T) { line := "MySK = socks5, 1.2.3.4, 1080, username=u, password=p" n := ParseNamedClientProxyLine(line, 0) nodeType(t, n, "socks5") if n["tls"] != false { t.Errorf("tls = %v, want false", n["tls"]) } }) t.Run("hysteria2", func(t *testing.T) { line := "MyH2 = hysteria2, 1.2.3.4, 443, pass, sni=sni.com, obfs=salamander, obfs-password=op, skip-cert-verify=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "hysteria2") if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if n["obfs"] != "salamander" { t.Errorf("obfs = %v", n["obfs"]) } if n["obfs-password"] != "op" { t.Errorf("obfs-password = %v", n["obfs-password"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } }) t.Run("hysteria2_gecko_alias", func(t *testing.T) { line := "MyH2 = hysteria2, 1.2.3.4, 443, pass, gecko-password=gp" n := ParseNamedClientProxyLine(line, 0) if n["obfs-password"] != "gp" { t.Errorf("obfs-password = %v, want gp", n["obfs-password"]) } }) t.Run("tuic", func(t *testing.T) { line := "MyTUIC = tuic, 1.2.3.4, 443, uuid-1, pass, sni=sni.com, alpn=\"h3,h4\", skip-cert-verify=true" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "tuic") if n["uuid"] != "uuid-1" { t.Errorf("uuid = %v", n["uuid"]) } if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } alpn, ok := n["alpn"].([]string) if !ok { t.Fatalf("expected alpn []string, got %T", n["alpn"]) } if !reflect.DeepEqual(alpn, []string{"h3", "h4"}) { t.Errorf("alpn = %v", alpn) } }) t.Run("snell_default_version", func(t *testing.T) { line := "MySNELL = snell, 1.2.3.4, 443, psk=mykey" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "snell") if n["psk"] != "mykey" { t.Errorf("psk = %v", n["psk"]) } v, _ := n["version"].(float64) if v != 3 { t.Errorf("default version = %v, want 3", n["version"]) } }) t.Run("snell_explicit_version", func(t *testing.T) { line := "MySNELL = snell, 1.2.3.4, 443, psk=mykey, version=4" n := ParseNamedClientProxyLine(line, 0) v, _ := n["version"].(float64) if v != 4 { t.Errorf("version = %v, want 4", v) } }) t.Run("ssh", func(t *testing.T) { line := "MySSH = ssh, 1.2.3.4, 22, user, pass, private-key=KEY, host-key=HK" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ssh") if n["username"] != "user" { t.Errorf("username = %v", n["username"]) } if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if n["private-key"] != "KEY" { t.Errorf("private-key = %v", n["private-key"]) } if n["host-key"] != "HK" { t.Errorf("host-key = %v", n["host-key"]) } }) t.Run("h2-connect", func(t *testing.T) { line := "MyH2 = h2-connect, 1.2.3.4, 443, username=u, password=p, sni=sni.com" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "h2-connect") if n["tls"] != true { t.Errorf("default tls = %v, want true", n["tls"]) } if n["username"] != "u" { t.Errorf("username = %v", n["username"]) } if n["sni"] != "sni.com" { t.Errorf("sni = %v", n["sni"]) } }) t.Run("h2-connect_tls_false", func(t *testing.T) { line := "MyH2 = h2-connect, 1.2.3.4, 443, tls=false" n := ParseNamedClientProxyLine(line, 0) if n["tls"] != false { t.Errorf("tls = %v, want false", n["tls"]) } }) t.Run("invalid_no_equal", func(t *testing.T) { if n := ParseNamedClientProxyLine("no equal here", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) t.Run("invalid_too_few_parts", func(t *testing.T) { if n := ParseNamedClientProxyLine("name = ss, 1.2.3.4", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) t.Run("invalid_unknown_kind", func(t *testing.T) { if n := ParseNamedClientProxyLine("name = unknown, 1.2.3.4, 443", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) t.Run("invalid_zero_port", func(t *testing.T) { if n := ParseNamedClientProxyLine("name = ss, 1.2.3.4, abc, pass", 0); n != nil { t.Errorf("expected nil for zero port, got %v", n) } }) t.Run("default_name", func(t *testing.T) { line := " = ss, 1.2.3.4, 8388, aes-256-gcm, pass" n := ParseNamedClientProxyLine(line, 2) if n["name"] != "proxy-3" { t.Errorf("default name = %v, want proxy-3", n["name"]) } }) t.Run("shadowsocks_alias", func(t *testing.T) { line := "MySS = shadowsocks, 1.2.3.4, 8388, aes-256-gcm, pass" n := ParseNamedClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ss") }) } // -------------------------------------------------------------------------------- // ParseClientProxyLine (dispatch) // -------------------------------------------------------------------------------- func TestParseClientProxyLine(t *testing.T) { t.Run("qx_format", func(t *testing.T) { line := "shadowsocks = 1.2.3.4:8388, tag=QX" n := ParseClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ss") if n["name"] != "QX" { t.Errorf("name = %v", n["name"]) } }) t.Run("named_format", func(t *testing.T) { line := "MyProxy = ss, 1.2.3.4, 8388, aes-256-gcm, pass" n := ParseClientProxyLine(line, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "ss") if n["name"] != "MyProxy" { t.Errorf("name = %v", n["name"]) } }) t.Run("invalid_returns_nil", func(t *testing.T) { if n := ParseClientProxyLine("just some random text", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) t.Run("qx_invalid_returns_nil", func(t *testing.T) { // Matches QX regex but invalid (no parts) → ParseQxProxyLine returns nil, // but recover wraps it if n := ParseClientProxyLine("shadowsocks = ", 0); n != nil { t.Errorf("expected nil, got %v", n) } }) } // -------------------------------------------------------------------------------- // URI parser: SplitHostPort // -------------------------------------------------------------------------------- func TestSplitHostPort(t *testing.T) { cases := []struct { in string wantHost string wantPort string }{ {"1.2.3.4:443", "1.2.3.4", "443"}, {" 1.2.3.4:443 ", "1.2.3.4", "443"}, {"example.com:8080", "example.com", "8080"}, {"noport", "noport", ""}, {":443", ":443", ""}, // lastColon==0 → returns whole string as host {"[::1]:443", "[::1]", "443"}, } for _, c := range cases { host, port := SplitHostPort(c.in) if host != c.wantHost || port != c.wantPort { t.Errorf("SplitHostPort(%q) = (%q, %q), want (%q, %q)", c.in, host, port, c.wantHost, c.wantPort) } } } // -------------------------------------------------------------------------------- // URI parser: ParseProxyUri dispatch // -------------------------------------------------------------------------------- func TestParseProxyUriDispatch(t *testing.T) { if n := ParseProxyUri("unknownscheme://foo", 0); n != nil { t.Errorf("expected nil for unknown scheme, got %v", n) } } // -------------------------------------------------------------------------------- // URI parser: ParseAnytls // -------------------------------------------------------------------------------- func TestParseAnytls(t *testing.T) { uri := "anytls://password123@1.2.3.4:443?sni=example.com&insecure=1&fp=chrome#TestAnytls" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "anytls") if n["password"] != "password123" { t.Errorf("password = %v", n["password"]) } if n["sni"] != "example.com" { t.Errorf("sni = %v", n["sni"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } if n["client-fingerprint"] != "chrome" { t.Errorf("client-fingerprint = %v", n["client-fingerprint"]) } port, _ := n["port"].(float64) if port != 443 { t.Errorf("port = %v", port) } if n["name"] != "TestAnytls" { t.Errorf("name = %v", n["name"]) } } func TestParseAnytlsPeerAndDefaults(t *testing.T) { // peer as sni alias; allowInsecure as insecure alias; no fp → default chrome uri := "anytls://pw@1.2.3.4#N" n := ParseProxyUri(uri, 0) // sni is empty → stripped by StripUndefined if v, ok := n["sni"]; ok { t.Errorf("expected no sni key, got %v", v) } if n["client-fingerprint"] != "chrome" { t.Errorf("default fp = %v", n["client-fingerprint"]) } port, _ := n["port"].(float64) if port != 443 { t.Errorf("default port = %v, want 443", port) } } // -------------------------------------------------------------------------------- // URI parser: ParseHysteria (hysteria:// and hy://) // -------------------------------------------------------------------------------- func TestParseHysteria(t *testing.T) { uri := "hysteria://authstr@1.2.3.4:443?protocol=udp&up=100&down=200&sni=h.com&alpn=h3,h4&obfs=obfs&obfs-password=op&insecure=1#TestHys" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "hysteria") if n["auth_str"] != "authstr" { t.Errorf("auth_str = %v", n["auth_str"]) } if n["protocol"] != "udp" { t.Errorf("protocol = %v", n["protocol"]) } if n["up"] != "100" { t.Errorf("up = %v", n["up"]) } if n["down"] != "200" { t.Errorf("down = %v", n["down"]) } alpn, ok := n["alpn"].([]string) if !ok { t.Fatalf("expected alpn []string, got %T", n["alpn"]) } if !reflect.DeepEqual(alpn, []string{"h3", "h4"}) { t.Errorf("alpn = %v", alpn) } if n["obfs"] != "obfs" { t.Errorf("obfs = %v", n["obfs"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } if n["name"] != "TestHys" { t.Errorf("name = %v", n["name"]) } } func TestParseHysteriaQueryAuth(t *testing.T) { // no userinfo; auth via query uri := "hysteria://1.2.3.4:443?auth=authquery&upmbps=50&downmbps=100&peer=peer.com&allowInsecure=true" n := ParseProxyUri(uri, 0) if n["auth_str"] != "authquery" { t.Errorf("auth_str = %v", n["auth_str"]) } if n["up"] != "50" { t.Errorf("up = %v", n["up"]) } if n["sni"] != "peer.com" { t.Errorf("sni via peer = %v", n["sni"]) } } func TestParseHysteriaHyAlias(t *testing.T) { uri := "hy://auth@1.2.3.4:443#HyAlias" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "hysteria") if n["auth_str"] != "auth" { t.Errorf("auth_str = %v", n["auth_str"]) } if n["name"] != "HyAlias" { t.Errorf("name = %v", n["name"]) } } // -------------------------------------------------------------------------------- // URI parser: ParseHysteria2 (hy2:// alias) // -------------------------------------------------------------------------------- func TestParseHysteria2Hy2Alias(t *testing.T) { uri := "hy2://password123@1.2.3.4:443?sni=h.com&obfs=salamander&obfs-password=op&allowInsecure=true#Test" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "hysteria2") if n["password"] != "password123" { t.Errorf("password = %v", n["password"]) } if n["obfs"] != "salamander" { t.Errorf("obfs = %v", n["obfs"]) } if n["obfs-password"] != "op" { t.Errorf("obfs-password = %v", n["obfs-password"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } } func TestParseHysteria2SalamanderAlias(t *testing.T) { uri := "hysteria2://pw@1.2.3.4:443?salamander-password=sal#N" n := ParseProxyUri(uri, 0) if n["obfs-password"] != "sal" { t.Errorf("obfs-password = %v, want sal", n["obfs-password"]) } } // -------------------------------------------------------------------------------- // URI parser: ParseSocks // -------------------------------------------------------------------------------- func TestParseSocks(t *testing.T) { t.Run("socks5_with_auth", func(t *testing.T) { uri := "socks5://user:pass@1.2.3.4:1080#TestSocks" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "socks5") if n["username"] != "user" { t.Errorf("username = %v", n["username"]) } if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if n["tls"] != false { t.Errorf("tls = %v", n["tls"]) } port, _ := n["port"].(float64) if port != 1080 { t.Errorf("port = %v", port) } }) t.Run("socks5_tls_scheme", func(t *testing.T) { uri := "socks5+tls://user:pass@1.2.3.4:1080#TestSocks" n := ParseProxyUri(uri, 0) if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } }) t.Run("socks_alias", func(t *testing.T) { uri := "socks://1.2.3.4:1080#TestSocks" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "socks5") }) t.Run("tls_query", func(t *testing.T) { uri := "socks5://1.2.3.4:1080?tls=1#TestSocks" n := ParseProxyUri(uri, 0) if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } }) t.Run("no_port_returns_nil", func(t *testing.T) { if n := ParseProxyUri("socks5://1.2.3.4#TestSocks", 0); n != nil { t.Errorf("expected nil without port, got %v", n) } }) } // -------------------------------------------------------------------------------- // URI parser: ParseHttpProxy // -------------------------------------------------------------------------------- func TestParseHttpProxy(t *testing.T) { t.Run("http", func(t *testing.T) { uri := "http://user:pass@1.2.3.4:8080#TestHTTP" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "http") if n["username"] != "user" { t.Errorf("username = %v", n["username"]) } if n["password"] != "pass" { t.Errorf("password = %v", n["password"]) } if n["tls"] != false { t.Errorf("tls = %v", n["tls"]) } port, _ := n["port"].(float64) if port != 8080 { t.Errorf("port = %v", port) } if n["name"] != "TestHTTP" { t.Errorf("name = %v", n["name"]) } }) t.Run("https", func(t *testing.T) { uri := "https://1.2.3.4:443#TestHTTPS" n := ParseProxyUri(uri, 0) if n["tls"] != true { t.Errorf("tls = %v", n["tls"]) } }) t.Run("no_port_returns_nil", func(t *testing.T) { if n := ParseProxyUri("http://1.2.3.4#TestHTTP", 0); n != nil { t.Errorf("expected nil without port, got %v", n) } }) } // -------------------------------------------------------------------------------- // URI parser: ParseTuic // -------------------------------------------------------------------------------- func TestParseTuic(t *testing.T) { uri := "tuic://uuid-1:password1@1.2.3.4:443?sni=sni.com&alpn=h3&allow_insecure=1&disable_sni=1&reduce_rtt=1&udp_relay_mode=native&congestion_control=bbr#TestTuic" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "tuic") if n["uuid"] != "uuid-1" { t.Errorf("uuid = %v", n["uuid"]) } if n["password"] != "password1" { t.Errorf("password = %v", n["password"]) } if n["sni"] != "sni.com" { t.Errorf("sni = %v", n["sni"]) } alpn, ok := n["alpn"].([]string) if !ok || len(alpn) != 1 || alpn[0] != "h3" { t.Errorf("alpn = %v", n["alpn"]) } if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } if n["disable-sni"] != true { t.Errorf("disable-sni = %v", n["disable-sni"]) } if n["reduce-rtt"] != true { t.Errorf("reduce-rtt = %v", n["reduce-rtt"]) } if n["udp-relay-mode"] != "native" { t.Errorf("udp-relay-mode = %v", n["udp-relay-mode"]) } if n["congestion-controller"] != "bbr" { t.Errorf("congestion-controller = %v", n["congestion-controller"]) } } func TestParseTuicDashedAlias(t *testing.T) { uri := "tuic://u:p@1.2.3.4:443?insecure=1&disable-sni=0&reduce-rtt=0&udp-relay-mode=quic&congestion-controller=cubic#T" n := ParseProxyUri(uri, 0) if n["skip-cert-verify"] != true { t.Errorf("skip-cert-verify = %v", n["skip-cert-verify"]) } if n["disable-sni"] != false { t.Errorf("disable-sni = %v", n["disable-sni"]) } if n["reduce-rtt"] != false { t.Errorf("reduce-rtt = %v", n["reduce-rtt"]) } if n["udp-relay-mode"] != "quic" { t.Errorf("udp-relay-mode = %v", n["udp-relay-mode"]) } if n["congestion-controller"] != "cubic" { t.Errorf("congestion-controller = %v", n["congestion-controller"]) } } // -------------------------------------------------------------------------------- // URI parser: ParseWireGuard // -------------------------------------------------------------------------------- func TestParseWireGuard(t *testing.T) { uri := "wireguard://privkey@1.2.3.4:51820?ip=10.0.0.2/32&ipv6=fd00::2&public-key=pubkey&pre-shared-key=psk&reserved=1,2,3#TestWG" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "wireguard") if n["private-key"] != "privkey" { t.Errorf("private-key = %v", n["private-key"]) } if n["ip"] != "10.0.0.2/32" { t.Errorf("ip = %v", n["ip"]) } if n["ipv6"] != "fd00::2" { t.Errorf("ipv6 = %v", n["ipv6"]) } if n["public-key"] != "pubkey" { t.Errorf("public-key = %v", n["public-key"]) } if n["pre-shared-key"] != "psk" { t.Errorf("pre-shared-key = %v", n["pre-shared-key"]) } if n["reserved"] != "1,2,3" { t.Errorf("reserved = %v", n["reserved"]) } port, _ := n["port"].(float64) if port != 51820 { t.Errorf("port = %v", port) } } func TestParseWireGuardWgAlias(t *testing.T) { uri := "wg://privkey@1.2.3.4:51820?address=10.0.0.2&publickey=pub&presharedkey=psk#WG" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "wireguard") if n["ip"] != "10.0.0.2" { t.Errorf("ip via address = %v", n["ip"]) } if n["public-key"] != "pub" { t.Errorf("public-key via publickey = %v", n["public-key"]) } if n["pre-shared-key"] != "psk" { t.Errorf("pre-shared-key via presharedkey = %v", n["pre-shared-key"]) } } // -------------------------------------------------------------------------------- // URI parser: ParseVless edge cases // -------------------------------------------------------------------------------- func TestParseVlessReality(t *testing.T) { uri := "vless://uuid@1.2.3.4:443?security=reality&pbk=PUB&sid=ab&spx=/x&fp=firefox&flow=flow&type=tcp#VR" n := ParseProxyUri(uri, 0) if n == nil { t.Fatal("expected node") } nodeType(t, n, "vless") ro, ok := n["reality-opts"].(map[string]any) if !ok { t.Fatalf("expected reality-opts map, got %T", n["reality-opts"]) } if ro["public-key"] != "PUB" { t.Errorf("public-key = %v", ro["public-key"]) } if ro["short-id"] != "ab" { t.Errorf("short-id = %v", ro["short-id"]) } if ro["spider-x"] != "/x" { t.Errorf("spider-x = %v", ro["spider-x"]) } if n["client-fingerprint"] != "firefox" { t.Errorf("fp = %v", n["client-fingerprint"]) } if n["flow"] != "flow" { t.Errorf("flow = %v", n["flow"]) } } func TestParseVlessAutoReality(t *testing.T) { // security empty but pbk present → auto-reality uri := "vless://uuid@1.2.3.4:443?pbk=PUB#AR" n := ParseProxyUri(uri, 0) if n["reality-opts"] == nil { t.Error("expected reality-opts auto-detected") } } func TestParseVlessSecurityNone(t *testing.T) { uri := "vless://uuid@1.2.3.4:443?security=none#N" n := ParseProxyUri(uri, 0) if n["tls"] != false { t.Errorf("tls = %v, want false", n["tls"]) } } func TestParseVlessDefaultFingerprint(t *testing.T) { uri := "vless://uuid@1.2.3.4:443#N" n := ParseProxyUri(uri, 0) if n["client-fingerprint"] != "chrome" { t.Errorf("default fp = %v", n["client-fingerprint"]) } } // -------------------------------------------------------------------------------- // URI parser: ParseVmess edge cases // -------------------------------------------------------------------------------- func TestParseVmessFailures(t *testing.T) { t.Run("invalid_base64", func(t *testing.T) { if n := ParseProxyUri("vmess://!!!invalid", 0); n != nil { t.Errorf("expected nil for invalid base64, got %v", n) } }) t.Run("invalid_json", func(t *testing.T) { encoded := base64.RawURLEncoding.EncodeToString([]byte("not json")) if n := ParseProxyUri("vmess://"+encoded, 0); n != nil { t.Errorf("expected nil for invalid json, got %v", n) } }) } // -------------------------------------------------------------------------------- // URI parser: ParseShadowsocks edge cases // -------------------------------------------------------------------------------- func TestParseShadowsocksInvalidBase64(t *testing.T) { if n := ParseProxyUri("ss://!!!invalid-base64", 0); n != nil { t.Errorf("expected nil, got %v", n) } } func TestParseShadowsocksNoAt(t *testing.T) { // fully base64 encoded but no @ after decode → nil encoded := base64.RawURLEncoding.EncodeToString([]byte("no-at-sign-here")) if n := ParseProxyUri("ss://"+encoded, 0); n != nil { t.Errorf("expected nil, got %v", n) } } // -------------------------------------------------------------------------------- // URI parser: ParseShadowsocksR edge cases // -------------------------------------------------------------------------------- func TestParseShadowsocksRInvalidBase64(t *testing.T) { if n := ParseProxyUri("ssr://!!!invalid", 0); n != nil { t.Errorf("expected nil, got %v", n) } } func TestParseShadowsocksRTooFewParts(t *testing.T) { // Only 5 parts (< 6 required) encoded := base64.RawURLEncoding.EncodeToString([]byte("1.2.3.4:8388:proto:method:obfs")) if n := ParseProxyUri("ssr://"+encoded, 0); n != nil { t.Errorf("expected nil, got %v", n) } } // -------------------------------------------------------------------------------- // URI parser: parseURL / fragmentName / userInfo / userPassword edge cases // -------------------------------------------------------------------------------- func TestParseProxyUriInvalidURL(t *testing.T) { // Control characters in URL cause parse error → panic caught by recover → nil if n := ParseProxyUri("vless://\x00bad", 0); n != nil { t.Errorf("expected nil for invalid URL, got %v", n) } } // -------------------------------------------------------------------------------- // format.go: ParseYamlProxies // -------------------------------------------------------------------------------- func TestParseYamlProxies(t *testing.T) { yaml := ` proxies: - name: YamlSS type: ss server: 1.2.3.4 port: 8388 cipher: aes-256-gcm password: pass ` n := ParseYamlProxies(yaml) if len(n) != 1 { t.Fatalf("expected 1 node, got %d", len(n)) } if n[0]["type"] != "ss" { t.Errorf("type = %v", n[0]["type"]) } if n[0]["name"] != "YamlSS" { t.Errorf("name = %v", n[0]["name"]) } } func TestParseYamlProxiesInvalid(t *testing.T) { if n := ParseYamlProxies("not: [valid: yaml"); len(n) != 0 { t.Errorf("expected 0 nodes for invalid yaml, got %d", len(n)) } } func TestParseYamlProxiesNoProxiesKey(t *testing.T) { yaml := "other: value\n" if n := ParseYamlProxies(yaml); len(n) != 0 { t.Errorf("expected 0 nodes, got %d", len(n)) } } // -------------------------------------------------------------------------------- // format.go: ParseJsonProxies edge cases // -------------------------------------------------------------------------------- func TestParseJsonProxiesInvalid(t *testing.T) { if n := ParseJsonProxies("not json"); len(n) != 0 { t.Errorf("expected 0 nodes for invalid json, got %d", len(n)) } } func TestParseJsonProxiesMapNoProxies(t *testing.T) { if n := ParseJsonProxies(`{"other": 1}`); len(n) != 0 { t.Errorf("expected 0 nodes, got %d", len(n)) } } func TestParseJsonProxiesScalarPayload(t *testing.T) { if n := ParseJsonProxies(`42`); len(n) != 0 { t.Errorf("expected 0 nodes for scalar, got %d", len(n)) } } func TestParseJsonProxiesMapProxiesNotArray(t *testing.T) { if n := ParseJsonProxies(`{"proxies": "notarray"}`); len(n) != 0 { t.Errorf("expected 0 nodes, got %d", len(n)) } } // -------------------------------------------------------------------------------- // format.go: ParseProxies dispatch / ParseProxyLines // -------------------------------------------------------------------------------- func TestParseProxiesEmpty(t *testing.T) { if n := ParseProxies(" "); len(n) != 0 { t.Errorf("expected 0 nodes for whitespace, got %d", len(n)) } } func TestParseProxyLinesCommentsAndSections(t *testing.T) { lines := []string{ "# comment line", "; semicolon comment", "[Proxy]", "", "ss://aes-256-gcm:pass@1.2.3.4:8388#Real", } nodes := ParseProxyLines(strings.Join(lines, "\n")) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } if nodes[0]["name"] != "Real" { t.Errorf("name = %v", nodes[0]["name"]) } } func TestParseProxyLinesClientConfig(t *testing.T) { lines := []string{ "shadowsocks = 1.2.3.4:8388, tag=QXSS, method=aes-256-gcm, password=pass", "MyNamed = ss, 1.2.3.4, 8388, aes-256-gcm, pass", } nodes := ParseProxyLines(strings.Join(lines, "\n")) if len(nodes) != 2 { t.Fatalf("expected 2 nodes, got %d", len(nodes)) } if nodes[0]["name"] != "QXSS" { t.Errorf("first name = %v", nodes[0]["name"]) } if nodes[1]["name"] != "MyNamed" { t.Errorf("second name = %v", nodes[1]["name"]) } } // -------------------------------------------------------------------------------- // format.go: LooksLikeStructuredSubscription // -------------------------------------------------------------------------------- func TestLooksLikeStructuredSubscription(t *testing.T) { cases := []struct { in string want bool }{ {"ss://pass@host:443", true}, {"vmess://base64", true}, {"proxies:\n - name: x", true}, {"[{\"type\":\"ss\"}]", true}, {"shadowsocks = host:443", true}, {"mynode = ss, 1.2.3.4, 443", true}, {"just plain text", false}, {"", false}, } for _, c := range cases { if got := LooksLikeStructuredSubscription(c.in); got != c.want { t.Errorf("LooksLikeStructuredSubscription(%q) = %v, want %v", c.in, got, c.want) } } } // -------------------------------------------------------------------------------- // format.go: DecodeMaybeBase64 edge cases // -------------------------------------------------------------------------------- func TestDecodeMaybeBase64InvalidBase64(t *testing.T) { // Not structured, not valid base64 → returns raw in := "!!!not base64 not structured" out := DecodeMaybeBase64(in) if out != in { t.Errorf("expected passthrough, got %q", out) } } func TestDecodeMaybeBase64StructuredPassthrough(t *testing.T) { in := "ss://pass@host:443#name" if out := DecodeMaybeBase64(in); out != in { t.Errorf("structured content should pass through, got %q", out) } } // -------------------------------------------------------------------------------- // normalize.go: NormalizeProxy // -------------------------------------------------------------------------------- func TestNormalizeProxyNil(t *testing.T) { if n := NormalizeProxy(nil); n != nil { t.Errorf("expected nil, got %v", n) } } func TestNormalizeProxyTypesAndPort(t *testing.T) { in := map[string]any{ "name": "Test", "type": "ss", "port": "8388", } n := NormalizeProxy(in) if n["name"] != "Test" { t.Errorf("name = %v", n["name"]) } if n["type"] != "ss" { t.Errorf("type = %v", n["type"]) } port, _ := n["port"].(float64) if port != 8388 { t.Errorf("port = %v, want 8388", n["port"]) } } func TestNormalizeProxyMissingNameType(t *testing.T) { in := map[string]any{"server": "1.2.3.4"} n := NormalizeProxy(in) // name/type set to "" then stripped by StripUndefined if v, ok := n["name"]; ok { t.Errorf("expected name stripped, got %v", v) } if v, ok := n["type"]; ok { t.Errorf("expected type stripped, got %v", v) } if n["server"] != "1.2.3.4" { t.Errorf("server = %v", n["server"]) } } func TestNormalizeProxyNilPort(t *testing.T) { in := map[string]any{"name": "n", "type": "ss", "port": nil} n := NormalizeProxy(in) if _, ok := n["port"]; ok { t.Errorf("expected port stripped, got %v", n["port"]) } } func TestNormalizeProxyBoolName(t *testing.T) { in := map[string]any{"name": true, "type": "ss"} n := NormalizeProxy(in) if n["name"] != "true" { t.Errorf("name = %v, want 'true'", n["name"]) } } // -------------------------------------------------------------------------------- // normalize.go: IsProxyNode // -------------------------------------------------------------------------------- func TestIsProxyNode(t *testing.T) { cases := []struct { name string node model.ProxyNode want bool }{ {"valid", model.ProxyNode{"name": "n", "type": "ss"}, true}, {"nil", nil, false}, {"no_name", model.ProxyNode{"type": "ss"}, false}, {"empty_name", model.ProxyNode{"name": "", "type": "ss"}, false}, {"name_not_string", model.ProxyNode{"name": 123, "type": "ss"}, false}, {"no_type", model.ProxyNode{"name": "n"}, false}, {"empty_type", model.ProxyNode{"name": "n", "type": ""}, false}, {"type_not_string", model.ProxyNode{"name": "n", "type": 123}, false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := IsProxyNode(c.node); got != c.want { t.Errorf("IsProxyNode(%v) = %v, want %v", c.node, got, c.want) } }) } } // -------------------------------------------------------------------------------- // normalize.go: StripUndefined // -------------------------------------------------------------------------------- func TestStripUndefined(t *testing.T) { t.Run("nil_input", func(t *testing.T) { got := StripUndefined(nil) if len(got) != 0 { t.Errorf("expected empty map, got %v", got) } }) t.Run("filters_nil_and_empty_string", func(t *testing.T) { in := map[string]any{ "a": "value", "b": "", "c": nil, "d": 0, "e": false, } got := StripUndefined(in) if _, ok := got["b"]; ok { t.Error("empty string should be stripped") } if _, ok := got["c"]; ok { t.Error("nil should be stripped") } if _, ok := got["a"]; !ok { t.Error("non-empty string should remain") } if _, ok := got["d"]; !ok { t.Error("zero should remain") } if _, ok := got["e"]; !ok { t.Error("false should remain") } }) } // -------------------------------------------------------------------------------- // normalize.go: EnsureUniqueProxyNames with nil // -------------------------------------------------------------------------------- func TestEnsureUniqueProxyNamesWithNil(t *testing.T) { nodes := []model.ProxyNode{ nil, {"name": "Real", "type": "ss"}, } result := EnsureUniqueProxyNames(nodes) if result[0] != nil { t.Errorf("expected nil preserved, got %v", result[0]) } if result[1]["name"] != "Real" { t.Errorf("name = %v", result[1]["name"]) } } // -------------------------------------------------------------------------------- // normalize.go: toString (covers more type branches) // -------------------------------------------------------------------------------- func TestToString(t *testing.T) { cases := []struct { name string v any want string }{ {"nil", nil, ""}, {"string", "hello", "hello"}, {"bool_true", true, "true"}, {"bool_false", false, "false"}, {"float64", float64(3.14), "3.14"}, {"float32", float32(2.5), "2.5"}, {"int", 42, "42"}, {"int64", int64(100), "100"}, {"slice", []int{1, 2}, "[1 2]"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := toString(c.v); got != c.want { t.Errorf("toString(%v) = %q, want %q", c.v, got, c.want) } }) } } func TestToStringJSONNumber(t *testing.T) { // json.Number is a string type, test via StableProxyId path // Use normalize indirectly via NormalizeProxy with json.Number port // Actually test via the StableProxyId which calls toString n := model.ProxyNode{ "name": "n", "type": "ss", "server": "1.2.3.4", "port": 443, } id := StableProxyId(n, 0) if id != "n|ss|1.2.3.4|443|0" { t.Errorf("id = %q", id) } } // -------------------------------------------------------------------------------- // normalize.go: portString // -------------------------------------------------------------------------------- func TestPortString(t *testing.T) { if got := portString(nil); got != "" { t.Errorf("portString(nil) = %q", got) } if got := portString(443); got != "443" { t.Errorf("portString(443) = %q", got) } } // -------------------------------------------------------------------------------- // normalize.go: toNumberOrUndefined // -------------------------------------------------------------------------------- func TestToNumberOrUndefined(t *testing.T) { cases := []struct { name string v any want any }{ {"nil", nil, nil}, {"float64", float64(443), float64(443)}, {"float32", float32(80), float64(80)}, {"int", 443, float64(443)}, {"int64", int64(8388), float64(8388)}, {"string_valid", "443", float64(443)}, {"string_empty", "", nil}, {"string_invalid", "abc", nil}, {"bool", true, nil}, {"slice", []int{1}, nil}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := toNumberOrUndefined(c.v) if !reflect.DeepEqual(got, c.want) { t.Errorf("toNumberOrUndefined(%v) = %v, want %v", c.v, got, c.want) } }) } } // -------------------------------------------------------------------------------- // uri_parser.go: CommaList // -------------------------------------------------------------------------------- func TestCommaList(t *testing.T) { cases := []struct { in string want []string }{ {"", nil}, {"h3", []string{"h3"}}, {"h3,h4", []string{"h3", "h4"}}, {"h3, h4 , h5", []string{"h3", "h4", "h5"}}, {" , , ", nil}, {"a,,b", []string{"a", "b"}}, } for _, c := range cases { got := CommaList(c.in) if !reflect.DeepEqual(got, c.want) { t.Errorf("CommaList(%q) = %v, want %v", c.in, got, c.want) } } } // -------------------------------------------------------------------------------- // uri_parser.go: NumberOrUndefined // -------------------------------------------------------------------------------- func TestNumberOrUndefined(t *testing.T) { if got := NumberOrUndefined("443"); got != float64(443) { t.Errorf("NumberOrUndefined(\"443\") = %v, want 443", got) } if got := NumberOrUndefined("abc"); got != nil { t.Errorf("NumberOrUndefined(\"abc\") = %v, want nil", got) } if got := NumberOrUndefined(nil); got != nil { t.Errorf("NumberOrUndefined(nil) = %v, want nil", got) } } // -------------------------------------------------------------------------------- // uri_parser.go: BoolParam // -------------------------------------------------------------------------------- func TestBoolParam(t *testing.T) { cases := []struct { in string want bool }{ {"1", true}, {"true", true}, {"0", false}, {"false", false}, {"yes", false}, {"", false}, } for _, c := range cases { if got := BoolParam(c.in); got != c.want { t.Errorf("BoolParam(%q) = %v, want %v", c.in, got, c.want) } } } // -------------------------------------------------------------------------------- // uri_parser.go: firstNonEmpty / orDefault // -------------------------------------------------------------------------------- func TestFirstNonEmpty(t *testing.T) { if got := firstNonEmpty("", "", "third"); got != "third" { t.Errorf("got %q", got) } if got := firstNonEmpty(); got != "" { t.Errorf("got %q", got) } if got := firstNonEmpty("first", "second"); got != "first" { t.Errorf("got %q", got) } } func TestOrDefault(t *testing.T) { if got := orDefault("val", "fallback"); got != "val" { t.Errorf("got %q", got) } if got := orDefault("", "fallback"); got != "fallback" { t.Errorf("got %q", got) } } // -------------------------------------------------------------------------------- // uri_parser.go: paramFirst (via ParseVless which uses params.Get directly, // but paramFirst is used in ParseAnytls/ParseHysteria etc. — already covered. // Test paramFirst directly.) // -------------------------------------------------------------------------------- func TestParamFirst(t *testing.T) { u, err := url.Parse("http://x?b=&a=1&c=2") if err != nil { t.Fatalf("url.Parse failed: %v", err) } if got := paramFirst(u, "missing", "b", "a"); got != "1" { t.Errorf("paramFirst = %q, want 1", got) } if got := paramFirst(u, "missing1", "missing2"); got != "" { t.Errorf("paramFirst = %q, want empty", got) } }