package proxy import ( "encoding/base64" "encoding/json" "strings" "testing" "github.com/peterqiu0516/sub-store/internal/model" ) func TestParseSS(t *testing.T) { plain := "aes-256-gcm:password123@1.2.3.4:8388" encoded := base64.RawURLEncoding.EncodeToString([]byte(plain)) uri := "ss://" + encoded + "#TestSS" nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "ss" { t.Errorf("expected type ss, got %v", n["type"]) } if n["server"] != "1.2.3.4" { t.Errorf("expected server 1.2.3.4, got %v", n["server"]) } if n["name"] != "TestSS" { t.Errorf("expected name TestSS, got %v", n["name"]) } if n["cipher"] != "aes-256-gcm" { t.Errorf("expected cipher aes-256-gcm, got %v", n["cipher"]) } if n["password"] != "password123" { t.Errorf("expected password password123, got %v", n["password"]) } port, _ := n["port"].(float64) if port != 8388 { t.Errorf("expected port 8388, got %v", n["port"]) } } func TestParseSSURLForm(t *testing.T) { uri := "ss://aes-256-gcm:password123@1.2.3.4:8388#TestSS" nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "ss" { t.Errorf("expected type ss, got %v", n["type"]) } if n["cipher"] != "aes-256-gcm" { t.Errorf("expected cipher aes-256-gcm, got %v", n["cipher"]) } } func TestParseSSR(t *testing.T) { // SSR format: base64(server:port:protocol:method:obfs:base64(password)/?remarks=base64(name)) ssrMain := "1.2.3.4:8388:auth_aes128_sha1:aes-256-cfb:http_simple:" + base64.RawURLEncoding.EncodeToString([]byte("pass")) encoded := base64.RawURLEncoding.EncodeToString([]byte(ssrMain + "/?remarks=" + base64.RawURLEncoding.EncodeToString([]byte("TestSSR")))) uri := "ssr://" + encoded nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "ssr" { t.Errorf("expected type ssr, got %v", n["type"]) } if n["server"] != "1.2.3.4" { t.Errorf("expected server 1.2.3.4, got %v", n["server"]) } if n["obfs"] != "http_simple" { t.Errorf("expected obfs http_simple, got %v", n["obfs"]) } } func TestParseVMess(t *testing.T) { vmJSON := `{"v":"2","ps":"TestVM","add":"1.2.3.4","port":"443","id":"uuid-1234","aid":"0","scy":"auto","net":"ws","type":"none","host":"example.com","path":"/path","tls":"tls","sni":"example.com"}` encoded := base64.RawURLEncoding.EncodeToString([]byte(vmJSON)) uri := "vmess://" + encoded nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "vmess" { t.Errorf("expected type vmess, got %v", n["type"]) } if n["server"] != "1.2.3.4" { t.Errorf("expected server 1.2.3.4, got %v", n["server"]) } if n["uuid"] != "uuid-1234" { t.Errorf("expected uuid uuid-1234, got %v", n["uuid"]) } if n["network"] != "ws" { t.Errorf("expected network ws, got %v", n["network"]) } ws, ok := n["ws-opts"].(map[string]any) if !ok { t.Fatalf("expected ws-opts map, got %T", n["ws-opts"]) } if ws["path"] != "/path" { t.Errorf("expected ws path /path, got %v", ws["path"]) } if n["tls"] != true { t.Errorf("expected tls true, got %v", n["tls"]) } } func TestParseVLESS(t *testing.T) { uri := "vless://uuid-1234@1.2.3.4:443?encryption=none&security=tls&type=ws&host=example.com&path=%2Fpath&sni=example.com#TestVLESS" nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "vless" { t.Errorf("expected type vless, got %v", n["type"]) } if n["uuid"] != "uuid-1234" { t.Errorf("expected uuid uuid-1234, got %v", n["uuid"]) } if n["network"] != "ws" { t.Errorf("expected network ws, got %v", n["network"]) } ws, ok := n["ws-opts"].(map[string]any) if !ok { t.Fatalf("expected ws-opts map, got %T", n["ws-opts"]) } if ws["path"] != "/path" { t.Errorf("expected ws path /path, got %v", ws["path"]) } if n["tls"] != true { t.Errorf("expected tls true, got %v", n["tls"]) } } func TestParseTrojan(t *testing.T) { uri := "trojan://password123@1.2.3.4:443?sni=example.com&type=ws&host=example.com&path=%2Fpath#TestTrojan" nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "trojan" { t.Errorf("expected type trojan, got %v", n["type"]) } if n["password"] != "password123" { t.Errorf("expected password password123, got %v", n["password"]) } if n["sni"] != "example.com" { t.Errorf("expected sni example.com, got %v", n["sni"]) } } func TestParseHysteria2(t *testing.T) { uri := "hysteria2://password123@1.2.3.4:443?sni=example.com&insecure=1#TestHys2" nodes := ParseProxies(uri) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } n := nodes[0] if n["type"] != "hysteria2" { t.Errorf("expected type hysteria2, got %v", n["type"]) } if n["password"] != "password123" { t.Errorf("expected password password123, got %v", n["password"]) } } func TestParseMultipleLines(t *testing.T) { lines := []string{ "ss://aes-256-gcm:pass@1.2.3.4:8388#Node1", "ss://aes-256-gcm:pass@5.6.7.8:8388#Node2", "ss://aes-256-gcm:pass@9.10.11.12:8388#Node3", } nodes := ParseProxies(strings.Join(lines, "\n")) if len(nodes) != 3 { t.Fatalf("expected 3 nodes, got %d", len(nodes)) } if nodes[0]["name"] != "Node1" { t.Errorf("expected name Node1, got %v", nodes[0]["name"]) } if nodes[2]["name"] != "Node3" { t.Errorf("expected name Node3, got %v", nodes[2]["name"]) } } func TestParseBase64Content(t *testing.T) { lines := []string{ "ss://aes-256-gcm:pass@1.2.3.4:8388#Node1", "ss://aes-256-gcm:pass@5.6.7.8:8388#Node2", } plain := strings.Join(lines, "\n") encoded := base64.StdEncoding.EncodeToString([]byte(plain)) // DecodeMaybeBase64 should detect and decode base64 content decoded := DecodeMaybeBase64(encoded) nodes := ParseProxies(decoded) if len(nodes) != 2 { t.Fatalf("expected 2 nodes from base64 content, got %d", len(nodes)) } } func TestParseEmptyContent(t *testing.T) { nodes := ParseProxies("") if len(nodes) != 0 { t.Errorf("expected 0 nodes for empty content, got %d", len(nodes)) } } func TestParseInvalidContent(t *testing.T) { nodes := ParseProxies("this is not a valid proxy URI") if len(nodes) != 0 { t.Errorf("expected 0 nodes for invalid content, got %d", len(nodes)) } } func TestParseJSONContent(t *testing.T) { jsonContent := `[{"type":"ss","server":"1.2.3.4","port":8388,"cipher":"aes-256-gcm","password":"pass","name":"TestJSON"}]` nodes := ParseProxies(jsonContent) if len(nodes) != 1 { t.Fatalf("expected 1 node from JSON, got %d", len(nodes)) } if nodes[0]["type"] != "ss" { t.Errorf("expected type ss, got %v", nodes[0]["type"]) } } func TestEnsureUniqueProxyNames(t *testing.T) { nodes := []model.ProxyNode{ {"name": "Same", "server": "1.1.1.1"}, {"name": "Same", "server": "2.2.2.2"}, {"name": "Same", "server": "3.3.3.3"}, {"name": "Unique", "server": "4.4.4.4"}, } result := EnsureUniqueProxyNames(nodes) if result[0]["name"] != "Same" { t.Errorf("first name should be unchanged, got %v", result[0]["name"]) } if result[1]["name"] == "Same" { t.Error("second duplicate should be renamed") } if result[3]["name"] != "Unique" { t.Errorf("unique name should be unchanged, got %v", result[3]["name"]) } } func TestAddPreviewIds(t *testing.T) { nodes := []model.ProxyNode{ {"name": "A", "server": "1.1.1.1"}, {"name": "B", "server": "2.2.2.2"}, } result := AddPreviewIds(nodes) if _, ok := result[0]["id"]; !ok { t.Error("expected id field on first node") } if _, ok := result[1]["id"]; !ok { t.Error("expected id field on second node") } } func TestDecodeMaybeBase64(t *testing.T) { plain := "ss://pass@host:port#name" if DecodeMaybeBase64(plain) != plain { t.Error("plain text should pass through") } encoded := base64.StdEncoding.EncodeToString([]byte(plain)) decoded := DecodeMaybeBase64(encoded) if !strings.Contains(decoded, "ss://") { t.Errorf("base64 should be decoded, got: %s", decoded) } } func TestNodeJSONSerialization(t *testing.T) { uri := "ss://aes-256-gcm:pass@1.2.3.4:8388#Test" nodes := ParseProxies(uri) if len(nodes) == 0 { t.Fatal("no nodes parsed") } data, err := json.Marshal(nodes[0]) if err != nil { t.Fatalf("failed to marshal node: %v", err) } var back map[string]any if err := json.Unmarshal(data, &back); err != nil { t.Fatalf("failed to unmarshal node: %v", err) } if back["type"] != "ss" { t.Errorf("expected type ss after round-trip, got %v", back["type"]) } } func vlessURI(name string) string { return "vless://uuid-1234@1.2.3.4:443?encryption=none&security=tls&type=ws&host=example.com&path=%2Fpath&sni=example.com#" + name } // TestParseProxyLinesLiteralNewline reproduces the production case: content // stored with literal "\n" (backslash + n) between links instead of real // newlines. It must normalize to real newlines and parse every link. func TestParseProxyLinesLiteralNewline(t *testing.T) { raw := strings.Join([]string{ vlessURI("ali-seoul"), vlessURI("Ali-Tokyo"), vlessURI("racknerd-la"), }, `\n`) nodes := ParseProxies(raw) if len(nodes) != 3 { t.Fatalf("expected 3 nodes from literal-\\n separated links, got %d", len(nodes)) } wantNames := []string{"ali-seoul", "Ali-Tokyo", "racknerd-la"} for i, want := range wantNames { if got := nodes[i]["name"]; got != want { t.Errorf("node %d: expected name %q, got %q", i, want, got) } } } // TestParseProxyLinesLiteralCRLF normalizes literal "\\r\\n" sequences too. func TestParseProxyLinesLiteralCRLF(t *testing.T) { raw := strings.Join([]string{ vlessURI("Node1"), vlessURI("Node2"), }, `\r\n`) nodes := ParseProxies(raw) if len(nodes) != 2 { t.Fatalf("expected 2 nodes from literal-\\r\\n separated links, got %d", len(nodes)) } if nodes[0]["name"] != "Node1" || nodes[1]["name"] != "Node2" { t.Errorf("unexpected names: %v / %v", nodes[0]["name"], nodes[1]["name"]) } } // TestParseProxyLinesRealNewlineUnaffected: real newline input still parses // the same after the normalization change. func TestParseProxyLinesRealNewlineUnaffected(t *testing.T) { raw := strings.Join([]string{ vlessURI("Node1"), "", "# comment", vlessURI("Node2"), }, "\n") nodes := ParseProxies(raw) if len(nodes) != 2 { t.Fatalf("expected 2 nodes, got %d", len(nodes)) } if nodes[0]["name"] != "Node1" || nodes[1]["name"] != "Node2" { t.Errorf("unexpected names: %v / %v", nodes[0]["name"], nodes[1]["name"]) } } // TestParseProxyLinesSingleLinkUnchanged: a single link with no separators // keeps its exact behavior (name untouched). func TestParseProxyLinesSingleLinkUnchanged(t *testing.T) { raw := vlessURI("solo") nodes := ParseProxies(raw) if len(nodes) != 1 { t.Fatalf("expected 1 node, got %d", len(nodes)) } if nodes[0]["name"] != "solo" { t.Errorf("expected name solo, got %v", nodes[0]["name"]) } } // TestDecodeMaybeBase64LiteralNewlinePassthrough: structured literal-\n content // must pass through DecodeMaybeBase64 unchanged — normalization happens inside // ParseProxyLines (after format/base64 detection, before splitting) and must // not interfere with base64 detection. func TestDecodeMaybeBase64LiteralNewlinePassthrough(t *testing.T) { raw := strings.Join([]string{ vlessURI("Node1"), vlessURI("Node2"), }, `\n`) if got := DecodeMaybeBase64(raw); got != raw { t.Error("structured literal-\\n content must pass through unchanged") } if nodes := ParseProxies(DecodeMaybeBase64(raw)); len(nodes) != 2 { t.Fatalf("expected 2 nodes through full pipeline, got %d", len(nodes)) } }