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.
2247 lines
67 KiB
Go
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)
|
|
}
|
|
}
|