Files
3x-ui-agent/internal/web/service/inbound_node_online_integration_test.go
T
Muse ac39370879 fix(onlines): 恢复被 P9 掏空的在线状态存储(第11条)
根因: 1035d00 删本地核心时, inbound_node.go 的在线存储层被一并掏空
(GetOnlineClients 写死空/SetNodeOnlineTree 等空函数/SetRemoteTraffic
末尾喂树整块删除), 而节点轮询与 agent 上报两条到达路径完好, 数据
送达即被扔, 客户端/入站在线永远全零。

修复: 把原 xray.Process 上的在线存储(算法/锁/grace 20s 语义逐字
移植)落到 service 包单例 onlineStore(sharedOnlineStore, 对应原
currentXrayProcess 的单一持有语义); 恢复六个 wrapper + 新增
SetNodeActiveInboundTree; setRemoteTrafficLocked 补回 guidShared
与喂树块(原 f727d04 逻辑, process. 换 s.)。

偏离: 本地集在精简主控无本地核心喂养, 仅 mtproto job 折入(该 job
已不被调度, 路径保留无害); GetOnlineClients 对 nil 归一为 []。

测试: online_store_test 9 例(-race 全过: 并集/按GUID/替换/清空/
grace 边界/跨实例共享/并发); SetRemoteTraffic 集成 2 例(传统节点
GUID 树+active 树落库、旧节点 flat 回退); 全量 go test 串行复跑失败集合与基线 9 红逐字一致(零新增);
service 包单跑唯一红为基线既有的 TestGetXrayConfig 红, 其余全过。
2026-10-22 09:19:20 +00:00

123 lines
3.9 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"
)
// The traditional-node path: NodeTrafficSyncJob fetches a TrafficSnapshot
// from each node panel (runtime.Remote.FetchTrafficSnapshot) and hands it
// to InboundService.SetRemoteTraffic. Before the online-store restoration,
// the snapshot's OnlineTree / ActiveInboundTree were dropped on the floor
// inside that call (the store setters were no-op stubs), so polled nodes
// never showed anyone online. This drives the exact public entry the job
// calls and asserts the snapshot's online data lands in the store.
func TestSetRemoteTraffic_FeedsOnlineStore(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
svc := InboundService{}
const nodeID = 4242
const nodeGuid = "trad-node-guid-4242"
svc.ClearNodeOnlineClients(nodeID)
t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) })
if err := db.Create(&model.Node{
Id: nodeID,
Name: "trad-node",
Address: "10.0.4.2",
Port: 2053,
ApiToken: "t",
Guid: nodeGuid,
}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
snap := &runtime.TrafficSnapshot{
Inbounds: []*model.Inbound{
{
Tag: "in-443-tcp",
Enable: true,
Port: 443,
Protocol: model.VLESS,
Settings: `{"clients":[{"email":"trad@example.com"}]}`,
},
},
OnlineTree: map[string][]string{nodeGuid: {"trad@example.com"}},
ActiveInboundTree: map[string][]string{nodeGuid: {"in-443-tcp"}},
}
if _, err := svc.SetRemoteTraffic(nodeID, snap, false, false); err != nil {
t.Fatalf("SetRemoteTraffic: %v", err)
}
assertStringSet(t, "online union", svc.GetOnlineClients(), []string{"trad@example.com"})
byGuid := svc.GetOnlineClientsByGuid()
assertStringSet(t, "online byGuid", byGuid[nodeGuid], []string{"trad@example.com"})
// The active tree is normalized from node-side tags to central tags;
// whatever tag the adopted central inbound carries is what must show.
var central model.Inbound
if err := db.Where("node_id = ?", nodeID).First(&central).Error; err != nil {
t.Fatalf("load central inbound: %v", err)
}
active := svc.GetActiveInboundsByGuid()
assertStringSet(t, "active byGuid", active[nodeGuid], []string{central.Tag})
// A failed probe makes the job clear the node; its clients must stop
// showing as online immediately.
svc.ClearNodeOnlineClients(nodeID)
if got := svc.GetOnlineClients(); len(got) != 0 {
t.Fatalf("online union after clear = %v, want empty", got)
}
}
// Old-build nodes have no GUID tree endpoint: the job passes their flat
// OnlineEmails in the snapshot and SetRemoteTraffic must key them under
// the node's own effective identity.
func TestSetRemoteTraffic_FlatOnlineEmailsFallback(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
svc := InboundService{}
const nodeID = 4243
const nodeGuid = "flat-node-guid-4243"
svc.ClearNodeOnlineClients(nodeID)
t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) })
if err := db.Create(&model.Node{
Id: nodeID,
Name: "flat-node",
Address: "10.0.4.3",
Port: 2053,
ApiToken: "t",
Guid: nodeGuid,
}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
snap := &runtime.TrafficSnapshot{
Inbounds: []*model.Inbound{
{
Tag: "in-8443-tcp",
Enable: true,
Port: 8443,
Protocol: model.VLESS,
Settings: `{"clients":[{"email":"flat@example.com"}]}`,
},
},
OnlineEmails: []string{"flat@example.com"},
}
if _, err := svc.SetRemoteTraffic(nodeID, snap, false, false); err != nil {
t.Fatalf("SetRemoteTraffic: %v", err)
}
assertStringSet(t, "online union (flat)", svc.GetOnlineClients(), []string{"flat@example.com"})
byGuid := svc.GetOnlineClientsByGuid()
assertStringSet(t, "online byGuid (flat)", byGuid[nodeGuid], []string{"flat@example.com"})
}