feat: add standalone controlplane support
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/cedar2025/xboard-node/internal/panel"
|
||||
"github.com/cedar2025/xboard-node/internal/model"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
)
|
||||
|
||||
func newTestDispatcher() *LimitDispatcher {
|
||||
@@ -13,10 +15,14 @@ func newTestDispatcher() *LimitDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
type nopReader struct{}
|
||||
|
||||
func (nopReader) ReadMultiBuffer() (buf.MultiBuffer, error) { return nil, nil }
|
||||
|
||||
func TestLimitDispatcher_DeviceLimitCheck(t *testing.T) {
|
||||
ld := newTestDispatcher()
|
||||
|
||||
users := []panel.User{
|
||||
users := []model.UserSpec{
|
||||
{ID: 1, UUID: "uuid-1", DeviceLimit: 2, SpeedLimit: 0},
|
||||
{ID: 2, UUID: "uuid-2", DeviceLimit: 0, SpeedLimit: 10},
|
||||
}
|
||||
@@ -93,75 +99,35 @@ func TestLimitDispatcher_DelConn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitDispatcher_SpeedBucket(t *testing.T) {
|
||||
ld := newTestDispatcher()
|
||||
|
||||
email1 := userEmail(1)
|
||||
email2 := userEmail(2)
|
||||
|
||||
speedLimits := map[string]int{email1: 10} // 10 Mbps
|
||||
ld.UpdateLimits(map[string]int{email1: 1, email2: 2}, nil, speedLimits)
|
||||
|
||||
// User 1 has speed limit — should get a limiter
|
||||
limiter := ld.getBucket(email1)
|
||||
if limiter == nil {
|
||||
t.Error("user with speed limit should get a limiter")
|
||||
}
|
||||
|
||||
// Same user should get the same limiter (cached)
|
||||
limiter2 := ld.getBucket(email1)
|
||||
if limiter != limiter2 {
|
||||
t.Error("same user should get cached limiter")
|
||||
}
|
||||
|
||||
// User 2 has no speed limit — should get nil
|
||||
limiter3 := ld.getBucket(email2)
|
||||
if limiter3 != nil {
|
||||
t.Error("user without speed limit should get nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitDispatcher_GetUserTraffic(t *testing.T) {
|
||||
func TestLimitDispatcher_GetConnectionState(t *testing.T) {
|
||||
ld := newTestDispatcher()
|
||||
|
||||
email1 := userEmail(1)
|
||||
email2 := userEmail(2)
|
||||
ld.UpdateLimits(map[string]int{email1: 1, email2: 2}, nil, nil)
|
||||
|
||||
// Simulate traffic by storing counters directly
|
||||
tc1 := &userTrafficCounter{}
|
||||
tc1.upload.Store(1000)
|
||||
tc1.download.Store(2000)
|
||||
ld.userTraffic.Store(email1, tc1)
|
||||
|
||||
tc2 := &userTrafficCounter{}
|
||||
tc2.upload.Store(500)
|
||||
tc2.download.Store(800)
|
||||
ld.userTraffic.Store(email2, tc2)
|
||||
|
||||
// Simulate IPs (use unlimitedIPs for users without device limit)
|
||||
ic1 := &ipCounter{}
|
||||
ic1.ips.Store("1.1.1.1", &atomic.Int64{})
|
||||
ic1.ips.Store("2.2.2.2", &atomic.Int64{})
|
||||
r1 := &atomic.Int64{}
|
||||
r1.Store(1)
|
||||
ic1.ips.Store("1.1.1.1", r1)
|
||||
r2 := &atomic.Int64{}
|
||||
r2.Store(1)
|
||||
ic1.ips.Store("2.2.2.2", r2)
|
||||
ld.unlimitedIPs.Store(email1, ic1)
|
||||
|
||||
ic2 := &ipCounter{}
|
||||
ic2.ips.Store("3.3.3.3", &atomic.Int64{})
|
||||
r3 := &atomic.Int64{}
|
||||
r3.Store(1)
|
||||
ic2.ips.Store("3.3.3.3", r3)
|
||||
ld.unlimitedIPs.Store(email2, ic2)
|
||||
|
||||
ld.connCount.Store(5)
|
||||
|
||||
traffic, aliveIPs, connCount := ld.GetUserTraffic()
|
||||
aliveIPs, connCount := ld.GetConnectionState()
|
||||
|
||||
if connCount != 5 {
|
||||
t.Errorf("expected connCount=5, got %d", connCount)
|
||||
}
|
||||
if traffic[1] != [2]int64{1000, 2000} {
|
||||
t.Errorf("user 1 traffic: got %v", traffic[1])
|
||||
}
|
||||
if traffic[2] != [2]int64{500, 800} {
|
||||
t.Errorf("user 2 traffic: got %v", traffic[2])
|
||||
}
|
||||
if len(aliveIPs[1]) != 2 {
|
||||
t.Errorf("user 1 IPs: got %d, want 2", len(aliveIPs[1]))
|
||||
}
|
||||
@@ -173,11 +139,9 @@ func TestLimitDispatcher_GetUserTraffic(t *testing.T) {
|
||||
func TestLimitDispatcher_ResetConns(t *testing.T) {
|
||||
ld := newTestDispatcher()
|
||||
|
||||
// Add some state
|
||||
ld.mu.Lock()
|
||||
ld.limitedIPs["user@1"] = map[string]int{"1.1.1.1": 1}
|
||||
ld.mu.Unlock()
|
||||
ld.userTraffic.Store("user@1", &userTrafficCounter{})
|
||||
ld.connCount.Store(3)
|
||||
|
||||
ld.ResetConns()
|
||||
@@ -189,16 +153,6 @@ func TestLimitDispatcher_ResetConns(t *testing.T) {
|
||||
t.Error("limitedIPs should be empty after reset")
|
||||
}
|
||||
|
||||
// Verify userTraffic is cleared
|
||||
count := 0
|
||||
ld.userTraffic.Range(func(_, _ interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != 0 {
|
||||
t.Error("userTraffic should be empty after reset")
|
||||
}
|
||||
|
||||
if ld.connCount.Load() != 0 {
|
||||
t.Error("connCount should be 0 after reset")
|
||||
}
|
||||
@@ -230,3 +184,52 @@ func TestLimitDispatcher_UnlimitedUserFastPath(t *testing.T) {
|
||||
t.Error("should have tracked some IPs")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestLimitDispatcher_TrackLinkPreservesReader(t *testing.T) {
|
||||
ld := newTestDispatcher()
|
||||
email := userEmail(1)
|
||||
ld.UpdateLimits(map[string]int{email: 1}, map[string]int{email: 1}, nil)
|
||||
|
||||
origReader := nopReader{}
|
||||
origWriter := &closeTrackingWriter{Writer: buf.Discard, onClose: func() {}}
|
||||
link := &transport.Link{Reader: origReader, Writer: origWriter}
|
||||
|
||||
ld.trackLink(link, email, "1.1.1.1", true)
|
||||
|
||||
if link.Reader != origReader {
|
||||
t.Fatal("trackLink must not replace link.Reader")
|
||||
}
|
||||
if link.Writer == origWriter {
|
||||
t.Fatal("trackLink should wrap link.Writer for lifecycle callbacks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitDispatcher_CloseTrackingWriterReleasesConn(t *testing.T) {
|
||||
ld := newTestDispatcher()
|
||||
email := userEmail(1)
|
||||
ld.UpdateLimits(map[string]int{email: 1}, map[string]int{email: 1}, nil)
|
||||
if ld.checkDeviceLimit(email, "1.1.1.1", true) {
|
||||
t.Fatal("first connection should be allowed")
|
||||
}
|
||||
|
||||
link := &transport.Link{Reader: nopReader{}, Writer: buf.Discard}
|
||||
ld.trackLink(link, email, "1.1.1.1", true)
|
||||
|
||||
if got := ld.connCount.Load(); got != 1 {
|
||||
t.Fatalf("expected connCount=1 after tracking, got %d", got)
|
||||
}
|
||||
cw, ok := link.Writer.(*closeTrackingWriter)
|
||||
if !ok {
|
||||
t.Fatal("expected closeTrackingWriter wrapper")
|
||||
}
|
||||
if err := cw.Close(); err != nil {
|
||||
t.Fatalf("closeTrackingWriter.Close() error = %v", err)
|
||||
}
|
||||
if got := ld.connCount.Load(); got != 0 {
|
||||
t.Fatalf("expected connCount=0 after close, got %d", got)
|
||||
}
|
||||
if ld.checkDeviceLimit(email, "2.2.2.2", true) {
|
||||
t.Fatal("device slot should be released after writer close")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user