Files
navi/internal/subscription/fetch_test.go
T

196 lines
5.6 KiB
Go

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