1
0

node_test.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  5. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  6. )
  7. func TestNormalizeBasePath(t *testing.T) {
  8. cases := []struct {
  9. in string
  10. want string
  11. }{
  12. {"", "/"},
  13. {" ", "/"},
  14. {"/", "/"},
  15. {"/panel", "/panel/"},
  16. {"panel", "/panel/"},
  17. {"panel/", "/panel/"},
  18. {"/panel/", "/panel/"},
  19. {" /panel ", "/panel/"},
  20. {"/a/b/c", "/a/b/c/"},
  21. }
  22. for _, c := range cases {
  23. t.Run(c.in, func(t *testing.T) {
  24. got := normalizeBasePath(c.in)
  25. if got != c.want {
  26. t.Fatalf("normalizeBasePath(%q) = %q, want %q", c.in, got, c.want)
  27. }
  28. })
  29. }
  30. }
  31. func TestNodeMetricKey(t *testing.T) {
  32. cases := []struct {
  33. id int
  34. metric string
  35. want string
  36. }{
  37. {1, "cpu", "node:1:cpu"},
  38. {42, "mem", "node:42:mem"},
  39. {0, "anything", "node:0:anything"},
  40. }
  41. for _, c := range cases {
  42. got := nodeMetricKey(c.id, c.metric)
  43. if got != c.want {
  44. t.Fatalf("nodeMetricKey(%d, %q) = %q, want %q", c.id, c.metric, got, c.want)
  45. }
  46. }
  47. }
  48. func TestHeartbeatPatch_ToUI_OnlineCopiesFields(t *testing.T) {
  49. p := HeartbeatPatch{
  50. Status: "ignored-source",
  51. LatencyMs: 42,
  52. XrayVersion: "1.8.4",
  53. PanelVersion: "3.0.0",
  54. CpuPct: 12.5,
  55. MemPct: 33.3,
  56. UptimeSecs: 12345,
  57. LastError: "",
  58. }
  59. ui := p.ToUI(true)
  60. if ui.Status != "online" {
  61. t.Fatalf("Status = %q, want online", ui.Status)
  62. }
  63. if ui.LatencyMs != 42 || ui.XrayVersion != "1.8.4" || ui.PanelVersion != "3.0.0" {
  64. t.Fatalf("scalar copy mismatch: %+v", ui)
  65. }
  66. if ui.CpuPct != 12.5 || ui.MemPct != 33.3 || ui.UptimeSecs != 12345 {
  67. t.Fatalf("metric copy mismatch: %+v", ui)
  68. }
  69. if ui.Error != "" {
  70. t.Fatalf("Error = %q, want empty", ui.Error)
  71. }
  72. }
  73. func TestHeartbeatPatch_ToUI_OfflinePreservesError(t *testing.T) {
  74. p := HeartbeatPatch{LastError: "connection refused"}
  75. ui := p.ToUI(false)
  76. if ui.Status != "offline" {
  77. t.Fatalf("Status = %q, want offline", ui.Status)
  78. }
  79. if ui.Error != "connection refused" {
  80. t.Fatalf("Error = %q, want %q", ui.Error, "connection refused")
  81. }
  82. }
  83. func TestNodeService_Normalize_Valid(t *testing.T) {
  84. s := &NodeService{}
  85. n := &model.Node{
  86. Name: " primary ",
  87. ApiToken: " abc ",
  88. Address: "example.com",
  89. Port: 8443,
  90. Scheme: "",
  91. BasePath: "panel",
  92. }
  93. if err := s.normalize(n); err != nil {
  94. t.Fatalf("unexpected error: %v", err)
  95. }
  96. if n.Name != "primary" {
  97. t.Fatalf("Name not trimmed: %q", n.Name)
  98. }
  99. if n.ApiToken != "abc" {
  100. t.Fatalf("ApiToken not trimmed: %q", n.ApiToken)
  101. }
  102. if n.Scheme != "https" {
  103. t.Fatalf("empty Scheme should default to https, got %q", n.Scheme)
  104. }
  105. if n.BasePath != "/panel/" {
  106. t.Fatalf("BasePath = %q, want /panel/", n.BasePath)
  107. }
  108. }
  109. func TestNodeService_Normalize_KeepsValidScheme(t *testing.T) {
  110. s := &NodeService{}
  111. n := &model.Node{Name: "n", Address: "example.com", Port: 80, Scheme: "http"}
  112. if err := s.normalize(n); err != nil {
  113. t.Fatalf("unexpected error: %v", err)
  114. }
  115. if n.Scheme != "http" {
  116. t.Fatalf("Scheme = %q, want http", n.Scheme)
  117. }
  118. }
  119. func TestNodeService_Normalize_RejectsEmptyName(t *testing.T) {
  120. s := &NodeService{}
  121. n := &model.Node{Name: " ", Address: "example.com", Port: 443}
  122. if err := s.normalize(n); err == nil {
  123. t.Fatal("expected error for empty name")
  124. }
  125. }
  126. func TestNodeService_Normalize_RejectsBadHost(t *testing.T) {
  127. s := &NodeService{}
  128. n := &model.Node{Name: "n", Address: "bad host name with spaces", Port: 443}
  129. if err := s.normalize(n); err == nil {
  130. t.Fatal("expected error for invalid host")
  131. }
  132. }
  133. func TestNodeService_Normalize_RejectsOutOfRangePort(t *testing.T) {
  134. s := &NodeService{}
  135. for _, port := range []int{0, -1, 65536, 100000} {
  136. n := &model.Node{Name: "n", Address: "example.com", Port: port}
  137. if err := s.normalize(n); err == nil {
  138. t.Fatalf("expected error for port %d", port)
  139. }
  140. }
  141. }
  142. func TestNodeService_Normalize_OverridesUnknownScheme(t *testing.T) {
  143. s := &NodeService{}
  144. n := &model.Node{Name: "n", Address: "example.com", Port: 443, Scheme: "ftp"}
  145. if err := s.normalize(n); err != nil {
  146. t.Fatalf("unexpected error: %v", err)
  147. }
  148. if n.Scheme != "https" {
  149. t.Fatalf("Scheme = %q, want https", n.Scheme)
  150. }
  151. }
  152. func TestNodeService_NormalizeInboundSelection(t *testing.T) {
  153. s := &NodeService{}
  154. n := &model.Node{
  155. Name: "n",
  156. Address: "example.com",
  157. Port: 443,
  158. InboundSyncMode: "selected",
  159. InboundTags: []string{" alpha ", "", "beta", "alpha"},
  160. }
  161. if err := s.normalize(n); err != nil {
  162. t.Fatalf("unexpected error: %v", err)
  163. }
  164. if n.InboundSyncMode != "selected" {
  165. t.Fatalf("InboundSyncMode = %q, want selected", n.InboundSyncMode)
  166. }
  167. if len(n.InboundTags) != 2 || n.InboundTags[0] != "alpha" || n.InboundTags[1] != "beta" {
  168. t.Fatalf("InboundTags = %#v, want [alpha beta]", n.InboundTags)
  169. }
  170. }
  171. func TestFilterNodeSnapshot(t *testing.T) {
  172. snapshot := func() *runtime.TrafficSnapshot {
  173. return &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
  174. {Tag: "alpha"},
  175. {Tag: "beta"},
  176. {Tag: "gamma"},
  177. }}
  178. }
  179. all := snapshot()
  180. FilterNodeSnapshot(&model.Node{InboundSyncMode: "all"}, all)
  181. if len(all.Inbounds) != 3 {
  182. t.Fatalf("all mode kept %d inbounds, want 3", len(all.Inbounds))
  183. }
  184. selected := snapshot()
  185. FilterNodeSnapshot(&model.Node{
  186. InboundSyncMode: "selected",
  187. InboundTags: []string{"beta"},
  188. }, selected)
  189. if len(selected.Inbounds) != 1 || selected.Inbounds[0].Tag != "beta" {
  190. t.Fatalf("selected mode produced %#v, want only beta", selected.Inbounds)
  191. }
  192. none := snapshot()
  193. FilterNodeSnapshot(&model.Node{InboundSyncMode: "selected"}, none)
  194. if len(none.Inbounds) != 0 {
  195. t.Fatalf("empty selection kept %d inbounds, want 0", len(none.Inbounds))
  196. }
  197. }