mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
163 lines
5.2 KiB
Go
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")
|
|
}
|
|
}
|