package subscription import ( "context" "encoding/base64" "net/http" "net/http/httptest" "strings" "testing" ) func TestParseBodyPlain(t *testing.T) { body := `# comment hysteria2://pass@ex.com:443/?sni=ex.com&obfs=salamander&obfs-password=x#EU naive+https://u:p@n.example.com#naive1 ` items, err := ParseBody(body) if err != nil { t.Fatal(err) } if len(items) != 2 { t.Fatalf("got %d items", len(items)) } } func TestParseBodyBase64(t *testing.T) { plain := "hysteria2://secret@host.example:443/?sni=host.example#HY\nhttps://user:pass@naive.example.com#NV\n" body := base64.StdEncoding.EncodeToString([]byte(plain)) items, err := ParseBody(body) if err != nil { t.Fatal(err) } if len(items) != 2 { t.Fatalf("got %d items: %+v", len(items), items) } } // TestParseBodyTolerant: one broken entry must never abort the import — // valid links are kept, unsupported/broken lines are skipped with warnings. func TestParseBodyTolerant(t *testing.T) { body := `vless://7e1c0fbf-1758-4781-97f6-ae8715ed3f60@cz.example.com:443?encryption=none&security=tls&type=tcp#CZ hysteria2://secret@de.example.com:443/?sni=de.example.com#DE-HY2 https://api.example.com/ZKszcVcC3xbWb8qj foobar://what@is.this:1 # comment line ` res, err := ParseBodyDetailed(body) if err != nil { t.Fatal(err) } if len(res.Items) != 2 { t.Fatalf("imported %d items, want 2: %+v", len(res.Items), res.Items) } if res.Items[0].Protocol != "vless" || res.Items[1].Protocol != "hysteria2" { t.Fatalf("protocols: %s, %s", res.Items[0].Protocol, res.Items[1].Protocol) } // https:// without userinfo + unknown scheme → skipped; empty/comment lines are not counted. if res.Skipped != 2 { t.Fatalf("skipped %d, want 2 (%v)", res.Skipped, res.Warnings) } if len(res.Warnings) != 2 { t.Fatalf("warnings: %v", res.Warnings) } } func TestParseBodyZeroValidListsReasons(t *testing.T) { body := "https://api.example.com/sub-token-line\nfoobar://x@y:1\n" res, err := ParseBodyDetailed(body) if err == nil { t.Fatal("want error for zero valid entries") } if res.Skipped != 2 { t.Fatalf("skipped %d, want 2", res.Skipped) } if !strings.Contains(err.Error(), "2 записей пропущено") { t.Fatalf("error should list skip count: %v", err) } } func TestParseUserInfoHeader(t *testing.T) { info := ParseUserInfoHeader("upload=0; download=5679561725; total=53687091200000; expire=1799366585") if info == nil { t.Fatal("nil info") } if info.Download != 5679561725 || info.Total != 53687091200000 || info.Expire != 1799366585 || info.Upload != 0 { t.Fatalf("bad parse: %+v", info) } if ParseUserInfoHeader("") != nil { t.Fatal("empty header must give nil") } if ParseUserInfoHeader("garbage") != nil { t.Fatal("garbage header must give nil") } } // Remnawave returns Clash YAML for Clash-like UAs and raw base64 links otherwise. // Fetch must obtain the full server list (plain UA) and the userinfo header. func TestFetchPrefersPlainUA(t *testing.T) { links := "vless://7e1c0fbf-1758-4781-97f6-ae8715ed3f60@cz.example.com:443?encryption=none&security=tls&type=tcp#CZ\n" + "trojan://pass@no.example.com:20000?security=tls&type=ws#NO\n" + "hysteria2://secret@de.example.com:443/?sni=de.example.com#DE\n" + "ss://YWVzLTI1Ni1nY206cGFzcw@ss.example.com:8388#SS\n" clashYAML := "proxies:\n - name: only-hy2\n type: hysteria2\n server: de.example.com\n port: 443\n password: secret\n" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Subscription-Userinfo", "upload=1; download=2; total=100; expire=1799366585") if strings.Contains(strings.ToLower(r.Header.Get("User-Agent")), "clash") { _, _ = w.Write([]byte(clashYAML)) return } _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString([]byte(links)))) })) defer srv.Close() res, err := Fetch(context.Background(), srv.URL) if err != nil { t.Fatal(err) } if len(res.Items) != 3 { t.Fatalf("imported %d, want 3 (vless+trojan+hy2): %+v", len(res.Items), res.Items) } if res.Skipped != 1 { t.Fatalf("skipped %d, want 1 (ss://)", res.Skipped) } if res.Info == nil || res.Info.Expire != 1799366585 || res.Info.Total != 100 { t.Fatalf("info: %+v", res.Info) } } // Indented Clash proxies with nested lists (alpn) must split into separate blocks. func TestParseBodyClashYAMLIndented(t *testing.T) { body := `proxies: - name: DE HY2 type: hysteria2 server: de.example.com port: 443 udp: true password: secret1 sni: de.example.com alpn: - h3 - name: NO HY2 type: hysteria2 server: no.example.com port: 9000 password: secret2 alpn: - h3 ` items, err := ParseBody(body) if err != nil { t.Fatal(err) } if len(items) != 2 { t.Fatalf("got %d items, want 2: %+v", len(items), items) } for i, host := range []string{"de.example.com:443", "no.example.com:9000"} { if !strings.Contains(items[i].URI, host) { t.Fatalf("item %d uri %s missing %s", i, items[i].URI, host) } } for i, pass := range []string{"secret1", "secret2"} { if !strings.Contains(items[i].URI, pass) { t.Fatalf("item %d uri %s missing password %s", i, items[i].URI, pass) } } } func TestParseBodyClashYAML(t *testing.T) { body := ` proxies: - name: "DE HY2" type: hysteria2 server: de.example.com port: 443 password: secret sni: de.example.com obfs: salamander obfs-password: obfspass ` items, err := ParseBody(body) if err != nil { t.Fatal(err) } if len(items) != 1 { t.Fatalf("got %d", len(items)) } if items[0].Protocol != "hysteria2" { t.Fatalf("proto %s", items[0].Protocol) } if !strings.Contains(items[0].URI, "de.example.com:443") { t.Fatalf("uri %s", items[0].URI) } }