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(¢ral).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"}) }