Files
sub-store/internal/proxy/client_parser_test.go
rogee cec95ff4e8 test: add comprehensive unit tests — coverage 20.2% → 91.8%
Add 12 test files covering all internal packages:

- internal/util/util_test.go (97.2%)
- internal/model/model_test.go (100.0%)
- internal/config/config_test.go (88.1%)
- internal/template/builtin_test.go (98.8%)
- internal/middleware/middleware_test.go (98.7%)
- internal/database/repo_extra_test.go (85.5%)
- internal/rules/converter_test.go (99.1%)
- internal/service/subscription_test.go (75.7%)
- internal/handler/handler_test.go (90.6%)
- internal/filter/filter_extra_test.go (91.9%)
- internal/proxy/client_parser_test.go (96.1%)
- internal/render/render_extra_test.go (99.2%)

Overall: 91.8% (4033/4400 statements) — exceeds 85% acceptance threshold.
All tests pass, go vet clean, go build clean.
2026-07-27 16:12:28 +08:00

2247 lines
67 KiB
Go

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)
}
}