package service import ( "context" "errors" "strings" "testing" "github.com/cedar2025/xboard-node/internal/cert" "github.com/cedar2025/xboard-node/internal/config" "github.com/cedar2025/xboard-node/internal/kernel" "github.com/cedar2025/xboard-node/internal/limiter" "github.com/cedar2025/xboard-node/internal/model" "golang.org/x/time/rate" ) type fakeKernel struct { running bool startErr error updateErr error addErr error startCalls int updateCalls int addCalls int removeCalls int onUpdateUsers func([]model.UserSpec) onAddUsers func([]model.UserSpec) onRemoveUsers func([]model.UserSpec) speedLimitFunc func(string) *rate.Limiter deviceLimitFunc func(string) (int, bool) } func (f *fakeKernel) Name() string { return "fake" } func (f *fakeKernel) Protocols() []string { return []string{"vless"} } func (f *fakeKernel) Capabilities() kernel.Capabilities { return kernel.Capabilities{} } func (f *fakeKernel) Start(nodeConfig *model.NodeSpec, users []model.UserSpec, tls kernel.TLSCert) error { _, _, _ = nodeConfig, users, tls f.startCalls++ if f.startErr != nil { return f.startErr } f.running = true return nil } func (f *fakeKernel) Stop() { f.running = false } func (f *fakeKernel) IsRunning() bool { return f.running } func (f *fakeKernel) Reload(nodeConfig *model.NodeSpec, users []model.UserSpec, tls kernel.TLSCert) error { _, _, _ = nodeConfig, users, tls return nil } func (f *fakeKernel) AddUsers(users []model.UserSpec) (int, error) { f.addCalls++ if f.onAddUsers != nil { f.onAddUsers(users) } if f.addErr != nil { return 0, f.addErr } return len(users), nil } func (f *fakeKernel) RemoveUsers(users []model.UserSpec) (int, error) { f.removeCalls++ if f.onRemoveUsers != nil { f.onRemoveUsers(users) } return len(users), nil } func (f *fakeKernel) UpdateUsers(users []model.UserSpec) (int, int, error) { f.updateCalls++ if f.onUpdateUsers != nil { f.onUpdateUsers(users) } if f.updateErr != nil { return 0, 0, f.updateErr } return len(users), 0, nil } func (f *fakeKernel) GetUserTraffic(ctx context.Context) (map[int][2]int64, map[int]map[string]bool, int, error) { _ = ctx return nil, nil, 0, nil } func (f *fakeKernel) CloseConnection(ctx context.Context, connID string) error { _, _ = ctx, connID return nil } func (f *fakeKernel) CloseUserConnections(ctx context.Context, uuid string) error { _, _ = ctx, uuid return nil } func (f *fakeKernel) SetSpeedLimitFunc(fn func(uuid string) *rate.Limiter) { f.speedLimitFunc = fn } func (f *fakeKernel) SetDeviceLimitFunc(fn func(uuid string) (int, bool)) { f.deviceLimitFunc = fn } func (f *fakeKernel) UpdateGlobalDevices(users map[int][]string) { _ = users } func (f *fakeKernel) ClearGlobalDevices() {} func newTestService(k *fakeKernel) *Service { sharedLimiter := limiter.New() s := &Service{ kernel: k, limiter: sharedLimiter, speedTracker: limiter.NewSpeedTracker(sharedLimiter), cert: cert.NewManager(config.CertConfig{}), } k.SetSpeedLimitFunc(s.speedTracker.GetLimiter) k.SetDeviceLimitFunc(s.limiter.GetDeviceLimitByUUID) return s } func TestApplyUserUpdatePreparesLimiterBeforeKernelUpdate(t *testing.T) { k := &fakeKernel{running: true} s := newTestService(k) s.lastConfig = &model.NodeSpec{Protocol: "vless"} oldUsers := []model.UserSpec{{ID: 1, UUID: "uuid-old", SpeedLimit: 4}} s.updateUserState(oldUsers) newUsers := []model.UserSpec{{ID: 2, UUID: "uuid-new", SpeedLimit: 8}} k.onUpdateUsers = func(users []model.UserSpec) { if len(users) != 1 || users[0].UUID != "uuid-new" { t.Fatalf("unexpected users passed to UpdateUsers: %#v", users) } if got := k.speedLimitFunc("uuid-new"); got == nil { t.Fatal("expected new user's limiter to be visible before kernel UpdateUsers") } } s.applyUserUpdate(context.Background(), newUsers, computeUserHash(newUsers)) if got := k.updateCalls; got != 1 { t.Fatalf("UpdateUsers call count = %d, want 1", got) } if len(s.lastUsers) != 1 || s.lastUsers[0].UUID != "uuid-new" { t.Fatalf("lastUsers = %#v, want new users", s.lastUsers) } if s.speedTracker.GetLimiter("uuid-new") == nil { t.Fatal("expected limiter for new user after successful update") } } func TestApplyUserUpdateRestoresStateWhenKernelAndRestartFail(t *testing.T) { k := &fakeKernel{ running: true, updateErr: errors.New("update failed"), startErr: errors.New("restart failed"), } s := newTestService(k) s.lastConfig = &model.NodeSpec{Protocol: "vless"} oldUsers := []model.UserSpec{{ID: 1, UUID: "uuid-old", SpeedLimit: 4}} s.updateUserState(oldUsers) oldHash := s.lastUserHash newUsers := []model.UserSpec{{ID: 2, UUID: "uuid-new", SpeedLimit: 8}} s.applyUserUpdate(context.Background(), newUsers, computeUserHash(newUsers)) if got := k.startCalls; got != 1 { t.Fatalf("Start call count = %d, want 1", got) } if len(s.lastUsers) != 1 || s.lastUsers[0].UUID != "uuid-old" { t.Fatalf("lastUsers = %#v, want restored old users", s.lastUsers) } if s.lastUserHash != oldHash { t.Fatalf("lastUserHash = %q, want %q", s.lastUserHash, oldHash) } if s.speedTracker.GetLimiter("uuid-old") == nil { t.Fatal("expected old limiter to be restored after rollback") } if s.speedTracker.GetLimiter("uuid-new") != nil { t.Fatal("expected new limiter to be removed after rollback") } } func TestApplyUserDeltaAddPreparesLimiterBeforeKernelUpdate(t *testing.T) { k := &fakeKernel{running: true} s := newTestService(k) s.lastConfig = &model.NodeSpec{Protocol: "vless"} oldUsers := []model.UserSpec{{ID: 1, UUID: "uuid-old", SpeedLimit: 4}} s.updateUserState(oldUsers) delta := []model.UserSpec{{ID: 2, UUID: "uuid-new", SpeedLimit: 8}} k.onAddUsers = func(users []model.UserSpec) { if len(users) != 1 || users[0].UUID != "uuid-new" { t.Fatalf("unexpected users passed to AddUsers: %#v", users) } if got := k.speedLimitFunc("uuid-new"); got == nil { t.Fatal("expected delta user's limiter to be visible before kernel AddUsers") } } s.applyUserDelta(context.Background(), "add", delta) if got := k.addCalls; got != 1 { t.Fatalf("AddUsers call count = %d, want 1", got) } if s.speedTracker.GetLimiter("uuid-new") == nil { t.Fatal("expected limiter for delta-added user after successful update") } } func TestValidateNodeRuntimeRejectsUnsupportedDNSProvider(t *testing.T) { cfg := &config.Config{Kernel: config.KernelConfig{Type: "singbox"}} err := validateNodeRuntime(cfg, []string{"http"}, &model.NodeSpec{ Protocol: "http", CertConfig: &config.CertConfig{ CertMode: "dns", DNSProvider: "3123123", Domain: "example.com", }, }, kernel.TLSCert{CertPEM: []byte("CERT"), KeyPEM: []byte("KEY")}) if err == nil { t.Fatal("expected error, got nil") } if err.Error() == "" { t.Fatal("expected non-empty error") } if got := err.Error(); !strings.HasPrefix(got, `unsupported cert_config.dns_provider "3123123" (supported: `) { t.Fatalf("unexpected error: %v", got) } } func TestValidateNodeRuntimeAllowsSelfManagedTLSBeforeFilesExist(t *testing.T) { cfg := &config.Config{Kernel: config.KernelConfig{Type: "singbox"}} err := validateNodeRuntime(cfg, []string{"anytls", "hysteria"}, &model.NodeSpec{ Protocol: "anytls", CertConfig: &config.CertConfig{ CertMode: "self", Domain: "example.com", }, }, kernel.TLSCert{}) if err != nil { t.Fatalf("expected self-managed TLS config to pass validation, got %v", err) } } func TestValidateNodeRuntimeAllowsSingboxRealityWithRequiredFields(t *testing.T) { cfg := &config.Config{Kernel: config.KernelConfig{Type: "singbox"}} err := validateNodeRuntime(cfg, []string{"vless"}, &model.NodeSpec{ Protocol: "vless", TLS: 2, TLSSettings: map[string]any{ "private_key": "test-key", "server_name": "example.com", }, }, kernel.TLSCert{CertPEM: []byte("CERT"), KeyPEM: []byte("KEY")}) if err != nil { t.Fatalf("expected sing-box reality validation to pass, got %v", err) } } func TestValidateNodeRuntimeRejectsRealityWithoutTLSSettings(t *testing.T) { cfg := &config.Config{Kernel: config.KernelConfig{Type: "singbox"}} err := validateNodeRuntime(cfg, []string{"vless"}, &model.NodeSpec{ Protocol: "vless", TLS: 2, }, kernel.TLSCert{CertPEM: []byte("CERT"), KeyPEM: []byte("KEY")}) if err == nil { t.Fatal("expected error, got nil") } if got := err.Error(); got != "reality tls requires tls_settings" { t.Fatalf("unexpected error: %v", got) } } func TestValidateNodeRuntimeRejectsRealityWithoutPrivateKey(t *testing.T) { cfg := &config.Config{Kernel: config.KernelConfig{Type: "singbox"}} err := validateNodeRuntime(cfg, []string{"vless"}, &model.NodeSpec{ Protocol: "vless", TLS: 2, TLSSettings: map[string]any{ "server_name": "example.com", }, }, kernel.TLSCert{CertPEM: []byte("CERT"), KeyPEM: []byte("KEY")}) if err == nil { t.Fatal("expected error, got nil") } if got := err.Error(); got != "reality tls requires tls_settings.private_key" { t.Fatalf("unexpected error: %v", got) } } func TestValidateNodeRuntimeRejectsRealityWithoutServerNameOrDest(t *testing.T) { cfg := &config.Config{Kernel: config.KernelConfig{Type: "singbox"}} err := validateNodeRuntime(cfg, []string{"vless"}, &model.NodeSpec{ Protocol: "vless", TLS: 2, TLSSettings: map[string]any{ "private_key": "test-key", }, }, kernel.TLSCert{CertPEM: []byte("CERT"), KeyPEM: []byte("KEY")}) if err == nil { t.Fatal("expected error, got nil") } if got := err.Error(); got != "reality tls requires tls_settings.server_name or tls_settings.dest" { t.Fatalf("unexpected error: %v", got) } }