Files
3x-ui-agent/internal/web/service/node_client_lists_test.go
T
Muse c91fb454a8 fix(nodes): per-category client email lists behind the node count chips
NodeView carried only the category counts, so the nodes page chips
could hover to nothing but the category title. GetAll/recountByGuid
now emit online/active/disabled/depleted email lists from the same
bucketing pass as the counts (statuses gain the client email;
the online list is the counted set), and the chips pop the list
over with the inbounds page's popover pattern.
2026-10-22 15:33:46 +00:00

98 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/xray"
)
// The nodes page category chips used to hover to a bare category title:
// the API carried only the counts, never WHO is in each category. GetAll
// must now emit the per-category email lists from the exact same bucketing
// pass as the counts (statuses by node id, online set by attribution GUID),
// and the tree view must carry them through to the wire contract.
func TestGetAll_ClientEmailListsMatchCounts(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
nodeSvc := NodeService{}
inboundSvc := InboundService{}
const nodeID = 8301
if err := db.Create(&model.Node{Id: nodeID, Name: "agent-node", Address: "10.0.8.3"}).Error; err != nil {
t.Fatalf("create node: %v", err)
}
nid := nodeID
ib := &model.Inbound{Tag: "lists-in", Enable: true, Port: 443, Protocol: model.VLESS, Settings: `{"clients":[]}`, NodeID: &nid}
if err := db.Create(ib).Error; err != nil {
t.Fatalf("create inbound: %v", err)
}
mk := func(id int, email string, enable bool) {
if err := db.Create(&model.ClientRecord{Id: id, Email: email, Enable: enable}).Error; err != nil {
t.Fatalf("create client %s: %v", email, err)
}
if !enable {
if err := db.Model(&model.ClientRecord{}).Where("id = ?", id).Update("enable", false).Error; err != nil {
t.Fatalf("disable client %s: %v", email, err)
}
}
if err := db.Create(&model.ClientInbound{ClientId: id, InboundId: ib.Id}).Error; err != nil {
t.Fatalf("attach client %s: %v", email, err)
}
}
mk(8301, "online@x", true)
mk(8302, "active@x", true)
mk(8303, "disabled@x", false)
mk(8304, "depleted@x", true)
if err := db.Create(&xray.ClientTraffic{InboundId: 999, Email: "depleted@x", Enable: true, Total: 100, Up: 60, Down: 60}).Error; err != nil {
t.Fatalf("create traffic: %v", err)
}
// The agent's online report — same entry the agent controller calls.
inboundSvc.SetAgentOnlineClients(nodeID, []string{"online@x"})
t.Cleanup(func() { inboundSvc.ClearNodeOnlineClients(nodeID) })
nodes, err := nodeSvc.GetAll()
if err != nil {
t.Fatalf("GetAll: %v", err)
}
var n *model.Node
for _, x := range nodes {
if x.Id == nodeID {
n = x
}
}
if n == nil {
t.Fatal("node not found")
}
if n.ClientCount != 4 || n.ActiveCount != 2 || n.DisabledCount != 1 || n.DepletedCount != 1 || n.OnlineCount != 1 {
t.Fatalf("counts = total %d active %d disabled %d depleted %d online %d, want 4/2/1/1/1",
n.ClientCount, n.ActiveCount, n.DisabledCount, n.DepletedCount, n.OnlineCount)
}
assertStringSet(t, "active emails", n.ActiveEmails, []string{"online@x", "active@x"})
assertStringSet(t, "disabled emails", n.DisabledEmails, []string{"disabled@x"})
assertStringSet(t, "depleted emails", n.DepletedEmails, []string{"depleted@x"})
assertStringSet(t, "online emails", n.OnlineEmails, []string{"online@x"})
// The wire contract (nodes page) must carry the same lists.
views, err := nodeSvc.GetNodeTreeView()
if err != nil {
t.Fatalf("GetNodeTreeView: %v", err)
}
var v *NodeView
for _, x := range views {
if x.Id == nodeID {
v = x
}
}
if v == nil {
t.Fatal("node view not found")
}
assertStringSet(t, "view online emails", v.OnlineEmails, []string{"online@x"})
assertStringSet(t, "view active emails", v.ActiveEmails, []string{"online@x", "active@x"})
assertStringSet(t, "view disabled emails", v.DisabledEmails, []string{"disabled@x"})
assertStringSet(t, "view depleted emails", v.DepletedEmails, []string{"depleted@x"})
}