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