mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-15 07:40:59 +00:00
bb29b6afec
* fix(node): adopt compatible origin inbounds without mutation * fix(nodes): preserve ambiguous and adopted aliases --------- Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
253 lines
6.7 KiB
Go
253 lines
6.7 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)
|
|
}
|
|
}
|
|
|
|
func TestFilterNodeSnapshotKeepsAdoptedAlias(t *testing.T) {
|
|
snap := &runtime.TrafficSnapshot{
|
|
Inbounds: []*model.Inbound{{Tag: "deployed-alias"}, {Tag: "unmanaged"}},
|
|
ManagedAliases: []string{"deployed-alias"},
|
|
}
|
|
FilterNodeSnapshot(&model.Node{
|
|
InboundSyncMode: "selected",
|
|
InboundTags: []string{"desired-name"},
|
|
}, snap)
|
|
|
|
if len(snap.Inbounds) != 1 || snap.Inbounds[0].Tag != "deployed-alias" {
|
|
t.Fatalf("filtered snapshot = %#v, want adopted alias only", snap.Inbounds)
|
|
}
|
|
}
|