128 lines
2.9 KiB
Go
128 lines
2.9 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"vpnclient/internal/config"
|
|
)
|
|
|
|
type fakeEngine struct {
|
|
mu sync.Mutex
|
|
running bool
|
|
stopDelay time.Duration
|
|
stops atomic.Int32
|
|
}
|
|
|
|
func (e *fakeEngine) Protocol() config.Protocol { return config.ProtocolNaive }
|
|
func (e *fakeEngine) Start(context.Context, config.Profile, string) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.running = true
|
|
return nil
|
|
}
|
|
func (e *fakeEngine) Stop() error {
|
|
e.stops.Add(1)
|
|
if e.stopDelay > 0 {
|
|
time.Sleep(e.stopDelay)
|
|
}
|
|
e.mu.Lock()
|
|
e.running = false
|
|
e.mu.Unlock()
|
|
return nil
|
|
}
|
|
func (e *fakeEngine) Running() bool {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
return e.running
|
|
}
|
|
func (e *fakeEngine) LocalHTTPProxy() (string, bool) { return "127.0.0.1:1081", true }
|
|
func (e *fakeEngine) LocalSOCKSProxy() (string, bool) { return "127.0.0.1:1080", true }
|
|
|
|
func TestDisconnectWaitsBeforeReconnect(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Active: "t",
|
|
SystemProxy: false,
|
|
BinDir: t.TempDir(),
|
|
Profiles: []config.Profile{{
|
|
Name: "t",
|
|
Protocol: config.ProtocolNaive,
|
|
Proxy: "https://u:p@example.com",
|
|
Listen: []string{"socks://127.0.0.1:1080", "http://127.0.0.1:1081"},
|
|
}},
|
|
}
|
|
m, err := NewManager(t.TempDir()+"/c.json", cfg, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
eng := &fakeEngine{stopDelay: 200 * time.Millisecond}
|
|
m.mu.Lock()
|
|
m.engine = eng
|
|
m.profile = &cfg.Profiles[0]
|
|
m.mu.Unlock()
|
|
|
|
start := time.Now()
|
|
_ = m.Disconnect()
|
|
// Connect must wait for Stop to finish even if Disconnect returned early...
|
|
// our Disconnect waits up to 3s, so with 200ms it should complete.
|
|
if time.Since(start) < 150*time.Millisecond {
|
|
t.Fatalf("disconnect returned too fast")
|
|
}
|
|
if eng.Running() {
|
|
t.Fatal("engine still running")
|
|
}
|
|
|
|
// Simulate slow stop that outlives Disconnect timeout.
|
|
eng2 := &fakeEngine{stopDelay: 500 * time.Millisecond}
|
|
m.mu.Lock()
|
|
m.engine = eng2
|
|
m.profile = &cfg.Profiles[0]
|
|
m.mu.Unlock()
|
|
|
|
// Force short path: call the wait mechanism like Connect does.
|
|
m.mu.Lock()
|
|
engine := m.engine
|
|
m.engine = nil
|
|
m.profile = nil
|
|
m.lastStop.Add(1)
|
|
m.mu.Unlock()
|
|
go func() {
|
|
defer m.lastStop.Done()
|
|
_ = engine.Stop()
|
|
}()
|
|
|
|
waitStart := time.Now()
|
|
m.waitEngineStopped()
|
|
if time.Since(waitStart) < 400*time.Millisecond {
|
|
t.Fatalf("waitEngineStopped returned before stop finished")
|
|
}
|
|
if eng2.Running() {
|
|
t.Fatal("engine2 still running after wait")
|
|
}
|
|
}
|
|
|
|
func TestConfigReturnsClone(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Active: "a",
|
|
Profiles: []config.Profile{{
|
|
Name: "a", Protocol: config.ProtocolNaive, Proxy: "https://x",
|
|
}},
|
|
}
|
|
m, err := NewManager(t.TempDir()+"/c.json", cfg, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snap := m.Config()
|
|
snap.Active = "mutated"
|
|
snap.Profiles[0].Proxy = "leaked"
|
|
if m.cfg.Active != "a" {
|
|
t.Fatal("Config() leaked mutable Active")
|
|
}
|
|
if m.cfg.Profiles[0].Proxy != "https://x" {
|
|
t.Fatal("Config() leaked mutable Profile")
|
|
}
|
|
}
|