Files
NexusGate/panel-go/subscriptions_test.go
aaaamoooo 2fafd85d1f feat(panel): build-before-bind + socks5 fixed node account (items 20+21)
Item 20: drop the zero-client deploy gate in ValidateNodeDeploy for
all protocols, stop RebuildNode from auto-removing a node whose
bindings reach zero, and let server/heartbeat reconciliation rebuild
zero-client nodes like any other. Nodes are built and deployed first;
bindings can be added and removed freely.

Item 21: socks5-auth nodes carry one node-level fixed account
(node.socksUser/socksPassword, minted at creation or by the store-load
migration MigrateSocksNodeCredentials, idempotent). The sing-box
inbound's only user is that pair regardless of bindings; subscription
links for every bound customer carry the same pair. The generated
username avoids the ng: prefix so the agent never attributes the
account's traffic to a customer — it stays on the node's inbound
counters. Edit form shows both fields (copyable) for socks nodes;
NormalizeNode validates operator-supplied usernames.
2026-10-21 10:40:58 +00:00

131 lines
5.0 KiB
Go

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