package main import ( "encoding/base64" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestParseLinkListBase64AndPlain(t *testing.T) { plain := "vless://11111111-1111-1111-1111-111111111111@a.example.com:443?security=reality#One\n" + "trojan://x@b.example.com:443#Bad\n" + "ss://YWVzLTI1Ni1nY206cGFzcw@b.example.com:8388#Two\n" encoded := base64.StdEncoding.EncodeToString([]byte(plain)) items, dropped := ParseLinkList(encoded) if len(items) != 2 || dropped != 1 { t.Fatalf("base64 list: items=%d dropped=%d", len(items), dropped) } if items[0].Name != "One" || items[1].Name != "Two" { t.Fatalf("names: %+v", items) } items2, dropped2 := ParseLinkList(plain) if len(items2) != 2 || dropped2 != 1 { t.Fatalf("plain list: items=%d dropped=%d", len(items2), dropped2) } } func TestFetchExternalSourceGuards(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("vless://11111111-1111-1111-1111-111111111111@a.example.com:443#X\n")) })) defer server.Close() source := ExtSource{ID: "src_t", URL: server.URL, Enabled: true} settings := &Settings{} blocked := FetchExternalSource(source, settings) if blocked.OK { t.Fatal("loopback source must be rejected by default") } settings.AllowPrivateSubSources = true allowed := FetchExternalSource(source, settings) if !allowed.OK || len(allowed.Items) != 1 { t.Fatalf("allowPrivate fetch: %+v", allowed) } } func TestAggregateEntriesOrderAndDedup(t *testing.T) { d := &Data{ Nodes: []*Node{ {ID: "nod_b", Name: "B", SortIndex: 1, ServerID: "srv_1", Protocol: "shadowsocks-aes256-gcm", Status: "active", Port: 20003}, {ID: "nod_a", Name: "A", SortIndex: 0, ServerID: "srv_1", Protocol: "shadowsocks-aes256-gcm", Status: "active", Port: 20001}, }, Servers: []*Server{{ID: "srv_1", Name: "S", PublicAddress: "s.example.com"}}, NodeClients: []*NodeClient{ {ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active"}, {ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active"}, }, SubSourceCache: map[string]*SubSourceCacheEntry{ "src_1": {Items: []SubCacheItem{{URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Dup", Name: "Dup"}}}, }, } okAt := NowISO() d.SubSourceCache["src_1"].OkAt = &okAt customer := &Customer{ ID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "pw-a", ShortID: "abcd0001ef01"}, ManualLinks: []ManualLink{ {ID: "lnk_1", URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Whatever", Remark: "手动一号", Enabled: true}, }, ExternalSources: []ExtSource{{ID: "src_1", URL: "https://example.com/sub", Remark: "机场", Enabled: true}}, } d.Customers = []*Customer{customer} entries := AggregateEntries(d, customer) names := []string{} for _, entry := range entries { names = append(names, NameFromURI(entry.ClientURI)) } // Own entries first in sortIndex order (A before B although B's // binding is stored first); the manual link follows; the external // item duplicates the manual link (same host/port/credential) and // is dropped. joined := strings.Join(names, ",") if joined != "A,B,手动一号" { t.Fatalf("aggregate order/dedup: %v", names) } } func TestPresenceWindow(t *testing.T) { now := time.Now() recent := ISOFromTime(now.Add(-60 * time.Second)) customer := &Customer{LastOnlineAt: &recent} if !CustomerOnline(customer, now.UnixMilli()) { t.Fatal("customer active 60s ago should be online") } old := ISOFromTime(now.Add(-10 * time.Minute)) customer.LastOnlineAt = &old if CustomerOnline(customer, now.UnixMilli()) { t.Fatal("customer active 10m ago should be offline") } } func TestHostIPsNoPanic(t *testing.T) { HostIPs() // just must not panic on any host }