Files

163 lines
5.2 KiB
Go

package proxy
import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
func TestSingBoxRegisterBridgeStoresNewBridge(t *testing.T) {
manager := &SingBoxManager{
Bridges: make(map[string]*SingBoxBridge),
}
bridge := &SingBoxBridge{
NodeKey: "node-a",
Port: 21001,
Running: true,
}
socksURL, reused := manager.registerBridge("node-a", bridge, false)
if reused {
t.Fatalf("expected new bridge registration, got reused with %q", socksURL)
}
if socksURL != "" {
t.Fatalf("expected empty socksURL for new bridge registration, got %q", socksURL)
}
if manager.Bridges["node-a"] != bridge {
t.Fatalf("bridge was not stored in manager")
}
}
func TestSingBoxRegisterBridgeIgnoresSamePointer(t *testing.T) {
manager := &SingBoxManager{
Bridges: make(map[string]*SingBoxBridge),
}
bridge := &SingBoxBridge{
NodeKey: "node-a",
Port: 21001,
Running: true,
}
manager.Bridges["node-a"] = bridge
socksURL, reused := manager.registerBridge("node-a", bridge, false)
if reused {
t.Fatalf("same bridge pointer must not be treated as duplicate, got reused with %q", socksURL)
}
if socksURL != "" {
t.Fatalf("expected empty socksURL when registering same pointer, got %q", socksURL)
}
if manager.Bridges["node-a"] != bridge {
t.Fatalf("bridge mapping changed unexpectedly")
}
if bridge.Stopping {
t.Fatalf("same bridge pointer should not be marked as stopping")
}
}
func TestSingBoxRegisterBridgePinsLongLivedBridge(t *testing.T) {
t.Parallel()
manager := &SingBoxManager{Bridges: make(map[string]*SingBoxBridge)}
bridge := &SingBoxBridge{NodeKey: "node-a", Port: 21001, Running: true}
_, reused := manager.registerBridge("node-a", bridge, true)
if reused {
t.Fatalf("new bridge should not be reported as reused")
}
if bridge.RefCount != 1 {
t.Fatalf("pinned bridge refcount = %d, want 1", bridge.RefCount)
}
manager.ReleaseBridge("node-a")
if bridge.RefCount != 0 {
t.Fatalf("released bridge refcount = %d, want 0", bridge.RefCount)
}
}
func TestSingBoxRecycleIdleBridgesSkipsPinnedBridge(t *testing.T) {
t.Parallel()
manager := &SingBoxManager{Bridges: make(map[string]*SingBoxBridge)}
pinned := &SingBoxBridge{NodeKey: "pinned-node-key", LastUsedAt: time.Now().Add(-2 * singBoxBridgeIdleTTL), RefCount: 1}
idle := &SingBoxBridge{NodeKey: "idle-node-key", LastUsedAt: time.Now().Add(-2 * singBoxBridgeIdleTTL)}
manager.Bridges[pinned.NodeKey] = pinned
manager.Bridges[idle.NodeKey] = idle
manager.recycleIdleBridges()
if manager.Bridges[pinned.NodeKey] != pinned {
t.Fatalf("pinned bridge should not be recycled")
}
if _, ok := manager.Bridges[idle.NodeKey]; ok {
t.Fatalf("idle bridge should be recycled")
}
}
func TestSingBoxLaunchErrorRetryClassification(t *testing.T) {
t.Parallel()
if isRetryableSingBoxLaunchError(&singBoxLaunchError{err: fmt.Errorf("config invalid"), retryable: false}) {
t.Fatalf("non-retryable sing-box launch error was classified as retryable")
}
if !isRetryableSingBoxLaunchError(&singBoxLaunchError{err: fmt.Errorf("port race"), retryable: true}) {
t.Fatalf("retryable sing-box launch error was classified as non-retryable")
}
if !isRetryableSingBoxLaunchError(fmt.Errorf("legacy error")) {
t.Fatalf("plain errors should remain retryable for backward compatibility")
}
}
func TestSingBoxBridgeReadyErrorRetryPolicy(t *testing.T) {
t.Parallel()
manager := &SingBoxManager{}
dir := t.TempDir()
cfgPath := filepath.Join(dir, "singbox-config.json")
stderrPath := filepath.Join(dir, "singbox-stderr.log")
if err := os.WriteFile(cfgPath, []byte(`{}`), 0o644); err != nil {
t.Fatalf("write config failed: %v", err)
}
if manager.isRetryableBridgeReadyError(fmt.Errorf("sing-box 进程提前退出: config invalid"), cfgPath, stderrPath) {
t.Fatalf("early process exit without bind evidence should not be retried")
}
if err := os.WriteFile(stderrPath, []byte("listen tcp 127.0.0.1:10001: bind: address already in use"), 0o644); err != nil {
t.Fatalf("write stderr failed: %v", err)
}
if !manager.isRetryableBridgeReadyError(fmt.Errorf("sing-box 进程提前退出"), cfgPath, stderrPath) {
t.Fatalf("bind conflict should be retried with another port")
}
}
func TestSingBoxRestartBridgeNotNeededWhenBridgeChanged(t *testing.T) {
t.Parallel()
manager := &SingBoxManager{Bridges: make(map[string]*SingBoxBridge)}
oldBridge := &SingBoxBridge{NodeKey: "node-a", Port: 21001, Outbound: map[string]interface{}{"type": "direct"}}
manager.Bridges["node-a"] = &SingBoxBridge{NodeKey: "node-a", Port: 21002}
err := manager.restartBridgeOnSamePort(nil, "node-a", oldBridge, oldBridge.RefCount)
if !errors.Is(err, errSingBoxBridgeRestartNotNeeded) {
t.Fatalf("restartBridgeOnSamePort() error = %v, want restart-not-needed", err)
}
}
func TestSingBoxRestartBridgeRequiresContext(t *testing.T) {
t.Parallel()
manager := &SingBoxManager{Bridges: make(map[string]*SingBoxBridge)}
bridge := &SingBoxBridge{NodeKey: "node-a", Port: 21001}
manager.Bridges["node-a"] = bridge
err := manager.restartBridgeOnSamePort(nil, "node-a", bridge, bridge.RefCount)
if err == nil {
t.Fatalf("restartBridgeOnSamePort() returned nil, want missing context error")
}
if errors.Is(err, errSingBoxBridgeRestartNotNeeded) {
t.Fatalf("missing restart context should not be treated as restart-not-needed")
}
}