tgbot_report_batching_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package tgbot
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "testing"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database"
  13. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  14. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  15. "github.com/mymmrac/telego"
  16. )
  17. // seedReportClients writes one inbound holding every email plus the traffic row
  18. // each of them needs to appear in the sorted usage report.
  19. func seedReportClients(t *testing.T, remark string, emails []string) {
  20. t.Helper()
  21. settings := make([]string, 0, len(emails))
  22. for _, email := range emails {
  23. settings = append(settings, fmt.Sprintf(`{"email":%q,"subId":"sub-%s"}`, email, email))
  24. }
  25. inbound := &model.Inbound{
  26. UserId: 1,
  27. Remark: remark,
  28. Port: 8443,
  29. Protocol: model.VLESS,
  30. Enable: true,
  31. Settings: `{"clients":[` + strings.Join(settings, ",") + `]}`,
  32. }
  33. if err := database.GetDB().Create(inbound).Error; err != nil {
  34. t.Fatalf("seed inbound: %v", err)
  35. }
  36. for _, email := range emails {
  37. if err := database.GetDB().Create(&xray.ClientTraffic{
  38. InboundId: inbound.Id,
  39. Email: email,
  40. Enable: true,
  41. Up: 1,
  42. Down: 1,
  43. }).Error; err != nil {
  44. t.Fatalf("seed traffic for %s: %v", email, err)
  45. }
  46. record := (&model.Client{Email: email, Enable: true, SubID: "sub-" + email}).ToRecord()
  47. if err := database.GetDB().Create(record).Error; err != nil {
  48. t.Fatalf("seed client %s: %v", email, err)
  49. }
  50. if err := database.GetDB().Create(&model.ClientInbound{ClientId: record.Id, InboundId: inbound.Id}).Error; err != nil {
  51. t.Fatalf("seed client_inbounds for %s: %v", email, err)
  52. }
  53. }
  54. }
  55. func initReportDB(t *testing.T) *Tgbot {
  56. t.Helper()
  57. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  58. t.Fatalf("InitDB: %v", err)
  59. }
  60. t.Cleanup(func() { _ = database.CloseDB() })
  61. origRunning := isRunning
  62. t.Cleanup(func() { isRunning = origRunning })
  63. isRunning = true
  64. return &Tgbot{}
  65. }
  66. type sentMessage struct {
  67. Text string `json:"text"`
  68. ReplyMarkup json.RawMessage `json:"reply_markup"`
  69. }
  70. // captureReportServer records every sendMessage call so a test can assert on
  71. // what Telegram would have received, not merely how many calls were made.
  72. func captureReportServer(t *testing.T) (*httptest.Server, func() []sentMessage) {
  73. t.Helper()
  74. var mu sync.Mutex
  75. var sent []sentMessage
  76. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  77. body, _ := io.ReadAll(r.Body)
  78. result := any(true)
  79. if r.URL.Path == "/bot"+testBotToken+"/sendMessage" {
  80. var payload sentMessage
  81. _ = json.Unmarshal(body, &payload)
  82. mu.Lock()
  83. sent = append(sent, payload)
  84. mu.Unlock()
  85. result = map[string]any{"message_id": 1, "date": 0, "chat": map[string]any{"id": 1, "type": "private"}}
  86. }
  87. w.Header().Set("Content-Type", "application/json")
  88. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "result": result})
  89. }))
  90. return srv, func() []sentMessage {
  91. mu.Lock()
  92. defer mu.Unlock()
  93. return append([]sentMessage(nil), sent...)
  94. }
  95. }
  96. // Regression test: the sorted usage report must reach Telegram as one message
  97. // whatever the client count; per-client sends burst past the rate limit.
  98. func TestTrafficUsageReportIsOneMessage(t *testing.T) {
  99. mock, calls := staleButtonServer(t, map[string]any{
  100. "sendMessage": map[string]any{"ok": true, "result": map[string]any{
  101. "message_id": 1,
  102. "date": 0,
  103. "chat": map[string]any{"id": 1, "type": "private"},
  104. }},
  105. "deleteMessage": map[string]any{"ok": true, "result": true},
  106. })
  107. swapTestBot(t, mock.URL)
  108. defer mock.Close()
  109. tb := initReportDB(t)
  110. seedReportClients(t, "report", []string{"a@x", "b@x", "c@x"})
  111. tb.answerCallback(&telego.CallbackQuery{
  112. ID: "q1",
  113. From: telego.User{ID: 1},
  114. Data: "get_sorted_traffic_usage_report",
  115. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  116. }, true) // admin
  117. if n := calls("sendMessage"); n != 1 {
  118. t.Errorf("sendMessage calls = %d, want 1: one report per tap, not one per client", n)
  119. }
  120. }
  121. // Regression test: batching must not swallow the reply on a panel with no
  122. // clients, where the old code still answered FinishProcess.
  123. func TestResetAllTrafficsAnswersWithNoClients(t *testing.T) {
  124. mock, sent := captureReportServer(t)
  125. swapTestBot(t, mock.URL)
  126. defer mock.Close()
  127. tb := initReportDB(t)
  128. tb.answerCallback(&telego.CallbackQuery{
  129. ID: "q1",
  130. From: telego.User{ID: 1},
  131. Data: "reset_all_traffics_c",
  132. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  133. }, true) // admin
  134. got := sent()
  135. if len(got) != 1 {
  136. t.Fatalf("sendMessage calls = %d, want 1: an empty panel must still answer the tap", len(got))
  137. }
  138. if got[0].Text == "" {
  139. t.Error("reset report text is empty, want the finish-process message")
  140. }
  141. if !strings.Contains(string(got[0].ReplyMarkup), `"remove_keyboard":true`) {
  142. t.Errorf("reply_markup = %s, want the reply keyboard removed", got[0].ReplyMarkup)
  143. }
  144. }
  145. // Regression test: the report leaves as one HTML-parsed message, so a remark
  146. // holding "<" must reach Telegram escaped instead of dropping the whole page.
  147. func TestTrafficUsageReportEscapesHtml(t *testing.T) {
  148. mock, sent := captureReportServer(t)
  149. swapTestBot(t, mock.URL)
  150. defer mock.Close()
  151. tb := initReportDB(t)
  152. seedReportClients(t, "DE <fast>", []string{"a@x"})
  153. tb.answerCallback(&telego.CallbackQuery{
  154. ID: "q1",
  155. From: telego.User{ID: 1},
  156. Data: "get_sorted_traffic_usage_report",
  157. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  158. }, true) // admin
  159. got := sent()
  160. if len(got) != 1 {
  161. t.Fatalf("sendMessage calls = %d, want 1", len(got))
  162. }
  163. if strings.Contains(got[0].Text, "<fast>") {
  164. t.Errorf("report text = %q, want the remark escaped", got[0].Text)
  165. }
  166. if !strings.Contains(got[0].Text, "&lt;fast&gt;") {
  167. t.Errorf("report text = %q, want the remark escaped as &lt;fast&gt;", got[0].Text)
  168. }
  169. }