Files
3x-ui/internal/web/service/traffic_runtime_apply_test.go
T
MHSanaei 152c84efa2 fix(inbounds): delete an inbound's dependent rows on every delete path
The node sync's sweep of an inbound the node stopped reporting deleted
the inbound but left its host rows, and the depleted-clients delete
left its sub-balancer references. SQLite hands a freed inbound id out
again once the table empties, so a node's host overrides could bind to
a new inbound it never hosted and point that inbound's subscribers at
an address, port and TLS settings the node chose.

delInbound's cleanup moves into deleteInboundRow (detach clients,
delete the inbound, its host rows and its sub-balancer references),
which the sweep and the depleted-clients delete now call as well.
2026-11-01 00:36:45 +01:00

97 lines
3.5 KiB
Go

package service
import (
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
func TestTrafficDisableImmediatelyUpdatesNodeRuntime(t *testing.T) {
setupConflictDB(t)
nodeID, fake := setupNodeRuntime(t)
client := model.Client{Email: "spent-node", Enable: true}
ib := nodeInbound(t, nodeID, 46301, []model.Client{client})
if err := database.GetDB().Create(&xray.ClientTraffic{
InboundId: ib.Id, Email: client.Email, Enable: true, Up: 100, Total: 100,
}).Error; err != nil {
t.Fatalf("seed traffic: %v", err)
}
if _, _, err := (&InboundService{}).AddTraffic(nil, nil); err != nil {
t.Fatalf("AddTraffic: %v", err)
}
if got := fake.updateInbound.Load(); got != 1 {
t.Fatalf("remote UpdateInbound calls = %d, want 1 after commit", got)
}
}
func TestTrafficDisableRefreshesLocalMTProtoSidecar(t *testing.T) {
setupConflictDB(t)
mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }})
fake := &fakeNodeRuntime{}
mgr.SetLocalRuntimeOverride(fake)
runtime.SetManager(mgr)
t.Cleanup(func() { runtime.SetManager(nil) })
seedInboundConflict(t, "mt-spent", "", 46302, model.MTProto, "",
`{"clients":[{"email":"spent-mt","secret":"`+mtprotoTestSecretA+`","enable":true}]}`)
ib := loadInboundByTag(t, "mt-spent")
clients, err := (&InboundService{}).GetClients(ib)
if err != nil {
t.Fatalf("GetClients: %v", err)
}
if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
t.Fatalf("SyncInbound: %v", err)
}
seedClientTraffic(t, ib.Id, "spent-mt", true)
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", "spent-mt").
Updates(map[string]any{"up": 100, "total": 100}).Error; err != nil {
t.Fatalf("deplete traffic: %v", err)
}
if _, _, err := (&InboundService{}).AddTraffic(nil, nil); err != nil {
t.Fatalf("AddTraffic: %v", err)
}
if got := fake.updateInbound.Load(); got != 1 {
t.Fatalf("MTProto sidecar UpdateInbound calls = %d, want 1 after commit", got)
}
}
func TestDelDepletedClientsCleansRuntimeAfterCommit(t *testing.T) {
setupConflictDB(t)
mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }})
fake := &fakeNodeRuntime{}
mgr.SetLocalRuntimeOverride(fake)
runtime.SetManager(mgr)
t.Cleanup(func() { runtime.SetManager(nil) })
seedInboundConflict(t, "depleted-only", "", 46303, model.VLESS, `{"network":"tcp"}`,
`{"clients":[{"email":"gone","enable":true}]}`)
ib := loadInboundByTag(t, "depleted-only")
seedClientTraffic(t, ib.Id, "gone", true)
if err := database.GetDB().Model(&xray.ClientTraffic{}).Where("email = ?", "gone").
Updates(map[string]any{"up": 100, "total": 100, "reset": 0}).Error; err != nil {
t.Fatalf("deplete traffic: %v", err)
}
balancer := &model.SubBalancer{Remark: "depleted", InboundIds: []int{ib.Id}, Enabled: true}
if err := database.GetDB().Create(balancer).Error; err != nil {
t.Fatalf("create balancer: %v", err)
}
if err := (&InboundService{}).DelDepletedClients(-1); err != nil {
t.Fatalf("DelDepletedClients: %v", err)
}
if got := fake.delInbound.Load(); got != 1 {
t.Fatalf("runtime DelInbound calls = %d, want 1 after commit", got)
}
if err := database.GetDB().First(balancer, balancer.Id).Error; err != nil {
t.Fatalf("reload balancer: %v", err)
}
if len(balancer.InboundIds) != 0 {
t.Fatalf("balancer members after the inbound went = %v, want none", balancer.InboundIds)
}
}