inbound_node_ips_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "time"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. )
  9. func TestRecordLocalClientIps_RoundTripByGuid(t *testing.T) {
  10. setupClientIpTestDB(t)
  11. now := time.Now().Unix()
  12. svc := &InboundService{}
  13. if err := svc.RecordLocalClientIps("guid-A", map[string][]model.ClientIpEntry{
  14. "u@x": {{IP: "1.1.1.1", Timestamp: now}, {IP: "2.2.2.2", Timestamp: now - 10}},
  15. }); err != nil {
  16. t.Fatalf("record: %v", err)
  17. }
  18. trees, err := svc.GetClientIpsByGuid()
  19. if err != nil {
  20. t.Fatalf("byGuid: %v", err)
  21. }
  22. got := trees["guid-A"]["u@x"]
  23. if len(got) != 2 {
  24. t.Fatalf("want 2 entries, got %v", got)
  25. }
  26. if got[0].IP != "1.1.1.1" { // newest-first ordering
  27. t.Fatalf("want newest first, got %v", got)
  28. }
  29. }
  30. func TestRecordLocalClientIps_MergesAndDropsStale(t *testing.T) {
  31. setupClientIpTestDB(t)
  32. now := time.Now().Unix()
  33. svc := &InboundService{}
  34. if err := svc.RecordLocalClientIps("g", map[string][]model.ClientIpEntry{
  35. "u@x": {{IP: "keep", Timestamp: now - 60}},
  36. }); err != nil {
  37. t.Fatalf("record 1: %v", err)
  38. }
  39. // Second scan refreshes keep, adds a stale entry (must be dropped) and a fresh one.
  40. if err := svc.RecordLocalClientIps("g", map[string][]model.ClientIpEntry{
  41. "u@x": {{IP: "keep", Timestamp: now}, {IP: "stale", Timestamp: now - 4000}, {IP: "new", Timestamp: now - 5}},
  42. }); err != nil {
  43. t.Fatalf("record 2: %v", err)
  44. }
  45. trees, _ := svc.GetClientIpsByGuid()
  46. got := map[string]int64{}
  47. for _, e := range trees["g"]["u@x"] {
  48. got[e.IP] = e.Timestamp
  49. }
  50. if got["keep"] != now {
  51. t.Fatalf("keep should refresh to now: %v", got)
  52. }
  53. if _, ok := got["stale"]; ok {
  54. t.Fatalf("stale entry should be dropped: %v", got)
  55. }
  56. if got["new"] != now-5 {
  57. t.Fatalf("new missing: %v", got)
  58. }
  59. }
  60. func TestUpsertNodeClientIps_EmptyMergeDeletesRow(t *testing.T) {
  61. setupClientIpTestDB(t)
  62. now := time.Now().Unix()
  63. db := database.GetDB()
  64. svc := &InboundService{}
  65. // Seed an already-stale row, then record another all-stale observation: the
  66. // merge yields nothing fresh, so the row must be removed (not left lingering).
  67. staleIps, _ := json.Marshal([]model.ClientIpEntry{{IP: "old", Timestamp: now - 999999}})
  68. if err := db.Create(&model.NodeClientIp{NodeGuid: "g", Email: "u@x", Ips: string(staleIps)}).Error; err != nil {
  69. t.Fatalf("seed: %v", err)
  70. }
  71. if err := svc.RecordLocalClientIps("g", map[string][]model.ClientIpEntry{
  72. "u@x": {{IP: "old2", Timestamp: now - 999999}},
  73. }); err != nil {
  74. t.Fatalf("record: %v", err)
  75. }
  76. var count int64
  77. database.GetDB().Model(&model.NodeClientIp{}).
  78. Where("node_guid = ? AND email = ?", "g", "u@x").Count(&count)
  79. if count != 0 {
  80. t.Fatalf("row should be deleted when merge is empty, got %d", count)
  81. }
  82. }
  83. func TestGetClientIpNodeAttribution_NewestGuidWins(t *testing.T) {
  84. setupClientIpTestDB(t)
  85. now := time.Now().Unix()
  86. svc := &InboundService{}
  87. // Same IP observed on two panels; the most recent observation attributes it.
  88. if err := svc.RecordLocalClientIps("gA", map[string][]model.ClientIpEntry{
  89. "u@x": {{IP: "9.9.9.9", Timestamp: now - 100}},
  90. }); err != nil {
  91. t.Fatalf("record gA: %v", err)
  92. }
  93. if err := svc.MergeClientIpsByGuid(map[string]map[string][]model.ClientIpEntry{
  94. "gB": {"u@x": {{IP: "9.9.9.9", Timestamp: now}}},
  95. }); err != nil {
  96. t.Fatalf("merge gB: %v", err)
  97. }
  98. attr, err := svc.GetClientIpNodeAttribution("u@x")
  99. if err != nil {
  100. t.Fatalf("attribution: %v", err)
  101. }
  102. if attr["9.9.9.9"] != "gB" {
  103. t.Fatalf("newest guid should win, got %q", attr["9.9.9.9"])
  104. }
  105. }
  106. func TestGetClientIpsWithNodes_LabelsNodes(t *testing.T) {
  107. setupClientIpTestDB(t)
  108. now := time.Now().Unix()
  109. db := database.GetDB()
  110. svc := &InboundService{}
  111. panelGuid, err := (&SettingService{}).GetPanelGuid()
  112. if err != nil || panelGuid == "" {
  113. t.Fatalf("panel guid: %v", err)
  114. }
  115. if err := db.Create(&model.Node{Name: "edge-1", Guid: "node-guid", Address: "x", Port: 2053, ApiToken: "t"}).Error; err != nil {
  116. t.Fatalf("seed node: %v", err)
  117. }
  118. // Flat display set (what the IP-log lists) holds both IPs.
  119. flat, _ := json.Marshal([]model.ClientIpEntry{{IP: "1.1.1.1", Timestamp: now}, {IP: "2.2.2.2", Timestamp: now}})
  120. if err := db.Create(&model.InboundClientIps{ClientEmail: "u@x", Ips: string(flat)}).Error; err != nil {
  121. t.Fatalf("seed flat ips: %v", err)
  122. }
  123. // Attribution: 1.1.1.1 seen locally, 2.2.2.2 seen on the node.
  124. if err := svc.RecordLocalClientIps(panelGuid, map[string][]model.ClientIpEntry{
  125. "u@x": {{IP: "1.1.1.1", Timestamp: now}},
  126. }); err != nil {
  127. t.Fatalf("record local: %v", err)
  128. }
  129. if err := svc.MergeClientIpsByGuid(map[string]map[string][]model.ClientIpEntry{
  130. "node-guid": {"u@x": {{IP: "2.2.2.2", Timestamp: now}}},
  131. }); err != nil {
  132. t.Fatalf("merge node: %v", err)
  133. }
  134. infos, err := svc.GetClientIpsWithNodes("u@x")
  135. if err != nil {
  136. t.Fatalf("getIpsWithNodes: %v", err)
  137. }
  138. byIP := map[string]string{}
  139. for _, in := range infos {
  140. byIP[in.IP] = in.Node
  141. }
  142. if byIP["1.1.1.1"] != "" {
  143. t.Fatalf("local IP should have empty node, got %q", byIP["1.1.1.1"])
  144. }
  145. if byIP["2.2.2.2"] != "edge-1" {
  146. t.Fatalf("node IP should be labelled edge-1, got %q", byIP["2.2.2.2"])
  147. }
  148. }