package main import ( "encoding/json" "os" "reflect" "testing" ) func goldenSubData() *Data { entryServer := &Server{ID: "srv_entry", PublicAddress: "entry.example.com", TLSDomain: "tls.example.com"} v6Server := &Server{ID: "srv_v6", PublicAddress: "v6.example.com", TLSDomain: "tls.example.com"} art := func(pub string) map[string]json.RawMessage { raw, _ := marshalJSON(pub) return map[string]json.RawMessage{"realityPublicKey": raw} } nodes := []*Node{ {ID: "nod_rf", Name: "RF-Reality", ServerID: "srv_entry", Protocol: "vless-reality-vision", Topology: "direct", NetworkMode: "ipv4", Port: 23456, Status: "active", Reality: &RealityConfig{KeyID: "reality-nod_rf", ServerName: "www.amd.com", DestPort: 443}, Artifacts: art("FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu")}, {ID: "nod_ss", Name: "SS-Direct", ServerID: "srv_entry", Protocol: "shadowsocks-2022-aes128", Topology: "direct", NetworkMode: "ipv4", Port: 23458, Status: "active", SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="), Artifacts: map[string]json.RawMessage{}}, {ID: "nod_hy", Name: "HY2", ServerID: "srv_entry", Protocol: "hysteria2", Topology: "direct", NetworkMode: "ipv4", Port: 23459, Status: "active", Artifacts: map[string]json.RawMessage{}}, {ID: "nod_any", Name: "AnyTLS", ServerID: "srv_entry", Protocol: "anytls", Topology: "direct", NetworkMode: "ipv4", Port: 23460, Status: "active", Artifacts: map[string]json.RawMessage{}}, {ID: "nod_ws", Name: "WS-TLS", ServerID: "srv_entry", Protocol: "vless-ws-tls", Topology: "direct", NetworkMode: "ipv4", Port: 23461, Status: "active", WSPath: StrPtr("/wspath123"), Artifacts: map[string]json.RawMessage{}}, {ID: "nod_sk", Name: "Socks", ServerID: "srv_entry", Protocol: "socks5-auth", Topology: "direct", NetworkMode: "ipv4", Port: 23462, Status: "active", Artifacts: map[string]json.RawMessage{}, // Round-3 item 21: the pair lives on the node now. The values // deliberately equal the old per-binding derivation // (ng:cus_1 + the customer's relay password) so every format // body stays byte-identical — the change is only in where // the pair comes from. SocksUser: "ng:cus_1", SocksPassword: "FRYXGBkaGxwdHh8gISIjJA=="}, {ID: "nod_rv", Name: "RV-Plain", ServerID: "srv_v6", Protocol: "vless-reality", Topology: "direct", NetworkMode: "ipv4", Port: 23463, Status: "active", Reality: &RealityConfig{KeyID: "reality-nod_rv", ServerName: "www.apple.com", DestPort: 443}, Artifacts: art("FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu")}, } nc := func(nodeID string) *NodeClient { return &NodeClient{NodeID: nodeID, CustomerID: "cus_1", Status: "active"} } clients := []*NodeClient{ nc("nod_rf"), nc("nod_ss"), nc("nod_hy"), nc("nod_any"), nc("nod_ws"), nc("nod_sk"), nc("nod_rv"), } return &Data{ Servers: []*Server{entryServer, v6Server}, Nodes: nodes, NodeClients: clients, // One credential set for the whole customer (round-2 item 2). The // values are the old RF binding's uuid/shortId and the old SS // binding's password, so the RF and SS entries are byte-stable // against the previous golden file; every other entry now carries // this same set. Customers: []*Customer{{ID: "cus_1", Credentials: &Credentials{ ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01", }}}, } } type goldenSubs struct { Entries []SubEntry `json:"entries"` Formats map[string]struct { Body string `json:"body"` ContentType string `json:"contentType"` } `json:"formats"` } func loadGoldenSubs(t *testing.T) goldenSubs { t.Helper() raw, err := os.ReadFile("testdata/golden_subscriptions.json") if err != nil { t.Fatal(err) } var g goldenSubs if err := unmarshalJSONUseNumber(raw, &g); err != nil { t.Fatal(err) } return g } func TestGoldenEntries(t *testing.T) { g := loadGoldenSubs(t) got := Entries(goldenSubData(), "cus_1") if !reflect.DeepEqual(got, g.Entries) { t.Fatalf("entries mismatch:\n got: %+v\nwant: %+v", got, g.Entries) } } func TestGoldenFormats(t *testing.T) { g := loadGoldenSubs(t) d := goldenSubData() customer := &Customer{ID: "cus_1"} cases := []struct { key string format string ua string }{ {"raw", "raw", ""}, {"base64", "base64", ""}, {"clash", "clash", ""}, {"clash-smart", "clash-smart", ""}, {"singbox", "singbox", ""}, {"surge", "surge", ""}, {"auto-clash-ua", "auto", "ClashForAndroid/2.0"}, {"auto-plain-ua", "auto", "Mozilla/5.0"}, } for _, c := range cases { want, ok := g.Formats[c.key] if !ok { t.Fatalf("golden missing format %s", c.key) } res, ok := FormatSubscription(d, customer, c.format, c.ua) if !ok || res == nil { t.Fatalf("format %s: FormatSubscription returned null", c.key) } if res.ContentType != want.ContentType { t.Errorf("format %s contentType: got %q want %q", c.key, res.ContentType, want.ContentType) } if res.Body != want.Body { t.Errorf("format %s body mismatch:\n got: %q\nwant: %q", c.key, res.Body, want.Body) } } }