package service import ( "fmt" "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) // Agent nodes report their online clients inside the traffic payload; the // master files them via InboundService.SetAgentOnlineClients. They must be // keyed under the node's effective attribution key — pre-fix the controller // stored every agent's list under one literal "agent" key, which the // inbounds page (lookup by originNodeGuid / node:{id}) never matches, so // agent inbounds never lit anyone online and all agents merged into one // bucket. These tests pin the per-node keying. func TestSetAgentOnlineClients_AttributionKeysPerNode(t *testing.T) { setupConflictDB(t) db := database.GetDB() svc := InboundService{} const nodeA = 7301 const nodeB = 7302 for _, id := range []int{nodeA, nodeB} { svc.ClearNodeOnlineClients(id) if err := db.Create(&model.Node{Id: id, Name: fmt.Sprintf("agent-node-%d", id), Address: "10.0.7.3"}).Error; err != nil { t.Fatalf("create node %d: %v", id, err) } } t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeA) svc.ClearNodeOnlineClients(nodeB) }) svc.SetAgentOnlineClients(nodeA, []string{"agent-a@example.com"}) svc.SetAgentOnlineClients(nodeB, []string{"agent-b@example.com"}) keyA := effectiveNodeKey(&model.Node{Id: nodeA}) keyB := effectiveNodeKey(&model.Node{Id: nodeB}) if keyA == keyB { t.Fatalf("effective keys collide: %q", keyA) } byGuid := svc.GetOnlineClientsByGuid() assertStringSet(t, "byGuid node A", byGuid[keyA], []string{"agent-a@example.com"}) assertStringSet(t, "byGuid node B", byGuid[keyB], []string{"agent-b@example.com"}) if _, ok := byGuid["agent"]; ok { t.Fatalf("byGuid still carries the legacy literal \"agent\" key: %v", byGuid) } union := svc.GetOnlineClients() for _, want := range []string{"agent-a@example.com", "agent-b@example.com"} { found := false for _, got := range union { if got == want { found = true break } } if !found { t.Fatalf("online union %v missing %q", union, want) } } } // An empty onlines list is the agent's disconnect signal: the node's // clients must stop showing as online immediately. func TestSetAgentOnlineClients_EmptyReportClears(t *testing.T) { setupConflictDB(t) db := database.GetDB() svc := InboundService{} const nodeID = 7303 svc.ClearNodeOnlineClients(nodeID) t.Cleanup(func() { svc.ClearNodeOnlineClients(nodeID) }) if err := db.Create(&model.Node{Id: nodeID, Name: "agent-node-7303", Address: "10.0.7.4"}).Error; err != nil { t.Fatalf("create node: %v", err) } svc.SetAgentOnlineClients(nodeID, []string{"gone@example.com"}) key := effectiveNodeKey(&model.Node{Id: nodeID}) assertStringSet(t, "byGuid before clear", svc.GetOnlineClientsByGuid()[key], []string{"gone@example.com"}) svc.SetAgentOnlineClients(nodeID, nil) if got := svc.GetOnlineClientsByGuid()[key]; len(got) != 0 { t.Fatalf("byGuid[%q] after empty report = %v, want empty", key, got) } for _, got := range svc.GetOnlineClients() { if got == "gone@example.com" { t.Fatalf("online union still contains cleared client: %v", svc.GetOnlineClients()) } } }