mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 17:16:07 +00:00
448e8c97c2
FilterNodeSnapshot compared a node snapshot's inbound tags against the raw selected-tag list with an exact match, while its two siblings (SnapshotHasUnadoptedInbounds and the reconcile tagToCentral map) expand each selected tag to both its bare node-side form and its n<id>- prefixed central form. A panel-created node inbound is recorded in the selected list under the central prefixed tag but reported by the node under the bare tag, so the exact match dropped it from every snapshot and the orphan sweep then deleted its central row one tick after creation. Expand the allowed set with the same prefix flip the siblings use.
238 lines
6.3 KiB
Go
238 lines
6.3 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
|
)
|
|
|
|
func TestNormalizeBasePath(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"", "/"},
|
|
{" ", "/"},
|
|
{"/", "/"},
|
|
{"/panel", "/panel/"},
|
|
{"panel", "/panel/"},
|
|
{"panel/", "/panel/"},
|
|
{"/panel/", "/panel/"},
|
|
{" /panel ", "/panel/"},
|
|
{"/a/b/c", "/a/b/c/"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.in, func(t *testing.T) {
|
|
got := normalizeBasePath(c.in)
|
|
if got != c.want {
|
|
t.Fatalf("normalizeBasePath(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeMetricKey(t *testing.T) {
|
|
cases := []struct {
|
|
id int
|
|
metric string
|
|
want string
|
|
}{
|
|
{1, "cpu", "node:1:cpu"},
|
|
{42, "mem", "node:42:mem"},
|
|
{0, "anything", "node:0:anything"},
|
|
}
|
|
for _, c := range cases {
|
|
got := nodeMetricKey(c.id, c.metric)
|
|
if got != c.want {
|
|
t.Fatalf("nodeMetricKey(%d, %q) = %q, want %q", c.id, c.metric, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHeartbeatPatch_ToUI_OnlineCopiesFields(t *testing.T) {
|
|
p := HeartbeatPatch{
|
|
Status: "ignored-source",
|
|
LatencyMs: 42,
|
|
XrayVersion: "1.8.4",
|
|
PanelVersion: "3.0.0",
|
|
CpuPct: 12.5,
|
|
MemPct: 33.3,
|
|
UptimeSecs: 12345,
|
|
LastError: "",
|
|
}
|
|
ui := p.ToUI(true)
|
|
if ui.Status != "online" {
|
|
t.Fatalf("Status = %q, want online", ui.Status)
|
|
}
|
|
if ui.LatencyMs != 42 || ui.XrayVersion != "1.8.4" || ui.PanelVersion != "3.0.0" {
|
|
t.Fatalf("scalar copy mismatch: %+v", ui)
|
|
}
|
|
if ui.CpuPct != 12.5 || ui.MemPct != 33.3 || ui.UptimeSecs != 12345 {
|
|
t.Fatalf("metric copy mismatch: %+v", ui)
|
|
}
|
|
if ui.Error != "" {
|
|
t.Fatalf("Error = %q, want empty", ui.Error)
|
|
}
|
|
}
|
|
|
|
func TestHeartbeatPatch_ToUI_OfflinePreservesError(t *testing.T) {
|
|
p := HeartbeatPatch{LastError: "connection refused"}
|
|
ui := p.ToUI(false)
|
|
if ui.Status != "offline" {
|
|
t.Fatalf("Status = %q, want offline", ui.Status)
|
|
}
|
|
if ui.Error != "connection refused" {
|
|
t.Fatalf("Error = %q, want %q", ui.Error, "connection refused")
|
|
}
|
|
}
|
|
|
|
func TestNodeService_Normalize_Valid(t *testing.T) {
|
|
s := &NodeService{}
|
|
n := &model.Node{
|
|
Name: " primary ",
|
|
ApiToken: " abc ",
|
|
Address: "example.com",
|
|
Port: 8443,
|
|
Scheme: "",
|
|
BasePath: "panel",
|
|
}
|
|
if err := s.normalize(n); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n.Name != "primary" {
|
|
t.Fatalf("Name not trimmed: %q", n.Name)
|
|
}
|
|
if n.ApiToken != "abc" {
|
|
t.Fatalf("ApiToken not trimmed: %q", n.ApiToken)
|
|
}
|
|
if n.Scheme != "https" {
|
|
t.Fatalf("empty Scheme should default to https, got %q", n.Scheme)
|
|
}
|
|
if n.BasePath != "/panel/" {
|
|
t.Fatalf("BasePath = %q, want /panel/", n.BasePath)
|
|
}
|
|
}
|
|
|
|
func TestNodeService_Normalize_KeepsValidScheme(t *testing.T) {
|
|
s := &NodeService{}
|
|
n := &model.Node{Name: "n", Address: "example.com", Port: 80, Scheme: "http"}
|
|
if err := s.normalize(n); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n.Scheme != "http" {
|
|
t.Fatalf("Scheme = %q, want http", n.Scheme)
|
|
}
|
|
}
|
|
|
|
func TestNodeService_Normalize_RejectsEmptyName(t *testing.T) {
|
|
s := &NodeService{}
|
|
n := &model.Node{Name: " ", Address: "example.com", Port: 443}
|
|
if err := s.normalize(n); err == nil {
|
|
t.Fatal("expected error for empty name")
|
|
}
|
|
}
|
|
|
|
func TestNodeService_Normalize_RejectsBadHost(t *testing.T) {
|
|
s := &NodeService{}
|
|
n := &model.Node{Name: "n", Address: "bad host name with spaces", Port: 443}
|
|
if err := s.normalize(n); err == nil {
|
|
t.Fatal("expected error for invalid host")
|
|
}
|
|
}
|
|
|
|
func TestNodeService_Normalize_RejectsOutOfRangePort(t *testing.T) {
|
|
s := &NodeService{}
|
|
for _, port := range []int{0, -1, 65536, 100000} {
|
|
n := &model.Node{Name: "n", Address: "example.com", Port: port}
|
|
if err := s.normalize(n); err == nil {
|
|
t.Fatalf("expected error for port %d", port)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodeService_Normalize_OverridesUnknownScheme(t *testing.T) {
|
|
s := &NodeService{}
|
|
n := &model.Node{Name: "n", Address: "example.com", Port: 443, Scheme: "ftp"}
|
|
if err := s.normalize(n); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n.Scheme != "https" {
|
|
t.Fatalf("Scheme = %q, want https", n.Scheme)
|
|
}
|
|
}
|
|
|
|
func TestNodeService_NormalizeInboundSelection(t *testing.T) {
|
|
s := &NodeService{}
|
|
n := &model.Node{
|
|
Name: "n",
|
|
Address: "example.com",
|
|
Port: 443,
|
|
InboundSyncMode: "selected",
|
|
InboundTags: []string{" alpha ", "", "beta", "alpha"},
|
|
}
|
|
if err := s.normalize(n); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n.InboundSyncMode != "selected" {
|
|
t.Fatalf("InboundSyncMode = %q, want selected", n.InboundSyncMode)
|
|
}
|
|
if len(n.InboundTags) != 2 || n.InboundTags[0] != "alpha" || n.InboundTags[1] != "beta" {
|
|
t.Fatalf("InboundTags = %#v, want [alpha beta]", n.InboundTags)
|
|
}
|
|
}
|
|
|
|
func TestFilterNodeSnapshot(t *testing.T) {
|
|
snapshot := func() *runtime.TrafficSnapshot {
|
|
return &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
|
|
{Tag: "alpha"},
|
|
{Tag: "beta"},
|
|
{Tag: "gamma"},
|
|
}}
|
|
}
|
|
|
|
all := snapshot()
|
|
FilterNodeSnapshot(&model.Node{InboundSyncMode: "all"}, all)
|
|
if len(all.Inbounds) != 3 {
|
|
t.Fatalf("all mode kept %d inbounds, want 3", len(all.Inbounds))
|
|
}
|
|
|
|
selected := snapshot()
|
|
FilterNodeSnapshot(&model.Node{
|
|
InboundSyncMode: "selected",
|
|
InboundTags: []string{"beta"},
|
|
}, selected)
|
|
if len(selected.Inbounds) != 1 || selected.Inbounds[0].Tag != "beta" {
|
|
t.Fatalf("selected mode produced %#v, want only beta", selected.Inbounds)
|
|
}
|
|
|
|
none := snapshot()
|
|
FilterNodeSnapshot(&model.Node{InboundSyncMode: "selected"}, none)
|
|
if len(none.Inbounds) != 0 {
|
|
t.Fatalf("empty selection kept %d inbounds, want 0", len(none.Inbounds))
|
|
}
|
|
}
|
|
|
|
func TestFilterNodeSnapshotMatchesPrefixedSelectedTag(t *testing.T) {
|
|
snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
|
|
{Tag: "in-100-tcp"},
|
|
{Tag: "in-443-tcp"},
|
|
}}
|
|
FilterNodeSnapshot(&model.Node{
|
|
Id: 5,
|
|
InboundSyncMode: "selected",
|
|
InboundTags: []string{"in-100-tcp", "n5-in-443-tcp"},
|
|
}, snap)
|
|
|
|
kept := make(map[string]bool, len(snap.Inbounds))
|
|
for _, ib := range snap.Inbounds {
|
|
kept[ib.Tag] = true
|
|
}
|
|
if !kept["in-443-tcp"] {
|
|
t.Fatalf("node-side tag in-443-tcp filtered out despite the prefixed central tag being selected; kept=%v", kept)
|
|
}
|
|
if !kept["in-100-tcp"] {
|
|
t.Fatalf("bare selected tag in-100-tcp was dropped; kept=%v", kept)
|
|
}
|
|
}
|