Files
SingboxForPanel/service/xboard/service_test.go
2026-04-16 10:29:41 +08:00

318 lines
9.4 KiB
Go

package xboard
import (
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
func TestXUserResolveKeyPrefersPasswordFields(t *testing.T) {
user := XUser{
UUID: "uuid-value",
Passwd: "passwd-value",
Password: "password-value",
Token: "token-value",
}
if got := user.ResolveKey(); got != "passwd-value" {
t.Fatalf("ResolveKey() = %q, want %q", got, "passwd-value")
}
}
func TestXUserIdentifierPrefersUUID(t *testing.T) {
user := XUser{
ID: 7,
UUID: "uuid-value",
Email: "user@example.com",
}
if got := user.Identifier(); got != "uuid-value" {
t.Fatalf("Identifier() = %q, want %q", got, "uuid-value")
}
}
func TestResolveUserKeyForSS2022CombinedPassword(t *testing.T) {
service := &Service{ssServerKey: "master-key"}
user := XUser{
ID: 1,
Password: "master-key:user-key",
UUID: "uuid-value",
}
if got := service.resolveUserKey(user, true); got != "user-key" {
t.Fatalf("resolveUserKey() = %q, want %q", got, "user-key")
}
}
func TestResolveUserKeyForNonSS2022UsesResolvedKey(t *testing.T) {
service := &Service{}
user := XUser{
UUID: "uuid-value",
Passwd: "passwd-value",
}
if got := service.resolveUserKey(user, false); got != "passwd-value" {
t.Fatalf("resolveUserKey() = %q, want %q", got, "passwd-value")
}
}
func TestXUserIdentifierFallsBackToEmailThenID(t *testing.T) {
userWithEmail := XUser{
ID: 8,
Email: "user@example.com",
}
if got := userWithEmail.Identifier(); got != "user@example.com" {
t.Fatalf("Identifier() = %q, want %q", got, "user@example.com")
}
userWithID := XUser{ID: 9}
if got := userWithID.Identifier(); got != "9" {
t.Fatalf("Identifier() = %q, want %q", got, "9")
}
}
func TestExpandNodeOptions(t *testing.T) {
base := option.XBoardServiceOptions{
PanelURL: "https://panel.example",
Key: "shared-token",
NodeType: "vless",
Nodes: []option.XBoardNodeOptions{
{NodeID: 1},
{NodeID: 2, NodeType: "anytls"},
},
}
nodes := expandNodeOptions(base)
if len(nodes) != 2 {
t.Fatalf("expandNodeOptions() len = %d, want 2", len(nodes))
}
if nodes[0].NodeID != 1 || nodes[0].NodeType != "vless" {
t.Fatalf("first node = %+v", nodes[0])
}
if nodes[1].NodeID != 2 || nodes[1].NodeType != "anytls" {
t.Fatalf("second node = %+v", nodes[1])
}
}
func TestExpandNodeOptionsMinimalInstallConfig(t *testing.T) {
base := option.XBoardServiceOptions{
PanelURL: "https://panel.example",
Key: "shared-token",
SyncInterval: 60,
ReportInterval: 60,
Nodes: []option.XBoardNodeOptions{
{NodeID: 286},
{NodeID: 774},
},
}
nodes := expandNodeOptions(base)
if len(nodes) != 2 {
t.Fatalf("expandNodeOptions() len = %d, want 2", len(nodes))
}
for index, node := range nodes {
if node.PanelURL != base.PanelURL {
t.Fatalf("node %d PanelURL = %q, want %q", index, node.PanelURL, base.PanelURL)
}
if node.Key != base.Key {
t.Fatalf("node %d Key = %q, want %q", index, node.Key, base.Key)
}
if node.SyncInterval != base.SyncInterval {
t.Fatalf("node %d SyncInterval = %v, want %v", index, node.SyncInterval, base.SyncInterval)
}
if node.ReportInterval != base.ReportInterval {
t.Fatalf("node %d ReportInterval = %v, want %v", index, node.ReportInterval, base.ReportInterval)
}
if node.ConfigPanelURL != "" || node.UserPanelURL != "" {
t.Fatalf("node %d unexpected panel overrides: config=%q user=%q", index, node.ConfigPanelURL, node.UserPanelURL)
}
if node.ConfigNodeID != 0 || node.UserNodeID != 0 {
t.Fatalf("node %d unexpected node overrides: config=%d user=%d", index, node.ConfigNodeID, node.UserNodeID)
}
}
if nodes[0].NodeID != 286 {
t.Fatalf("first node NodeID = %d, want 286", nodes[0].NodeID)
}
if nodes[1].NodeID != 774 {
t.Fatalf("second node NodeID = %d, want 774", nodes[1].NodeID)
}
}
func TestExpandedNodeTagFallsBackToNodeID(t *testing.T) {
tag := expandedNodeTag("", 0, option.XBoardNodeOptions{NodeID: 286}, option.XBoardServiceOptions{NodeID: 286})
if tag != "xboard-286" {
t.Fatalf("expandedNodeTag() = %q, want %q", tag, "xboard-286")
}
}
func TestApplyACMEConfigFromAutoTLS(t *testing.T) {
var tlsOptions option.InboundTLSOptions
ok := applyACMEConfig(&tlsOptions, nil, true, "example.com", 8443)
if !ok {
t.Fatal("applyACMEConfig() returned false")
}
if tlsOptions.ACME == nil {
t.Fatal("ACME options not configured")
}
if len(tlsOptions.ACME.Domain) != 1 || tlsOptions.ACME.Domain[0] != "example.com" {
t.Fatalf("ACME domains = %+v", tlsOptions.ACME.Domain)
}
if tlsOptions.ACME.DisableHTTPChallenge {
t.Fatal("DisableHTTPChallenge should be false for auto_tls/http mode")
}
if !tlsOptions.ACME.DisableTLSALPNChallenge {
t.Fatal("DisableTLSALPNChallenge should be true for auto_tls/http mode")
}
}
func TestApplyACMEConfigFromDNSCertMode(t *testing.T) {
var tlsOptions option.InboundTLSOptions
ok := applyACMEConfig(&tlsOptions, &XCertConfig{
CertMode: "dns",
Domain: "example.com",
DNSProvider: "cloudflare",
DNSEnv: map[string]string{
"CF_API_TOKEN": "token",
},
}, false, "example.com", 443)
if !ok {
t.Fatal("applyACMEConfig() returned false")
}
if tlsOptions.ACME == nil || tlsOptions.ACME.DNS01Challenge == nil {
t.Fatal("DNS01Challenge not configured")
}
if tlsOptions.ACME.DNS01Challenge.Provider != "cloudflare" {
t.Fatalf("DNS provider = %q", tlsOptions.ACME.DNS01Challenge.Provider)
}
if tlsOptions.ACME.DNS01Challenge.CloudflareOptions.APIToken != "token" {
t.Fatalf("Cloudflare API token = %q", tlsOptions.ACME.DNS01Challenge.CloudflareOptions.APIToken)
}
if !tlsOptions.ACME.DisableTLSALPNChallenge {
t.Fatal("DisableTLSALPNChallenge should be true for dns mode")
}
}
func TestApplyACMEConfigFromTencentCloudDNSCertMode(t *testing.T) {
var tlsOptions option.InboundTLSOptions
ok := applyACMEConfig(&tlsOptions, &XCertConfig{
CertMode: "dns",
Domain: "code.littlediary.cn",
DNSProvider: "tencentcloud",
DNSEnv: map[string]string{
"TENCENTCLOUD_SECRET_ID": "sid",
"TENCENTCLOUD_SECRET_KEY": "skey",
},
}, false, "code.littlediary.cn", 45365)
if !ok {
t.Fatal("applyACMEConfig() returned false")
}
if tlsOptions.ACME == nil || tlsOptions.ACME.DNS01Challenge == nil {
t.Fatal("DNS01Challenge not configured")
}
if tlsOptions.ACME.DNS01Challenge.Provider != C.DNSProviderTencentCloud {
t.Fatalf("DNS provider = %q", tlsOptions.ACME.DNS01Challenge.Provider)
}
if tlsOptions.ACME.DNS01Challenge.TencentCloudOptions.SecretID != "sid" {
t.Fatalf("TencentCloud SecretID = %q", tlsOptions.ACME.DNS01Challenge.TencentCloudOptions.SecretID)
}
if tlsOptions.ACME.DNS01Challenge.TencentCloudOptions.SecretKey != "skey" {
t.Fatalf("TencentCloud SecretKey = %q", tlsOptions.ACME.DNS01Challenge.TencentCloudOptions.SecretKey)
}
if tlsOptions.ACME.AlternativeTLSPort != 45365 {
t.Fatalf("AlternativeTLSPort = %d, want 45365", tlsOptions.ACME.AlternativeTLSPort)
}
}
func TestApplyACMEConfigFromDNSPodAliasWithTencentCredentials(t *testing.T) {
var tlsOptions option.InboundTLSOptions
ok := applyACMEConfig(&tlsOptions, &XCertConfig{
CertMode: "dns",
Domain: "code.littlediary.cn",
DNSProvider: "dnspod",
DNSEnv: map[string]string{
"TENCENTCLOUD_SECRET_ID": "sid",
"TENCENTCLOUD_SECRET_KEY": "skey",
},
}, false, "code.littlediary.cn", 443)
if !ok {
t.Fatal("applyACMEConfig() returned false")
}
if tlsOptions.ACME == nil || tlsOptions.ACME.DNS01Challenge == nil {
t.Fatal("DNS01Challenge not configured")
}
if tlsOptions.ACME.DNS01Challenge.Provider != C.DNSProviderTencentCloud {
t.Fatalf("DNS provider = %q, want %q", tlsOptions.ACME.DNS01Challenge.Provider, C.DNSProviderTencentCloud)
}
}
func TestMergedTLSSettingsUsesTopLevelServerName(t *testing.T) {
tlsSettings := mergedTLSSettings(XInnerConfig{}, &XNodeConfig{
ServerName: "code.littlediary.cn",
})
if tlsSettings == nil {
t.Fatal("mergedTLSSettings() returned nil")
}
if tlsSettings.ServerName != "code.littlediary.cn" {
t.Fatalf("ServerName = %q, want %q", tlsSettings.ServerName, "code.littlediary.cn")
}
}
func TestHasUsableServerTLS(t *testing.T) {
if hasUsableServerTLS(option.InboundTLSOptions{}) {
t.Fatal("empty TLS options should not be usable")
}
if !hasUsableServerTLS(option.InboundTLSOptions{
CertificatePath: "cert.pem",
KeyPath: "key.pem",
}) {
t.Fatal("file certificate should be usable")
}
if !hasUsableServerTLS(option.InboundTLSOptions{
ACME: &option.InboundACMEOptions{
Domain: badoption.Listable[string]{"example.com"},
},
}) {
t.Fatal("ACME certificate should be usable")
}
}
func TestBuildInboundMultiplex(t *testing.T) {
config := &XMultiplexConfig{
Enabled: true,
Padding: true,
Brutal: &XBrutalConfig{
Enabled: true,
UpMbps: 100,
DownMbps: 200,
},
}
got := buildInboundMultiplex(config)
if got == nil {
t.Fatal("buildInboundMultiplex() returned nil")
}
if !got.Enabled || !got.Padding {
t.Fatalf("buildInboundMultiplex() = %+v", got)
}
if got.Brutal == nil || !got.Brutal.Enabled || got.Brutal.UpMbps != 100 || got.Brutal.DownMbps != 200 {
t.Fatalf("buildInboundMultiplex() brutal = %+v", got.Brutal)
}
}
func TestNormalizePanelNodeType(t *testing.T) {
tests := map[string]string{
"v2ray": "vmess",
"hysteria2": "hysteria",
"vless": "vless",
"": "",
}
for input, want := range tests {
if got := normalizePanelNodeType(input); got != want {
t.Fatalf("normalizePanelNodeType(%q) = %q, want %q", input, got, want)
}
}
}