Files
3x-ui-agent/internal/web/service/inbound_agent_origin_test.go
T
Muse 3df652a67d fix(inbounds): slim list stamps agent-hosted inbounds with the agent node's key
Agent inbounds defined centrally store an empty originNodeGuid, and
inbounds re-homed from a panel node keep the previous host's GUID, so
the inbounds page looked the agent's online set up under a key it is
never filed under. GetInboundsSlim now annotates agent-hosted
inbounds at read time with the node's effective attribution key
(the key SetAgentOnlineClients files onlines under).
2026-10-22 15:33:51 +00:00

96 lines
3.5 KiB
Go

package service
import (
"fmt"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// The inbounds page looks a node's online set up under the inbound's
// originNodeGuid (falling back to node:{id}). Agent inbounds are defined
// centrally: a fresh one stores no origin GUID, and one re-homed from a
// panel node keeps the GUID the snapshot merge stamped for its PREVIOUS
// host — while the agent's onlines are filed under the agent node's own
// effective key. The slim list must therefore stamp agent-hosted inbounds
// with the hosting node's effective key at read time, or their clients
// never show as online on the inbounds page (production: every converted
// agent node was affected). Traditional inbounds' stored origins are
// chain attribution (#4983) and must pass through untouched.
func TestGetInboundsSlim_AnnotatesAgentOriginGuid(t *testing.T) {
setupConflictDB(t)
db := database.GetDB()
svc := InboundService{}
const agentNodeID = 8201
const tradNodeID = 8202
const userID = 1
if err := db.Create(&model.Node{Id: agentNodeID, Name: "agent-node", Address: "10.0.8.1"}).Error; err != nil {
t.Fatalf("create agent node: %v", err)
}
if err := db.Create(&model.Node{Id: tradNodeID, Name: "trad-node", Address: "10.0.8.2", Port: 2053, ApiToken: "t", Guid: "trad-guid-8202"}).Error; err != nil {
t.Fatalf("create trad node: %v", err)
}
// The agent binding is what makes the node "agent-managed".
agentID := agentNodeID
if err := db.Create(&model.AgentNode{
TokenHash: fmt.Sprintf("%064d", agentNodeID),
Name: "agent-node",
NodeID: &agentID,
}).Error; err != nil {
t.Fatalf("create agent binding: %v", err)
}
mkInbound := func(tag string, nodeID *int, origin string) {
ib := &model.Inbound{
UserId: userID,
Tag: tag,
Enable: true,
Port: 443,
Protocol: model.VLESS,
Settings: `{"clients":[{"email":"a@example.com","enable":true}]}`,
NodeID: nodeID,
OriginNodeGuid: origin,
}
if err := db.Create(ib).Error; err != nil {
t.Fatalf("create inbound %s: %v", tag, err)
}
}
agentIDCopy := agentNodeID
tradIDCopy := tradNodeID
mkInbound("agent-stale", &agentIDCopy, "guid-of-previous-panel-host")
mkInbound("agent-fresh", &agentIDCopy, "")
mkInbound("trad-synced", &tradIDCopy, "trad-guid-8202")
slim, err := svc.GetInboundsSlim(userID)
if err != nil {
t.Fatalf("GetInboundsSlim: %v", err)
}
byTag := make(map[string]*model.Inbound, len(slim))
for _, ib := range slim {
byTag[ib.Tag] = ib
}
wantKey := fmt.Sprintf("node:%d", agentNodeID)
if got := byTag["agent-stale"].OriginNodeGuid; got != wantKey {
t.Errorf("agent-stale originNodeGuid = %q, want %q (stale host GUID healed to the agent's key)", got, wantKey)
}
if got := byTag["agent-fresh"].OriginNodeGuid; got != wantKey {
t.Errorf("agent-fresh originNodeGuid = %q, want %q", got, wantKey)
}
if got := byTag["trad-synced"].OriginNodeGuid; got != "trad-guid-8202" {
t.Errorf("trad-synced originNodeGuid = %q, want stored GUID untouched", got)
}
// Read-time only: the stored value must not be rewritten.
var stored model.Inbound
if err := db.Where("tag = ?", "agent-stale").First(&stored).Error; err != nil {
t.Fatalf("reload agent-stale: %v", err)
}
if stored.OriginNodeGuid != "guid-of-previous-panel-host" {
t.Errorf("stored origin_node_guid = %q, want the stale value preserved (annotation is read-time only)", stored.OriginNodeGuid)
}
}